Home | History | Annotate | Download | only in pm
      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. */
     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. */
     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. */
     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 
    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 
    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      */
    156     public boolean shouldLetInstallerDecide() {
    157         return mShouldLetInstallerDecide;
    158     }
    159 
    160     public byte[] getDigestBytes() {
    161         return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST;
    162     }
    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 
    176     /**
    177      * @deprecated Use {@link #getLongVersionCode} instead.
    178      */
    179     @Deprecated
    180     public int getVersionCode() {
    181         return (int) (mVersionCode & 0xffffffff);
    182     }
    183 
    184     public long getLongVersionCode() {
    185         return mVersionCode;
    186     }
    187 
    188     @Nullable
    189     public Bundle getExtras() {
    190         return mExtras;
    191     }
    192 
    193     @Override
    194     public int describeContents() {
    195         return 0;
    196     }
    197 
    198     @Override
    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 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 
    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 
    281         private InstantAppDigest(byte[][] digestBytes, int[] prefix) {
    282             this.mDigestPrefix = prefix;
    283             this.mDigestBytes = digestBytes;
    284         }
    285 
    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 
    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 
    333         public byte[][] getDigestBytes() {
    334             return mDigestBytes;
    335         }
    336 
    337         public int[] getDigestPrefix() {
    338             return mDigestPrefix;
    339         }
    340 
    341         /**
    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 
    361         @Override
    362         public int describeContents() {
    363             return 0;
    364         }
    365 
    366         @Override
    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 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