1 /* 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 17 package android.content.pm; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SystemApi; 22 import android.content.Intent; 23 import android.os.Bundle; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 27 import java.security.MessageDigest; 28 import java.security.NoSuchAlgorithmException; 29 import java.security.SecureRandom; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.Collections; 33 import java.util.List; 34 import java.util.Locale; 35 import java.util.Random; 36 37 /** 38 * 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> 62 * @hide 63 */ 64 @SystemApi 65 public final class InstantAppResolveInfo implements Parcelable { 66 /** Algorithm that will be used to generate the domain digest */ 67 private static final String SHA_ALGORITHM = "SHA-256"; 68 69 private static final byte[] EMPTY_DIGEST = new byte[0]; 70 71 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 */ 76 private final long mVersionCode; 77 /** Data about the app that should be passed along to the Instant App installer on resolve */ 78 private final Bundle mExtras; 79 /** 80 * A flag that indicates that the resolver is aware that an app may match, but would prefer 81 * that the installer get the sanitized intent to decide. 82 */ 83 private final boolean mShouldLetInstallerDecide; 84 85 /** Constructor for intent-based InstantApp resolution results. */ InstantAppResolveInfo(@onNull InstantAppDigest digest, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters, int versionCode)86 public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, 87 @Nullable List<InstantAppIntentFilter> filters, int versionCode) { 88 this(digest, packageName, filters, (long) versionCode, null /* extras */); 89 } 90 91 /** Constructor for intent-based InstantApp resolution results with extras. */ InstantAppResolveInfo(@onNull InstantAppDigest digest, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters, long versionCode, @Nullable Bundle extras)92 public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, 93 @Nullable List<InstantAppIntentFilter> filters, long versionCode, 94 @Nullable Bundle extras) { 95 this(digest, packageName, filters, versionCode, extras, false); 96 } 97 98 /** Constructor for intent-based InstantApp resolution results by hostname. */ InstantAppResolveInfo(@onNull String hostName, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters)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 */ InstantAppResolveInfo(@ullable Bundle extras)109 public InstantAppResolveInfo(@Nullable Bundle extras) { 110 this(InstantAppDigest.UNDEFINED, null, null, -1, extras, true); 111 } 112 InstantAppResolveInfo(@onNull InstantAppDigest digest, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters, long versionCode, @Nullable Bundle extras, boolean shouldLetInstallerDecide)113 private InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, 114 @Nullable List<InstantAppIntentFilter> filters, long versionCode, 115 @Nullable Bundle extras, boolean shouldLetInstallerDecide) { 116 // 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) { 123 mFilters = new ArrayList<>(filters.size()); 124 mFilters.addAll(filters); 125 } else { 126 mFilters = null; 127 } 128 mPackageName = packageName; 129 mVersionCode = versionCode; 130 mExtras = extras; 131 mShouldLetInstallerDecide = shouldLetInstallerDecide; 132 } 133 InstantAppResolveInfo(Parcel in)134 InstantAppResolveInfo(Parcel in) { 135 mShouldLetInstallerDecide = in.readBoolean(); 136 mExtras = in.readBundle(); 137 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 151 /** 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 */ shouldLetInstallerDecide()156 public boolean shouldLetInstallerDecide() { 157 return mShouldLetInstallerDecide; 158 } 159 getDigestBytes()160 public byte[] getDigestBytes() { 161 return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST; 162 } 163 getDigestPrefix()164 public int getDigestPrefix() { 165 return mDigest.getDigestPrefix()[0]; 166 } 167 getPackageName()168 public String getPackageName() { 169 return mPackageName; 170 } 171 getIntentFilters()172 public List<InstantAppIntentFilter> getIntentFilters() { 173 return mFilters; 174 } 175 176 /** 177 * @deprecated Use {@link #getLongVersionCode} instead. 178 */ 179 @Deprecated getVersionCode()180 public int getVersionCode() { 181 return (int) (mVersionCode & 0xffffffff); 182 } 183 getLongVersionCode()184 public long getLongVersionCode() { 185 return mVersionCode; 186 } 187 188 @Nullable getExtras()189 public Bundle getExtras() { 190 return mExtras; 191 } 192 193 @Override describeContents()194 public int describeContents() { 195 return 0; 196 } 197 198 @Override writeToParcel(Parcel out, int flags)199 public void writeToParcel(Parcel out, int flags) { 200 out.writeBoolean(mShouldLetInstallerDecide); 201 out.writeBundle(mExtras); 202 if (mShouldLetInstallerDecide) { 203 return; 204 } 205 out.writeParcelable(mDigest, flags); 206 out.writeString(mPackageName); 207 out.writeList(mFilters); 208 out.writeLong(mVersionCode); 209 } 210 211 public static final @android.annotation.NonNull 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 { 235 static final int DIGEST_MASK = 0xfffff000; 236 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 */ 241 public static final InstantAppDigest UNDEFINED = 242 new InstantAppDigest(new byte[][]{}, new int[]{}); 243 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 } 253 /** Full digest of the domain hashes */ 254 private final byte[][] mDigestBytes; 255 /** The first 5 bytes of the domain hashes */ 256 private final int[] mDigestPrefix; 257 /** The first 5 bytes of the domain hashes interspersed with random data */ 258 private int[] mDigestPrefixSecure; 259 InstantAppDigest(@onNull String hostName)260 public InstantAppDigest(@NonNull String hostName) { 261 this(hostName, -1 /*maxDigests*/); 262 } 263 264 /** @hide */ InstantAppDigest(@onNull String hostName, int maxDigests)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 InstantAppDigest(byte[][] digestBytes, int[] prefix)281 private InstantAppDigest(byte[][] digestBytes, int[] prefix) { 282 this.mDigestPrefix = prefix; 283 this.mDigestBytes = digestBytes; 284 } 285 generateDigest(String hostName, int maxDigests)286 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 InstantAppDigest(Parcel in)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(); 330 mDigestPrefixSecure = in.createIntArray(); 331 } 332 getDigestBytes()333 public byte[][] getDigestBytes() { 334 return mDigestBytes; 335 } 336 getDigestPrefix()337 public int[] getDigestPrefix() { 338 return mDigestPrefix; 339 } 340 341 /** 342 * Returns a digest prefix with additional random prefixes interspersed. 343 * @hide 344 */ getDigestPrefixSecure()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 361 @Override describeContents()362 public int describeContents() { 363 return 0; 364 } 365 366 @Override writeToParcel(Parcel out, int flags)367 public void writeToParcel(Parcel out, int flags) { 368 final boolean isUndefined = this == UNDEFINED; 369 out.writeBoolean(isUndefined); 370 if (isUndefined) { 371 return; 372 } 373 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); 382 out.writeIntArray(mDigestPrefixSecure); 383 } 384 385 @SuppressWarnings("hiding") 386 public static final @android.annotation.NonNull Parcelable.Creator<InstantAppDigest> CREATOR = 387 new Parcelable.Creator<InstantAppDigest>() { 388 @Override 389 public InstantAppDigest createFromParcel(Parcel in) { 390 if (in.readBoolean() /* is undefined */) { 391 return UNDEFINED; 392 } 393 return new InstantAppDigest(in); 394 } 395 @Override 396 public InstantAppDigest[] newArray(int size) { 397 return new InstantAppDigest[size]; 398 } 399 }; 400 } 401 } 402