Add <uri-relative-filter-group> to intent filters.
Introduce new <uri-relative-filter-group> tag to define a new matching
rule where all matchers in the group must match. Each rule group will be
evaluated in the order they are defined in the manifest. These rules
will only be evaluated if no matches found in mDataPaths.
Each URI relative filter group can define matchers for URI fragments and
queries in addtion to paths.
Bug: 307556883
Test: atest CtsContentTestCases:IntentFilterTest
Test: atest PackageParserTest
Change-Id: Ida1c7a1440b99fd5af2913e5c10c6886c511256f
diff --git a/core/api/current.txt b/core/api/current.txt
index 4f5e519..6f89837 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -442,6 +442,7 @@
field public static final int alertDialogTheme = 16843529; // 0x1010309
field public static final int alignmentMode = 16843642; // 0x101037a
field public static final int allContactsName = 16843468; // 0x10102cc
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int allow;
field public static final int allowAudioPlaybackCapture = 16844289; // 0x1010601
field public static final int allowBackup = 16843392; // 0x1010280
field public static final int allowClearUserData = 16842757; // 0x1010005
@@ -845,6 +846,7 @@
field public static final int format24Hour = 16843723; // 0x10103cb
field public static final int fraction = 16843992; // 0x10104d8
field public static final int fragment = 16843491; // 0x10102e3
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentAdvancedPattern;
field public static final int fragmentAllowEnterTransitionOverlap = 16843976; // 0x10104c8
field public static final int fragmentAllowReturnTransitionOverlap = 16843977; // 0x10104c9
field public static final int fragmentCloseEnterAnimation = 16843495; // 0x10102e7
@@ -855,10 +857,13 @@
field public static final int fragmentFadeExitAnimation = 16843498; // 0x10102ea
field public static final int fragmentOpenEnterAnimation = 16843493; // 0x10102e5
field public static final int fragmentOpenExitAnimation = 16843494; // 0x10102e6
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentPattern;
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentPrefix;
field public static final int fragmentReenterTransition = 16843975; // 0x10104c7
field public static final int fragmentReturnTransition = 16843973; // 0x10104c5
field public static final int fragmentSharedElementEnterTransition = 16843972; // 0x10104c4
field public static final int fragmentSharedElementReturnTransition = 16843974; // 0x10104c6
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentSuffix;
field public static final int freezesText = 16843116; // 0x101016c
field public static final int fromAlpha = 16843210; // 0x10101ca
field public static final int fromDegrees = 16843187; // 0x10101b3
@@ -1327,10 +1332,15 @@
field public static final int propertyYName = 16843893; // 0x1010475
field public static final int protectionLevel = 16842761; // 0x1010009
field public static final int publicKey = 16843686; // 0x10103a6
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int query;
field public static final int queryActionMsg = 16843227; // 0x10101db
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int queryAdvancedPattern;
field public static final int queryAfterZeroResults = 16843394; // 0x1010282
field public static final int queryBackground = 16843911; // 0x1010487
field public static final int queryHint = 16843608; // 0x1010358
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int queryPattern;
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int queryPrefix;
+ field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int querySuffix;
field public static final int quickContactBadgeStyleSmallWindowLarge = 16843443; // 0x10102b3
field public static final int quickContactBadgeStyleSmallWindowMedium = 16843442; // 0x10102b2
field public static final int quickContactBadgeStyleSmallWindowSmall = 16843441; // 0x10102b1
@@ -11385,10 +11395,12 @@
method public final void addDataScheme(String);
method public final void addDataSchemeSpecificPart(String, int);
method public final void addDataType(String) throws android.content.IntentFilter.MalformedMimeTypeException;
+ method @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final void addUriRelativeFilterGroup(@NonNull android.content.UriRelativeFilterGroup);
method @NonNull public java.util.function.Predicate<android.content.Intent> asPredicate();
method @NonNull public java.util.function.Predicate<android.content.Intent> asPredicateWithTypeResolution(@NonNull android.content.ContentResolver);
method public final java.util.Iterator<android.content.IntentFilter.AuthorityEntry> authoritiesIterator();
method public final java.util.Iterator<java.lang.String> categoriesIterator();
+ method @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final void clearUriRelativeFilterGroups();
method public final int countActions();
method public final int countCategories();
method public final int countDataAuthorities();
@@ -11396,6 +11408,7 @@
method public final int countDataSchemeSpecificParts();
method public final int countDataSchemes();
method public final int countDataTypes();
+ method @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final int countUriRelativeFilterGroups();
method public static android.content.IntentFilter create(String, String);
method public final int describeContents();
method public void dump(android.util.Printer, String);
@@ -11407,6 +11420,7 @@
method public final android.os.PatternMatcher getDataSchemeSpecificPart(int);
method public final String getDataType(int);
method public final int getPriority();
+ method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @NonNull public final android.content.UriRelativeFilterGroup getUriRelativeFilterGroup(int);
method public final boolean hasAction(String);
method public final boolean hasCategory(String);
method public final boolean hasDataAuthority(android.net.Uri);
@@ -11828,6 +11842,27 @@
field public static final long INVALID_TIME = -9223372036854775808L; // 0x8000000000000000L
}
+ @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final class UriRelativeFilter {
+ ctor public UriRelativeFilter(int, int, @NonNull String);
+ method @NonNull public String getFilter();
+ method public int getPatternType();
+ method public int getUriPart();
+ method public boolean matchData(@NonNull android.net.Uri);
+ field public static final int FRAGMENT = 2; // 0x2
+ field public static final int PATH = 0; // 0x0
+ field public static final int QUERY = 1; // 0x1
+ }
+
+ @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final class UriRelativeFilterGroup {
+ ctor public UriRelativeFilterGroup(int);
+ method public void addUriRelativeFilter(@NonNull android.content.UriRelativeFilter);
+ method public int getAction();
+ method @NonNull public java.util.Collection<android.content.UriRelativeFilter> getUriRelativeFilters();
+ method public boolean matchData(@NonNull android.net.Uri);
+ field public static final int ACTION_ALLOW = 0; // 0x0
+ field public static final int ACTION_BLOCK = 1; // 0x1
+ }
+
}
package android.content.om {
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index ad3acd7..79af65a 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -16,11 +16,13 @@
package android.content;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
+import android.content.pm.Flags;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -175,6 +177,7 @@
private static final String ACTION_STR = "action";
private static final String AUTO_VERIFY_STR = "autoVerify";
private static final String EXTRAS_STR = "extras";
+ private static final String URI_RELATIVE_FILTER_GROUP_STR = "uriRelativeFilterGroup";
private static final int[] EMPTY_INT_ARRAY = new int[0];
private static final long[] EMPTY_LONG_ARRAY = new long[0];
@@ -324,6 +327,7 @@
private ArrayList<PatternMatcher> mDataSchemeSpecificParts = null;
private ArrayList<AuthorityEntry> mDataAuthorities = null;
private ArrayList<PatternMatcher> mDataPaths = null;
+ private ArrayList<UriRelativeFilterGroup> mUriRelativeFilterGroups = null;
private ArrayList<String> mStaticDataTypes = null;
private ArrayList<String> mDataTypes = null;
private ArrayList<String> mMimeGroups = null;
@@ -520,6 +524,10 @@
if (o.mDataPaths != null) {
mDataPaths = new ArrayList<PatternMatcher>(o.mDataPaths);
}
+ if (o.mUriRelativeFilterGroups != null) {
+ mUriRelativeFilterGroups =
+ new ArrayList<UriRelativeFilterGroup>(o.mUriRelativeFilterGroups);
+ }
if (o.mMimeGroups != null) {
mMimeGroups = new ArrayList<String>(o.mMimeGroups);
}
@@ -1563,6 +1571,63 @@
}
/**
+ * Add a new URI relative filter group to match against the Intent data. The
+ * intent filter must include one or more schemes (via {@link #addDataScheme})
+ * <em>and</em> one or more authorities (via {@link #addDataAuthority}) for
+ * the group to be considered.
+ *
+ * <p>Groups will be matched in the order they were added and matching will only
+ * be done if no data paths match or if none are included. If both data paths and
+ * groups are not included, then only the scheme/authority must match.</p>
+ *
+ * @param group A {@link UriRelativeFilterGroup} to match the URI.
+ *
+ * @see UriRelativeFilterGroup
+ * @see #matchData
+ * @see #addDataScheme
+ * @see #addDataAuthority
+ */
+ @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ public final void addUriRelativeFilterGroup(@NonNull UriRelativeFilterGroup group) {
+ Objects.requireNonNull(group);
+ if (mUriRelativeFilterGroups == null) {
+ mUriRelativeFilterGroups = new ArrayList<>();
+ }
+ mUriRelativeFilterGroups.add(group);
+ }
+
+ /**
+ * Return the number of URI relative filter groups in the intent filter.
+ */
+ @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ public final int countUriRelativeFilterGroups() {
+ return mUriRelativeFilterGroups == null ? 0 : mUriRelativeFilterGroups.size();
+ }
+
+ /**
+ * Return a URI relative filter group in the intent filter.
+ *
+ * <p>Note: use of this method will result in a NullPointerException
+ * if no groups exists for this intent filter.</p>
+ *
+ * @param index index of the element to return
+ * @throws IndexOutOfBoundsException if index is out of range
+ */
+ @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ @NonNull
+ public final UriRelativeFilterGroup getUriRelativeFilterGroup(int index) {
+ return mUriRelativeFilterGroups.get(index);
+ }
+
+ /**
+ * Removes all existing URI relative filter groups in the intent filter.
+ */
+ @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ public final void clearUriRelativeFilterGroups() {
+ mUriRelativeFilterGroups = null;
+ }
+
+ /**
* Match this intent filter against the given Intent data. This ignores
* the data scheme -- unlike {@link #matchData}, the authority will match
* regardless of whether there is a matching scheme.
@@ -1677,12 +1742,24 @@
int authMatch = matchDataAuthority(data, wildcardSupported);
if (authMatch >= 0) {
final ArrayList<PatternMatcher> paths = mDataPaths;
- if (paths == null) {
- match = authMatch;
- } else if (hasDataPath(data.getPath(), wildcardSupported)) {
- match = MATCH_CATEGORY_PATH;
+ final ArrayList<UriRelativeFilterGroup> groups = mUriRelativeFilterGroups;
+ if (Flags.relativeReferenceIntentFilters()) {
+ if (paths == null && groups == null) {
+ match = authMatch;
+ } else if (hasDataPath(data.getPath(), wildcardSupported)
+ || matchRelRefGroups(data)) {
+ match = MATCH_CATEGORY_PATH;
+ } else {
+ return NO_MATCH_DATA;
+ }
} else {
- return NO_MATCH_DATA;
+ if (paths == null) {
+ match = authMatch;
+ } else if (hasDataPath(data.getPath(), wildcardSupported)) {
+ match = MATCH_CATEGORY_PATH;
+ } else {
+ return NO_MATCH_DATA;
+ }
}
} else {
return NO_MATCH_DATA;
@@ -1726,6 +1803,19 @@
return match + MATCH_ADJUSTMENT_NORMAL;
}
+ private boolean matchRelRefGroups(Uri data) {
+ if (mUriRelativeFilterGroups == null) {
+ return false;
+ }
+ for (int i = 0; i < mUriRelativeFilterGroups.size(); i++) {
+ UriRelativeFilterGroup group = mUriRelativeFilterGroups.get(i);
+ if (group.matchData(data)) {
+ return group.getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
+ }
+ }
+ return false;
+ }
+
/**
* Add a new Intent category to match against. The semantics of
* categories is the opposite of actions -- an Intent includes the
@@ -2486,6 +2576,12 @@
}
serializer.endTag(null, EXTRAS_STR);
}
+ if (Flags.relativeReferenceIntentFilters()) {
+ N = countUriRelativeFilterGroups();
+ for (int i = 0; i < N; i++) {
+ mUriRelativeFilterGroups.get(i).writeToXml(serializer);
+ }
+ }
}
/**
@@ -2614,6 +2710,9 @@
}
} else if (tagName.equals(EXTRAS_STR)) {
mExtras = PersistableBundle.restoreFromXml(parser);
+ } else if (Flags.relativeReferenceIntentFilters()
+ && URI_RELATIVE_FILTER_GROUP_STR.equals(tagName)) {
+ addUriRelativeFilterGroup(new UriRelativeFilterGroup(parser));
} else {
Log.w("IntentFilter", "Unknown tag parsing IntentFilter: " + tagName);
}
@@ -2680,6 +2779,12 @@
if (mExtras != null) {
mExtras.dumpDebug(proto, IntentFilterProto.EXTRAS);
}
+ if (Flags.relativeReferenceIntentFilters() && mUriRelativeFilterGroups != null) {
+ Iterator<UriRelativeFilterGroup> it = mUriRelativeFilterGroups.iterator();
+ while (it.hasNext()) {
+ it.next().dumpDebug(proto, IntentFilterProto.URI_RELATIVE_FILTER_GROUPS);
+ }
+ }
proto.end(token);
}
@@ -2744,6 +2849,15 @@
du.println(sb.toString());
}
}
+ if (mUriRelativeFilterGroups != null) {
+ Iterator<UriRelativeFilterGroup> it = mUriRelativeFilterGroups.iterator();
+ while (it.hasNext()) {
+ sb.setLength(0);
+ sb.append(prefix); sb.append("UriRelativeFilterGroup: \"");
+ sb.append(it.next()); sb.append("\"");
+ du.println(sb.toString());
+ }
+ }
if (mStaticDataTypes != null) {
Iterator<String> it = mStaticDataTypes.iterator();
while (it.hasNext()) {
@@ -2883,6 +2997,15 @@
} else {
dest.writeInt(0);
}
+ if (Flags.relativeReferenceIntentFilters() && mUriRelativeFilterGroups != null) {
+ final int N = mUriRelativeFilterGroups.size();
+ dest.writeInt(N);
+ for (int i = 0; i < N; i++) {
+ mUriRelativeFilterGroups.get(i).writeToParcel(dest, flags);
+ }
+ } else {
+ dest.writeInt(0);
+ }
}
/**
@@ -2989,6 +3112,13 @@
if (source.readInt() != 0) {
mExtras = PersistableBundle.CREATOR.createFromParcel(source);
}
+ N = source.readInt();
+ if (Flags.relativeReferenceIntentFilters() && N > 0) {
+ mUriRelativeFilterGroups = new ArrayList<UriRelativeFilterGroup>(N);
+ for (int i = 0; i < N; i++) {
+ mUriRelativeFilterGroups.add(new UriRelativeFilterGroup(source));
+ }
+ }
}
private boolean hasPartialTypes() {
diff --git a/core/java/android/content/UriRelativeFilter.java b/core/java/android/content/UriRelativeFilter.java
new file mode 100644
index 0000000..9866cd0
--- /dev/null
+++ b/core/java/android/content/UriRelativeFilter.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.Flags;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.PatternMatcher;
+import android.util.proto.ProtoOutputStream;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A filter for matching Intent URI Data as part of a
+ * {@link UriRelativeFilterGroup}. A single filter can only be
+ * matched against either a URI path, query or fragment
+ */
+@FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+public final class UriRelativeFilter {
+ private static final String FILTER_STR = "filter";
+ private static final String PART_STR = "part";
+ private static final String PATTERN_STR = "pattern";
+ static final String URI_RELATIVE_FILTER_STR = "uriRelativeFilter";
+
+ /**
+ * Value to indicate that the filter is to be applied to a URI path.
+ */
+ public static final int PATH = 0;
+ /**
+ * Value to indicate that the filter is to be applied to a URI query.
+ */
+ public static final int QUERY = 1;
+ /**
+ * Value to indicate that the filter is to be applied to a URI fragment.
+ */
+ public static final int FRAGMENT = 2;
+
+ /** @hide */
+ @IntDef(value = {
+ PATH,
+ QUERY,
+ FRAGMENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UriPart {}
+
+ private final @UriPart int mUriPart;
+ private final @PatternMatcher.PatternType int mPatternType;
+ private final String mFilter;
+
+ /**
+ * Creates a new UriRelativeFilter.
+ *
+ * @param uriPart The URI part this filter operates on. Can be either a
+ * {@link UriRelativeFilter#PATH}, {@link UriRelativeFilter#QUERY},
+ * or {@link UriRelativeFilter#FRAGMENT}.
+ * @param patternType The pattern type of the filter. Can be either a
+ * {@link PatternMatcher#PATTERN_LITERAL},
+ * {@link PatternMatcher#PATTERN_PREFIX},
+* {@link PatternMatcher#PATTERN_SUFFIX},
+ * {@link PatternMatcher#PATTERN_SIMPLE_GLOB},
+ * or {@link PatternMatcher#PATTERN_ADVANCED_GLOB}.
+ * @param filter A literal or pattern string depedning on patterType
+ * used to match a uriPart .
+ */
+ public UriRelativeFilter(
+ @UriPart int uriPart,
+ @PatternMatcher.PatternType int patternType,
+ @NonNull String filter) {
+ mUriPart = uriPart;
+ com.android.internal.util.AnnotationValidations.validate(
+ UriPart.class, null, mUriPart);
+ mPatternType = patternType;
+ com.android.internal.util.AnnotationValidations.validate(
+ PatternMatcher.PatternType.class, null, mPatternType);
+ mFilter = filter;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mFilter);
+ }
+
+ /**
+ * The URI part this filter operates on.
+ */
+ public @UriPart int getUriPart() {
+ return mUriPart;
+ }
+
+ /**
+ * The pattern type of the filter.
+ */
+ public @PatternMatcher.PatternType int getPatternType() {
+ return mPatternType;
+ }
+
+ /**
+ * The string used to filter the URI.
+ */
+ public @NonNull String getFilter() {
+ return mFilter;
+ }
+
+ /**
+ * Match this URI filter against an Intent's data. QUERY filters can
+ * match against any key value pair in the query string. PATH and
+ * FRAGMENT filters must match the entire string.
+ *
+ * @param data The full data string to match against, as supplied in
+ * Intent.data.
+ *
+ * @return true if there is a match.
+ */
+ public boolean matchData(@NonNull Uri data) {
+ PatternMatcher pe = new PatternMatcher(mFilter, mPatternType);
+ switch (getUriPart()) {
+ case PATH:
+ return pe.match(data.getPath());
+ case QUERY:
+ return matchQuery(pe, data.getQuery());
+ case FRAGMENT:
+ return pe.match(data.getFragment());
+ default:
+ return false;
+ }
+ }
+
+ private boolean matchQuery(PatternMatcher pe, String query) {
+ if (query != null) {
+ String[] params = query.split("&");
+ if (params.length == 1) {
+ params = query.split(";");
+ }
+ for (int i = 0; i < params.length; i++) {
+ if (pe.match(params[i])) return true;
+ }
+ }
+ return false;
+ }
+
+ /** @hide */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ long token = proto.start(fieldId);
+ proto.write(UriRelativeFilterProto.URI_PART, mUriPart);
+ proto.write(UriRelativeFilterProto.PATTERN_TYPE, mPatternType);
+ proto.write(UriRelativeFilterProto.FILTER, mFilter);
+ proto.end(token);
+ }
+
+ /** @hide */
+ public void writeToXml(XmlSerializer serializer) throws IOException {
+ serializer.startTag(null, URI_RELATIVE_FILTER_STR);
+ serializer.attribute(null, PATTERN_STR, Integer.toString(mPatternType));
+ serializer.attribute(null, PART_STR, Integer.toString(mUriPart));
+ serializer.attribute(null, FILTER_STR, mFilter);
+ serializer.endTag(null, URI_RELATIVE_FILTER_STR);
+ }
+
+ private String uriPartToString() {
+ switch (mUriPart) {
+ case PATH:
+ return "PATH";
+ case QUERY:
+ return "QUERY";
+ case FRAGMENT:
+ return "FRAGMENT";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private String patternTypeToString() {
+ switch (mPatternType) {
+ case PatternMatcher.PATTERN_LITERAL:
+ return "LITERAL";
+ case PatternMatcher.PATTERN_PREFIX:
+ return "PREFIX";
+ case PatternMatcher.PATTERN_SIMPLE_GLOB:
+ return "GLOB";
+ case PatternMatcher.PATTERN_ADVANCED_GLOB:
+ return "ADVANCED_GLOB";
+ case PatternMatcher.PATTERN_SUFFIX:
+ return "SUFFIX";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "UriRelativeFilter { "
+ + "uriPart = " + uriPartToString() + ", "
+ + "patternType = " + patternTypeToString() + ", "
+ + "filter = " + mFilter
+ + " }";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ UriRelativeFilter that = (UriRelativeFilter) o;
+ return mUriPart == that.mUriPart
+ && mPatternType == that.mPatternType
+ && java.util.Objects.equals(mFilter, that.mFilter);
+ }
+
+ @Override
+ public int hashCode() {
+ int _hash = 1;
+ _hash = 31 * _hash + mUriPart;
+ _hash = 31 * _hash + mPatternType;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mFilter);
+ return _hash;
+ }
+
+ /** @hide */
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mUriPart);
+ dest.writeInt(mPatternType);
+ dest.writeString(mFilter);
+ }
+
+ /** @hide */
+ UriRelativeFilter(@NonNull android.os.Parcel in) {
+ mUriPart = in.readInt();
+ mPatternType = in.readInt();
+ mFilter = in.readString();
+ }
+
+ /** @hide */
+ public UriRelativeFilter(XmlPullParser parser) throws XmlPullParserException, IOException {
+ mUriPart = Integer.parseInt(parser.getAttributeValue(null, PART_STR));
+ mPatternType = Integer.parseInt(parser.getAttributeValue(null, PATTERN_STR));
+ mFilter = parser.getAttributeValue(null, FILTER_STR);
+ }
+}
diff --git a/core/java/android/content/UriRelativeFilterGroup.java b/core/java/android/content/UriRelativeFilterGroup.java
new file mode 100644
index 0000000..72c396a
--- /dev/null
+++ b/core/java/android/content/UriRelativeFilterGroup.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.pm.Flags;
+import android.net.Uri;
+import android.os.Parcel;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * An intent data matching group based on a URI's relative reference which
+ * includes the path, query and fragment. The group is only considered as
+ * matching if <em>all</em> UriRelativeFilters in the group match. Each
+ * UriRelativeFilter defines a matching rule for a URI path, query or fragment.
+ * A group must contain one or more UriRelativeFilters to match but does not need to
+ * contain UriRelativeFilters for all existing parts of a URI to match.
+ *
+ * <p>For example, given a URI that contains path, query and fragment parts,
+ * a group containing only a path filter will match the URI if the path
+ * filter matches the URI path. If the group contains a path and query
+ * filter, then the group will only match if both path and query filters
+ * match. If a URI contains only a path with no query or fragment then a
+ * group can only match if it contains only a matching path filter. If the
+ * group also contained additional query or fragment filters then it will
+ * not match.</p>
+ */
+@FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+public final class UriRelativeFilterGroup {
+ private static final String ALLOW_STR = "allow";
+ private static final String URI_RELATIVE_FILTER_GROUP_STR = "uriRelativeFilterGroup";
+
+ /**
+ * Value to indicate that the group match is allowed.
+ */
+ public static final int ACTION_ALLOW = 0;
+ /**
+ * Value to indicate that the group match is blocked.
+ */
+ public static final int ACTION_BLOCK = 1;
+
+ /** @hide */
+ @IntDef(value = {
+ ACTION_ALLOW,
+ ACTION_BLOCK
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Action {}
+
+ private final @Action int mAction;
+ private final ArraySet<UriRelativeFilter> mUriRelativeFilters = new ArraySet<>();
+
+ /**
+ * New UriRelativeFilterGroup that matches a Intent data.
+ *
+ * @param action Whether this matching group should be allowed or disallowed.
+ */
+ public UriRelativeFilterGroup(@Action int action) {
+ mAction = action;
+ }
+
+ /** @hide */
+ public UriRelativeFilterGroup(XmlPullParser parser) throws XmlPullParserException, IOException {
+ mAction = Integer.parseInt(parser.getAttributeValue(null, ALLOW_STR));
+
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG
+ || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ String tagName = parser.getName();
+ if (tagName.equals(UriRelativeFilter.URI_RELATIVE_FILTER_STR)) {
+ addUriRelativeFilter(new UriRelativeFilter(parser));
+ } else {
+ Log.w("IntentFilter", "Unknown tag parsing IntentFilter: " + tagName);
+ }
+ XmlUtils.skipCurrentTag(parser);
+ }
+ }
+
+ /**
+ * Return {@link UriRelativeFilterGroup#ACTION_ALLOW} if a URI is allowed when matched
+ * and {@link UriRelativeFilterGroup#ACTION_BLOCK} if a URI is blacked when matched.
+ */
+ public @Action int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Add a filter to the group.
+ */
+ public void addUriRelativeFilter(@NonNull UriRelativeFilter uriRelativeFilter) {
+ Objects.requireNonNull(uriRelativeFilter);
+ if (!CollectionUtils.contains(mUriRelativeFilters, uriRelativeFilter)) {
+ mUriRelativeFilters.add(uriRelativeFilter);
+ }
+ }
+
+ /**
+ * Returns a unmodifiable view of the UriRelativeFilters list in this group.
+ */
+ @NonNull
+ public Collection<UriRelativeFilter> getUriRelativeFilters() {
+ return Collections.unmodifiableCollection(mUriRelativeFilters);
+ }
+
+ /**
+ * Match all URI filter in this group against {@link Intent#getData()}.
+ *
+ * @param data The full data string to match against, as supplied in
+ * Intent.data.
+ * @return true if all filters match.
+ */
+ public boolean matchData(@NonNull Uri data) {
+ if (mUriRelativeFilters.size() == 0) {
+ return false;
+ }
+ for (UriRelativeFilter filter : mUriRelativeFilters) {
+ if (!filter.matchData(data)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** @hide */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ long token = proto.start(fieldId);
+ proto.write(UriRelativeFilterGroupProto.ACTION, mAction);
+ Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator();
+ while (it.hasNext()) {
+ it.next().dumpDebug(proto, UriRelativeFilterGroupProto.URI_RELATIVE_FILTERS);
+ }
+ proto.end(token);
+ }
+
+ /** @hide */
+ public void writeToXml(XmlSerializer serializer) throws IOException {
+ serializer.startTag(null, URI_RELATIVE_FILTER_GROUP_STR);
+ serializer.attribute(null, ALLOW_STR, Integer.toString(mAction));
+ Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator();
+ while (it.hasNext()) {
+ UriRelativeFilter filter = it.next();
+ filter.writeToXml(serializer);
+ }
+ serializer.endTag(null, URI_RELATIVE_FILTER_GROUP_STR);
+ }
+
+ @Override
+ public String toString() {
+ return "UriRelativeFilterGroup { allow = " + mAction
+ + ", uri_filters = " + mUriRelativeFilters + ", }";
+ }
+
+ /** @hide */
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mAction);
+ final int n = mUriRelativeFilters.size();
+ if (n > 0) {
+ dest.writeInt(n);
+ Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator();
+ while (it.hasNext()) {
+ it.next().writeToParcel(dest, flags);
+ }
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ /** @hide */
+ UriRelativeFilterGroup(@NonNull Parcel src) {
+ mAction = src.readInt();
+ final int n = src.readInt();
+ for (int i = 0; i < n; i++) {
+ mUriRelativeFilters.add(new UriRelativeFilter(src));
+ }
+ }
+}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index e4e9fba..cbf5274 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -110,6 +110,14 @@
}
flag {
+ name: "relative_reference_intent_filters"
+ namespace: "package_manager_service"
+ description: "Feature flag to enable relative reference intent filters"
+ bug: "307556883"
+ is_fixed_read_only: true
+}
+
+flag {
name: "fix_duplicated_flags"
namespace: "package_manager_service"
description: "Feature flag to fix duplicated PackageManager flag values"
diff --git a/core/java/android/os/PatternMatcher.java b/core/java/android/os/PatternMatcher.java
index b5425b4..79a2c59 100644
--- a/core/java/android/os/PatternMatcher.java
+++ b/core/java/android/os/PatternMatcher.java
@@ -16,9 +16,12 @@
package android.os;
+import android.annotation.IntDef;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
/**
@@ -68,6 +71,17 @@
*/
public static final int PATTERN_SUFFIX = 4;
+ /** @hide */
+ @IntDef(value = {
+ PATTERN_LITERAL,
+ PATTERN_PREFIX,
+ PATTERN_SIMPLE_GLOB,
+ PATTERN_ADVANCED_GLOB,
+ PATTERN_SUFFIX,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PatternType {}
+
// token types for advanced matching
private static final int TOKEN_TYPE_LITERAL = 0;
private static final int TOKEN_TYPE_ANY = 1;
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java
index c6683cf..05728ee 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java
@@ -18,9 +18,13 @@
import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.UriRelativeFilter;
+import android.content.UriRelativeFilterGroup;
+import android.content.pm.Flags;
import android.content.pm.parsing.result.ParseInput;
import android.content.pm.parsing.result.ParseResult;
import android.content.res.Resources;
@@ -132,6 +136,11 @@
case "data":
result = parseData(intentInfo, res, parser, allowGlobs, input);
break;
+ case "uri-relative-filter-group":
+ if (Flags.relativeReferenceIntentFilters()) {
+ result = parseRelRefGroup(intentInfo, pkg, res, parser, allowGlobs, input);
+ break;
+ }
default:
result = ParsingUtils.unknownTag("<intent-filter>", pkg, parser, input);
break;
@@ -163,6 +172,197 @@
}
@NonNull
+ @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ private static ParseResult<ParsedIntentInfo> parseRelRefGroup(ParsedIntentInfo intentInfo,
+ ParsingPackage pkg, Resources res, XmlResourceParser parser, boolean allowGlobs,
+ ParseInput input) throws XmlPullParserException, IOException {
+ IntentFilter intentFilter = intentInfo.getIntentFilter();
+ TypedArray sa = res.obtainAttributes(parser,
+ R.styleable.AndroidManifestUriRelativeFilterGroup);
+ UriRelativeFilterGroup group;
+ try {
+ int action = UriRelativeFilterGroup.ACTION_ALLOW;
+ if (!sa.getBoolean(R.styleable.AndroidManifestUriRelativeFilterGroup_allow, true)) {
+ action = UriRelativeFilterGroup.ACTION_BLOCK;
+ }
+ group = new UriRelativeFilterGroup(action);
+ } finally {
+ sa.recycle();
+ }
+ final int depth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > depth)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ final ParseResult result;
+ String nodeName = parser.getName();
+ switch (nodeName) {
+ case "data":
+ result = parseRelRefGroupData(group, res, parser, allowGlobs, input);
+ break;
+ default:
+ result = ParsingUtils.unknownTag("<uri-relative-filter-group>",
+ pkg, parser, input);
+ break;
+ }
+
+ if (result.isError()) {
+ return input.error(result);
+ }
+ }
+
+ if (group.getUriRelativeFilters().size() > 0) {
+ intentFilter.addUriRelativeFilterGroup(group);
+ }
+ return input.success(null);
+ }
+
+ @NonNull
+ @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ private static ParseResult<ParsedIntentInfo> parseRelRefGroupData(UriRelativeFilterGroup group,
+ Resources res, XmlResourceParser parser, boolean allowGlobs, ParseInput input) {
+ TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestData);
+ try {
+ String str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_path, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH,
+ PatternMatcher.PATTERN_LITERAL, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_pathPrefix, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH,
+ PatternMatcher.PATTERN_PREFIX, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_pathPattern, 0);
+ if (str != null) {
+ if (!allowGlobs) {
+ return input.error(
+ "pathPattern not allowed here; path must be literal");
+ }
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH,
+ PatternMatcher.PATTERN_SIMPLE_GLOB, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_pathAdvancedPattern, 0);
+ if (str != null) {
+ if (!allowGlobs) {
+ return input.error(
+ "pathAdvancedPattern not allowed here; path must be literal");
+ }
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH,
+ PatternMatcher.PATTERN_ADVANCED_GLOB, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_pathSuffix, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH,
+ PatternMatcher.PATTERN_SUFFIX, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_fragment, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT,
+ PatternMatcher.PATTERN_LITERAL, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_fragmentPrefix, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT,
+ PatternMatcher.PATTERN_PREFIX, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_fragmentPattern, 0);
+ if (str != null) {
+ if (!allowGlobs) {
+ return input.error(
+ "fragmentPattern not allowed here; fragment must be literal");
+ }
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT,
+ PatternMatcher.PATTERN_SIMPLE_GLOB, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_fragmentAdvancedPattern, 0);
+ if (str != null) {
+ if (!allowGlobs) {
+ return input.error(
+ "fragmentAdvancedPattern not allowed here; fragment must be literal");
+ }
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT,
+ PatternMatcher.PATTERN_ADVANCED_GLOB, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_fragmentSuffix, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT,
+ PatternMatcher.PATTERN_SUFFIX, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_query, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY,
+ PatternMatcher.PATTERN_LITERAL, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_queryPrefix, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY,
+ PatternMatcher.PATTERN_PREFIX, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_queryPattern, 0);
+ if (str != null) {
+ if (!allowGlobs) {
+ return input.error(
+ "queryPattern not allowed here; query must be literal");
+ }
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY,
+ PatternMatcher.PATTERN_SIMPLE_GLOB, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_queryAdvancedPattern, 0);
+ if (str != null) {
+ if (!allowGlobs) {
+ return input.error(
+ "queryAdvancedPattern not allowed here; query must be literal");
+ }
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY,
+ PatternMatcher.PATTERN_ADVANCED_GLOB, str));
+ }
+
+ str = sa.getNonConfigurationString(
+ R.styleable.AndroidManifestData_querySuffix, 0);
+ if (str != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY,
+ PatternMatcher.PATTERN_SUFFIX, str));
+ }
+
+ return input.success(null);
+ } finally {
+ sa.recycle();
+ }
+ }
+
+ @NonNull
private static ParseResult<ParsedIntentInfo> parseData(ParsedIntentInfo intentInfo,
Resources resources, XmlResourceParser parser, boolean allowGlobs, ParseInput input) {
IntentFilter intentFilter = intentInfo.getIntentFilter();
diff --git a/core/proto/android/content/intent.proto b/core/proto/android/content/intent.proto
index 75e2908..1d1f88b 100644
--- a/core/proto/android/content/intent.proto
+++ b/core/proto/android/content/intent.proto
@@ -66,7 +66,7 @@
optional string identifier = 13 [ (.android.privacy).dest = DEST_EXPLICIT ];
}
-// Next Tag: 12
+// Next Tag: 14
message IntentFilterProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -89,6 +89,7 @@
optional bool get_auto_verify = 10;
repeated string mime_groups = 11;
optional android.os.PersistableBundleProto extras = 12;
+ repeated UriRelativeFilterGroupProto uri_relative_filter_groups = 13;
}
message AuthorityEntryProto {
@@ -98,3 +99,23 @@
optional bool wild = 2;
optional int32 port = 3;
}
+
+message UriRelativeFilterGroupProto {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+ enum Action {
+ ACTION_ALLOW = 0;
+ ACTION_BLOCK = 1;
+ }
+
+ optional Action action = 1;
+ repeated UriRelativeFilterProto uri_relative_filters = 2;
+}
+
+message UriRelativeFilterProto {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+ required int32 uri_part = 1;
+ required int32 pattern_type = 2;
+ required string filter = 3;
+}
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 35276bf..6884fc0 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -3438,6 +3438,20 @@
<!-- Attributes that can be supplied in an AndroidManifest.xml
<code>data</code> tag, a child of the
{@link #AndroidManifestIntentFilter intent-filter} tag, describing
+ a group matching rule consisting of one or more
+ {@link #AndroidManifestData data} tags that must all match. This
+ tag can be specified multiple times to create multiple groups that
+ will be matched in the order they are defined. -->
+ <declare-styleable name="AndroidManifestUriRelativeFilterGroup"
+ parent="AndroidManifestIntentFilter">
+ <!-- Specify if this group is allow rule or disallow rule. If this
+ attribute is not specified then it is assumed to be true -->
+ <attr name="allow" format="boolean"/>
+ </declare-styleable>
+
+ <!-- Attributes that can be supplied in an AndroidManifest.xml
+ <code>data</code> tag, a child of the
+ {@link #AndroidManifestIntentFilter intent-filter} tag, describing
the types of data that match. This tag can be specified multiple
times to supply multiple data options, as described in the
{@link android.content.IntentFilter} class. Note that all such
@@ -3445,7 +3459,8 @@
<code><data android:scheme="myscheme" android:host="me.com" /></code>
is equivalent to <code><data android:scheme="myscheme" />
<data android:host="me.com" /></code>. -->
- <declare-styleable name="AndroidManifestData" parent="AndroidManifestIntentFilter">
+ <declare-styleable name="AndroidManifestData"
+ parent="AndroidManifestIntentFilter AndroidManifestUriRelativeFilterGroup">
<!-- Specify a MIME type that is handled, as per
{@link android.content.IntentFilter#addDataType
IntentFilter.addDataType()}.
@@ -3549,6 +3564,70 @@
IntentFilter.addDataPath()} with
{@link android.os.PatternMatcher#PATTERN_SUFFIX}. -->
<attr name="pathSuffix" />
+ <!-- Specify a URI query that must exactly match, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_LITERAL}. -->
+ <attr name="query" format="string" />
+ <!-- Specify a URI query that must be a prefix to match, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_PREFIX}. -->
+ <attr name="queryPrefix" format="string" />
+ <!-- Specify a URI query that matches a simple pattern, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_SIMPLE_GLOB}.
+ Note that because '\' is used as an escape character when
+ reading the string from XML (before it is parsed as a pattern),
+ you will need to double-escape: for example a literal "*" would
+ be written as "\\*" and a literal "\" would be written as
+ "\\\\". This is basically the same as what you would need to
+ write if constructing the string in Java code. -->
+ <attr name="queryPattern" format="string" />
+ <!-- Specify a URI query that matches an advanced pattern, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_ADVANCED_GLOB}.
+ Note that because '\' is used as an escape character when
+ reading the string from XML (before it is parsed as a pattern),
+ you will need to double-escape: for example a literal "*" would
+ be written as "\\*" and a literal "\" would be written as
+ "\\\\". This is basically the same as what you would need to
+ write if constructing the string in Java code. -->
+ <attr name="queryAdvancedPattern" format="string" />
+ <!-- Specify a URI query that must be a suffix to match, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_SUFFIX}. -->
+ <attr name="querySuffix" format="string" />
+ <!-- Specify a URI fragment that must exactly match, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_LITERAL}. -->
+ <attr name="fragment" format="string" />
+ <!-- Specify a URI fragment that must be a prefix to match, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_PREFIX}. -->
+ <attr name="fragmentPrefix" format="string" />
+ <!-- Specify a URI fragment that matches a simple pattern, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_SIMPLE_GLOB}.
+ Note that because '\' is used as an escape character when
+ reading the string from XML (before it is parsed as a pattern),
+ you will need to double-escape: for example a literal "*" would
+ be written as "\\*" and a literal "\" would be written as
+ "\\\\". This is basically the same as what you would need to
+ write if constructing the string in Java code. -->
+ <attr name="fragmentPattern" format="string" />
+ <!-- Specify a URI fragment that matches an advanced pattern, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_ADVANCED_GLOB}.
+ Note that because '\' is used as an escape character when
+ reading the string from XML (before it is parsed as a pattern),
+ you will need to double-escape: for example a literal "*" would
+ be written as "\\*" and a literal "\" would be written as
+ "\\\\". This is basically the same as what you would need to
+ write if constructing the string in Java code. -->
+ <attr name="fragmentAdvancedPattern" format="string" />
+ <!-- Specify a URI fragment that must be a suffix to match, as a
+ {@link android.content.UriRelativeFilter UriRelativeFilter} with
+ {@link android.os.PatternMatcher#PATTERN_SUFFIX}. -->
+ <attr name="fragmentSuffix" format="string" />
</declare-styleable>
<!-- Attributes that can be supplied in an AndroidManifest.xml
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index b8fc052..830e99c 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -123,6 +123,26 @@
<public name="featureFlag"/>
<!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") -->
<public name="systemUserOnly"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="allow"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="query"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="queryPrefix"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="queryPattern"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="queryAdvancedPattern"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="querySuffix"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="fragmentPrefix"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="fragmentPattern"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="fragmentAdvancedPattern"/>
+ <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") -->
+ <public name="fragmentSuffix"/>
</staging-public-group>
<staging-public-group type="id" first-id="0x01bc0000">
diff --git a/nfc/jarjar-rules.txt b/nfc/jarjar-rules.txt
index 4cd652d..99ae144 100644
--- a/nfc/jarjar-rules.txt
+++ b/nfc/jarjar-rules.txt
@@ -4,6 +4,7 @@
rule android.content.IntentProto* com.android.nfc.x.@0
rule android.content.IntentFilterProto* com.android.nfc.x.@0
rule android.content.AuthorityEntryProto* com.android.nfc.x.@0
+rule android.content.UriRelativeFilter* com.android.nfc.x.@0
rule android.nfc.cardemulation.AidGroupProto* com.android.nfc.x.@0
rule android.nfc.cardemulation.ApduServiceInfoProto* com.android.nfc.x.@0
rule android.nfc.cardemulation.NfcFServiceInfoProto* com.android.nfc.x.@0
diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp
index 3aca1ca..f8accc3 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -103,6 +103,7 @@
":PackageParserTestApp4",
":PackageParserTestApp5",
":PackageParserTestApp6",
+ ":PackageParserTestApp7",
],
resource_zips: [":PackageManagerServiceServerTests_apks_as_resources"],
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
index 71f5c75..a0e0e1e 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
@@ -15,6 +15,17 @@
*/
package com.android.server.pm;
+import static android.content.UriRelativeFilter.PATH;
+import static android.content.UriRelativeFilter.QUERY;
+import static android.content.UriRelativeFilter.FRAGMENT;
+import static android.content.UriRelativeFilterGroup.ACTION_ALLOW;
+import static android.content.UriRelativeFilterGroup.ACTION_BLOCK;
+import static android.os.PatternMatcher.PATTERN_ADVANCED_GLOB;
+import static android.os.PatternMatcher.PATTERN_LITERAL;
+import static android.os.PatternMatcher.PATTERN_PREFIX;
+import static android.os.PatternMatcher.PATTERN_SIMPLE_GLOB;
+import static android.os.PatternMatcher.PATTERN_SUFFIX;
+
import static com.android.internal.pm.permission.CompatibilityPermissionInfo.COMPAT_PERMS;
import static com.google.common.truth.Truth.assertThat;
@@ -36,11 +47,15 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.content.IntentFilter;
+import android.content.UriRelativeFilter;
+import android.content.UriRelativeFilterGroup;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ConfigurationInfo;
import android.content.pm.FeatureGroupInfo;
import android.content.pm.FeatureInfo;
+import android.content.pm.Flags;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.Property;
import android.content.pm.ServiceInfo;
@@ -50,6 +65,9 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.ArraySet;
import androidx.annotation.Nullable;
@@ -106,6 +124,7 @@
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
@@ -123,6 +142,9 @@
@Rule
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private File mTmpDir;
private static final File FRAMEWORK = new File("/system/framework/framework-res.apk");
private static final String TEST_APP1_APK = "PackageParserTestApp1.apk";
@@ -131,6 +153,7 @@
private static final String TEST_APP4_APK = "PackageParserTestApp4.apk";
private static final String TEST_APP5_APK = "PackageParserTestApp5.apk";
private static final String TEST_APP6_APK = "PackageParserTestApp6.apk";
+ private static final String TEST_APP7_APK = "PackageParserTestApp7.apk";
private static final String PACKAGE_NAME = "com.android.servicestests.apps.packageparserapp";
@Before
@@ -375,6 +398,87 @@
assertNotEquals("$automotive", actualDisplayCategory);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ public void testParseUriRelativeFilterGroups() throws Exception {
+ final File testFile = extractFile(TEST_APP7_APK);
+ try {
+ final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+ final List<ParsedActivity> activities = pkg.getActivities();
+ final List<ParsedIntentInfo> intents = activities.get(0).getIntents();
+ final IntentFilter intentFilter = intents.get(0).getIntentFilter();
+ assertEquals(7, intentFilter.countUriRelativeFilterGroups());
+
+ UriRelativeFilterGroup group = intentFilter.getUriRelativeFilterGroup(0);
+ Collection<UriRelativeFilter> filters = group.getUriRelativeFilters();
+ assertEquals(ACTION_BLOCK, group.getAction());
+ assertEquals(3, filters.size());
+ assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_PREFIX, "/gizmos")));
+ assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_SIMPLE_GLOB,
+ ".*query=string.*")));
+ assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_LITERAL,
+ "fragment")));
+
+ group = intentFilter.getUriRelativeFilterGroup(1);
+ filters = group.getUriRelativeFilters();
+ assertEquals(ACTION_ALLOW, group.getAction());
+ assertEquals(2, filters.size());
+ assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_LITERAL,
+ "query=string")));
+ assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_SUFFIX,
+ "fragment")));
+
+ group = intentFilter.getUriRelativeFilterGroup(2);
+ filters = group.getUriRelativeFilters();
+ assertEquals(ACTION_ALLOW, group.getAction());
+ assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_LITERAL, "/gizmos")));
+ assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_LITERAL,
+ ".*query=string.*")));
+ assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_LITERAL,
+ "fragment")));
+
+ group = intentFilter.getUriRelativeFilterGroup(3);
+ filters = group.getUriRelativeFilters();
+ assertEquals(ACTION_ALLOW, group.getAction());
+ assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_PREFIX, "/gizmos")));
+ assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_PREFIX,
+ ".*query=string.*")));
+ assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_PREFIX,
+ "fragment")));
+
+ group = intentFilter.getUriRelativeFilterGroup(4);
+ filters = group.getUriRelativeFilters();
+ assertEquals(ACTION_ALLOW, group.getAction());
+ assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_SIMPLE_GLOB,
+ "/gizmos")));
+ assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_SIMPLE_GLOB,
+ ".*query=string.*")));
+ assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_SIMPLE_GLOB,
+ "fragment")));
+
+ group = intentFilter.getUriRelativeFilterGroup(5);
+ filters = group.getUriRelativeFilters();
+ assertEquals(ACTION_ALLOW, group.getAction());
+ assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_ADVANCED_GLOB,
+ "/gizmos")));
+ assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_ADVANCED_GLOB,
+ ".*query=string.*")));
+ assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_ADVANCED_GLOB,
+ "fragment")));
+
+ group = intentFilter.getUriRelativeFilterGroup(6);
+ filters = group.getUriRelativeFilters();
+ assertEquals(ACTION_ALLOW, group.getAction());
+ assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_SUFFIX, "/gizmos")));
+ assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_SUFFIX,
+ ".*query=string.*")));
+ assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_SUFFIX,
+ "fragment")));
+ } finally {
+ testFile.delete();
+ }
+ }
+
private static final int PROPERTY_TYPE_BOOLEAN = 1;
private static final int PROPERTY_TYPE_FLOAT = 2;
private static final int PROPERTY_TYPE_INTEGER = 3;
diff --git a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
index 3e78f9a..131b380 100644
--- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
+++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
@@ -102,3 +102,17 @@
resource_dirs: ["res"],
manifest: "AndroidManifestApp6.xml",
}
+
+android_test_helper_app {
+ name: "PackageParserTestApp7",
+ sdk_version: "current",
+ srcs: ["**/*.java"],
+ dex_preopt: {
+ enabled: false,
+ },
+ optimize: {
+ enabled: false,
+ },
+ resource_dirs: ["res"],
+ manifest: "AndroidManifestApp7.xml",
+}
diff --git a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp7.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp7.xml
new file mode 100644
index 0000000..cb87a48
--- /dev/null
+++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp7.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.servicestests.apps.packageparserapp" >
+
+ <application>
+ <activity android:name=".TestActivity"
+ android:exported="true" >
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com" />
+ <uri-relative-filter-group android:allow="false">
+ <data android:pathPrefix="/gizmos" />
+ <data android:queryPattern=".*query=string.*" />
+ <data android:fragment="fragment" />
+ </uri-relative-filter-group>
+ <uri-relative-filter-group>
+ <data android:query="query=string" />
+ <data android:fragmentSuffix="fragment" />
+ </uri-relative-filter-group>
+ <uri-relative-filter-group>
+ <data android:path="/gizmos" />
+ <data android:query=".*query=string.*" />
+ <data android:fragment="fragment" />
+ </uri-relative-filter-group>
+ <uri-relative-filter-group>
+ <data android:pathPrefix="/gizmos" />
+ <data android:queryPrefix=".*query=string.*" />
+ <data android:fragmentPrefix="fragment" />
+ </uri-relative-filter-group>
+ <uri-relative-filter-group>
+ <data android:pathPattern="/gizmos" />
+ <data android:queryPattern=".*query=string.*" />
+ <data android:fragmentPattern="fragment" />
+ </uri-relative-filter-group>
+ <uri-relative-filter-group>
+ <data android:pathAdvancedPattern="/gizmos" />
+ <data android:queryAdvancedPattern=".*query=string.*" />
+ <data android:fragmentAdvancedPattern="fragment" />
+ </uri-relative-filter-group>
+ <uri-relative-filter-group>
+ <data android:pathSuffix="/gizmos" />
+ <data android:querySuffix=".*query=string.*" />
+ <data android:fragmentSuffix="fragment" />
+ </uri-relative-filter-group>
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index 0b16e2c..d03f97e 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -415,6 +415,8 @@
intent_filter_action["action"].Action(RequiredNameIsNotEmpty);
intent_filter_action["category"].Action(RequiredNameIsNotEmpty);
intent_filter_action["data"];
+ intent_filter_action["uri-relative-filter-group"];
+ intent_filter_action["uri-relative-filter-group"]["data"];
// Common <meta-data> actions.
xml::XmlNodeAction meta_data_action;