blob: 8184361bca80007639116159394ad7562622cf1f [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 Baumann577d4022018-01-31 16:55:10 +000022import 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;
Patrick Baumann4db6bc12018-02-06 09:55:36 -080029import java.security.SecureRandom;
Todd Kennedy1fb34042017-03-01 13:56:58 -080030import java.util.ArrayList;
Patrick Baumann4db6bc12018-02-06 09:55:36 -080031import java.util.Arrays;
Patrick Baumann577d4022018-01-31 16:55:10 +000032import java.util.Collections;
Todd Kennedy1fb34042017-03-01 13:56:58 -080033import java.util.List;
34import java.util.Locale;
Patrick Baumann4db6bc12018-02-06 09:55:36 -080035import java.util.Random;
Todd Kennedy1fb34042017-03-01 13:56:58 -080036
37/**
Patrick Baumann577d4022018-01-31 16:55:10 +000038 * Describes an externally resolvable instant application. There are three states that this class
39 * can represent: <p/>
40 * <ul>
41 * <li>
42 * The first, usable only for non http/s intents, implies that the resolver cannot
43 * immediately resolve this intent and would prefer that resolution be deferred to the
44 * instant app installer. Represent this state with {@link #InstantAppResolveInfo(Bundle)}.
45 * If the {@link android.content.Intent} has the scheme set to http/s and a set of digest
46 * prefixes were passed into one of the resolve methods in
47 * {@link android.app.InstantAppResolverService}, this state cannot be used.
48 * </li>
49 * <li>
50 * The second represents a partial match and is constructed with any of the other
51 * constructors. By setting one or more of the {@link Nullable}arguments to null, you
52 * communicate to the resolver in response to
53 * {@link android.app.InstantAppResolverService#onGetInstantAppResolveInfo(Intent, int[],
54 * String, InstantAppResolverService.InstantAppResolutionCallback)}
55 * that you need a 2nd round of resolution to complete the request.
56 * </li>
57 * <li>
58 * The third represents a complete match and is constructed with all @Nullable parameters
59 * populated.
60 * </li>
61 * </ul>
Todd Kennedy1fb34042017-03-01 13:56:58 -080062 * @hide
63 */
64@SystemApi
65public final class InstantAppResolveInfo implements Parcelable {
66 /** Algorithm that will be used to generate the domain digest */
Todd Kennedy877e9792017-06-02 07:53:44 -070067 private static final String SHA_ALGORITHM = "SHA-256";
Todd Kennedy1fb34042017-03-01 13:56:58 -080068
Patrick Baumann577d4022018-01-31 16:55:10 +000069 private static final byte[] EMPTY_DIGEST = new byte[0];
70
Todd Kennedy1fb34042017-03-01 13:56:58 -080071 private final InstantAppDigest mDigest;
72 private final String mPackageName;
73 /** The filters used to match domain */
74 private final List<InstantAppIntentFilter> mFilters;
75 /** The version code of the app that this class resolves to */
Dianne Hackborn3accca02013-09-20 09:32:11 -070076 private final long mVersionCode;
Patrick Baumann709ee152017-12-04 16:12:52 -080077 /** Data about the app that should be passed along to the Instant App installer on resolve */
78 private final Bundle mExtras;
Patrick Baumann577d4022018-01-31 16:55:10 +000079 /**
80 * A flag that indicates that the resolver is aware that an app may match, but would prefer
Patrick Baumann3abf5472018-03-01 09:58:23 -080081 * that the installer get the sanitized intent to decide.
Patrick Baumann577d4022018-01-31 16:55:10 +000082 */
83 private final boolean mShouldLetInstallerDecide;
Todd Kennedy1fb34042017-03-01 13:56:58 -080084
Patrick Baumann577d4022018-01-31 16:55:10 +000085 /** Constructor for intent-based InstantApp resolution results. */
Todd Kennedy1fb34042017-03-01 13:56:58 -080086 public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
Todd Kennedyc0dd03a2017-05-05 17:15:38 +000087 @Nullable List<InstantAppIntentFilter> filters, int versionCode) {
Patrick Baumann709ee152017-12-04 16:12:52 -080088 this(digest, packageName, filters, (long) versionCode, null /* extras */);
Dianne Hackborn3accca02013-09-20 09:32:11 -070089 }
90
Patrick Baumann577d4022018-01-31 16:55:10 +000091 /** Constructor for intent-based InstantApp resolution results with extras. */
Dianne Hackborn3accca02013-09-20 09:32:11 -070092 public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
Patrick Baumann709ee152017-12-04 16:12:52 -080093 @Nullable List<InstantAppIntentFilter> filters, long versionCode,
94 @Nullable Bundle extras) {
Patrick Baumann577d4022018-01-31 16:55:10 +000095 this(digest, packageName, filters, versionCode, extras, false);
96 }
97
Patrick Baumann3abf5472018-03-01 09:58:23 -080098 /** Constructor for intent-based InstantApp resolution results by hostname. */
99 public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName,
100 @Nullable List<InstantAppIntentFilter> filters) {
101 this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/,
102 null /* extras */);
103 }
104
105 /**
106 * Constructor that indicates that resolution could be delegated to the installer when the
107 * sanitized intent contains enough information to resolve completely.
108 */
109 public InstantAppResolveInfo(@Nullable Bundle extras) {
110 this(InstantAppDigest.UNDEFINED, null, null, -1, extras, true);
111 }
112
Patrick Baumann577d4022018-01-31 16:55:10 +0000113 private InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
114 @Nullable List<InstantAppIntentFilter> filters, long versionCode,
115 @Nullable Bundle extras, boolean shouldLetInstallerDecide) {
Todd Kennedy1fb34042017-03-01 13:56:58 -0800116 // validate arguments
117 if ((packageName == null && (filters != null && filters.size() != 0))
118 || (packageName != null && (filters == null || filters.size() == 0))) {
119 throw new IllegalArgumentException();
120 }
121 mDigest = digest;
122 if (filters != null) {
Patrick Baumann577d4022018-01-31 16:55:10 +0000123 mFilters = new ArrayList<>(filters.size());
Todd Kennedy1fb34042017-03-01 13:56:58 -0800124 mFilters.addAll(filters);
125 } else {
126 mFilters = null;
127 }
128 mPackageName = packageName;
Todd Kennedyc0dd03a2017-05-05 17:15:38 +0000129 mVersionCode = versionCode;
Patrick Baumann709ee152017-12-04 16:12:52 -0800130 mExtras = extras;
Patrick Baumann577d4022018-01-31 16:55:10 +0000131 mShouldLetInstallerDecide = shouldLetInstallerDecide;
Todd Kennedy1fb34042017-03-01 13:56:58 -0800132 }
133
Todd Kennedy1fb34042017-03-01 13:56:58 -0800134 InstantAppResolveInfo(Parcel in) {
Patrick Baumann577d4022018-01-31 16:55:10 +0000135 mShouldLetInstallerDecide = in.readBoolean();
Patrick Baumann709ee152017-12-04 16:12:52 -0800136 mExtras = in.readBundle();
Patrick Baumann577d4022018-01-31 16:55:10 +0000137 if (mShouldLetInstallerDecide) {
138 mDigest = InstantAppDigest.UNDEFINED;
139 mPackageName = null;
140 mFilters = Collections.emptyList();
141 mVersionCode = -1;
142 } else {
143 mDigest = in.readParcelable(null /*loader*/);
144 mPackageName = in.readString();
145 mFilters = new ArrayList<>();
146 in.readList(mFilters, null /*loader*/);
147 mVersionCode = in.readLong();
148 }
149 }
150
Patrick Baumann3abf5472018-03-01 09:58:23 -0800151 /**
152 * Returns true if the resolver is aware that an app may match, but would prefer
153 * that the installer get the sanitized intent to decide. This should not be true for
154 * resolutions that include a host and will be ignored in such cases.
155 */
Patrick Baumann577d4022018-01-31 16:55:10 +0000156 public boolean shouldLetInstallerDecide() {
157 return mShouldLetInstallerDecide;
Todd Kennedy1fb34042017-03-01 13:56:58 -0800158 }
159
160 public byte[] getDigestBytes() {
Patrick Baumann577d4022018-01-31 16:55:10 +0000161 return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST;
Todd Kennedy1fb34042017-03-01 13:56:58 -0800162 }
163
164 public int getDigestPrefix() {
165 return mDigest.getDigestPrefix()[0];
166 }
167
168 public String getPackageName() {
169 return mPackageName;
170 }
171
172 public List<InstantAppIntentFilter> getIntentFilters() {
173 return mFilters;
174 }
175
Dianne Hackborn3accca02013-09-20 09:32:11 -0700176 /**
177 * @deprecated Use {@link #getLongVersionCode} instead.
178 */
179 @Deprecated
Todd Kennedy1fb34042017-03-01 13:56:58 -0800180 public int getVersionCode() {
Dianne Hackborn3accca02013-09-20 09:32:11 -0700181 return (int) (mVersionCode & 0xffffffff);
182 }
183
184 public long getLongVersionCode() {
Todd Kennedy1fb34042017-03-01 13:56:58 -0800185 return mVersionCode;
186 }
187
Patrick Baumann709ee152017-12-04 16:12:52 -0800188 @Nullable
189 public Bundle getExtras() {
190 return mExtras;
191 }
192
Todd Kennedy1fb34042017-03-01 13:56:58 -0800193 @Override
194 public int describeContents() {
195 return 0;
196 }
197
198 @Override
199 public void writeToParcel(Parcel out, int flags) {
Patrick Baumann577d4022018-01-31 16:55:10 +0000200 out.writeBoolean(mShouldLetInstallerDecide);
201 out.writeBundle(mExtras);
202 if (mShouldLetInstallerDecide) {
203 return;
204 }
Todd Kennedy1fb34042017-03-01 13:56:58 -0800205 out.writeParcelable(mDigest, flags);
206 out.writeString(mPackageName);
207 out.writeList(mFilters);
Dianne Hackborn3accca02013-09-20 09:32:11 -0700208 out.writeLong(mVersionCode);
Todd Kennedy1fb34042017-03-01 13:56:58 -0800209 }
210
211 public static final Parcelable.Creator<InstantAppResolveInfo> CREATOR
212 = new Parcelable.Creator<InstantAppResolveInfo>() {
213 public InstantAppResolveInfo createFromParcel(Parcel in) {
214 return new InstantAppResolveInfo(in);
215 }
216
217 public InstantAppResolveInfo[] newArray(int size) {
218 return new InstantAppResolveInfo[size];
219 }
220 };
221
222 /**
223 * Helper class to generate and store each of the digests and prefixes
224 * sent to the Instant App Resolver.
225 * <p>
226 * Since intent filters may want to handle multiple hosts within a
227 * domain [eg “*.google.com”], the resolver is presented with multiple
228 * hash prefixes. For example, "a.b.c.d.e" generates digests for
229 * "d.e", "c.d.e", "b.c.d.e" and "a.b.c.d.e".
230 *
231 * @hide
232 */
233 @SystemApi
234 public static final class InstantAppDigest implements Parcelable {
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800235 static final int DIGEST_MASK = 0xfffff000;
Patrick Baumann3abf5472018-03-01 09:58:23 -0800236
237 /**
238 * A special instance that represents and undefined digest used for cases that a host was
239 * not provided or is irrelevant to the response.
240 */
Patrick Baumann577d4022018-01-31 16:55:10 +0000241 public static final InstantAppDigest UNDEFINED =
242 new InstantAppDigest(new byte[][]{}, new int[]{});
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800243
244 private static Random sRandom = null;
245 static {
246 try {
247 sRandom = SecureRandom.getInstance("SHA1PRNG");
248 } catch (NoSuchAlgorithmException e) {
249 // oh well
250 sRandom = new Random();
251 }
252 }
Todd Kennedy1fb34042017-03-01 13:56:58 -0800253 /** Full digest of the domain hashes */
254 private final byte[][] mDigestBytes;
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800255 /** The first 5 bytes of the domain hashes */
Todd Kennedy1fb34042017-03-01 13:56:58 -0800256 private final int[] mDigestPrefix;
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800257 /** The first 5 bytes of the domain hashes interspersed with random data */
258 private int[] mDigestPrefixSecure;
Todd Kennedy1fb34042017-03-01 13:56:58 -0800259
260 public InstantAppDigest(@NonNull String hostName) {
261 this(hostName, -1 /*maxDigests*/);
262 }
263
264 /** @hide */
265 public InstantAppDigest(@NonNull String hostName, int maxDigests) {
266 if (hostName == null) {
267 throw new IllegalArgumentException();
268 }
269 mDigestBytes = generateDigest(hostName.toLowerCase(Locale.ENGLISH), maxDigests);
270 mDigestPrefix = new int[mDigestBytes.length];
271 for (int i = 0; i < mDigestBytes.length; i++) {
272 mDigestPrefix[i] =
273 ((mDigestBytes[i][0] & 0xFF) << 24
274 | (mDigestBytes[i][1] & 0xFF) << 16
275 | (mDigestBytes[i][2] & 0xFF) << 8
276 | (mDigestBytes[i][3] & 0xFF) << 0)
277 & DIGEST_MASK;
278 }
279 }
280
Patrick Baumann577d4022018-01-31 16:55:10 +0000281 private InstantAppDigest(byte[][] digestBytes, int[] prefix) {
282 this.mDigestPrefix = prefix;
283 this.mDigestBytes = digestBytes;
284 }
285
Todd Kennedy1fb34042017-03-01 13:56:58 -0800286 private static byte[][] generateDigest(String hostName, int maxDigests) {
287 ArrayList<byte[]> digests = new ArrayList<>();
288 try {
289 final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM);
290 if (maxDigests <= 0) {
291 final byte[] hostBytes = hostName.getBytes();
292 digests.add(digest.digest(hostBytes));
293 } else {
294 int prevDot = hostName.lastIndexOf('.');
295 prevDot = hostName.lastIndexOf('.', prevDot - 1);
296 // shortcut for short URLs
297 if (prevDot < 0) {
298 digests.add(digest.digest(hostName.getBytes()));
299 } else {
300 byte[] hostBytes =
301 hostName.substring(prevDot + 1, hostName.length()).getBytes();
302 digests.add(digest.digest(hostBytes));
303 int digestCount = 1;
304 while (prevDot >= 0 && digestCount < maxDigests) {
305 prevDot = hostName.lastIndexOf('.', prevDot - 1);
306 hostBytes =
307 hostName.substring(prevDot + 1, hostName.length()).getBytes();
308 digests.add(digest.digest(hostBytes));
309 digestCount++;
310 }
311 }
312 }
313 } catch (NoSuchAlgorithmException e) {
314 throw new IllegalStateException("could not find digest algorithm");
315 }
316 return digests.toArray(new byte[digests.size()][]);
317 }
318
319 InstantAppDigest(Parcel in) {
320 final int digestCount = in.readInt();
321 if (digestCount == -1) {
322 mDigestBytes = null;
323 } else {
324 mDigestBytes = new byte[digestCount][];
325 for (int i = 0; i < digestCount; i++) {
326 mDigestBytes[i] = in.createByteArray();
327 }
328 }
329 mDigestPrefix = in.createIntArray();
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800330 mDigestPrefixSecure = in.createIntArray();
Todd Kennedy1fb34042017-03-01 13:56:58 -0800331 }
332
333 public byte[][] getDigestBytes() {
334 return mDigestBytes;
335 }
336
337 public int[] getDigestPrefix() {
338 return mDigestPrefix;
339 }
340
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800341 /**
342 * Returns a digest prefix with additional random prefixes interspersed.
343 * @hide
344 */
345 public int[] getDigestPrefixSecure() {
346 if (this == InstantAppResolveInfo.InstantAppDigest.UNDEFINED) {
347 return getDigestPrefix();
348 } else if (mDigestPrefixSecure == null) {
349 // let's generate some random data to intersperse throughout the set of prefixes
350 final int realSize = getDigestPrefix().length;
351 final int manufacturedSize = realSize + 10 + sRandom.nextInt(10);
352 mDigestPrefixSecure = Arrays.copyOf(getDigestPrefix(), manufacturedSize);
353 for (int i = realSize; i < manufacturedSize; i++) {
354 mDigestPrefixSecure[i] = sRandom.nextInt() & DIGEST_MASK;
355 }
356 Arrays.sort(mDigestPrefixSecure);
357 }
358 return mDigestPrefixSecure;
359 }
360
Todd Kennedy1fb34042017-03-01 13:56:58 -0800361 @Override
362 public int describeContents() {
363 return 0;
364 }
365
366 @Override
367 public void writeToParcel(Parcel out, int flags) {
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800368 final boolean isUndefined = this == UNDEFINED;
369 out.writeBoolean(isUndefined);
370 if (isUndefined) {
371 return;
372 }
Todd Kennedy1fb34042017-03-01 13:56:58 -0800373 if (mDigestBytes == null) {
374 out.writeInt(-1);
375 } else {
376 out.writeInt(mDigestBytes.length);
377 for (int i = 0; i < mDigestBytes.length; i++) {
378 out.writeByteArray(mDigestBytes[i]);
379 }
380 }
381 out.writeIntArray(mDigestPrefix);
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800382 out.writeIntArray(mDigestPrefixSecure);
Todd Kennedy1fb34042017-03-01 13:56:58 -0800383 }
384
385 @SuppressWarnings("hiding")
386 public static final Parcelable.Creator<InstantAppDigest> CREATOR =
387 new Parcelable.Creator<InstantAppDigest>() {
388 @Override
389 public InstantAppDigest createFromParcel(Parcel in) {
Patrick Baumann4db6bc12018-02-06 09:55:36 -0800390 if (in.readBoolean() /* is undefined */) {
391 return UNDEFINED;
392 }
Todd Kennedy1fb34042017-03-01 13:56:58 -0800393 return new InstantAppDigest(in);
394 }
395 @Override
396 public InstantAppDigest[] newArray(int size) {
397 return new InstantAppDigest[size];
398 }
399 };
400 }
401}