New API w/ multiple prefixes

Use the new API that contains multiple hash prefixes and a mask. Also
do some small refactoring necessary to handle multiple prefixes and
use a common implementation of the hash generation

Change-Id: Ib52f767ea6aadc30c67c5bdee949e9f9c5f04e44
diff --git a/core/java/android/content/pm/EphemeralResolveInfo.java b/core/java/android/content/pm/EphemeralResolveInfo.java
index b87731b..2b9cfa0 100644
--- a/core/java/android/content/pm/EphemeralResolveInfo.java
+++ b/core/java/android/content/pm/EphemeralResolveInfo.java
@@ -37,10 +37,7 @@
     /** Algorithm that will be used to generate the domain digest */
     public static final String SHA_ALGORITHM = "SHA-256";
 
-    /** Full digest of the domain hash */
-    private final byte[] mDigestBytes;
-    /** The first 4 bytes of the domain hash */
-    private final int mDigestPrefix;
+    private final EphemeralDigest mDigest;
     private final String mPackageName;
     /** The filters used to match domain */
     private final List<IntentFilter> mFilters = new ArrayList<IntentFilter>();
@@ -55,29 +52,23 @@
             throw new IllegalArgumentException();
         }
 
-        mDigestBytes = generateDigest(uri);
-        mDigestPrefix =
-                (mDigestBytes[0] & 0xFF) << 24
-                | (mDigestBytes[1] & 0xFF) << 16
-                | (mDigestBytes[2] & 0xFF) << 8
-                | (mDigestBytes[3] & 0xFF) << 0;
+        mDigest = new EphemeralDigest(uri, -1);
         mFilters.addAll(filters);
         mPackageName = packageName;
     }
 
     EphemeralResolveInfo(Parcel in) {
-        mDigestBytes = in.createByteArray();
-        mDigestPrefix = in.readInt();
+        mDigest = in.readParcelable(null /*loader*/);
         mPackageName = in.readString();
         in.readList(mFilters, null /*loader*/);
     }
 
     public byte[] getDigestBytes() {
-        return mDigestBytes;
+        return mDigest.getDigestBytes()[0];
     }
 
     public int getDigestPrefix() {
-        return mDigestPrefix;
+        return mDigest.getDigestPrefix()[0];
     }
 
     public String getPackageName() {
@@ -88,16 +79,6 @@
         return mFilters;
     }
 
-    private static byte[] generateDigest(Uri uri) {
-        try {
-            final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM);
-            final byte[] hostBytes = uri.getHost().getBytes();
-            return digest.digest(hostBytes);
-        } catch (NoSuchAlgorithmException e) {
-            throw new IllegalStateException("could not find digest algorithm");
-        }
-    }
-
     @Override
     public int describeContents() {
         return 0;
@@ -105,8 +86,7 @@
 
     @Override
     public void writeToParcel(Parcel out, int flags) {
-        out.writeByteArray(mDigestBytes);
-        out.writeInt(mDigestPrefix);
+        out.writeParcelable(mDigest, flags);
         out.writeString(mPackageName);
         out.writeList(mFilters);
     }
@@ -136,4 +116,120 @@
             return mResolveInfo;
         }
     }
+
+    /**
+     * Helper class to generate and store each of the digests and prefixes
+     * sent to the Ephemeral Resolver.
+     * <p>
+     * Since intent filters may want to handle multiple hosts within a
+     * domain [eg “*.google.com”], the resolver is presented with multiple
+     * hash prefixes. For example, "a.b.c.d.e" generates digests for
+     * "d.e", "c.d.e", "b.c.d.e" and "a.b.c.d.e".
+     *
+     * @hide
+     */
+    public static final class EphemeralDigest implements Parcelable {
+        /** Full digest of the domain hashes */
+        private final byte[][] mDigestBytes;
+        /** The first 4 bytes of the domain hashes */
+        private final int[] mDigestPrefix;
+
+        public EphemeralDigest(@NonNull Uri uri, int maxDigests) {
+            if (uri == null) {
+                throw new IllegalArgumentException();
+            }
+            mDigestBytes = generateDigest(uri, maxDigests);
+            mDigestPrefix = new int[mDigestBytes.length];
+            for (int i = 0; i < mDigestBytes.length; i++) {
+                mDigestPrefix[i] =
+                        (mDigestBytes[i][0] & 0xFF) << 24
+                        | (mDigestBytes[i][1] & 0xFF) << 16
+                        | (mDigestBytes[i][2] & 0xFF) << 8
+                        | (mDigestBytes[i][3] & 0xFF) << 0;
+            }
+        }
+
+        private static byte[][] generateDigest(Uri uri, int maxDigests) {
+            ArrayList<byte[]> digests = new ArrayList<>();
+            try {
+                final String host = uri.getHost();
+                final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM);
+                if (maxDigests <= 0) {
+                    final byte[] hostBytes = host.getBytes();
+                    digests.add(digest.digest(hostBytes));
+                } else {
+                    int prevDot = host.lastIndexOf('.');
+                    prevDot = host.lastIndexOf('.', prevDot - 1);
+                    // shortcut for short URLs
+                    if (prevDot < 0) {
+                        digests.add(digest.digest(host.getBytes()));
+                    } else {
+                        byte[] hostBytes = host.substring(prevDot + 1, host.length()).getBytes();
+                        digests.add(digest.digest(hostBytes));
+                        int digestCount = 1;
+                        while (prevDot >= 0 && digestCount < maxDigests) {
+                            prevDot = host.lastIndexOf('.', prevDot - 1);
+                            hostBytes = host.substring(prevDot + 1, host.length()).getBytes();
+                            digests.add(digest.digest(hostBytes));
+                            digestCount++;
+                        }
+                    }
+                }
+            } catch (NoSuchAlgorithmException e) {
+                throw new IllegalStateException("could not find digest algorithm");
+            }
+            return digests.toArray(new byte[digests.size()][]);
+        }
+
+        EphemeralDigest(Parcel in) {
+            final int digestCount = in.readInt();
+            if (digestCount == -1) {
+                mDigestBytes = null;
+            } else {
+                mDigestBytes = new byte[digestCount][];
+                for (int i = 0; i < digestCount; i++) {
+                    mDigestBytes[i] = in.createByteArray();
+                }
+            }
+            mDigestPrefix = in.createIntArray();
+        }
+
+        public byte[][] getDigestBytes() {
+            return mDigestBytes;
+        }
+
+        public int[] getDigestPrefix() {
+            return mDigestPrefix;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            if (mDigestBytes == null) {
+                out.writeInt(-1);
+            } else {
+                out.writeInt(mDigestBytes.length);
+                for (int i = 0; i < mDigestBytes.length; i++) {
+                    out.writeByteArray(mDigestBytes[i]);
+                }
+            }
+            out.writeIntArray(mDigestPrefix);
+        }
+
+        @SuppressWarnings("hiding")
+        public static final Parcelable.Creator<EphemeralDigest> CREATOR =
+                new Parcelable.Creator<EphemeralDigest>() {
+            public EphemeralDigest createFromParcel(Parcel in) {
+                return new EphemeralDigest(in);
+            }
+
+            public EphemeralDigest[] newArray(int size) {
+                return new EphemeralDigest[size];
+            }
+        };
+    }
 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 43eb751..88cf138 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8583,6 +8583,24 @@
                 "ephemeral_cookie_max_size_bytes";
 
         /**
+         * A mask applied to the ephemeral hash to generate the hash prefix.
+         * <p>
+         * Type: int
+         *
+         * @hide
+         */
+        public static final String EPHEMERAL_HASH_PREFIX_MASK = "ephemeral_hash_prefix_mask";
+
+        /**
+         * Number of hash prefixes to send during ephemeral resolution.
+         * <p>
+         * Type: int
+         *
+         * @hide
+         */
+        public static final String EPHEMERAL_HASH_PREFIX_COUNT = "ephemeral_hash_prefix_count";
+
+        /**
          * The duration for caching uninstalled ephemeral apps.
          * <p>
          * Type: long
diff --git a/core/java/com/android/internal/app/EphemeralResolverService.java b/core/java/com/android/internal/app/EphemeralResolverService.java
index 6ba04a9..68724a7 100644
--- a/core/java/com/android/internal/app/EphemeralResolverService.java
+++ b/core/java/com/android/internal/app/EphemeralResolverService.java
@@ -39,14 +39,19 @@
 public abstract class EphemeralResolverService extends Service {
     public static final String EXTRA_RESOLVE_INFO = "com.android.internal.app.RESOLVE_INFO";
     public static final String EXTRA_SEQUENCE = "com.android.internal.app.SEQUENCE";
+    private static final String EXTRA_PREFIX = "com.android.internal.app.PREFIX";
     private Handler mHandler;
 
     /**
      * Called to retrieve resolve info for ephemeral applications.
      *
      * @param digestPrefix The hash prefix of the ephemeral's domain.
+     * @param prefixMask A mask that was applied to each digest prefix. This should
+     *      be used when comparing against the digest prefixes as all bits might
+     *      not be set.
      */
-    protected abstract List<EphemeralResolveInfo> getEphemeralResolveInfoList(int digestPrefix);
+    protected abstract List<EphemeralResolveInfo> getEphemeralResolveInfoList(
+            int digestPrefix[], int prefixMask);
 
     @Override
     protected final void attachBaseContext(Context base) {
@@ -59,10 +64,13 @@
         return new IEphemeralResolver.Stub() {
             @Override
             public void getEphemeralResolveInfoList(
-                    IRemoteCallback callback, int digestPrefix, int sequence) {
-                mHandler.obtainMessage(ServiceHandler.MSG_GET_EPHEMERAL_RESOLVE_INFO,
-                        digestPrefix, sequence, callback)
-                    .sendToTarget();
+                    IRemoteCallback callback, int digestPrefix[], int prefixMask, int sequence) {
+                final Message msg = mHandler.obtainMessage(
+                        ServiceHandler.MSG_GET_EPHEMERAL_RESOLVE_INFO, prefixMask, sequence, callback);
+                final Bundle data = new Bundle();
+                data.putIntArray(EXTRA_PREFIX, digestPrefix);
+                msg.setData(data);
+                msg.sendToTarget();
             }
         };
     }
@@ -81,8 +89,9 @@
             switch (action) {
                 case MSG_GET_EPHEMERAL_RESOLVE_INFO: {
                     final IRemoteCallback callback = (IRemoteCallback) message.obj;
+                    final int[] digestPrefix = message.getData().getIntArray(EXTRA_PREFIX);
                     final List<EphemeralResolveInfo> resolveInfo =
-                            getEphemeralResolveInfoList(message.arg1);
+                            getEphemeralResolveInfoList(digestPrefix, message.arg1);
                     final Bundle data = new Bundle();
                     data.putInt(EXTRA_SEQUENCE, message.arg2);
                     data.putParcelableList(EXTRA_RESOLVE_INFO, resolveInfo);
diff --git a/core/java/com/android/internal/app/IEphemeralResolver.aidl b/core/java/com/android/internal/app/IEphemeralResolver.aidl
index 40429ee..9ff1322 100644
--- a/core/java/com/android/internal/app/IEphemeralResolver.aidl
+++ b/core/java/com/android/internal/app/IEphemeralResolver.aidl
@@ -20,5 +20,6 @@
 import android.os.IRemoteCallback;
 
 oneway interface IEphemeralResolver {
-    void getEphemeralResolveInfoList(IRemoteCallback callback, int digestPrefix, int sequence);
+    void getEphemeralResolveInfoList(IRemoteCallback callback, in int[] digestPrefix,
+            int prefixMask, int sequence);
 }
diff --git a/services/core/java/com/android/server/pm/EphemeralResolverConnection.java b/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
index fe6fb1f..b25ef175 100644
--- a/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
+++ b/services/core/java/com/android/server/pm/EphemeralResolverConnection.java
@@ -64,11 +64,12 @@
         mIntent = new Intent().setComponent(componentName);
     }
 
-    public final List<EphemeralResolveInfo> getEphemeralResolveInfoList(int hashPrefix) {
+    public final List<EphemeralResolveInfo> getEphemeralResolveInfoList(
+            int hashPrefix[], int prefixMask) {
         throwIfCalledOnMainThread();
         try {
             return mGetEphemeralResolveInfoCaller.getEphemeralResolveInfoList(
-                    getRemoteInstanceLazy(), hashPrefix);
+                    getRemoteInstanceLazy(), hashPrefix, prefixMask);
         } catch (RemoteException re) {
         } catch (TimeoutException te) {
         } finally {
@@ -177,10 +178,10 @@
         }
 
         public List<EphemeralResolveInfo> getEphemeralResolveInfoList(
-                IEphemeralResolver target, int hashPrefix)
+                IEphemeralResolver target, int hashPrefix[], int prefixMask)
                         throws RemoteException, TimeoutException {
             final int sequence = onBeforeRemoteCall();
-            target.getEphemeralResolveInfoList(mCallback, hashPrefix, sequence);
+            target.getEphemeralResolveInfoList(mCallback, hashPrefix, prefixMask, sequence);
             return getResultTimed(sequence);
         }
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 0bd9822..2562707 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -125,6 +125,7 @@
 import android.content.pm.ComponentInfo;
 import android.content.pm.EphemeralApplicationInfo;
 import android.content.pm.EphemeralResolveInfo;
+import android.content.pm.EphemeralResolveInfo.EphemeralDigest;
 import android.content.pm.EphemeralResolveInfo.EphemeralResolveIntentInfo;
 import android.content.pm.FeatureInfo;
 import android.content.pm.IOnPermissionsChangeListener;
@@ -197,6 +198,7 @@
 import android.os.storage.StorageManager;
 import android.os.storage.VolumeInfo;
 import android.os.storage.VolumeRecord;
+import android.provider.Settings.Global;
 import android.security.KeyStore;
 import android.security.SystemKeyStore;
 import android.system.ErrnoException;
@@ -462,6 +464,9 @@
 
     private static final String VENDOR_OVERLAY_DIR = "/vendor/overlay";
 
+    private static int DEFAULT_EPHEMERAL_HASH_PREFIX_MASK = 0xFFFFFFFF;
+    private static int DEFAULT_EPHEMERAL_HASH_PREFIX_COUNT = 5;
+
     /** Permission grant: not grant the permission. */
     private static final int GRANT_DENIED = 1;
 
@@ -4991,48 +4996,44 @@
 
     private EphemeralResolveInfo getEphemeralResolveInfo(Intent intent, String resolvedType,
             int userId) {
-        MessageDigest digest = null;
-        try {
-            digest = MessageDigest.getInstance(EphemeralResolveInfo.SHA_ALGORITHM);
-        } catch (NoSuchAlgorithmException e) {
-            // If we can't create a digest, ignore ephemeral apps.
-            return null;
-        }
-
-        final byte[] hostBytes = intent.getData().getHost().getBytes();
-        final byte[] digestBytes = digest.digest(hostBytes);
-        int shaPrefix =
-                (digestBytes[0] & 0xFF) << 24
-                | (digestBytes[1] & 0xFF) << 16
-                | (digestBytes[2] & 0xFF) << 8
-                | (digestBytes[3] & 0xFF) << 0;
+        final int ephemeralPrefixMask = Global.getInt(mContext.getContentResolver(),
+                Global.EPHEMERAL_HASH_PREFIX_MASK, DEFAULT_EPHEMERAL_HASH_PREFIX_MASK);
+        final int ephemeralPrefixCount = Global.getInt(mContext.getContentResolver(),
+                Global.EPHEMERAL_HASH_PREFIX_COUNT, DEFAULT_EPHEMERAL_HASH_PREFIX_COUNT);
+        final EphemeralDigest digest = new EphemeralDigest(intent.getData(), ephemeralPrefixCount);
+        final int[] shaPrefix = digest.getDigestPrefix();
+        final byte[][] digestBytes = digest.getDigestBytes();
         final List<EphemeralResolveInfo> ephemeralResolveInfoList =
-                mEphemeralResolverConnection.getEphemeralResolveInfoList(shaPrefix);
+                mEphemeralResolverConnection.getEphemeralResolveInfoList(
+                        shaPrefix, ephemeralPrefixMask);
         if (ephemeralResolveInfoList == null || ephemeralResolveInfoList.size() == 0) {
             // No hash prefix match; there are no ephemeral apps for this domain.
             return null;
         }
-        for (int i = ephemeralResolveInfoList.size() - 1; i >= 0; --i) {
-            EphemeralResolveInfo ephemeralApplication = ephemeralResolveInfoList.get(i);
-            if (!Arrays.equals(digestBytes, ephemeralApplication.getDigestBytes())) {
-                continue;
-            }
-            final List<IntentFilter> filters = ephemeralApplication.getFilters();
-            // No filters; this should never happen.
-            if (filters.isEmpty()) {
-                continue;
-            }
-            // We have a domain match; resolve the filters to see if anything matches.
-            final EphemeralIntentResolver ephemeralResolver = new EphemeralIntentResolver();
-            for (int j = filters.size() - 1; j >= 0; --j) {
-                final EphemeralResolveIntentInfo intentInfo =
-                        new EphemeralResolveIntentInfo(filters.get(j), ephemeralApplication);
-                ephemeralResolver.addFilter(intentInfo);
-            }
-            List<EphemeralResolveInfo> matchedResolveInfoList = ephemeralResolver.queryIntent(
-                    intent, resolvedType, false /*defaultOnly*/, userId);
-            if (!matchedResolveInfoList.isEmpty()) {
-                return matchedResolveInfoList.get(0);
+
+        // Go in reverse order so we match the narrowest scope first.
+        for (int i = shaPrefix.length; i >= 0 ; --i) {
+            for (EphemeralResolveInfo ephemeralApplication : ephemeralResolveInfoList) {
+                if (!Arrays.equals(digestBytes[i], ephemeralApplication.getDigestBytes())) {
+                    continue;
+                }
+                final List<IntentFilter> filters = ephemeralApplication.getFilters();
+                // No filters; this should never happen.
+                if (filters.isEmpty()) {
+                    continue;
+                }
+                // We have a domain match; resolve the filters to see if anything matches.
+                final EphemeralIntentResolver ephemeralResolver = new EphemeralIntentResolver();
+                for (int j = filters.size() - 1; j >= 0; --j) {
+                    final EphemeralResolveIntentInfo intentInfo =
+                            new EphemeralResolveIntentInfo(filters.get(j), ephemeralApplication);
+                    ephemeralResolver.addFilter(intentInfo);
+                }
+                List<EphemeralResolveInfo> matchedResolveInfoList = ephemeralResolver.queryIntent(
+                        intent, resolvedType, false /*defaultOnly*/, userId);
+                if (!matchedResolveInfoList.isEmpty()) {
+                    return matchedResolveInfoList.get(0);
+                }
             }
         }
         // Hash or filter mis-match; no ephemeral apps for this domain.