blob: 112c5dae6731f4bae2f7c2e97f78df7ae2ca70fa [file] [log] [blame]
Todd Kennedy1fb34042017-03-01 13:56:58 -08001/*
2 * Copyright (C) 2017 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
17package android.content.pm;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.SystemApi;
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080022import android.content.Intent;
Patrick Baumann709ee152017-12-04 16:12:52 -080023import android.os.Bundle;
Todd Kennedy1fb34042017-03-01 13:56:58 -080024import android.os.Parcel;
25import android.os.Parcelable;
26
27import java.security.MessageDigest;
28import java.security.NoSuchAlgorithmException;
29import java.util.ArrayList;
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080030import java.util.Collections;
Todd Kennedy1fb34042017-03-01 13:56:58 -080031import java.util.List;
32import java.util.Locale;
33
34/**
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080035 * Describes an externally resolvable instant application. There are three states that this class
36 * can represent: <p/>
37 * <ul>
38 * <li>
39 * The first, usable only for non http/s intents, implies that the resolver cannot
40 * immediately resolve this intent and would prefer that resolution be deferred to the
41 * instant app installer. Represent this state with {@link #InstantAppResolveInfo(Bundle)}.
42 * If the {@link android.content.Intent} has the scheme set to http/s and a set of digest
43 * prefixes were passed into one of the resolve methods in
44 * {@link android.app.InstantAppResolverService}, this state cannot be used.
45 * </li>
46 * <li>
47 * The second represents a partial match and is constructed with any of the other
48 * constructors. By setting one or more of the {@link Nullable}arguments to null, you
49 * communicate to the resolver in response to
50 * {@link android.app.InstantAppResolverService#onGetInstantAppResolveInfo(Intent, int[],
51 * String, InstantAppResolverService.InstantAppResolutionCallback)}
52 * that you need a 2nd round of resolution to complete the request.
53 * </li>
54 * <li>
55 * The third represents a complete match and is constructed with all @Nullable parameters
56 * populated.
57 * </li>
58 * </ul>
Todd Kennedy1fb34042017-03-01 13:56:58 -080059 * @hide
60 */
61@SystemApi
62public final class InstantAppResolveInfo implements Parcelable {
63 /** Algorithm that will be used to generate the domain digest */
Todd Kennedy877e9792017-06-02 07:53:44 -070064 private static final String SHA_ALGORITHM = "SHA-256";
Todd Kennedy1fb34042017-03-01 13:56:58 -080065
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080066 private static final byte[] EMPTY_DIGEST = new byte[0];
67
Todd Kennedy1fb34042017-03-01 13:56:58 -080068 private final InstantAppDigest mDigest;
69 private final String mPackageName;
70 /** The filters used to match domain */
71 private final List<InstantAppIntentFilter> mFilters;
72 /** The version code of the app that this class resolves to */
Dianne Hackborn3accca02013-09-20 09:32:11 -070073 private final long mVersionCode;
Patrick Baumann709ee152017-12-04 16:12:52 -080074 /** Data about the app that should be passed along to the Instant App installer on resolve */
75 private final Bundle mExtras;
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080076 /**
77 * A flag that indicates that the resolver is aware that an app may match, but would prefer
78 * that the installer get the sanitized intent to decide. This should not be used for
79 * resolutions that include a host and will be ignored in such cases.
80 */
81 private final boolean mShouldLetInstallerDecide;
Todd Kennedy1fb34042017-03-01 13:56:58 -080082
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080083 /** Constructor for intent-based InstantApp resolution results. */
Todd Kennedy1fb34042017-03-01 13:56:58 -080084 public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
Todd Kennedyc0dd03a2017-05-05 17:15:38 +000085 @Nullable List<InstantAppIntentFilter> filters, int versionCode) {
Patrick Baumann709ee152017-12-04 16:12:52 -080086 this(digest, packageName, filters, (long) versionCode, null /* extras */);
Dianne Hackborn3accca02013-09-20 09:32:11 -070087 }
88
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080089 /** Constructor for intent-based InstantApp resolution results with extras. */
Dianne Hackborn3accca02013-09-20 09:32:11 -070090 public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
Patrick Baumann709ee152017-12-04 16:12:52 -080091 @Nullable List<InstantAppIntentFilter> filters, long versionCode,
92 @Nullable Bundle extras) {
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -080093 this(digest, packageName, filters, versionCode, extras, false);
94 }
95
96 /** Constructor for intent-based InstantApp resolution results with extras. */
97 private InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
98 @Nullable List<InstantAppIntentFilter> filters, long versionCode,
99 @Nullable Bundle extras, boolean shouldLetInstallerDecide) {
Todd Kennedy1fb34042017-03-01 13:56:58 -0800100 // validate arguments
101 if ((packageName == null && (filters != null && filters.size() != 0))
102 || (packageName != null && (filters == null || filters.size() == 0))) {
103 throw new IllegalArgumentException();
104 }
105 mDigest = digest;
106 if (filters != null) {
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800107 mFilters = new ArrayList<>(filters.size());
Todd Kennedy1fb34042017-03-01 13:56:58 -0800108 mFilters.addAll(filters);
109 } else {
110 mFilters = null;
111 }
112 mPackageName = packageName;
Todd Kennedyc0dd03a2017-05-05 17:15:38 +0000113 mVersionCode = versionCode;
Patrick Baumann709ee152017-12-04 16:12:52 -0800114 mExtras = extras;
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800115 mShouldLetInstallerDecide = shouldLetInstallerDecide;
Todd Kennedy1fb34042017-03-01 13:56:58 -0800116 }
117
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800118 /** Constructor for intent-based InstantApp resolution results by hostname. */
Todd Kennedy1fb34042017-03-01 13:56:58 -0800119 public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName,
120 @Nullable List<InstantAppIntentFilter> filters) {
Patrick Baumann709ee152017-12-04 16:12:52 -0800121 this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/,
122 null /* extras */);
Todd Kennedy1fb34042017-03-01 13:56:58 -0800123 }
124
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800125 /**
126 * Constructor that creates a "let the installer decide" response with optional included
127 * extras.
128 */
129 public InstantAppResolveInfo(@Nullable Bundle extras) {
130 this(InstantAppDigest.UNDEFINED, null, null, -1, extras, true);
131 }
132
Todd Kennedy1fb34042017-03-01 13:56:58 -0800133 InstantAppResolveInfo(Parcel in) {
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800134 mShouldLetInstallerDecide = in.readBoolean();
Patrick Baumann709ee152017-12-04 16:12:52 -0800135 mExtras = in.readBundle();
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800136 if (mShouldLetInstallerDecide) {
137 mDigest = InstantAppDigest.UNDEFINED;
138 mPackageName = null;
139 mFilters = Collections.emptyList();
140 mVersionCode = -1;
141 } else {
142 mDigest = in.readParcelable(null /*loader*/);
143 mPackageName = in.readString();
144 mFilters = new ArrayList<>();
145 in.readList(mFilters, null /*loader*/);
146 mVersionCode = in.readLong();
147 }
148 }
149
150 /** Returns true if the installer should be notified that it should query for packages. */
151 public boolean shouldLetInstallerDecide() {
152 return mShouldLetInstallerDecide;
Todd Kennedy1fb34042017-03-01 13:56:58 -0800153 }
154
155 public byte[] getDigestBytes() {
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800156 return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST;
Todd Kennedy1fb34042017-03-01 13:56:58 -0800157 }
158
159 public int getDigestPrefix() {
160 return mDigest.getDigestPrefix()[0];
161 }
162
163 public String getPackageName() {
164 return mPackageName;
165 }
166
167 public List<InstantAppIntentFilter> getIntentFilters() {
168 return mFilters;
169 }
170
Dianne Hackborn3accca02013-09-20 09:32:11 -0700171 /**
172 * @deprecated Use {@link #getLongVersionCode} instead.
173 */
174 @Deprecated
Todd Kennedy1fb34042017-03-01 13:56:58 -0800175 public int getVersionCode() {
Dianne Hackborn3accca02013-09-20 09:32:11 -0700176 return (int) (mVersionCode & 0xffffffff);
177 }
178
179 public long getLongVersionCode() {
Todd Kennedy1fb34042017-03-01 13:56:58 -0800180 return mVersionCode;
181 }
182
Patrick Baumann709ee152017-12-04 16:12:52 -0800183 @Nullable
184 public Bundle getExtras() {
185 return mExtras;
186 }
187
Todd Kennedy1fb34042017-03-01 13:56:58 -0800188 @Override
189 public int describeContents() {
190 return 0;
191 }
192
193 @Override
194 public void writeToParcel(Parcel out, int flags) {
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800195 out.writeBoolean(mShouldLetInstallerDecide);
196 out.writeBundle(mExtras);
197 if (mShouldLetInstallerDecide) {
198 return;
199 }
Todd Kennedy1fb34042017-03-01 13:56:58 -0800200 out.writeParcelable(mDigest, flags);
201 out.writeString(mPackageName);
202 out.writeList(mFilters);
Dianne Hackborn3accca02013-09-20 09:32:11 -0700203 out.writeLong(mVersionCode);
Todd Kennedy1fb34042017-03-01 13:56:58 -0800204 }
205
206 public static final Parcelable.Creator<InstantAppResolveInfo> CREATOR
207 = new Parcelable.Creator<InstantAppResolveInfo>() {
208 public InstantAppResolveInfo createFromParcel(Parcel in) {
209 return new InstantAppResolveInfo(in);
210 }
211
212 public InstantAppResolveInfo[] newArray(int size) {
213 return new InstantAppResolveInfo[size];
214 }
215 };
216
217 /**
218 * Helper class to generate and store each of the digests and prefixes
219 * sent to the Instant App Resolver.
220 * <p>
221 * Since intent filters may want to handle multiple hosts within a
222 * domain [eg “*.google.com”], the resolver is presented with multiple
223 * hash prefixes. For example, "a.b.c.d.e" generates digests for
224 * "d.e", "c.d.e", "b.c.d.e" and "a.b.c.d.e".
225 *
226 * @hide
227 */
228 @SystemApi
229 public static final class InstantAppDigest implements Parcelable {
230 private static final int DIGEST_MASK = 0xfffff000;
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800231
232 public static final InstantAppDigest UNDEFINED =
233 new InstantAppDigest(new byte[][]{}, new int[]{});
Todd Kennedy1fb34042017-03-01 13:56:58 -0800234 /** Full digest of the domain hashes */
235 private final byte[][] mDigestBytes;
236 /** The first 4 bytes of the domain hashes */
237 private final int[] mDigestPrefix;
238
239 public InstantAppDigest(@NonNull String hostName) {
240 this(hostName, -1 /*maxDigests*/);
241 }
242
243 /** @hide */
244 public InstantAppDigest(@NonNull String hostName, int maxDigests) {
245 if (hostName == null) {
246 throw new IllegalArgumentException();
247 }
248 mDigestBytes = generateDigest(hostName.toLowerCase(Locale.ENGLISH), maxDigests);
249 mDigestPrefix = new int[mDigestBytes.length];
250 for (int i = 0; i < mDigestBytes.length; i++) {
251 mDigestPrefix[i] =
252 ((mDigestBytes[i][0] & 0xFF) << 24
253 | (mDigestBytes[i][1] & 0xFF) << 16
254 | (mDigestBytes[i][2] & 0xFF) << 8
255 | (mDigestBytes[i][3] & 0xFF) << 0)
256 & DIGEST_MASK;
257 }
258 }
259
Patrick Baumann3e8bd0f2018-01-08 11:23:38 -0800260 private InstantAppDigest(byte[][] digestBytes, int[] prefix) {
261 this.mDigestPrefix = prefix;
262 this.mDigestBytes = digestBytes;
263 }
264
Todd Kennedy1fb34042017-03-01 13:56:58 -0800265 private static byte[][] generateDigest(String hostName, int maxDigests) {
266 ArrayList<byte[]> digests = new ArrayList<>();
267 try {
268 final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM);
269 if (maxDigests <= 0) {
270 final byte[] hostBytes = hostName.getBytes();
271 digests.add(digest.digest(hostBytes));
272 } else {
273 int prevDot = hostName.lastIndexOf('.');
274 prevDot = hostName.lastIndexOf('.', prevDot - 1);
275 // shortcut for short URLs
276 if (prevDot < 0) {
277 digests.add(digest.digest(hostName.getBytes()));
278 } else {
279 byte[] hostBytes =
280 hostName.substring(prevDot + 1, hostName.length()).getBytes();
281 digests.add(digest.digest(hostBytes));
282 int digestCount = 1;
283 while (prevDot >= 0 && digestCount < maxDigests) {
284 prevDot = hostName.lastIndexOf('.', prevDot - 1);
285 hostBytes =
286 hostName.substring(prevDot + 1, hostName.length()).getBytes();
287 digests.add(digest.digest(hostBytes));
288 digestCount++;
289 }
290 }
291 }
292 } catch (NoSuchAlgorithmException e) {
293 throw new IllegalStateException("could not find digest algorithm");
294 }
295 return digests.toArray(new byte[digests.size()][]);
296 }
297
298 InstantAppDigest(Parcel in) {
299 final int digestCount = in.readInt();
300 if (digestCount == -1) {
301 mDigestBytes = null;
302 } else {
303 mDigestBytes = new byte[digestCount][];
304 for (int i = 0; i < digestCount; i++) {
305 mDigestBytes[i] = in.createByteArray();
306 }
307 }
308 mDigestPrefix = in.createIntArray();
309 }
310
311 public byte[][] getDigestBytes() {
312 return mDigestBytes;
313 }
314
315 public int[] getDigestPrefix() {
316 return mDigestPrefix;
317 }
318
319 @Override
320 public int describeContents() {
321 return 0;
322 }
323
324 @Override
325 public void writeToParcel(Parcel out, int flags) {
326 if (mDigestBytes == null) {
327 out.writeInt(-1);
328 } else {
329 out.writeInt(mDigestBytes.length);
330 for (int i = 0; i < mDigestBytes.length; i++) {
331 out.writeByteArray(mDigestBytes[i]);
332 }
333 }
334 out.writeIntArray(mDigestPrefix);
335 }
336
337 @SuppressWarnings("hiding")
338 public static final Parcelable.Creator<InstantAppDigest> CREATOR =
339 new Parcelable.Creator<InstantAppDigest>() {
340 @Override
341 public InstantAppDigest createFromParcel(Parcel in) {
342 return new InstantAppDigest(in);
343 }
344 @Override
345 public InstantAppDigest[] newArray(int size) {
346 return new InstantAppDigest[size];
347 }
348 };
349 }
350}