diff options
| author | 2023-12-06 11:11:19 -0800 | |
|---|---|---|
| committer | 2024-01-25 23:20:04 -0800 | |
| commit | d31458436119bc9e78477855cfca20f92c818fbc (patch) | |
| tree | ccddac31f8eecc5f3ef3a2bdb5a5fb1d14fd54b8 | |
| parent | c96387d3ce4d216956a0bd8b63ad766725aa0698 (diff) | |
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
| -rw-r--r-- | core/api/current.txt | 35 | ||||
| -rw-r--r-- | core/java/android/content/IntentFilter.java | 140 | ||||
| -rw-r--r-- | core/java/android/content/UriRelativeFilter.java | 260 | ||||
| -rw-r--r-- | core/java/android/content/UriRelativeFilterGroup.java | 216 | ||||
| -rw-r--r-- | core/java/android/content/pm/flags.aconfig | 8 | ||||
| -rw-r--r-- | core/java/android/os/PatternMatcher.java | 14 | ||||
| -rw-r--r-- | core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java | 200 | ||||
| -rw-r--r-- | core/proto/android/content/intent.proto | 23 | ||||
| -rw-r--r-- | core/res/res/values/attrs_manifest.xml | 81 | ||||
| -rw-r--r-- | core/res/res/values/public-staging.xml | 20 | ||||
| -rw-r--r-- | nfc/jarjar-rules.txt | 1 | ||||
| -rw-r--r-- | services/tests/PackageManagerServiceTests/server/Android.bp | 1 | ||||
| -rw-r--r-- | services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java | 104 | ||||
| -rw-r--r-- | services/tests/servicestests/test-apps/PackageParserApp/Android.bp | 14 | ||||
| -rw-r--r-- | services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp7.xml | 67 | ||||
| -rw-r--r-- | tools/aapt2/link/ManifestFixer.cpp | 2 |
16 files changed, 1179 insertions, 7 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 4f5e519e85d9..6f898378c563 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -442,6 +442,7 @@ package android { 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 @@ package android { 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 @@ package android { 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 @@ package android { 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 @@ package android.content { 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 @@ package android.content { 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 @@ package android.content { 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 @@ package android.content { 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 ad3acd713c6b..79af65a3a3e5 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 @@ public class IntentFilter implements Parcelable { 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 @@ public class IntentFilter implements Parcelable { 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 @@ public class IntentFilter implements Parcelable { 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 @@ public class IntentFilter implements Parcelable { } /** + * 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 @@ public class IntentFilter implements Parcelable { 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 @@ public class IntentFilter implements Parcelable { 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 @@ public class IntentFilter implements Parcelable { } 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 @@ public class IntentFilter implements Parcelable { } } 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 @@ public class IntentFilter implements Parcelable { 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 @@ public class IntentFilter implements Parcelable { 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 @@ public class IntentFilter implements Parcelable { } 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 @@ public class IntentFilter implements Parcelable { 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 000000000000..9866cd0e992a --- /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 000000000000..72c396a73ec8 --- /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 e4e9fbaf2c55..cbf5274c4fa1 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -110,6 +110,14 @@ flag { } 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 b5425b43fe7e..79a2c59d6a03 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 class PatternMatcher implements Parcelable { */ 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 c6683cfc8331..05728eee174f 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 @@ package com.android.internal.pm.pkg.component; 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 @@ public class ParsedIntentInfoUtils { 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 @@ public class ParsedIntentInfoUtils { } @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 75e29082508b..1d1f88b01838 100644 --- a/core/proto/android/content/intent.proto +++ b/core/proto/android/content/intent.proto @@ -66,7 +66,7 @@ message IntentProto { 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 @@ message IntentFilterProto { 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 @@ message AuthorityEntryProto { 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 35276bf8ead2..6884fc0057d9 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 b8fc052a2fa9..830e99ca907b 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 4cd652d6af7f..99ae14414dad 100644 --- a/nfc/jarjar-rules.txt +++ b/nfc/jarjar-rules.txt @@ -4,6 +4,7 @@ rule android.content.ComponentNameProto* com.android.nfc.x.@0 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 3aca1cafbf75..f8accc309931 100644 --- a/services/tests/PackageManagerServiceTests/server/Android.bp +++ b/services/tests/PackageManagerServiceTests/server/Android.bp @@ -103,6 +103,7 @@ android_test { ":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 71f5c754f22f..a0e0e1ef36ee 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 static java.util.stream.Collectors.toList; 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.Bundle; 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.lang.reflect.Field; 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 @@ public class PackageParserTest { @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 @@ public class PackageParserTest { 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 @@ public class PackageParserTest { 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 3e78f9a9674a..131b380d9215 100644 --- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp +++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp @@ -102,3 +102,17 @@ android_test_helper_app { 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 000000000000..cb87a48eb524 --- /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 0b16e2c7efe4..d03f97e28156 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -415,6 +415,8 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn 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; |