Merge "Support wildcard domains for UriRelativeFilterGroups" into main
diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
index 4dcc517..a908456 100644
--- a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
+++ b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
@@ -163,14 +163,31 @@
     }
 
     /**
-     * Update the URI relative filter groups for a package. All previously existing groups
-     * will be cleared before the new groups will be applied.
+     * Update the URI relative filter groups for a package. The groups set using this API acts
+     * as an additional filtering layer during intent resolution. It does not replace any
+     * existing groups that have been added to the package's intent filters either using the
+     * {@link android.content.IntentFilter#addUriRelativeFilterGroup(UriRelativeFilterGroup)}
+     * API or defined in the manifest.
+     * <p>
+     * Groups can be indexed to any domain or can be indexed for all subdomains by prefixing the
+     * hostname with a wildcard (i.e. "*.example.com"). Priority will be first given to groups
+     * that are indexed to the specific subdomain of the intent's data URI followed by any groups
+     * indexed to wildcard subdomains. If the subdomain consists of more than one label, priority
+     * will decrease corresponding to the decreasing number of subdomain labels after the wildcard.
+     * For example "a.b.c.d" will match "*.b.c.d" before "*.c.d".
+     * <p>
+     * All previously existing groups set for a domain index using this API will be cleared when
+     * new groups are set.
      *
      * @param packageName The name of the package.
      * @param domainToGroupsMap A map of domains to a list of {@link UriRelativeFilterGroup}s that
      *                         should apply to them. Groups for each domain will replace any groups
-     *                         provided for that domain in a prior call to this method. Groups will
+     *                         provided for that domain in a prior call to this method. To clear
+     *                         existing groups, set the list to null or a empty list. Groups will
      *                         be evaluated in the order they are provided.
+     *
+     * @see UriRelativeFilterGroup
+     * @see android.content.IntentFilter
      * @hide
      */
     @SystemApi
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index 305b087..5c8215e 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -16,6 +16,10 @@
 
 package com.android.server.pm.verify.domain;
 
+import static android.content.IntentFilter.WILDCARD;
+
+import static com.android.server.pm.verify.domain.DomainVerificationUtils.isValidDomain;
+
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 
@@ -253,9 +257,18 @@
             Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap =
                     pkgState.getUriRelativeFilterGroupMap();
             for (String domain : bundle.keySet()) {
+                if (!isValidDomain(domain)) {
+                    continue;
+                }
                 ArrayList<UriRelativeFilterGroupParcel> parcels =
                         bundle.getParcelableArrayList(domain, UriRelativeFilterGroupParcel.class);
-                domainToGroupsMap.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+                List<UriRelativeFilterGroup> groups =
+                        UriRelativeFilterGroup.parcelsToGroups(parcels);
+                if (groups == null || groups.isEmpty()) {
+                    domainToGroupsMap.remove(domain);
+                } else {
+                    domainToGroupsMap.put(domain, groups);
+                }
             }
         }
     }
@@ -273,9 +286,11 @@
                 Map<String, List<UriRelativeFilterGroup>> map =
                         pkgState.getUriRelativeFilterGroupMap();
                 for (int i = 0; i < domains.size(); i++) {
-                    List<UriRelativeFilterGroup> groups = map.get(domains.get(i));
-                    bundle.putParcelableList(domains.get(i),
-                            UriRelativeFilterGroup.groupsToParcels(groups));
+                    if (map.containsKey(domains.get(i))) {
+                        List<UriRelativeFilterGroup> groups = map.get(domains.get(i));
+                        bundle.putParcelableList(domains.get(i),
+                                UriRelativeFilterGroup.groupsToParcels(groups));
+                    }
                 }
             }
         }
@@ -285,15 +300,29 @@
     @NonNull
     private List<UriRelativeFilterGroup> getUriRelativeFilterGroups(@NonNull String packageName,
             @NonNull String domain) {
-        List<UriRelativeFilterGroup> groups = Collections.emptyList();
+        List<UriRelativeFilterGroup> groups;
         synchronized (mLock) {
             DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
             if (pkgState != null) {
-                groups = pkgState.getUriRelativeFilterGroupMap().getOrDefault(domain,
-                        Collections.emptyList());
+                Map<String, List<UriRelativeFilterGroup>> groupMap =
+                        pkgState.getUriRelativeFilterGroupMap();
+                groups = groupMap.get(domain);
+                if (groups != null) {
+                    return groups;
+                }
+                int first = domain.indexOf(".");
+                int second = domain.indexOf('.', first + 1);
+                while (first > 0 && second > 0) {
+                    groups = groupMap.get(WILDCARD + domain.substring(first));
+                    if (groups != null) {
+                        return groups;
+                    }
+                    first = second;
+                    second = domain.indexOf('.', second + 1);
+                }
             }
         }
-        return groups;
+        return Collections.emptyList();
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java
index 3fd00c6..b8c4d22 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java
@@ -35,6 +35,9 @@
 
 public final class DomainVerificationUtils {
 
+    public static final int MAX_DOMAIN_LENGTH = 254;
+    public static final int MAX_DOMAIN_LABEL_LENGTH = 63;
+
     private static final ThreadLocal<Matcher> sCachedMatcher = ThreadLocal.withInitial(
             () -> Patterns.DOMAIN_NAME.matcher(""));
 
@@ -108,4 +111,41 @@
         appInfo.targetSdkVersion = pkg.getTargetSdkVersion();
         return appInfo;
     }
+
+    static boolean isValidDomain(String domain) {
+        if (domain.length() > MAX_DOMAIN_LENGTH || domain.equals("*")) {
+            return false;
+        }
+        if (domain.charAt(0) == '*') {
+            if (domain.charAt(1) != '.') {
+                return false;
+            }
+            domain = domain.substring(2);
+        }
+        int labels = 1;
+        int labelStart = -1;
+        for (int i = 0; i < domain.length(); i++) {
+            char c = domain.charAt(i);
+            if (c == '.') {
+                int labelLength = i - labelStart - 1;
+                if (labelLength == 0 || labelLength > MAX_DOMAIN_LABEL_LENGTH) {
+                    return false;
+                }
+                labelStart = i;
+                labels += 1;
+            } else if (!isValidDomainChar(c)) {
+                return false;
+            }
+        }
+        int lastLabelLength = domain.length() - labelStart - 1;
+        if (lastLabelLength == 0 || lastLabelLength > 63) {
+            return false;
+        }
+        return labels > 1;
+    }
+
+    private static boolean isValidDomainChar(char c) {
+        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+                || (c >= '0' && c <= '9') || c == '-';
+    }
 }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
index 66e0717..c54a94e 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
@@ -83,11 +83,7 @@
         }
 
         val bundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1, DOMAIN_2))
-        assertThat(bundle.keySet()).containsExactlyElementsIn(listOf(DOMAIN_1, DOMAIN_2))
-        assertThat(bundle.getParcelableArrayList(DOMAIN_1, UriRelativeFilterGroup::class.java))
-            .isEmpty()
-        assertThat(bundle.getParcelableArrayList(DOMAIN_2, UriRelativeFilterGroup::class.java))
-            .isEmpty()
+        assertThat(bundle.keySet()).isEmpty()
 
         val pathGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
         pathGroup.addUriRelativeFilter(