Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 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; |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 22 | import android.content.Intent; |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 23 | import android.os.Bundle; |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 24 | import android.os.Parcel; |
| 25 | import android.os.Parcelable; |
| 26 | |
| 27 | import java.security.MessageDigest; |
| 28 | import java.security.NoSuchAlgorithmException; |
| 29 | import java.util.ArrayList; |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 30 | import java.util.Collections; |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 31 | import java.util.List; |
| 32 | import java.util.Locale; |
| 33 | |
| 34 | /** |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 35 | * 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 Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 59 | * @hide |
| 60 | */ |
| 61 | @SystemApi |
| 62 | public final class InstantAppResolveInfo implements Parcelable { |
| 63 | /** Algorithm that will be used to generate the domain digest */ |
Todd Kennedy | 877e979 | 2017-06-02 07:53:44 -0700 | [diff] [blame] | 64 | private static final String SHA_ALGORITHM = "SHA-256"; |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 65 | |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 66 | private static final byte[] EMPTY_DIGEST = new byte[0]; |
| 67 | |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 68 | 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 Hackborn | 3accca0 | 2013-09-20 09:32:11 -0700 | [diff] [blame] | 73 | private final long mVersionCode; |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 74 | /** Data about the app that should be passed along to the Instant App installer on resolve */ |
| 75 | private final Bundle mExtras; |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 76 | /** |
| 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 Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 82 | |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 83 | /** Constructor for intent-based InstantApp resolution results. */ |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 84 | public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, |
Todd Kennedy | c0dd03a | 2017-05-05 17:15:38 +0000 | [diff] [blame] | 85 | @Nullable List<InstantAppIntentFilter> filters, int versionCode) { |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 86 | this(digest, packageName, filters, (long) versionCode, null /* extras */); |
Dianne Hackborn | 3accca0 | 2013-09-20 09:32:11 -0700 | [diff] [blame] | 87 | } |
| 88 | |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 89 | /** Constructor for intent-based InstantApp resolution results with extras. */ |
Dianne Hackborn | 3accca0 | 2013-09-20 09:32:11 -0700 | [diff] [blame] | 90 | public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 91 | @Nullable List<InstantAppIntentFilter> filters, long versionCode, |
| 92 | @Nullable Bundle extras) { |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 93 | 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 Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 100 | // 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 Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 107 | mFilters = new ArrayList<>(filters.size()); |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 108 | mFilters.addAll(filters); |
| 109 | } else { |
| 110 | mFilters = null; |
| 111 | } |
| 112 | mPackageName = packageName; |
Todd Kennedy | c0dd03a | 2017-05-05 17:15:38 +0000 | [diff] [blame] | 113 | mVersionCode = versionCode; |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 114 | mExtras = extras; |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 115 | mShouldLetInstallerDecide = shouldLetInstallerDecide; |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 116 | } |
| 117 | |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 118 | /** Constructor for intent-based InstantApp resolution results by hostname. */ |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 119 | public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName, |
| 120 | @Nullable List<InstantAppIntentFilter> filters) { |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 121 | this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/, |
| 122 | null /* extras */); |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 123 | } |
| 124 | |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 125 | /** |
| 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 Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 133 | InstantAppResolveInfo(Parcel in) { |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 134 | mShouldLetInstallerDecide = in.readBoolean(); |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 135 | mExtras = in.readBundle(); |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 136 | 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 Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 153 | } |
| 154 | |
| 155 | public byte[] getDigestBytes() { |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 156 | return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST; |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 157 | } |
| 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 Hackborn | 3accca0 | 2013-09-20 09:32:11 -0700 | [diff] [blame] | 171 | /** |
| 172 | * @deprecated Use {@link #getLongVersionCode} instead. |
| 173 | */ |
| 174 | @Deprecated |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 175 | public int getVersionCode() { |
Dianne Hackborn | 3accca0 | 2013-09-20 09:32:11 -0700 | [diff] [blame] | 176 | return (int) (mVersionCode & 0xffffffff); |
| 177 | } |
| 178 | |
| 179 | public long getLongVersionCode() { |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 180 | return mVersionCode; |
| 181 | } |
| 182 | |
Patrick Baumann | 709ee15 | 2017-12-04 16:12:52 -0800 | [diff] [blame] | 183 | @Nullable |
| 184 | public Bundle getExtras() { |
| 185 | return mExtras; |
| 186 | } |
| 187 | |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 188 | @Override |
| 189 | public int describeContents() { |
| 190 | return 0; |
| 191 | } |
| 192 | |
| 193 | @Override |
| 194 | public void writeToParcel(Parcel out, int flags) { |
Patrick Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 195 | out.writeBoolean(mShouldLetInstallerDecide); |
| 196 | out.writeBundle(mExtras); |
| 197 | if (mShouldLetInstallerDecide) { |
| 198 | return; |
| 199 | } |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 200 | out.writeParcelable(mDigest, flags); |
| 201 | out.writeString(mPackageName); |
| 202 | out.writeList(mFilters); |
Dianne Hackborn | 3accca0 | 2013-09-20 09:32:11 -0700 | [diff] [blame] | 203 | out.writeLong(mVersionCode); |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 204 | } |
| 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 Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 231 | |
| 232 | public static final InstantAppDigest UNDEFINED = |
| 233 | new InstantAppDigest(new byte[][]{}, new int[]{}); |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 234 | /** 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 Baumann | 3e8bd0f | 2018-01-08 11:23:38 -0800 | [diff] [blame] | 260 | private InstantAppDigest(byte[][] digestBytes, int[] prefix) { |
| 261 | this.mDigestPrefix = prefix; |
| 262 | this.mDigestBytes = digestBytes; |
| 263 | } |
| 264 | |
Todd Kennedy | 1fb3404 | 2017-03-01 13:56:58 -0800 | [diff] [blame] | 265 | 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 | } |