1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.content;
18 
19 import android.annotation.FlaggedApi;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.pm.Flags;
24 import android.net.Uri;
25 import android.os.Parcel;
26 import android.util.ArraySet;
27 import android.util.Log;
28 import android.util.proto.ProtoOutputStream;
29 
30 import com.android.internal.util.CollectionUtils;
31 import com.android.internal.util.XmlUtils;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 import org.xmlpull.v1.XmlSerializer;
36 
37 import java.io.IOException;
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.ArrayList;
41 import java.util.Collection;
42 import java.util.Collections;
43 import java.util.Iterator;
44 import java.util.List;
45 import java.util.Objects;
46 
47 /**
48  * An intent data matching group based on a URI's relative reference which
49  * includes the path, query and fragment.  The group is only considered as
50  * matching if <em>all</em> UriRelativeFilters in the group match.  Each
51  * UriRelativeFilter defines a matching rule for a URI path, query or fragment.
52  * A group must contain one or more UriRelativeFilters to match but does not need to
53  * contain UriRelativeFilters for all existing parts of a URI to match.
54  *
55  * <p>For example, given a URI that contains path, query and fragment parts,
56  * a group containing only a path filter will match the URI if the path
57  * filter matches the URI path.  If the group contains a path and query
58  * filter, then the group will only match if both path and query filters
59  * match.  If a URI contains only a path with no query or fragment then a
60  * group can only match if it contains only a matching path filter. If the
61  * group also contained additional query or fragment filters then it will
62  * not match.</p>
63  */
64 @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
65 public final class UriRelativeFilterGroup {
66     private static final String ALLOW_STR = "allow";
67     private static final String URI_RELATIVE_FILTER_GROUP_STR = "uriRelativeFilterGroup";
68 
69     /**
70      * Value to indicate that the group match is allowed.
71      */
72     public static final int ACTION_ALLOW = 0;
73     /**
74      * Value to indicate that the group match is blocked.
75      */
76     public static final int ACTION_BLOCK = 1;
77 
78     /** @hide */
79     @IntDef(value = {
80             ACTION_ALLOW,
81             ACTION_BLOCK
82     })
83     @Retention(RetentionPolicy.SOURCE)
84     public @interface Action {}
85 
86     private final @Action int mAction;
87     private final ArraySet<UriRelativeFilter> mUriRelativeFilters = new ArraySet<>();
88 
89     /** @hide */
matchGroupsToUri(List<UriRelativeFilterGroup> groups, Uri uri)90     public static boolean matchGroupsToUri(List<UriRelativeFilterGroup> groups, Uri uri) {
91         for (int i = 0; i < groups.size(); i++) {
92             if (groups.get(i).matchData(uri)) {
93                 return groups.get(i).getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
94             }
95         }
96         return false;
97     }
98 
99     /** @hide */
parcelsToGroups( @ullable List<UriRelativeFilterGroupParcel> parcels)100     public static List<UriRelativeFilterGroup> parcelsToGroups(
101             @Nullable List<UriRelativeFilterGroupParcel> parcels) {
102         List<UriRelativeFilterGroup> groups = new ArrayList<>();
103         if (parcels != null) {
104             for (int i = 0; i < parcels.size(); i++) {
105                 groups.add(new UriRelativeFilterGroup(parcels.get(i)));
106             }
107         }
108         return groups;
109     }
110 
111     /** @hide */
groupsToParcels( @ullable List<UriRelativeFilterGroup> groups)112     public static List<UriRelativeFilterGroupParcel> groupsToParcels(
113             @Nullable List<UriRelativeFilterGroup> groups) {
114         List<UriRelativeFilterGroupParcel> parcels = new ArrayList<>();
115         if (groups != null) {
116             for (int i = 0; i < groups.size(); i++) {
117                 parcels.add(groups.get(i).toParcel());
118             }
119         }
120         return parcels;
121     }
122 
123     /**
124      * New UriRelativeFilterGroup that matches a Intent data.
125      *
126      * @param action Whether this matching group should be allowed or disallowed.
127      */
UriRelativeFilterGroup(@ction int action)128     public UriRelativeFilterGroup(@Action int action) {
129         mAction = action;
130     }
131 
132     /** @hide */
UriRelativeFilterGroup(XmlPullParser parser)133     public UriRelativeFilterGroup(XmlPullParser parser) throws XmlPullParserException, IOException {
134         mAction = Integer.parseInt(parser.getAttributeValue(null, ALLOW_STR));
135 
136         int outerDepth = parser.getDepth();
137         int type;
138         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
139                 && (type != XmlPullParser.END_TAG
140                 || parser.getDepth() > outerDepth)) {
141             if (type == XmlPullParser.END_TAG
142                     || type == XmlPullParser.TEXT) {
143                 continue;
144             }
145 
146             String tagName = parser.getName();
147             if (tagName.equals(UriRelativeFilter.URI_RELATIVE_FILTER_STR)) {
148                 addUriRelativeFilter(new UriRelativeFilter(parser));
149             } else {
150                 Log.w("IntentFilter", "Unknown tag parsing IntentFilter: " + tagName);
151             }
152             XmlUtils.skipCurrentTag(parser);
153         }
154     }
155 
156     /**
157      * Return {@link UriRelativeFilterGroup#ACTION_ALLOW} if a URI is allowed when matched
158      * and {@link UriRelativeFilterGroup#ACTION_BLOCK} if a URI is blacked when matched.
159      */
getAction()160     public @Action int getAction() {
161         return mAction;
162     }
163 
164     /**
165      * Add a filter to the group.
166      */
addUriRelativeFilter(@onNull UriRelativeFilter uriRelativeFilter)167     public void addUriRelativeFilter(@NonNull UriRelativeFilter uriRelativeFilter) {
168         Objects.requireNonNull(uriRelativeFilter);
169         if (!CollectionUtils.contains(mUriRelativeFilters, uriRelativeFilter)) {
170             mUriRelativeFilters.add(uriRelativeFilter);
171         }
172     }
173 
174     /**
175      * Returns a unmodifiable view of the UriRelativeFilters list in this group.
176      */
177     @NonNull
getUriRelativeFilters()178     public Collection<UriRelativeFilter> getUriRelativeFilters() {
179         return Collections.unmodifiableCollection(mUriRelativeFilters);
180     }
181 
182     /**
183      * Match all URI filter in this group against {@link Intent#getData()}.
184      *
185      * @param data The full data string to match against, as supplied in
186      *             Intent.data.
187      * @return true if all filters match.
188      */
matchData(@onNull Uri data)189     public boolean matchData(@NonNull Uri data) {
190         if (mUriRelativeFilters.size() == 0) {
191             return false;
192         }
193         for (UriRelativeFilter filter : mUriRelativeFilters) {
194             if (!filter.matchData(data)) {
195                 return false;
196             }
197         }
198         return true;
199     }
200 
201     /** @hide */
dumpDebug(ProtoOutputStream proto, long fieldId)202     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
203         long token = proto.start(fieldId);
204         proto.write(UriRelativeFilterGroupProto.ACTION, mAction);
205         Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator();
206         while (it.hasNext()) {
207             it.next().dumpDebug(proto, UriRelativeFilterGroupProto.URI_RELATIVE_FILTERS);
208         }
209         proto.end(token);
210     }
211 
212     /** @hide */
writeToXml(XmlSerializer serializer)213     public void writeToXml(XmlSerializer serializer) throws IOException {
214         serializer.startTag(null, URI_RELATIVE_FILTER_GROUP_STR);
215         serializer.attribute(null, ALLOW_STR, Integer.toString(mAction));
216         Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator();
217         while (it.hasNext()) {
218             UriRelativeFilter filter = it.next();
219             filter.writeToXml(serializer);
220         }
221         serializer.endTag(null, URI_RELATIVE_FILTER_GROUP_STR);
222     }
223 
224     @Override
toString()225     public String toString() {
226         return "UriRelativeFilterGroup { allow = " + mAction
227                 + ", uri_filters = " + mUriRelativeFilters + ",  }";
228     }
229 
230     /** @hide */
writeToParcel(@onNull Parcel dest, int flags)231     public void writeToParcel(@NonNull Parcel dest, int flags) {
232         dest.writeInt(mAction);
233         final int n = mUriRelativeFilters.size();
234         if (n > 0) {
235             dest.writeInt(n);
236             Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator();
237             while (it.hasNext()) {
238                 it.next().writeToParcel(dest, flags);
239             }
240         } else {
241             dest.writeInt(0);
242         }
243     }
244 
245     @Override
equals(@ullable Object o)246     public boolean equals(@Nullable Object o) {
247         if (this == o) return true;
248         if (o == null || getClass() != o.getClass()) return false;
249         @SuppressWarnings("unchecked")
250         UriRelativeFilterGroup that = (UriRelativeFilterGroup) o;
251         if (mAction != that.mAction) return false;
252         return mUriRelativeFilters.equals(that.mUriRelativeFilters);
253     }
254 
255     @Override
hashCode()256     public int hashCode() {
257         int _hash = 0;
258         _hash = 31 * _hash + mAction;
259         _hash = 31 * _hash + java.util.Objects.hashCode(mUriRelativeFilters);
260         return _hash;
261     }
262 
263     /** @hide */
toParcel()264     public UriRelativeFilterGroupParcel toParcel() {
265         UriRelativeFilterGroupParcel parcel = new UriRelativeFilterGroupParcel();
266         parcel.action = mAction;
267         parcel.filters = new ArrayList<>();
268         for (UriRelativeFilter filter : mUriRelativeFilters) {
269             parcel.filters.add(filter.toParcel());
270         }
271         return parcel;
272     }
273 
274     /** @hide */
UriRelativeFilterGroup(@onNull Parcel src)275     UriRelativeFilterGroup(@NonNull Parcel src) {
276         mAction = src.readInt();
277         final int n = src.readInt();
278         for (int i = 0; i < n; i++) {
279             mUriRelativeFilters.add(new UriRelativeFilter(src));
280         }
281     }
282 
283     /** @hide */
UriRelativeFilterGroup(UriRelativeFilterGroupParcel parcel)284     public UriRelativeFilterGroup(UriRelativeFilterGroupParcel parcel) {
285         mAction = parcel.action;
286         for (int i = 0; i < parcel.filters.size(); i++) {
287             mUriRelativeFilters.add(new UriRelativeFilter(parcel.filters.get(i)));
288         }
289     }
290 }
291