Merge "Fixing some small issues with the overlay."
diff --git a/api/current.txt b/api/current.txt
index e347c4c..4f801cb 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -19297,6 +19297,7 @@
     method public void adjustVolume(int, int);
     method public void dispatchMediaKeyEvent(android.view.KeyEvent);
     method public int generateAudioSessionId();
+    method public android.media.AudioRecordConfiguration[] getActiveRecordConfigurations();
     method public android.media.AudioDeviceInfo[] getDevices(int);
     method public int getMode();
     method public java.lang.String getParameters(java.lang.String);
@@ -19319,6 +19320,7 @@
     method public void playSoundEffect(int);
     method public void playSoundEffect(int, float);
     method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler);
+    method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler);
     method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void registerRemoteControlClient(android.media.RemoteControlClient);
@@ -19342,6 +19344,7 @@
     method public void stopBluetoothSco();
     method public void unloadSoundEffects();
     method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback);
+    method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
     method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void unregisterRemoteControlClient(android.media.RemoteControlClient);
@@ -19438,6 +19441,11 @@
     field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0
   }
 
+  public static abstract class AudioManager.AudioRecordingCallback {
+    ctor public AudioManager.AudioRecordingCallback();
+    method public void onRecordConfigChanged();
+  }
+
   public static abstract interface AudioManager.OnAudioFocusChangeListener {
     method public abstract void onAudioFocusChange(int);
   }
@@ -19508,6 +19516,14 @@
     method public abstract void onRoutingChanged(android.media.AudioRecord);
   }
 
+  public class AudioRecordConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioSessionId();
+    method public int getClientAudioSource();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.media.AudioRecordConfiguration> CREATOR;
+  }
+
   public abstract interface AudioRouting {
     method public abstract void addOnRoutingListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public abstract android.media.AudioDeviceInfo getPreferredDevice();
@@ -28590,6 +28606,7 @@
     field public static final java.lang.String DISALLOW_OUTGOING_CALLS = "no_outgoing_calls";
     field public static final java.lang.String DISALLOW_REMOVE_USER = "no_remove_user";
     field public static final java.lang.String DISALLOW_SAFE_BOOT = "no_safe_boot";
+    field public static final java.lang.String DISALLOW_SET_USER_ICON = "no_set_user_icon";
     field public static final java.lang.String DISALLOW_SHARE_LOCATION = "no_share_location";
     field public static final java.lang.String DISALLOW_SMS = "no_sms";
     field public static final java.lang.String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps";
diff --git a/api/system-current.txt b/api/system-current.txt
index 1a76097..47a51d4 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5941,6 +5941,7 @@
     method public deprecated android.content.ComponentName getDeviceInitializerComponent();
     method public java.lang.String getDeviceOwner();
     method public java.lang.String getDeviceOwnerLockScreenInfo();
+    method public java.lang.String getDeviceOwnerNameOnAnyUser();
     method public java.util.List<byte[]> getInstalledCaCerts(android.content.ComponentName);
     method public int getKeyguardDisabledFeatures(android.content.ComponentName);
     method public java.lang.String getLongSupportMessage(android.content.ComponentName);
@@ -20622,6 +20623,7 @@
     method public void adjustVolume(int, int);
     method public void dispatchMediaKeyEvent(android.view.KeyEvent);
     method public int generateAudioSessionId();
+    method public android.media.AudioRecordConfiguration[] getActiveRecordConfigurations();
     method public android.media.AudioDeviceInfo[] getDevices(int);
     method public int getMode();
     method public java.lang.String getParameters(java.lang.String);
@@ -20646,6 +20648,7 @@
     method public void playSoundEffect(int, float);
     method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler);
     method public int registerAudioPolicy(android.media.audiopolicy.AudioPolicy);
+    method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler);
     method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void registerRemoteControlClient(android.media.RemoteControlClient);
@@ -20672,6 +20675,7 @@
     method public void unloadSoundEffects();
     method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback);
     method public void unregisterAudioPolicyAsync(android.media.audiopolicy.AudioPolicy);
+    method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
     method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void unregisterRemoteControlClient(android.media.RemoteControlClient);
@@ -20771,6 +20775,11 @@
     field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0
   }
 
+  public static abstract class AudioManager.AudioRecordingCallback {
+    ctor public AudioManager.AudioRecordingCallback();
+    method public void onRecordConfigChanged();
+  }
+
   public static abstract interface AudioManager.OnAudioFocusChangeListener {
     method public abstract void onAudioFocusChange(int);
   }
@@ -20844,6 +20853,14 @@
     method public abstract void onRoutingChanged(android.media.AudioRecord);
   }
 
+  public class AudioRecordConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioSessionId();
+    method public int getClientAudioSource();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.media.AudioRecordConfiguration> CREATOR;
+  }
+
   public abstract interface AudioRouting {
     method public abstract void addOnRoutingListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public abstract android.media.AudioDeviceInfo getPreferredDevice();
@@ -30642,6 +30659,7 @@
     field public static final java.lang.String DISALLOW_OUTGOING_CALLS = "no_outgoing_calls";
     field public static final java.lang.String DISALLOW_REMOVE_USER = "no_remove_user";
     field public static final java.lang.String DISALLOW_SAFE_BOOT = "no_safe_boot";
+    field public static final java.lang.String DISALLOW_SET_USER_ICON = "no_set_user_icon";
     field public static final java.lang.String DISALLOW_SHARE_LOCATION = "no_share_location";
     field public static final java.lang.String DISALLOW_SMS = "no_sms";
     field public static final java.lang.String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps";
diff --git a/api/test-current.txt b/api/test-current.txt
index 111145b..6b5b8b0 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -19305,6 +19305,7 @@
     method public void adjustVolume(int, int);
     method public void dispatchMediaKeyEvent(android.view.KeyEvent);
     method public int generateAudioSessionId();
+    method public android.media.AudioRecordConfiguration[] getActiveRecordConfigurations();
     method public android.media.AudioDeviceInfo[] getDevices(int);
     method public int getMode();
     method public java.lang.String getParameters(java.lang.String);
@@ -19327,6 +19328,7 @@
     method public void playSoundEffect(int);
     method public void playSoundEffect(int, float);
     method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler);
+    method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler);
     method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void registerRemoteControlClient(android.media.RemoteControlClient);
@@ -19350,6 +19352,7 @@
     method public void stopBluetoothSco();
     method public void unloadSoundEffects();
     method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback);
+    method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
     method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void unregisterRemoteControlClient(android.media.RemoteControlClient);
@@ -19446,6 +19449,11 @@
     field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0
   }
 
+  public static abstract class AudioManager.AudioRecordingCallback {
+    ctor public AudioManager.AudioRecordingCallback();
+    method public void onRecordConfigChanged();
+  }
+
   public static abstract interface AudioManager.OnAudioFocusChangeListener {
     method public abstract void onAudioFocusChange(int);
   }
@@ -19516,6 +19524,14 @@
     method public abstract void onRoutingChanged(android.media.AudioRecord);
   }
 
+  public class AudioRecordConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioSessionId();
+    method public int getClientAudioSource();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.media.AudioRecordConfiguration> CREATOR;
+  }
+
   public abstract interface AudioRouting {
     method public abstract void addOnRoutingListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public abstract android.media.AudioDeviceInfo getPreferredDevice();
@@ -28599,6 +28615,7 @@
     field public static final java.lang.String DISALLOW_OUTGOING_CALLS = "no_outgoing_calls";
     field public static final java.lang.String DISALLOW_REMOVE_USER = "no_remove_user";
     field public static final java.lang.String DISALLOW_SAFE_BOOT = "no_safe_boot";
+    field public static final java.lang.String DISALLOW_SET_USER_ICON = "no_set_user_icon";
     field public static final java.lang.String DISALLOW_SHARE_LOCATION = "no_share_location";
     field public static final java.lang.String DISALLOW_SMS = "no_sms";
     field public static final java.lang.String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps";
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 0161119..20cacff 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3057,6 +3057,7 @@
      *
      * @hide
      */
+    @SystemApi
     public String getDeviceOwnerNameOnAnyUser() {
         if (mService != null) {
             try {
diff --git a/core/java/android/bluetooth/BluetoothHeadsetClientCall.java b/core/java/android/bluetooth/BluetoothHeadsetClientCall.java
index 002f63f..c73bc3c 100644
--- a/core/java/android/bluetooth/BluetoothHeadsetClientCall.java
+++ b/core/java/android/bluetooth/BluetoothHeadsetClientCall.java
@@ -196,7 +196,7 @@
 
     public String toString(boolean loggable) {
         StringBuilder builder = new StringBuilder("BluetoothHeadsetClientCall{mDevice: ");
-        builder.append(loggable ? mDevice.hashCode() : mDevice);
+        builder.append(loggable ? mDevice : mDevice.hashCode());
         builder.append(", mId: ");
         builder.append(mId);
         builder.append(", mUUID: ");
@@ -214,7 +214,7 @@
             default: builder.append(mState); break;
         }
         builder.append(", mNumber: ");
-        builder.append(loggable ? mNumber.hashCode() : mNumber);
+        builder.append(loggable ? mNumber : mNumber.hashCode());
         builder.append(", mMultiParty: ");
         builder.append(mMultiParty);
         builder.append(", mOutgoing: ");
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index f01f597..fe834a1 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -529,6 +529,21 @@
     public static final String DISALLOW_DATA_ROAMING = "no_data_roaming";
 
     /**
+     * Specifies if a user is not allowed to change their icon. Device owner and profile owner
+     * can set this restriction. When it is set by device owner, only the target user will be
+     * affected. The default value is <code>false</code>.
+     *
+     * <p>Key for user restrictions.
+     *
+     * <p>Type: Boolean
+     *
+     * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+     * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+     * @see #getUserRestrictions()
+     */
+    public static final String DISALLOW_SET_USER_ICON = "no_set_user_icon";
+
+    /**
      * Allows apps in the parent profile to handle web links from the managed profile.
      *
      * This user restriction has an effect only in a managed profile.
diff --git a/core/java/android/os/UserManagerInternal.java b/core/java/android/os/UserManagerInternal.java
index 898b6cf..f765336 100644
--- a/core/java/android/os/UserManagerInternal.java
+++ b/core/java/android/os/UserManagerInternal.java
@@ -17,6 +17,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Bitmap;
 
 /**
  * @hide Only for use within the system server.
@@ -81,4 +82,13 @@
      * whether the user is managed by profile owner.
      */
     public abstract void setUserManaged(int userId, boolean isManaged);
+
+    /**
+     * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to omit
+     * restriction check, because DevicePolicyManager must always be able to set user icon
+     * regardless of any restriction.
+     * Also called by {@link com.android.server.pm.UserManagerService} because the logic of setting
+     * the icon is in this method.
+     */
+    public abstract void setUserIcon(int userId, Bitmap bitmap);
 }
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 6a5d857c..e7c4a07 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -38,6 +38,7 @@
 import android.telecom.TelecomManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.internal.telephony.CallerInfo;
 import com.android.internal.telephony.PhoneConstants;
@@ -49,6 +50,7 @@
  */
 public class CallLog {
     private static final String LOG_TAG = "CallLog";
+    private static final boolean VERBOSE_LOG = false; // DON'T SUBMIT WITH TRUE.
 
     public static final String AUTHORITY = "call_log";
 
@@ -58,6 +60,17 @@
     public static final Uri CONTENT_URI =
         Uri.parse("content://" + AUTHORITY);
 
+
+    /**
+     * The "shadow" provider stores calllog when the real calllog provider is encrypted.  The
+     * real provider will alter copy from it when it starts, and remove the entries in the shadow.
+     *
+     * <p>See the comment in {@link Calls#addCall} for the details.
+     *
+     * @hide
+     */
+    public static final String SHADOW_AUTHORITY = "call_log_shadow";
+
     /**
      * Contains the recent calls.
      */
@@ -68,6 +81,10 @@
         public static final Uri CONTENT_URI =
                 Uri.parse("content://call_log/calls");
 
+        /** @hide */
+        public static final Uri SHADOW_CONTENT_URI =
+                Uri.parse("content://call_log_shadow/calls");
+
         /**
          * The content:// style URL for filtering this table on phone numbers
          */
@@ -458,8 +475,10 @@
         public static Uri addCall(CallerInfo ci, Context context, String number,
                 int presentation, int callType, int features, PhoneAccountHandle accountHandle,
                 long start, int duration, Long dataUsage) {
-            return addCall(ci, context, number, "", presentation, callType, features, accountHandle,
-                    start, duration, dataUsage, false, null, false);
+            return addCall(ci, context, number, /* postDialDigits =*/ "", presentation,
+                    callType, features, accountHandle,
+                    start, duration, dataUsage, /* addForAllUsers =*/ false,
+                    /* userToBeInsertedTo =*/ null, /* is_read =*/ false);
         }
 
 
@@ -495,7 +514,7 @@
                 boolean addForAllUsers, UserHandle userToBeInsertedTo) {
             return addCall(ci, context, number, postDialDigits, presentation, callType, features,
                     accountHandle, start, duration, dataUsage, addForAllUsers, userToBeInsertedTo,
-                    false);
+                    /* is_read =*/ false);
         }
 
         /**
@@ -526,13 +545,18 @@
          *                Used for call log restore of missed calls.
          *
          * @result The URI of the call log entry belonging to the user that made or received this
-         *        call.
+         *        call.  This could be of the shadow provider.  Do not return it to non-system apps,
+         *        as they don't have permissions.
          * {@hide}
          */
         public static Uri addCall(CallerInfo ci, Context context, String number,
                 String postDialDigits, int presentation, int callType, int features,
                 PhoneAccountHandle accountHandle, long start, int duration, Long dataUsage,
                 boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean is_read) {
+            if (VERBOSE_LOG) {
+                Log.v(LOG_TAG, String.format("Add call: number=%s, user=%s, for all=%s",
+                        number, userToBeInsertedTo, addForAllUsers));
+            }
             final ContentResolver resolver = context.getContentResolver();
             int numberPresentation = PRESENTATION_ALLOWED;
 
@@ -647,41 +671,104 @@
                 }
             }
 
+            /*
+                Writing the calllog works in the following way:
+                - All user entries
+                    - if user-0 is encrypted, insert to user-0's shadow only.
+                      (other users should also be encrypted, so nothing to do for other users.)
+                    - if user-0 is decrypted, insert to user-0's real provider, as well as
+                      all other users that are running and decrypted and should have calllog.
+
+                - Single user entry.
+                    - If the target user is encryted, insert to its shadow.
+                    - Otherwise insert to its real provider.
+
+                When the (real) calllog provider starts, it copies entries that it missed from
+                elsewhere.
+                - When user-0's (real) provider starts, it copies from user-0's shadow, and clears
+                  the shadow.
+
+                - When other users (real) providers start, unless it shouldn't have calllog entries,
+                     - Copy from the user's shadow, and clears the shadow.
+                     - Copy from user-0's entries that are FOR_ALL_USERS = 1.  (and don't clear it.)
+             */
+
             Uri result = null;
 
+            final UserManager userManager = context.getSystemService(UserManager.class);
+            final int currentUserId = userManager.getUserHandle();
+
             if (addForAllUsers) {
-                // Insert the entry for all currently running users, in order to trigger any
-                // ContentObservers currently set on the call log.
-                final UserManager userManager = (UserManager) context.getSystemService(
-                        Context.USER_SERVICE);
-                List<UserInfo> users = userManager.getUsers(true);
-                final int currentUserId = userManager.getUserHandle();
+                // First, insert to the system user.
+                final Uri uriForSystem = addEntryAndRemoveExpiredEntries(
+                        context, userManager, UserHandle.SYSTEM, values);
+                if (uriForSystem == null
+                        || SHADOW_AUTHORITY.equals(uriForSystem.getAuthority())) {
+                    // This means the system user is still encrypted and the entry has inserted
+                    // into the shadow.  This means other users are still all encrypted.
+                    // Nothing further to do; just return null.
+                    return null;
+                }
+                if (UserHandle.USER_SYSTEM == currentUserId) {
+                    result = uriForSystem;
+                }
+
+                // Otherwise, insert to all other users that are running and unlocked.
+
+                final List<UserInfo> users = userManager.getUsers(true);
+
                 final int count = users.size();
                 for (int i = 0; i < count; i++) {
-                    final UserInfo user = users.get(i);
-                    final UserHandle userHandle = user.getUserHandle();
+                    final UserInfo userInfo = users.get(i);
+                    final UserHandle userHandle = userInfo.getUserHandle();
+                    final int userId = userHandle.getIdentifier();
+
+                    if (userHandle.isSystem()) {
+                        // Already written.
+                        continue;
+                    }
+
+                    if (!shouldHaveSharedCallLogEntries(context, userManager, userId)) {
+                        // Shouldn't have calllog entries.
+                        continue;
+                    }
+
+                    // For other users, we write only when they're running *and* decrypted.
+                    // Other providers will copy from the system user's real provider, when they
+                    // start.
                     if (userManager.isUserRunning(userHandle)
-                            && !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
-                                    userHandle)
-                            && !user.isManagedProfile()) {
-                        Uri uri = addEntryAndRemoveExpiredEntries(context,
-                                ContentProvider.maybeAddUserId(CONTENT_URI, user.id), values);
-                        if (user.id == currentUserId) {
+                            && userManager.isUserUnlocked(userHandle)) {
+                        final Uri uri = addEntryAndRemoveExpiredEntries(context, userManager,
+                                userHandle, values);
+                        if (userId == currentUserId) {
                             result = uri;
                         }
                     }
                 }
             } else {
-                Uri uri = CONTENT_URI;
-                if (userToBeInsertedTo != null) {
-                    uri = ContentProvider
-                            .maybeAddUserId(CONTENT_URI, userToBeInsertedTo.getIdentifier());
-                }
-                result = addEntryAndRemoveExpiredEntries(context, uri, values);
+                // Single-user entry. Just write to that user, assuming it's running.  If the
+                // user is encrypted, we write to the shadow calllog.
+
+                final UserHandle targetUserHandle = userToBeInsertedTo != null
+                        ? userToBeInsertedTo
+                        : UserHandle.of(currentUserId);
+                result = addEntryAndRemoveExpiredEntries(context, userManager, targetUserHandle,
+                        values);
             }
             return result;
         }
 
+        /** @hide */
+        public static boolean shouldHaveSharedCallLogEntries(Context context,
+                UserManager userManager, int userId) {
+            if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
+                    UserHandle.of(userId))) {
+                return false;
+            }
+            final UserInfo userInfo = userManager.getUserInfo(userId);
+            return userInfo != null && !userInfo.isManagedProfile();
+        }
+
         /**
          * Query the call log database for the last dialed number.
          * @param context Used to get the content resolver.
@@ -707,14 +794,31 @@
             }
         }
 
-        private static Uri addEntryAndRemoveExpiredEntries(Context context, Uri uri,
-                ContentValues values) {
+        private static Uri addEntryAndRemoveExpiredEntries(Context context, UserManager userManager,
+                UserHandle user, ContentValues values) {
             final ContentResolver resolver = context.getContentResolver();
-            Uri result = resolver.insert(uri, values);
-            resolver.delete(uri, "_id IN " +
-                    "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
-                    + " LIMIT -1 OFFSET 500)", null);
-            return result;
+
+            final Uri uri = ContentProvider.maybeAddUserId(
+                    userManager.isUserUnlocked(user) ? CONTENT_URI : SHADOW_CONTENT_URI,
+                    user.getIdentifier());
+
+            if (VERBOSE_LOG) {
+                Log.v(LOG_TAG, String.format("Inserting to %s", uri));
+            }
+
+            try {
+                final Uri result = resolver.insert(uri, values);
+                resolver.delete(uri, "_id IN " +
+                        "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+                        + " LIMIT -1 OFFSET 500)", null);
+                return result;
+            } catch (IllegalArgumentException e) {
+                Log.w(LOG_TAG, "Failed to insert calllog", e);
+                // Even though we make sure the target user is running and decrypted before calling
+                // this method, there's a chance that the user just got shut down, in which case
+                // we'll still get "IllegalArgumentException: Unknown URL content://call_log/calls".
+                return null;
+            }
         }
 
         private static void updateDataUsageStatForData(ContentResolver resolver, String dataId) {
diff --git a/core/java/android/util/LocaleList.java b/core/java/android/util/LocaleList.java
index 24883e3..8a2d015 100644
--- a/core/java/android/util/LocaleList.java
+++ b/core/java/android/util/LocaleList.java
@@ -301,10 +301,19 @@
             // is a pseudo-locale. So this is not a match.
             return 0;
         }
+        final String supportedScr = getLikelyScript(supported);
+        if (supportedScr.isEmpty()) {
+            // If we can't guess a script, we don't know enough about the locales' language to find
+            // if the locales match. So we fall back to old behavior of matching, which considered
+            // locales with different regions different.
+            final String supportedRegion = supported.getCountry();
+            return (supportedRegion.isEmpty() ||
+                    supportedRegion.equals(desired.getCountry()))
+                    ? 1 : 0;
+        }
+        final String desiredScr = getLikelyScript(desired);
         // There is no match if the two locales use different scripts. This will most imporantly
         // take care of traditional vs simplified Chinese.
-        final String supportedScr = getLikelyScript(supported);
-        final String desiredScr = getLikelyScript(desired);
         return supportedScr.equals(desiredScr) ? 1 : 0;
     }
 
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 2fd4d83..7b53697 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -700,9 +700,6 @@
         mGroupFlags |= (focusability & FLAG_MASK_FOCUSABILITY);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
         if (mFocused != null) {
@@ -712,9 +709,7 @@
         super.handleFocusGainInternal(direction, previouslyFocusedRect);
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void requestChildFocus(View child, View focused) {
         if (DBG) {
             System.out.println(this + " requestChildFocus()");
@@ -739,9 +734,7 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void focusableViewAvailable(View v) {
         if (mParent != null
                 // shortcut: don't report a new focusable view if we block our descendants from
@@ -760,9 +753,7 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public boolean showContextMenuForChild(View originalView) {
         return mParent != null && mParent.showContextMenuForChild(originalView);
     }
@@ -772,9 +763,6 @@
         return mParent != null && mParent.showContextMenuForChild(originalView, x, y);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback) {
         if ((mGroupFlags & FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED) == 0) {
@@ -791,9 +779,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public ActionMode startActionModeForChild(
             View originalView, ActionMode.Callback callback, int type) {
@@ -848,6 +833,7 @@
      * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
      *        FOCUS_RIGHT, or 0 for not applicable.
      */
+    @Override
     public View focusSearch(View focused, int direction) {
         if (isRootNamespace()) {
             // root namespace means we should consider ourselves the top of the
@@ -860,16 +846,11 @@
         return null;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
         ViewParent parent = mParent;
@@ -921,6 +902,7 @@
     /**
      * Called when a child view has changed whether or not it is tracking transient state.
      */
+    @Override
     public void childHasTransientStateChanged(View child, boolean childHasTransientState) {
         final boolean oldHasTransientState = hasTransientState();
         if (childHasTransientState) {
@@ -945,18 +927,13 @@
         return mChildCountWithTransientState > 0 || super.hasTransientState();
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean dispatchUnhandledMove(View focused, int direction) {
         return mFocused != null &&
                 mFocused.dispatchUnhandledMove(focused, direction);
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void clearChildFocus(View child) {
         if (DBG) {
             System.out.println(this + " clearChildFocus()");
@@ -968,9 +945,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void clearFocus() {
         if (DBG) {
@@ -985,9 +959,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     void unFocus(View focused) {
         if (DBG) {
@@ -1054,9 +1025,6 @@
         return null;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean hasFocusable() {
         if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
@@ -1083,9 +1051,6 @@
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
         final int focusableCount = views.size();
@@ -1195,9 +1160,6 @@
         return null;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void dispatchWindowFocusChanged(boolean hasFocus) {
         super.dispatchWindowFocusChanged(hasFocus);
@@ -1208,9 +1170,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void addTouchables(ArrayList<View> views) {
         super.addTouchables(views);
@@ -1239,9 +1198,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void dispatchDisplayHint(int hint) {
         super.dispatchDisplayHint(hint);
@@ -1287,9 +1243,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected void dispatchVisibilityChanged(View changedView, int visibility) {
         super.dispatchVisibilityChanged(changedView, visibility);
@@ -1300,9 +1253,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void dispatchWindowVisibilityChanged(int visibility) {
         super.dispatchWindowVisibilityChanged(visibility);
@@ -1313,9 +1263,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void dispatchConfigurationChanged(Configuration newConfig) {
         super.dispatchConfigurationChanged(newConfig);
@@ -1326,9 +1273,7 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void recomputeViewAttributes(View child) {
         if (mAttachInfo != null && !mAttachInfo.mRecomputeGlobalAttributes) {
             ViewParent parent = mParent;
@@ -1350,9 +1295,7 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void bringChildToFront(View child) {
         final int index = indexOfChild(child);
         if (index >= 0) {
@@ -1369,9 +1312,6 @@
         return mLocalPoint;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     // TODO: Write real docs
     @Override
     public boolean dispatchDragEvent(DragEvent event) {
@@ -1461,6 +1401,10 @@
                 root.setDragFocus(target);
 
                 final int action = event.mAction;
+                // Position should not be available for ACTION_DRAG_ENTERED and ACTION_DRAG_EXITED.
+                event.mX = 0;
+                event.mY = 0;
+
                 // If we've dragged off of a child view or this window, send it the EXITED message
                 if (mCurrentDragView != null) {
                     final View view = mCurrentDragView;
@@ -1489,6 +1433,8 @@
                     }
                 }
                 event.mAction = action;  // restore the event's original state
+                event.mX = tx;
+                event.mY = ty;
             }
 
             // Dispatch the actual drag location notice, localized into its coordinates
@@ -1631,9 +1577,6 @@
         return changed;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean dispatchKeyEventPreIme(KeyEvent event) {
         if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
@@ -1646,9 +1589,6 @@
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         if (mInputEventConsistencyVerifier != null) {
@@ -1673,9 +1613,6 @@
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean dispatchKeyShortcutEvent(KeyEvent event) {
         if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
@@ -1688,9 +1625,6 @@
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean dispatchTrackballEvent(MotionEvent event) {
         if (mInputEventConsistencyVerifier != null) {
@@ -1744,9 +1678,6 @@
         return super.getPointerIcon(event, x, y);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @SuppressWarnings({"ConstantConditions"})
     @Override
     protected boolean dispatchHoverEvent(MotionEvent event) {
@@ -2045,9 +1976,6 @@
         return MotionEvent.obtainNoHistory(event);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected boolean dispatchGenericPointerEvent(MotionEvent event) {
         // Send the event to the child under the pointer.
@@ -2081,9 +2009,6 @@
         return super.dispatchGenericPointerEvent(event);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
         // Send the event to the focused child or to this view group if it has focus.
@@ -2124,9 +2049,6 @@
         return handled;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         if (mInputEventConsistencyVerifier != null) {
@@ -2721,9 +2643,7 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
 
         if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
@@ -2893,9 +2813,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     void dispatchAttachedToWindow(AttachInfo info, int visibility) {
         mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
@@ -2962,6 +2879,7 @@
      * adds in all child views of the view group, in addition to calling the default View
      * implementation.
      */
+    @Override
     public void dispatchProvideStructure(ViewStructure structure) {
         super.dispatchProvideStructure(structure);
         if (!isAssistBlocked()) {
@@ -3118,9 +3036,6 @@
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     void dispatchDetachedFromWindow() {
         // If we still have a touch target, we are still in the process of
@@ -3172,9 +3087,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
         super.dispatchSaveInstanceState(container);
@@ -3200,9 +3112,6 @@
         super.dispatchSaveInstanceState(container);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
         super.dispatchRestoreInstanceState(container);
@@ -3275,6 +3184,7 @@
         return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS;
     }
 
+    @Override
     Insets computeOpticalInsets() {
         if (isLayoutModeOptical()) {
             int left = 0;
@@ -3406,9 +3316,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected void dispatchDraw(Canvas canvas) {
         boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
@@ -3534,6 +3441,7 @@
             // drawChild() after the animation is over
             mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
             final Runnable end = new Runnable() {
+               @Override
                public void run() {
                    notifyAnimationListener();
                }
@@ -3632,6 +3540,7 @@
 
         if (mAnimationListener != null) {
            final Runnable end = new Runnable() {
+               @Override
                public void run() {
                    mAnimationListener.onAnimationEnd(mLayoutAnimationController.getAnimation());
                }
@@ -3781,9 +3690,6 @@
         return hasBooleanFlag(FLAG_CLIP_TO_PADDING);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void dispatchSetSelected(boolean selected) {
         final View[] children = mChildren;
@@ -3793,9 +3699,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void dispatchSetActivated(boolean activated) {
         final View[] children = mChildren;
@@ -4200,6 +4103,7 @@
      * @param child the child view to add
      * @param params the layout parameters to set on the child
      */
+    @Override
     public void addView(View child, LayoutParams params) {
         addView(child, -1, params);
     }
@@ -4232,9 +4136,7 @@
         addViewInner(child, index, params, false);
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
         if (!checkLayoutParams(params)) {
             throw new IllegalArgumentException("Invalid LayoutParams supplied to " + this);
@@ -4245,9 +4147,6 @@
         view.setLayoutParams(params);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
         return  p != null;
     }
@@ -4594,6 +4493,7 @@
      * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
      * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
      */
+    @Override
     public void removeView(View view) {
         if (removeViewInternal(view)) {
             requestLayout();
@@ -5095,6 +4995,7 @@
      * Don't call or override this method. It is used for the implementation of
      * the view hierarchy.
      */
+    @Override
     public final void invalidateChild(View child, final Rect dirty) {
         ViewParent parent = this;
 
@@ -5203,6 +5104,7 @@
      * if this ViewGroup is already fully invalidated or if the dirty rectangle
      * does not intersect with this ViewGroup's bounds.
      */
+    @Override
     public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
         if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
                 (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
@@ -5496,9 +5398,7 @@
         notifySubtreeAccessibilityStateChangedIfNeeded();
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
         // It doesn't make a whole lot of sense to call this on a view that isn't attached,
         // but for some simple tests it can be useful. If we don't have attach info this
@@ -5558,9 +5458,6 @@
         return rectIsVisible;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public final void layout(int l, int t, int r, int b) {
         if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
@@ -5574,9 +5471,6 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected abstract void onLayout(boolean changed,
             int l, int t, int r, int b);
@@ -5941,9 +5835,6 @@
         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     protected void debug(int depth) {
         super.debug(depth);
@@ -6360,9 +6251,6 @@
         return mSuppressLayout;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public boolean gatherTransparentRegion(Region region) {
         // If no transparent regions requested, we are always opaque.
@@ -6386,9 +6274,7 @@
         return meOpaque || noneOfTheChildrenAreTransparent;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void requestTransparentRegion(View child) {
         if (child != null) {
             child.mPrivateFlags |= View.PFLAG_REQUEST_TRANSPARENT_REGIONS;
@@ -6514,6 +6400,7 @@
      * If {@link #addStatesFromChildren} is true, refreshes this group's
      * drawable state (to include the states from its children).
      */
+    @Override
     public void childDrawableStateChanged(View child) {
         if ((mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) != 0) {
             refreshDrawableState();
@@ -7259,9 +7146,6 @@
             a.recycle();
         }
 
-        /**
-         * {@inheritDoc}
-         */
         public MarginLayoutParams(int width, int height) {
             super(width, height);
 
@@ -7291,9 +7175,6 @@
             this.mMarginFlags = source.mMarginFlags;
         }
 
-        /**
-         * {@inheritDoc}
-         */
         public MarginLayoutParams(LayoutParams source) {
             super(source);
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 2db482d..1bb0311 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -3589,9 +3589,7 @@
 
         // tell the window manager
         try {
-            if (!isInLocalFocusMode()) {
-                mWindowSession.setInTouchMode(inTouchMode);
-            }
+            mWindowSession.setInTouchMode(inTouchMode);
         } catch (RemoteException e) {
             throw new RuntimeException(e);
         }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 81ebdb3..7535caa 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -645,6 +645,16 @@
      */
     private Editor mEditor;
 
+    private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
+    private static final int DEVICE_PROVISIONED_NO = 1;
+    private static final int DEVICE_PROVISIONED_YES = 2;
+
+    /**
+     * Some special options such as sharing selected text should only be shown if the device
+     * is provisioned. Only check the provisioned state once for a given view instance.
+     */
+    private int mDeviceProvisionedState = DEVICE_PROVISIONED_UNKNOWN;
+
     /*
      * Kick-start the font cache for the zygote process (to pay the cost of
      * initializing freetype for our default font only once).
@@ -9613,7 +9623,17 @@
     }
 
     boolean canShare() {
-        return canCopy();
+        return canCopy() && isDeviceProvisioned();
+    }
+
+    boolean isDeviceProvisioned() {
+        if (mDeviceProvisionedState == DEVICE_PROVISIONED_UNKNOWN) {
+            mDeviceProvisionedState = Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0
+                    ? DEVICE_PROVISIONED_YES
+                    : DEVICE_PROVISIONED_NO;
+        }
+        return mDeviceProvisionedState == DEVICE_PROVISIONED_YES;
     }
 
     boolean canPaste() {
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index 24eb961..f9936ae 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -23,6 +23,7 @@
 #include <inttypes.h>
 
 #include <android_runtime/AndroidRuntime.h>
+#include <androidfw/DisplayEventDispatcher.h>
 #include <utils/Log.h>
 #include <utils/Looper.h>
 #include <utils/threads.h>
@@ -48,14 +49,12 @@
 } gDisplayEventReceiverClassInfo;
 
 
-class NativeDisplayEventReceiver : public LooperCallback {
+class NativeDisplayEventReceiver : public DisplayEventDispatcher {
 public:
     NativeDisplayEventReceiver(JNIEnv* env,
             jobject receiverWeak, const sp<MessageQueue>& messageQueue);
 
-    status_t initialize();
     void dispose();
-    status_t scheduleVsync();
 
 protected:
     virtual ~NativeDisplayEventReceiver();
@@ -66,15 +65,14 @@
     DisplayEventReceiver mReceiver;
     bool mWaitingForVsync;
 
-    virtual int handleEvent(int receiveFd, int events, void* data);
-    bool processPendingEvents(nsecs_t* outTimestamp, int32_t* id, uint32_t* outCount);
-    void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count);
-    void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected);
+    virtual void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count);
+    virtual void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected);
 };
 
 
 NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env,
         jobject receiverWeak, const sp<MessageQueue>& messageQueue) :
+        DisplayEventDispatcher(messageQueue->getLooper()),
         mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),
         mMessageQueue(messageQueue), mWaitingForVsync(false) {
     ALOGV("receiver %p ~ Initializing display event receiver.", this);
@@ -85,21 +83,6 @@
     env->DeleteGlobalRef(mReceiverWeakGlobal);
 }
 
-status_t NativeDisplayEventReceiver::initialize() {
-    status_t result = mReceiver.initCheck();
-    if (result) {
-        ALOGW("Failed to initialize display event receiver, status=%d", result);
-        return result;
-    }
-
-    int rc = mMessageQueue->getLooper()->addFd(mReceiver.getFd(), 0, Looper::EVENT_INPUT,
-            this, NULL);
-    if (rc < 0) {
-        return UNKNOWN_ERROR;
-    }
-    return OK;
-}
-
 void NativeDisplayEventReceiver::dispose() {
     ALOGV("receiver %p ~ Disposing display event receiver.", this);
 
@@ -108,87 +91,6 @@
     }
 }
 
-status_t NativeDisplayEventReceiver::scheduleVsync() {
-    if (!mWaitingForVsync) {
-        ALOGV("receiver %p ~ Scheduling vsync.", this);
-
-        // Drain all pending events.
-        nsecs_t vsyncTimestamp;
-        int32_t vsyncDisplayId;
-        uint32_t vsyncCount;
-        processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount);
-
-        status_t status = mReceiver.requestNextVsync();
-        if (status) {
-            ALOGW("Failed to request next vsync, status=%d", status);
-            return status;
-        }
-
-        mWaitingForVsync = true;
-    }
-    return OK;
-}
-
-int NativeDisplayEventReceiver::handleEvent(int receiveFd, int events, void* data) {
-    if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
-        ALOGE("Display event receiver pipe was closed or an error occurred.  "
-                "events=0x%x", events);
-        return 0; // remove the callback
-    }
-
-    if (!(events & Looper::EVENT_INPUT)) {
-        ALOGW("Received spurious callback for unhandled poll event.  "
-                "events=0x%x", events);
-        return 1; // keep the callback
-    }
-
-    // Drain all pending events, keep the last vsync.
-    nsecs_t vsyncTimestamp;
-    int32_t vsyncDisplayId;
-    uint32_t vsyncCount;
-    if (processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount)) {
-        ALOGV("receiver %p ~ Vsync pulse: timestamp=%" PRId64 ", id=%d, count=%d",
-                this, vsyncTimestamp, vsyncDisplayId, vsyncCount);
-        mWaitingForVsync = false;
-        dispatchVsync(vsyncTimestamp, vsyncDisplayId, vsyncCount);
-    }
-
-    return 1; // keep the callback
-}
-
-bool NativeDisplayEventReceiver::processPendingEvents(
-        nsecs_t* outTimestamp, int32_t* outId, uint32_t* outCount) {
-    bool gotVsync = false;
-    DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
-    ssize_t n;
-    while ((n = mReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
-        ALOGV("receiver %p ~ Read %d events.", this, int(n));
-        for (ssize_t i = 0; i < n; i++) {
-            const DisplayEventReceiver::Event& ev = buf[i];
-            switch (ev.header.type) {
-            case DisplayEventReceiver::DISPLAY_EVENT_VSYNC:
-                // Later vsync events will just overwrite the info from earlier
-                // ones. That's fine, we only care about the most recent.
-                gotVsync = true;
-                *outTimestamp = ev.header.timestamp;
-                *outId = ev.header.id;
-                *outCount = ev.vsync.count;
-                break;
-            case DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG:
-                dispatchHotplug(ev.header.timestamp, ev.header.id, ev.hotplug.connected);
-                break;
-            default:
-                ALOGW("receiver %p ~ ignoring unknown event type %#x", this, ev.header.type);
-                break;
-            }
-        }
-    }
-    if (n < 0) {
-        ALOGW("Failed to get events from display event receiver, status=%d", status_t(n));
-    }
-    return gotVsync;
-}
-
 void NativeDisplayEventReceiver::dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a65cdb8..a82bfbe 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -356,6 +356,20 @@
     <protected-broadcast android:name="android.intent.action.WALLPAPER_CHANGED" />
 
     <protected-broadcast android:name="android.app.action.DEVICE_POLICY_MANAGER_STATE_CHANGED" />
+    <protected-broadcast android:name="android.app.action.CHOOSE_PRIVATE_KEY_ALIAS" />
+    <protected-broadcast android:name="android.app.action.DEVICE_ADMIN_DISABLED" />
+    <protected-broadcast android:name="android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED" />
+    <protected-broadcast android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+    <protected-broadcast android:name="android.app.action.LOCK_TASK_ENTERING" />
+    <protected-broadcast android:name="android.app.action.LOCK_TASK_EXITING" />
+    <protected-broadcast android:name="android.app.action.NOTIFY_PENDING_SYSTEM_UPDATE" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_CHANGED" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_EXPIRING" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_FAILED" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_SUCCEEDED" />
+    <protected-broadcast android:name="com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION" />
+    <protected-broadcast android:name="android.intent.action.MANAGED_PROFILE_ADDED" />
+
     <protected-broadcast android:name="android.bluetooth.adapter.action.BLE_STATE_CHANGED" />
     <protected-broadcast android:name="android.content.jobscheduler.JOB_DELAY_EXPIRED" />
     <protected-broadcast android:name="android.content.syncmanager.SYNC_ALARM" />
diff --git a/include/androidfw/DisplayEventDispatcher.h b/include/androidfw/DisplayEventDispatcher.h
new file mode 100644
index 0000000..3ade215
--- /dev/null
+++ b/include/androidfw/DisplayEventDispatcher.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gui/DisplayEventReceiver.h>
+#include <utils/Log.h>
+#include <utils/Looper.h>
+
+namespace android {
+
+class DisplayEventDispatcher : public LooperCallback {
+public:
+    DisplayEventDispatcher(const sp<Looper>& looper);
+
+    status_t initialize();
+    void dispose();
+    status_t scheduleVsync();
+
+protected:
+    virtual ~DisplayEventDispatcher() = default;
+
+private:
+    sp<Looper> mLooper;
+    DisplayEventReceiver mReceiver;
+    bool mWaitingForVsync;
+
+    virtual void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count) = 0;
+    virtual void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected) = 0;
+
+    virtual int handleEvent(int receiveFd, int events, void* data);
+    bool processPendingEvents(nsecs_t* outTimestamp, int32_t* id, uint32_t* outCount);
+};
+}
diff --git a/include/androidfw/LocaleData.h b/include/androidfw/LocaleData.h
new file mode 100644
index 0000000..add0ab5
--- /dev/null
+++ b/include/androidfw/LocaleData.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _LIBS_UTILS_LOCALE_DATA_H
+#define _LIBS_UTILS_LOCALE_DATA_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+namespace android {
+
+int localeDataCompareRegions(
+        const char* left_region, const char* right_region,
+        const char* requested_language, const char* requested_script,
+        const char* requested_region);
+
+void localeDataComputeScript(char out[4], const char* language, const char* region);
+
+} // namespace android
+
+#endif // _LIBS_UTILS_LOCALE_DATA_H
diff --git a/include/androidfw/ResourceTypes.h b/include/androidfw/ResourceTypes.h
index 428a2b8..88fecce 100644
--- a/include/androidfw/ResourceTypes.h
+++ b/include/androidfw/ResourceTypes.h
@@ -21,6 +21,7 @@
 #define _LIBS_UTILS_RESOURCE_TYPES_H
 
 #include <androidfw/Asset.h>
+#include <androidfw/LocaleData.h>
 #include <utils/ByteOrder.h>
 #include <utils/Errors.h>
 #include <utils/String16.h>
@@ -1127,8 +1128,9 @@
     // configuration. (eg. Hant, Latn, etc.). Interpreted in conjunction with
     // the locale field.
     char localeScript[4];
+    bool localeScriptWasProvided;
 
-    // A single BCP-47 variant subtag. Will vary in length between 5 and 8
+    // A single BCP-47 variant subtag. Will vary in length between 4 and 8
     // chars. Interpreted in conjunction with the locale field.
     char localeVariant[8];
 
@@ -1228,10 +1230,15 @@
 
     inline void clearLocale() {
         locale = 0;
+        localeScriptWasProvided = false;
         memset(localeScript, 0, sizeof(localeScript));
         memset(localeVariant, 0, sizeof(localeVariant));
     }
 
+    inline void computeScript() {
+        localeDataComputeScript(localeScript, language, country);
+    }
+
     // Get the 2 or 3 letter language code of this configuration. Trailing
     // bytes are set to '\0'.
     size_t unpackLanguage(char language[4]) const;
@@ -1255,6 +1262,12 @@
     // and 0 if they're equally specific.
     int isLocaleMoreSpecificThan(const ResTable_config &o) const;
 
+    // Return true if 'this' is a better locale match than 'o' for the
+    // 'requested' configuration. Similar to isBetterThan(), this assumes that
+    // match() has already been used to remove any configurations that don't
+    // match the requested configuration at all.
+    bool isLocaleBetterThan(const ResTable_config& o, const ResTable_config* requested) const;
+
     String8 toString() const;
 };
 
diff --git a/libs/androidfw/Android.mk b/libs/androidfw/Android.mk
index f682fb8..6bbfcd2 100644
--- a/libs/androidfw/Android.mk
+++ b/libs/androidfw/Android.mk
@@ -21,6 +21,7 @@
     Asset.cpp \
     AssetDir.cpp \
     AssetManager.cpp \
+    LocaleData.cpp \
     misc.cpp \
     ObbFile.cpp \
     ResourceTypes.cpp \
@@ -33,7 +34,8 @@
     $(commonSources) \
     BackupData.cpp \
     BackupHelpers.cpp \
-    CursorWindow.cpp
+    CursorWindow.cpp \
+    DisplayEventDispatcher.cpp
 
 hostSources := $(commonSources)
 
@@ -65,6 +67,7 @@
     libbinder \
     liblog \
     libcutils \
+    libgui \
     libutils \
     libz
 
diff --git a/libs/androidfw/DisplayEventDispatcher.cpp b/libs/androidfw/DisplayEventDispatcher.cpp
new file mode 100644
index 0000000..99cd173
--- /dev/null
+++ b/libs/androidfw/DisplayEventDispatcher.cpp
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "DisplayEventDispatcher"
+
+#include <cinttypes>
+#include <cstdint>
+
+#include <androidfw/DisplayEventDispatcher.h>
+#include <gui/DisplayEventReceiver.h>
+#include <utils/Log.h>
+#include <utils/Looper.h>
+
+#include <utils/Timers.h>
+
+namespace android {
+
+// Number of events to read at a time from the DisplayEventDispatcher pipe.
+// The value should be large enough that we can quickly drain the pipe
+// using just a few large reads.
+static const size_t EVENT_BUFFER_SIZE = 100;
+
+DisplayEventDispatcher::DisplayEventDispatcher(const sp<Looper>& looper) :
+        mLooper(looper), mWaitingForVsync(false) {
+    ALOGV("dispatcher %p ~ Initializing display event dispatcher.", this);
+}
+
+status_t DisplayEventDispatcher::initialize() {
+    status_t result = mReceiver.initCheck();
+    if (result) {
+        ALOGW("Failed to initialize display event receiver, status=%d", result);
+        return result;
+    }
+
+    int rc = mLooper->addFd(mReceiver.getFd(), 0, Looper::EVENT_INPUT,
+            this, NULL);
+    if (rc < 0) {
+        return UNKNOWN_ERROR;
+    }
+    return OK;
+}
+
+void DisplayEventDispatcher::dispose() {
+    ALOGV("dispatcher %p ~ Disposing display event dispatcher.", this);
+
+    if (!mReceiver.initCheck()) {
+        mLooper->removeFd(mReceiver.getFd());
+    }
+}
+
+status_t DisplayEventDispatcher::scheduleVsync() {
+    if (!mWaitingForVsync) {
+        ALOGV("dispatcher %p ~ Scheduling vsync.", this);
+
+        // Drain all pending events.
+        nsecs_t vsyncTimestamp;
+        int32_t vsyncDisplayId;
+        uint32_t vsyncCount;
+        if (processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount)) {
+            ALOGE("dispatcher %p ~ last event processed while scheduling was for %" PRId64 "",
+                    this, ns2ms(static_cast<nsecs_t>(vsyncTimestamp)));
+        }
+
+        status_t status = mReceiver.requestNextVsync();
+        if (status) {
+            ALOGW("Failed to request next vsync, status=%d", status);
+            return status;
+        }
+
+        mWaitingForVsync = true;
+    }
+    return OK;
+}
+
+int DisplayEventDispatcher::handleEvent(int, int events, void*) {
+    if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
+        ALOGE("Display event receiver pipe was closed or an error occurred.  "
+                "events=0x%x", events);
+        return 0; // remove the callback
+    }
+
+    if (!(events & Looper::EVENT_INPUT)) {
+        ALOGW("Received spurious callback for unhandled poll event.  "
+                "events=0x%x", events);
+        return 1; // keep the callback
+    }
+
+    // Drain all pending events, keep the last vsync.
+    nsecs_t vsyncTimestamp;
+    int32_t vsyncDisplayId;
+    uint32_t vsyncCount;
+    if (processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount)) {
+        ALOGE("dispatcher %p ~ Vsync pulse: timestamp=%" PRId64 ", id=%d, count=%d",
+                this, ns2ms(vsyncTimestamp), vsyncDisplayId, vsyncCount);
+        mWaitingForVsync = false;
+        dispatchVsync(vsyncTimestamp, vsyncDisplayId, vsyncCount);
+    }
+
+    return 1; // keep the callback
+}
+
+bool DisplayEventDispatcher::processPendingEvents(
+        nsecs_t* outTimestamp, int32_t* outId, uint32_t* outCount) {
+    bool gotVsync = false;
+    DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
+    ssize_t n;
+    while ((n = mReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
+        ALOGV("dispatcher %p ~ Read %d events.", this, int(n));
+        for (ssize_t i = 0; i < n; i++) {
+            const DisplayEventReceiver::Event& ev = buf[i];
+            switch (ev.header.type) {
+            case DisplayEventReceiver::DISPLAY_EVENT_VSYNC:
+                // Later vsync events will just overwrite the info from earlier
+                // ones. That's fine, we only care about the most recent.
+                gotVsync = true;
+                *outTimestamp = ev.header.timestamp;
+                *outId = ev.header.id;
+                *outCount = ev.vsync.count;
+                break;
+            case DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG:
+                dispatchHotplug(ev.header.timestamp, ev.header.id, ev.hotplug.connected);
+                break;
+            default:
+                ALOGW("dispatcher %p ~ ignoring unknown event type %#x", this, ev.header.type);
+                break;
+            }
+        }
+    }
+    if (n < 0) {
+        ALOGW("Failed to get events from display event dispatcher, status=%d", status_t(n));
+    }
+    return gotVsync;
+}
+}
diff --git a/libs/androidfw/LocaleData.cpp b/libs/androidfw/LocaleData.cpp
new file mode 100644
index 0000000..c0c3ab8
--- /dev/null
+++ b/libs/androidfw/LocaleData.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+#include <androidfw/LocaleData.h>
+
+namespace android {
+
+#include "LocaleDataTables.cpp"
+
+inline uint32_t packLocale(const char* language, const char* region) {
+    return (((uint8_t) language[0]) << 24u) | (((uint8_t) language[1]) << 16u) |
+           (((uint8_t) region[0]) << 8u) | ((uint8_t) region[1]);
+}
+
+inline uint32_t dropRegion(uint32_t packed_locale) {
+    return packed_locale & 0xFFFF0000lu;
+}
+
+inline bool hasRegion(uint32_t packed_locale) {
+    return (packed_locale & 0x0000FFFFlu) != 0;
+}
+
+const size_t SCRIPT_LENGTH = 4;
+const size_t SCRIPT_PARENTS_COUNT = sizeof(SCRIPT_PARENTS)/sizeof(SCRIPT_PARENTS[0]);
+const uint32_t PACKED_ROOT = 0; // to represent the root locale
+
+uint32_t findParent(uint32_t packed_locale, const char* script) {
+    if (hasRegion(packed_locale)) {
+        for (size_t i = 0; i < SCRIPT_PARENTS_COUNT; i++) {
+            if (memcmp(script, SCRIPT_PARENTS[i].script, SCRIPT_LENGTH) == 0) {
+                auto map = SCRIPT_PARENTS[i].map;
+                auto lookup_result = map->find(packed_locale);
+                if (lookup_result != map->end()) {
+                    return lookup_result->second;
+                }
+                break;
+            }
+        }
+        return dropRegion(packed_locale);
+    }
+    return PACKED_ROOT;
+}
+
+// Find the ancestors of a locale, and fill 'out' with it (assumes out has enough
+// space). If any of the members of stop_list was seen, write it in the
+// output but stop afterwards.
+//
+// This also outputs the index of the last written ancestor in the stop_list
+// to stop_list_index, which will be -1 if it is not found in the stop_list.
+//
+// Returns the number of ancestors written in the output, which is always
+// at least one.
+size_t findAncestors(uint32_t* out, ssize_t* stop_list_index,
+                     uint32_t packed_locale, const char* script,
+                     const uint32_t* stop_list, size_t stop_set_length) {
+    uint32_t ancestor = packed_locale;
+    size_t count = 0;
+    do {
+        out[count++] = ancestor;
+        for (size_t i = 0; i < stop_set_length; i++) {
+            if (stop_list[i] == ancestor) {
+                *stop_list_index = (ssize_t) i;
+                return count;
+            }
+        }
+        ancestor = findParent(ancestor, script);
+    } while (ancestor != PACKED_ROOT);
+    *stop_list_index = (ssize_t) -1;
+    return count;
+}
+
+size_t findDistance(uint32_t supported,
+                    const char* script,
+                    const uint32_t* request_ancestors,
+                    size_t request_ancestors_count) {
+    uint32_t supported_ancestors[MAX_PARENT_DEPTH+1];
+    ssize_t request_ancestors_index;
+    const size_t supported_ancestor_count = findAncestors(
+            supported_ancestors, &request_ancestors_index,
+            supported, script,
+            request_ancestors, request_ancestors_count);
+    // Since both locales share the same root, there will always be a shared
+    // ancestor, so the distance in the parent tree is the sum of the distance
+    // of 'supported' to the lowest common ancestor (number of ancestors
+    // written for 'supported' minus 1) plus the distance of 'request' to the
+    // lowest common ancestor (the index of the ancestor in request_ancestors).
+    return supported_ancestor_count + request_ancestors_index - 1;
+}
+
+inline bool isRepresentative(uint32_t language_and_region, const char* script) {
+    const uint64_t packed_locale = (
+            (((uint64_t) language_and_region) << 32u) |
+            (((uint64_t) script[0]) << 24u) |
+            (((uint64_t) script[1]) << 16u) |
+            (((uint64_t) script[2]) <<  8u) |
+            ((uint64_t) script[3]));
+
+    return (REPRESENTATIVE_LOCALES.count(packed_locale) != 0);
+}
+
+int localeDataCompareRegions(
+        const char* left_region, const char* right_region,
+        const char* requested_language, const char* requested_script,
+        const char* requested_region) {
+
+    if (left_region[0] == right_region[0] && left_region[1] == right_region[1]) {
+        return 0;
+    }
+    const uint32_t left = packLocale(requested_language, left_region);
+    const uint32_t right = packLocale(requested_language, right_region);
+    const uint32_t request = packLocale(requested_language, requested_region);
+
+    uint32_t request_ancestors[MAX_PARENT_DEPTH+1];
+    ssize_t left_right_index;
+    // Find the parents of the request, but stop as soon as we saw left or right
+    const uint32_t left_and_right[] = {left, right};
+    const size_t ancestor_count = findAncestors(
+            request_ancestors, &left_right_index,
+            request, requested_script,
+            left_and_right, sizeof(left_and_right)/sizeof(left_and_right[0]));
+    if (left_right_index == 0) { // We saw left earlier
+        return 1;
+    }
+    if (left_right_index == 1) { // We saw right earlier
+        return -1;
+    }
+
+    // If we are here, neither left nor right are an ancestor of the
+    // request. This means that all the ancestors have been computed and
+    // the last ancestor is just the language by itself. We will use the
+    // distance in the parent tree for determining the better match.
+    const size_t left_distance = findDistance(
+            left, requested_script, request_ancestors, ancestor_count);
+    const size_t right_distance = findDistance(
+            right, requested_script, request_ancestors, ancestor_count);
+    if (left_distance != right_distance) {
+        return (int) right_distance - (int) left_distance; // smaller distance is better
+    }
+
+    // If we are here, left and right are equidistant from the request. We will
+    // try and see if any of them is a representative locale.
+    const bool left_is_representative = isRepresentative(left, requested_script);
+    const bool right_is_representative = isRepresentative(right, requested_script);
+    if (left_is_representative != right_is_representative) {
+        return (int) left_is_representative - (int) right_is_representative;
+    }
+
+    // We have no way of figuring out which locale is a better match. For
+    // the sake of stability, we consider the locale with the lower region
+    // code (in dictionary order) better, with two-letter codes before
+    // three-digit codes (since two-letter codes are more specific).
+    return (int64_t) right - (int64_t) left;
+}
+
+void localeDataComputeScript(char out[4], const char* language, const char* region) {
+    if (language[0] == '\0') {
+        memset(out, '\0', SCRIPT_LENGTH);
+        return;
+    }
+    uint32_t lookup_key = packLocale(language, region);
+    auto lookup_result = LIKELY_SCRIPTS.find(lookup_key);
+    if (lookup_result == LIKELY_SCRIPTS.end()) {
+        // We couldn't find the locale. Let's try without the region
+        if (region[0] != '\0') {
+            lookup_key = dropRegion(lookup_key);
+            lookup_result = LIKELY_SCRIPTS.find(lookup_key);
+            if (lookup_result != LIKELY_SCRIPTS.end()) {
+                memcpy(out, SCRIPT_CODES[lookup_result->second], SCRIPT_LENGTH);
+                return;
+            }
+        }
+        // We don't know anything about the locale
+        memset(out, '\0', SCRIPT_LENGTH);
+        return;
+    } else {
+        // We found the locale.
+        memcpy(out, SCRIPT_CODES[lookup_result->second], SCRIPT_LENGTH);
+    }
+}
+
+} // namespace android
diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp
new file mode 100644
index 0000000..1ac5085
--- /dev/null
+++ b/libs/androidfw/LocaleDataTables.cpp
@@ -0,0 +1,1701 @@
+// Auto-generated by frameworks/base/tools/localedata/extract_icu_data.py
+
+const char SCRIPT_CODES[][4] = {
+    /* 0  */ {'A', 'h', 'o', 'm'},
+    /* 1  */ {'A', 'r', 'a', 'b'},
+    /* 2  */ {'A', 'r', 'm', 'i'},
+    /* 3  */ {'A', 'r', 'm', 'n'},
+    /* 4  */ {'A', 'v', 's', 't'},
+    /* 5  */ {'B', 'a', 'm', 'u'},
+    /* 6  */ {'B', 'a', 's', 's'},
+    /* 7  */ {'B', 'e', 'n', 'g'},
+    /* 8  */ {'B', 'r', 'a', 'h'},
+    /* 9  */ {'C', 'a', 'n', 's'},
+    /* 10 */ {'C', 'a', 'r', 'i'},
+    /* 11 */ {'C', 'h', 'a', 'm'},
+    /* 12 */ {'C', 'h', 'e', 'r'},
+    /* 13 */ {'C', 'o', 'p', 't'},
+    /* 14 */ {'C', 'p', 'r', 't'},
+    /* 15 */ {'C', 'y', 'r', 'l'},
+    /* 16 */ {'D', 'e', 'v', 'a'},
+    /* 17 */ {'E', 'g', 'y', 'p'},
+    /* 18 */ {'E', 't', 'h', 'i'},
+    /* 19 */ {'G', 'e', 'o', 'r'},
+    /* 20 */ {'G', 'o', 't', 'h'},
+    /* 21 */ {'G', 'r', 'e', 'k'},
+    /* 22 */ {'G', 'u', 'j', 'r'},
+    /* 23 */ {'G', 'u', 'r', 'u'},
+    /* 24 */ {'H', 'a', 'n', 's'},
+    /* 25 */ {'H', 'a', 'n', 't'},
+    /* 26 */ {'H', 'a', 't', 'r'},
+    /* 27 */ {'H', 'e', 'b', 'r'},
+    /* 28 */ {'H', 'l', 'u', 'w'},
+    /* 29 */ {'H', 'm', 'n', 'g'},
+    /* 30 */ {'I', 't', 'a', 'l'},
+    /* 31 */ {'J', 'p', 'a', 'n'},
+    /* 32 */ {'K', 'a', 'l', 'i'},
+    /* 33 */ {'K', 'a', 'n', 'a'},
+    /* 34 */ {'K', 'h', 'a', 'r'},
+    /* 35 */ {'K', 'h', 'm', 'r'},
+    /* 36 */ {'K', 'n', 'd', 'a'},
+    /* 37 */ {'K', 'o', 'r', 'e'},
+    /* 38 */ {'K', 't', 'h', 'i'},
+    /* 39 */ {'L', 'a', 'n', 'a'},
+    /* 40 */ {'L', 'a', 'o', 'o'},
+    /* 41 */ {'L', 'a', 't', 'n'},
+    /* 42 */ {'L', 'e', 'p', 'c'},
+    /* 43 */ {'L', 'i', 'n', 'a'},
+    /* 44 */ {'L', 'i', 's', 'u'},
+    /* 45 */ {'L', 'y', 'c', 'i'},
+    /* 46 */ {'L', 'y', 'd', 'i'},
+    /* 47 */ {'M', 'a', 'n', 'd'},
+    /* 48 */ {'M', 'a', 'n', 'i'},
+    /* 49 */ {'M', 'e', 'r', 'c'},
+    /* 50 */ {'M', 'l', 'y', 'm'},
+    /* 51 */ {'M', 'o', 'n', 'g'},
+    /* 52 */ {'M', 'r', 'o', 'o'},
+    /* 53 */ {'M', 'y', 'm', 'r'},
+    /* 54 */ {'N', 'a', 'r', 'b'},
+    /* 55 */ {'N', 'k', 'o', 'o'},
+    /* 56 */ {'O', 'g', 'a', 'm'},
+    /* 57 */ {'O', 'r', 'k', 'h'},
+    /* 58 */ {'O', 'r', 'y', 'a'},
+    /* 59 */ {'P', 'a', 'u', 'c'},
+    /* 60 */ {'P', 'h', 'l', 'i'},
+    /* 61 */ {'P', 'h', 'n', 'x'},
+    /* 62 */ {'P', 'l', 'r', 'd'},
+    /* 63 */ {'P', 'r', 't', 'i'},
+    /* 64 */ {'R', 'u', 'n', 'r'},
+    /* 65 */ {'S', 'a', 'm', 'r'},
+    /* 66 */ {'S', 'a', 'r', 'b'},
+    /* 67 */ {'S', 'a', 'u', 'r'},
+    /* 68 */ {'S', 'g', 'n', 'w'},
+    /* 69 */ {'S', 'i', 'n', 'h'},
+    /* 70 */ {'S', 'o', 'r', 'a'},
+    /* 71 */ {'S', 'y', 'r', 'c'},
+    /* 72 */ {'T', 'a', 'l', 'e'},
+    /* 73 */ {'T', 'a', 'l', 'u'},
+    /* 74 */ {'T', 'a', 'm', 'l'},
+    /* 75 */ {'T', 'a', 'v', 't'},
+    /* 76 */ {'T', 'e', 'l', 'u'},
+    /* 77 */ {'T', 'f', 'n', 'g'},
+    /* 78 */ {'T', 'h', 'a', 'a'},
+    /* 79 */ {'T', 'h', 'a', 'i'},
+    /* 80 */ {'T', 'i', 'b', 't'},
+    /* 81 */ {'U', 'g', 'a', 'r'},
+    /* 82 */ {'V', 'a', 'i', 'i'},
+    /* 83 */ {'X', 'p', 'e', 'o'},
+    /* 84 */ {'X', 's', 'u', 'x'},
+    /* 85 */ {'Y', 'i', 'i', 'i'},
+    /* 86 */ {'~', '~', '~', 'A'},
+    /* 87 */ {'~', '~', '~', 'B'},
+};
+
+
+const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({
+    {0x61610000u, 41u}, // aa -> Latn
+    {0x61620000u, 15u}, // ab -> Cyrl
+    {0xC4200000u, 41u}, // abr -> Latn
+    {0x90400000u, 41u}, // ace -> Latn
+    {0x9C400000u, 41u}, // ach -> Latn
+    {0x80600000u, 41u}, // ada -> Latn
+    {0xE0600000u, 15u}, // ady -> Cyrl
+    {0x61650000u,  4u}, // ae -> Avst
+    {0x84800000u,  1u}, // aeb -> Arab
+    {0x61660000u, 41u}, // af -> Latn
+    {0xC0C00000u, 41u}, // agq -> Latn
+    {0xB8E00000u,  0u}, // aho -> Ahom
+    {0x616B0000u, 41u}, // ak -> Latn
+    {0xA9400000u, 84u}, // akk -> Xsux
+    {0xB5600000u, 41u}, // aln -> Latn
+    {0xCD600000u, 15u}, // alt -> Cyrl
+    {0x616D0000u, 18u}, // am -> Ethi
+    {0xB9800000u, 41u}, // amo -> Latn
+    {0xE5C00000u, 41u}, // aoz -> Latn
+    {0x61720000u,  1u}, // ar -> Arab
+    {0x61725842u, 87u}, // ar-XB -> ~~~B
+    {0x8A200000u,  2u}, // arc -> Armi
+    {0xB6200000u, 41u}, // arn -> Latn
+    {0xBA200000u, 41u}, // aro -> Latn
+    {0xC2200000u,  1u}, // arq -> Arab
+    {0xE2200000u,  1u}, // ary -> Arab
+    {0xE6200000u,  1u}, // arz -> Arab
+    {0x61730000u,  7u}, // as -> Beng
+    {0x82400000u, 41u}, // asa -> Latn
+    {0x92400000u, 68u}, // ase -> Sgnw
+    {0xCE400000u, 41u}, // ast -> Latn
+    {0xA6600000u, 41u}, // atj -> Latn
+    {0x61760000u, 15u}, // av -> Cyrl
+    {0x82C00000u, 16u}, // awa -> Deva
+    {0x61790000u, 41u}, // ay -> Latn
+    {0x617A0000u, 41u}, // az -> Latn
+    {0x617A4951u,  1u}, // az-IQ -> Arab
+    {0x617A4952u,  1u}, // az-IR -> Arab
+    {0x617A5255u, 15u}, // az-RU -> Cyrl
+    {0x62610000u, 15u}, // ba -> Cyrl
+    {0xAC010000u,  1u}, // bal -> Arab
+    {0xB4010000u, 41u}, // ban -> Latn
+    {0xBC010000u, 16u}, // bap -> Deva
+    {0xC4010000u, 41u}, // bar -> Latn
+    {0xC8010000u, 41u}, // bas -> Latn
+    {0xDC010000u,  5u}, // bax -> Bamu
+    {0x88210000u, 41u}, // bbc -> Latn
+    {0xA4210000u, 41u}, // bbj -> Latn
+    {0xA0410000u, 41u}, // bci -> Latn
+    {0x62650000u, 15u}, // be -> Cyrl
+    {0xA4810000u,  1u}, // bej -> Arab
+    {0xB0810000u, 41u}, // bem -> Latn
+    {0xD8810000u, 41u}, // bew -> Latn
+    {0xE4810000u, 41u}, // bez -> Latn
+    {0x8CA10000u, 41u}, // bfd -> Latn
+    {0xC0A10000u, 74u}, // bfq -> Taml
+    {0xCCA10000u,  1u}, // bft -> Arab
+    {0xE0A10000u, 16u}, // bfy -> Deva
+    {0x62670000u, 15u}, // bg -> Cyrl
+    {0x88C10000u, 16u}, // bgc -> Deva
+    {0xB4C10000u,  1u}, // bgn -> Arab
+    {0xDCC10000u, 21u}, // bgx -> Grek
+    {0x62680000u, 38u}, // bh -> Kthi
+    {0x84E10000u, 16u}, // bhb -> Deva
+    {0xA0E10000u, 16u}, // bhi -> Deva
+    {0xA8E10000u, 41u}, // bhk -> Latn
+    {0xB8E10000u, 16u}, // bho -> Deva
+    {0x62690000u, 41u}, // bi -> Latn
+    {0xA9010000u, 41u}, // bik -> Latn
+    {0xB5010000u, 41u}, // bin -> Latn
+    {0xA5210000u, 16u}, // bjj -> Deva
+    {0xB5210000u, 41u}, // bjn -> Latn
+    {0xB1410000u, 41u}, // bkm -> Latn
+    {0xD1410000u, 41u}, // bku -> Latn
+    {0xCD610000u, 75u}, // blt -> Tavt
+    {0x626D0000u, 41u}, // bm -> Latn
+    {0xC1810000u, 41u}, // bmq -> Latn
+    {0x626E0000u,  7u}, // bn -> Beng
+    {0x626F0000u, 80u}, // bo -> Tibt
+    {0xE1E10000u,  7u}, // bpy -> Beng
+    {0xA2010000u,  1u}, // bqi -> Arab
+    {0xD6010000u, 41u}, // bqv -> Latn
+    {0x62720000u, 41u}, // br -> Latn
+    {0x82210000u, 16u}, // bra -> Deva
+    {0x9E210000u,  1u}, // brh -> Arab
+    {0xDE210000u, 16u}, // brx -> Deva
+    {0x62730000u, 41u}, // bs -> Latn
+    {0xC2410000u,  6u}, // bsq -> Bass
+    {0xCA410000u, 41u}, // bss -> Latn
+    {0xBA610000u, 41u}, // bto -> Latn
+    {0xD6610000u, 16u}, // btv -> Deva
+    {0x82810000u, 15u}, // bua -> Cyrl
+    {0x8A810000u, 41u}, // buc -> Latn
+    {0x9A810000u, 41u}, // bug -> Latn
+    {0xB2810000u, 41u}, // bum -> Latn
+    {0x86A10000u, 41u}, // bvb -> Latn
+    {0xB7010000u, 18u}, // byn -> Ethi
+    {0xD7010000u, 41u}, // byv -> Latn
+    {0x93210000u, 41u}, // bze -> Latn
+    {0x63610000u, 41u}, // ca -> Latn
+    {0x9C420000u, 41u}, // cch -> Latn
+    {0xBC420000u,  7u}, // ccp -> Beng
+    {0x63650000u, 15u}, // ce -> Cyrl
+    {0x84820000u, 41u}, // ceb -> Latn
+    {0x98C20000u, 41u}, // cgg -> Latn
+    {0x63680000u, 41u}, // ch -> Latn
+    {0xA8E20000u, 41u}, // chk -> Latn
+    {0xB0E20000u, 15u}, // chm -> Cyrl
+    {0xB8E20000u, 41u}, // cho -> Latn
+    {0xBCE20000u, 41u}, // chp -> Latn
+    {0xC4E20000u, 12u}, // chr -> Cher
+    {0x81220000u,  1u}, // cja -> Arab
+    {0xB1220000u, 11u}, // cjm -> Cham
+    {0x85420000u,  1u}, // ckb -> Arab
+    {0x636F0000u, 41u}, // co -> Latn
+    {0xBDC20000u, 13u}, // cop -> Copt
+    {0xC9E20000u, 41u}, // cps -> Latn
+    {0x63720000u,  9u}, // cr -> Cans
+    {0xA6220000u,  9u}, // crj -> Cans
+    {0xAA220000u,  9u}, // crk -> Cans
+    {0xAE220000u,  9u}, // crl -> Cans
+    {0xB2220000u,  9u}, // crm -> Cans
+    {0xCA220000u, 41u}, // crs -> Latn
+    {0x63730000u, 41u}, // cs -> Latn
+    {0x86420000u, 41u}, // csb -> Latn
+    {0xDA420000u,  9u}, // csw -> Cans
+    {0x8E620000u, 59u}, // ctd -> Pauc
+    {0x63750000u, 15u}, // cu -> Cyrl
+    {0x63760000u, 15u}, // cv -> Cyrl
+    {0x63790000u, 41u}, // cy -> Latn
+    {0x64610000u, 41u}, // da -> Latn
+    {0xA8030000u, 41u}, // dak -> Latn
+    {0xC4030000u, 15u}, // dar -> Cyrl
+    {0xD4030000u, 41u}, // dav -> Latn
+    {0x88430000u,  1u}, // dcc -> Arab
+    {0x64650000u, 41u}, // de -> Latn
+    {0xB4830000u, 41u}, // den -> Latn
+    {0xC4C30000u, 41u}, // dgr -> Latn
+    {0x91230000u, 41u}, // dje -> Latn
+    {0xA5A30000u, 41u}, // dnj -> Latn
+    {0xA1C30000u,  1u}, // doi -> Arab
+    {0x86430000u, 41u}, // dsb -> Latn
+    {0xB2630000u, 41u}, // dtm -> Latn
+    {0xBE630000u, 41u}, // dtp -> Latn
+    {0x82830000u, 41u}, // dua -> Latn
+    {0x64760000u, 78u}, // dv -> Thaa
+    {0xBB030000u, 41u}, // dyo -> Latn
+    {0xD3030000u, 41u}, // dyu -> Latn
+    {0x647A0000u, 80u}, // dz -> Tibt
+    {0xD0240000u, 41u}, // ebu -> Latn
+    {0x65650000u, 41u}, // ee -> Latn
+    {0xA0A40000u, 41u}, // efi -> Latn
+    {0xACC40000u, 41u}, // egl -> Latn
+    {0xE0C40000u, 17u}, // egy -> Egyp
+    {0xE1440000u, 32u}, // eky -> Kali
+    {0x656C0000u, 21u}, // el -> Grek
+    {0x656E0000u, 41u}, // en -> Latn
+    {0x656E5841u, 86u}, // en-XA -> ~~~A
+    {0x656F0000u, 41u}, // eo -> Latn
+    {0x65730000u, 41u}, // es -> Latn
+    {0xD2440000u, 41u}, // esu -> Latn
+    {0x65740000u, 41u}, // et -> Latn
+    {0xCE640000u, 30u}, // ett -> Ital
+    {0x65750000u, 41u}, // eu -> Latn
+    {0xBAC40000u, 41u}, // ewo -> Latn
+    {0xCEE40000u, 41u}, // ext -> Latn
+    {0x66610000u,  1u}, // fa -> Arab
+    {0xB4050000u, 41u}, // fan -> Latn
+    {0x66660000u, 41u}, // ff -> Latn
+    {0xB0A50000u, 41u}, // ffm -> Latn
+    {0x66690000u, 41u}, // fi -> Latn
+    {0x81050000u,  1u}, // fia -> Arab
+    {0xAD050000u, 41u}, // fil -> Latn
+    {0xCD050000u, 41u}, // fit -> Latn
+    {0x666A0000u, 41u}, // fj -> Latn
+    {0x666F0000u, 41u}, // fo -> Latn
+    {0xB5C50000u, 41u}, // fon -> Latn
+    {0x66720000u, 41u}, // fr -> Latn
+    {0x8A250000u, 41u}, // frc -> Latn
+    {0xBE250000u, 41u}, // frp -> Latn
+    {0xC6250000u, 41u}, // frr -> Latn
+    {0xCA250000u, 41u}, // frs -> Latn
+    {0x8E850000u, 41u}, // fud -> Latn
+    {0xC2850000u, 41u}, // fuq -> Latn
+    {0xC6850000u, 41u}, // fur -> Latn
+    {0xD6850000u, 41u}, // fuv -> Latn
+    {0xC6A50000u, 41u}, // fvr -> Latn
+    {0x66790000u, 41u}, // fy -> Latn
+    {0x67610000u, 41u}, // ga -> Latn
+    {0x80060000u, 41u}, // gaa -> Latn
+    {0x98060000u, 41u}, // gag -> Latn
+    {0xB4060000u, 24u}, // gan -> Hans
+    {0xE0060000u, 41u}, // gay -> Latn
+    {0xB0260000u, 16u}, // gbm -> Deva
+    {0xE4260000u,  1u}, // gbz -> Arab
+    {0xC4460000u, 41u}, // gcr -> Latn
+    {0x67640000u, 41u}, // gd -> Latn
+    {0xE4860000u, 18u}, // gez -> Ethi
+    {0xB4C60000u, 16u}, // ggn -> Deva
+    {0xAD060000u, 41u}, // gil -> Latn
+    {0xA9260000u,  1u}, // gjk -> Arab
+    {0xD1260000u,  1u}, // gju -> Arab
+    {0x676C0000u, 41u}, // gl -> Latn
+    {0xA9660000u,  1u}, // glk -> Arab
+    {0x676E0000u, 41u}, // gn -> Latn
+    {0xB1C60000u, 16u}, // gom -> Deva
+    {0xB5C60000u, 76u}, // gon -> Telu
+    {0xC5C60000u, 41u}, // gor -> Latn
+    {0xC9C60000u, 41u}, // gos -> Latn
+    {0xCDC60000u, 20u}, // got -> Goth
+    {0x8A260000u, 14u}, // grc -> Cprt
+    {0xCE260000u,  7u}, // grt -> Beng
+    {0xDA460000u, 41u}, // gsw -> Latn
+    {0x67750000u, 22u}, // gu -> Gujr
+    {0x86860000u, 41u}, // gub -> Latn
+    {0x8A860000u, 41u}, // guc -> Latn
+    {0xC6860000u, 41u}, // gur -> Latn
+    {0xE6860000u, 41u}, // guz -> Latn
+    {0x67760000u, 41u}, // gv -> Latn
+    {0xC6A60000u, 16u}, // gvr -> Deva
+    {0xA2C60000u, 41u}, // gwi -> Latn
+    {0x68610000u, 41u}, // ha -> Latn
+    {0x6861434Du,  1u}, // ha-CM -> Arab
+    {0x68615344u,  1u}, // ha-SD -> Arab
+    {0xA8070000u, 24u}, // hak -> Hans
+    {0xD8070000u, 41u}, // haw -> Latn
+    {0xE4070000u,  1u}, // haz -> Arab
+    {0x68650000u, 27u}, // he -> Hebr
+    {0x68690000u, 16u}, // hi -> Deva
+    {0x95070000u, 41u}, // hif -> Latn
+    {0xAD070000u, 41u}, // hil -> Latn
+    {0xD1670000u, 28u}, // hlu -> Hluw
+    {0x8D870000u, 62u}, // hmd -> Plrd
+    {0x8DA70000u,  1u}, // hnd -> Arab
+    {0x91A70000u, 16u}, // hne -> Deva
+    {0xA5A70000u, 29u}, // hnj -> Hmng
+    {0xB5A70000u, 41u}, // hnn -> Latn
+    {0xB9A70000u,  1u}, // hno -> Arab
+    {0x686F0000u, 41u}, // ho -> Latn
+    {0x89C70000u, 16u}, // hoc -> Deva
+    {0xA5C70000u, 16u}, // hoj -> Deva
+    {0x68720000u, 41u}, // hr -> Latn
+    {0x86470000u, 41u}, // hsb -> Latn
+    {0xB6470000u, 24u}, // hsn -> Hans
+    {0x68740000u, 41u}, // ht -> Latn
+    {0x68750000u, 41u}, // hu -> Latn
+    {0x68790000u,  3u}, // hy -> Armn
+    {0x687A0000u, 41u}, // hz -> Latn
+    {0x69610000u, 41u}, // ia -> Latn
+    {0x80280000u, 41u}, // iba -> Latn
+    {0x84280000u, 41u}, // ibb -> Latn
+    {0x69640000u, 41u}, // id -> Latn
+    {0x69670000u, 41u}, // ig -> Latn
+    {0x69690000u, 85u}, // ii -> Yiii
+    {0x696B0000u, 41u}, // ik -> Latn
+    {0xCD480000u, 41u}, // ikt -> Latn
+    {0xB9680000u, 41u}, // ilo -> Latn
+    {0x696E0000u, 41u}, // in -> Latn
+    {0x9DA80000u, 15u}, // inh -> Cyrl
+    {0x69730000u, 41u}, // is -> Latn
+    {0x69740000u, 41u}, // it -> Latn
+    {0x69750000u,  9u}, // iu -> Cans
+    {0x69770000u, 27u}, // iw -> Hebr
+    {0x9F280000u, 41u}, // izh -> Latn
+    {0x6A610000u, 31u}, // ja -> Jpan
+    {0xB0090000u, 41u}, // jam -> Latn
+    {0xB8C90000u, 41u}, // jgo -> Latn
+    {0x6A690000u, 27u}, // ji -> Hebr
+    {0x89890000u, 41u}, // jmc -> Latn
+    {0xAD890000u, 16u}, // jml -> Deva
+    {0xCE890000u, 41u}, // jut -> Latn
+    {0x6A760000u, 41u}, // jv -> Latn
+    {0x6A770000u, 41u}, // jw -> Latn
+    {0x6B610000u, 19u}, // ka -> Geor
+    {0x800A0000u, 15u}, // kaa -> Cyrl
+    {0x840A0000u, 41u}, // kab -> Latn
+    {0x880A0000u, 41u}, // kac -> Latn
+    {0xA40A0000u, 41u}, // kaj -> Latn
+    {0xB00A0000u, 41u}, // kam -> Latn
+    {0xB80A0000u, 41u}, // kao -> Latn
+    {0x8C2A0000u, 15u}, // kbd -> Cyrl
+    {0x984A0000u, 41u}, // kcg -> Latn
+    {0xA84A0000u, 41u}, // kck -> Latn
+    {0x906A0000u, 41u}, // kde -> Latn
+    {0xCC6A0000u, 79u}, // kdt -> Thai
+    {0x808A0000u, 41u}, // kea -> Latn
+    {0xB48A0000u, 41u}, // ken -> Latn
+    {0xB8AA0000u, 41u}, // kfo -> Latn
+    {0xC4AA0000u, 16u}, // kfr -> Deva
+    {0xE0AA0000u, 16u}, // kfy -> Deva
+    {0x6B670000u, 41u}, // kg -> Latn
+    {0x90CA0000u, 41u}, // kge -> Latn
+    {0xBCCA0000u, 41u}, // kgp -> Latn
+    {0x80EA0000u, 41u}, // kha -> Latn
+    {0x84EA0000u, 73u}, // khb -> Talu
+    {0xB4EA0000u, 16u}, // khn -> Deva
+    {0xC0EA0000u, 41u}, // khq -> Latn
+    {0xCCEA0000u, 53u}, // kht -> Mymr
+    {0xD8EA0000u,  1u}, // khw -> Arab
+    {0x6B690000u, 41u}, // ki -> Latn
+    {0xD10A0000u, 41u}, // kiu -> Latn
+    {0x6B6A0000u, 41u}, // kj -> Latn
+    {0x992A0000u, 40u}, // kjg -> Laoo
+    {0x6B6B0000u, 15u}, // kk -> Cyrl
+    {0x6B6B4146u,  1u}, // kk-AF -> Arab
+    {0x6B6B434Eu,  1u}, // kk-CN -> Arab
+    {0x6B6B4952u,  1u}, // kk-IR -> Arab
+    {0x6B6B4D4Eu,  1u}, // kk-MN -> Arab
+    {0xA54A0000u, 41u}, // kkj -> Latn
+    {0x6B6C0000u, 41u}, // kl -> Latn
+    {0xB56A0000u, 41u}, // kln -> Latn
+    {0x6B6D0000u, 35u}, // km -> Khmr
+    {0x858A0000u, 41u}, // kmb -> Latn
+    {0x6B6E0000u, 36u}, // kn -> Knda
+    {0x6B6F0000u, 37u}, // ko -> Kore
+    {0xA1CA0000u, 15u}, // koi -> Cyrl
+    {0xA9CA0000u, 16u}, // kok -> Deva
+    {0xC9CA0000u, 41u}, // kos -> Latn
+    {0x91EA0000u, 41u}, // kpe -> Latn
+    {0x8A2A0000u, 15u}, // krc -> Cyrl
+    {0xA22A0000u, 41u}, // kri -> Latn
+    {0xA62A0000u, 41u}, // krj -> Latn
+    {0xAE2A0000u, 41u}, // krl -> Latn
+    {0xD22A0000u, 16u}, // kru -> Deva
+    {0x6B730000u,  1u}, // ks -> Arab
+    {0x864A0000u, 41u}, // ksb -> Latn
+    {0x964A0000u, 41u}, // ksf -> Latn
+    {0x9E4A0000u, 41u}, // ksh -> Latn
+    {0x6B750000u, 41u}, // ku -> Latn
+    {0x6B754952u,  1u}, // ku-IR -> Arab
+    {0x6B754C42u,  1u}, // ku-LB -> Arab
+    {0xB28A0000u, 15u}, // kum -> Cyrl
+    {0x6B760000u, 15u}, // kv -> Cyrl
+    {0xC6AA0000u, 41u}, // kvr -> Latn
+    {0xDEAA0000u,  1u}, // kvx -> Arab
+    {0x6B770000u, 41u}, // kw -> Latn
+    {0xB2EA0000u, 79u}, // kxm -> Thai
+    {0xBEEA0000u,  1u}, // kxp -> Arab
+    {0x6B790000u, 15u}, // ky -> Cyrl
+    {0x6B79434Eu,  1u}, // ky-CN -> Arab
+    {0x6B795452u, 41u}, // ky-TR -> Latn
+    {0x6C610000u, 41u}, // la -> Latn
+    {0x840B0000u, 43u}, // lab -> Lina
+    {0x8C0B0000u, 27u}, // lad -> Hebr
+    {0x980B0000u, 41u}, // lag -> Latn
+    {0x9C0B0000u,  1u}, // lah -> Arab
+    {0xA40B0000u, 41u}, // laj -> Latn
+    {0x6C620000u, 41u}, // lb -> Latn
+    {0x902B0000u, 15u}, // lbe -> Cyrl
+    {0xD82B0000u, 41u}, // lbw -> Latn
+    {0xBC4B0000u, 79u}, // lcp -> Thai
+    {0xBC8B0000u, 42u}, // lep -> Lepc
+    {0xE48B0000u, 15u}, // lez -> Cyrl
+    {0x6C670000u, 41u}, // lg -> Latn
+    {0x6C690000u, 41u}, // li -> Latn
+    {0x950B0000u, 16u}, // lif -> Deva
+    {0xA50B0000u, 41u}, // lij -> Latn
+    {0xC90B0000u, 44u}, // lis -> Lisu
+    {0xBD2B0000u, 41u}, // ljp -> Latn
+    {0xA14B0000u,  1u}, // lki -> Arab
+    {0xCD4B0000u, 41u}, // lkt -> Latn
+    {0xB58B0000u, 76u}, // lmn -> Telu
+    {0xB98B0000u, 41u}, // lmo -> Latn
+    {0x6C6E0000u, 41u}, // ln -> Latn
+    {0x6C6F0000u, 40u}, // lo -> Laoo
+    {0xADCB0000u, 41u}, // lol -> Latn
+    {0xE5CB0000u, 41u}, // loz -> Latn
+    {0x8A2B0000u,  1u}, // lrc -> Arab
+    {0x6C740000u, 41u}, // lt -> Latn
+    {0x9A6B0000u, 41u}, // ltg -> Latn
+    {0x6C750000u, 41u}, // lu -> Latn
+    {0x828B0000u, 41u}, // lua -> Latn
+    {0xBA8B0000u, 41u}, // luo -> Latn
+    {0xE28B0000u, 41u}, // luy -> Latn
+    {0xE68B0000u,  1u}, // luz -> Arab
+    {0x6C760000u, 41u}, // lv -> Latn
+    {0xAECB0000u, 79u}, // lwl -> Thai
+    {0x9F2B0000u, 24u}, // lzh -> Hans
+    {0xE72B0000u, 41u}, // lzz -> Latn
+    {0x8C0C0000u, 41u}, // mad -> Latn
+    {0x940C0000u, 41u}, // maf -> Latn
+    {0x980C0000u, 16u}, // mag -> Deva
+    {0xA00C0000u, 16u}, // mai -> Deva
+    {0xA80C0000u, 41u}, // mak -> Latn
+    {0xB40C0000u, 41u}, // man -> Latn
+    {0xB40C474Eu, 55u}, // man-GN -> Nkoo
+    {0xC80C0000u, 41u}, // mas -> Latn
+    {0xE40C0000u, 41u}, // maz -> Latn
+    {0x946C0000u, 15u}, // mdf -> Cyrl
+    {0x9C6C0000u, 41u}, // mdh -> Latn
+    {0xC46C0000u, 41u}, // mdr -> Latn
+    {0xB48C0000u, 41u}, // men -> Latn
+    {0xC48C0000u, 41u}, // mer -> Latn
+    {0x80AC0000u,  1u}, // mfa -> Arab
+    {0x90AC0000u, 41u}, // mfe -> Latn
+    {0x6D670000u, 41u}, // mg -> Latn
+    {0x9CCC0000u, 41u}, // mgh -> Latn
+    {0xB8CC0000u, 41u}, // mgo -> Latn
+    {0xBCCC0000u, 16u}, // mgp -> Deva
+    {0xE0CC0000u, 41u}, // mgy -> Latn
+    {0x6D680000u, 41u}, // mh -> Latn
+    {0x6D690000u, 41u}, // mi -> Latn
+    {0xB50C0000u, 41u}, // min -> Latn
+    {0xC90C0000u, 26u}, // mis -> Hatr
+    {0x6D6B0000u, 15u}, // mk -> Cyrl
+    {0x6D6C0000u, 50u}, // ml -> Mlym
+    {0xC96C0000u, 41u}, // mls -> Latn
+    {0x6D6E0000u, 15u}, // mn -> Cyrl
+    {0x6D6E434Eu, 51u}, // mn-CN -> Mong
+    {0xA1AC0000u,  7u}, // mni -> Beng
+    {0xD9AC0000u, 53u}, // mnw -> Mymr
+    {0x91CC0000u, 41u}, // moe -> Latn
+    {0x9DCC0000u, 41u}, // moh -> Latn
+    {0xC9CC0000u, 41u}, // mos -> Latn
+    {0x6D720000u, 16u}, // mr -> Deva
+    {0x8E2C0000u, 16u}, // mrd -> Deva
+    {0xA62C0000u, 15u}, // mrj -> Cyrl
+    {0xD22C0000u, 52u}, // mru -> Mroo
+    {0x6D730000u, 41u}, // ms -> Latn
+    {0x6D734343u,  1u}, // ms-CC -> Arab
+    {0x6D734944u,  1u}, // ms-ID -> Arab
+    {0x6D740000u, 41u}, // mt -> Latn
+    {0xC66C0000u, 16u}, // mtr -> Deva
+    {0x828C0000u, 41u}, // mua -> Latn
+    {0xCA8C0000u, 41u}, // mus -> Latn
+    {0xE2AC0000u,  1u}, // mvy -> Arab
+    {0xAACC0000u, 41u}, // mwk -> Latn
+    {0xC6CC0000u, 16u}, // mwr -> Deva
+    {0xD6CC0000u, 41u}, // mwv -> Latn
+    {0x8AEC0000u, 41u}, // mxc -> Latn
+    {0x6D790000u, 53u}, // my -> Mymr
+    {0xD70C0000u, 15u}, // myv -> Cyrl
+    {0xDF0C0000u, 41u}, // myx -> Latn
+    {0xE70C0000u, 47u}, // myz -> Mand
+    {0xB72C0000u,  1u}, // mzn -> Arab
+    {0x6E610000u, 41u}, // na -> Latn
+    {0xB40D0000u, 24u}, // nan -> Hans
+    {0xBC0D0000u, 41u}, // nap -> Latn
+    {0xC00D0000u, 41u}, // naq -> Latn
+    {0x6E620000u, 41u}, // nb -> Latn
+    {0x9C4D0000u, 41u}, // nch -> Latn
+    {0x6E640000u, 41u}, // nd -> Latn
+    {0x886D0000u, 41u}, // ndc -> Latn
+    {0xC86D0000u, 41u}, // nds -> Latn
+    {0x6E650000u, 16u}, // ne -> Deva
+    {0xD88D0000u, 16u}, // new -> Deva
+    {0x6E670000u, 41u}, // ng -> Latn
+    {0xACCD0000u, 41u}, // ngl -> Latn
+    {0x90ED0000u, 41u}, // nhe -> Latn
+    {0xD8ED0000u, 41u}, // nhw -> Latn
+    {0xA50D0000u, 41u}, // nij -> Latn
+    {0xD10D0000u, 41u}, // niu -> Latn
+    {0xB92D0000u, 41u}, // njo -> Latn
+    {0x6E6C0000u, 41u}, // nl -> Latn
+    {0x998D0000u, 41u}, // nmg -> Latn
+    {0x6E6E0000u, 41u}, // nn -> Latn
+    {0x9DAD0000u, 41u}, // nnh -> Latn
+    {0x6E6F0000u, 41u}, // no -> Latn
+    {0x8DCD0000u, 39u}, // nod -> Lana
+    {0x91CD0000u, 16u}, // noe -> Deva
+    {0xB5CD0000u, 64u}, // non -> Runr
+    {0xBA0D0000u, 55u}, // nqo -> Nkoo
+    {0x6E720000u, 41u}, // nr -> Latn
+    {0xAA4D0000u,  9u}, // nsk -> Cans
+    {0xBA4D0000u, 41u}, // nso -> Latn
+    {0xCA8D0000u, 41u}, // nus -> Latn
+    {0x6E760000u, 41u}, // nv -> Latn
+    {0xC2ED0000u, 41u}, // nxq -> Latn
+    {0x6E790000u, 41u}, // ny -> Latn
+    {0xB30D0000u, 41u}, // nym -> Latn
+    {0xB70D0000u, 41u}, // nyn -> Latn
+    {0xA32D0000u, 41u}, // nzi -> Latn
+    {0x6F630000u, 41u}, // oc -> Latn
+    {0x6F6D0000u, 41u}, // om -> Latn
+    {0x6F720000u, 58u}, // or -> Orya
+    {0x6F730000u, 15u}, // os -> Cyrl
+    {0xAA6E0000u, 57u}, // otk -> Orkh
+    {0x70610000u, 23u}, // pa -> Guru
+    {0x7061504Bu,  1u}, // pa-PK -> Arab
+    {0x980F0000u, 41u}, // pag -> Latn
+    {0xAC0F0000u, 60u}, // pal -> Phli
+    {0xB00F0000u, 41u}, // pam -> Latn
+    {0xBC0F0000u, 41u}, // pap -> Latn
+    {0xD00F0000u, 41u}, // pau -> Latn
+    {0x8C4F0000u, 41u}, // pcd -> Latn
+    {0xB04F0000u, 41u}, // pcm -> Latn
+    {0x886F0000u, 41u}, // pdc -> Latn
+    {0xCC6F0000u, 41u}, // pdt -> Latn
+    {0xB88F0000u, 83u}, // peo -> Xpeo
+    {0xACAF0000u, 41u}, // pfl -> Latn
+    {0xB4EF0000u, 61u}, // phn -> Phnx
+    {0x814F0000u,  8u}, // pka -> Brah
+    {0xB94F0000u, 41u}, // pko -> Latn
+    {0x706C0000u, 41u}, // pl -> Latn
+    {0xC98F0000u, 41u}, // pms -> Latn
+    {0xCDAF0000u, 21u}, // pnt -> Grek
+    {0xB5CF0000u, 41u}, // pon -> Latn
+    {0x822F0000u, 34u}, // pra -> Khar
+    {0x8E2F0000u,  1u}, // prd -> Arab
+    {0x9A2F0000u, 41u}, // prg -> Latn
+    {0x70730000u,  1u}, // ps -> Arab
+    {0x70740000u, 41u}, // pt -> Latn
+    {0xD28F0000u, 41u}, // puu -> Latn
+    {0x71750000u, 41u}, // qu -> Latn
+    {0x8A900000u, 41u}, // quc -> Latn
+    {0x9A900000u, 41u}, // qug -> Latn
+    {0xA4110000u, 16u}, // raj -> Deva
+    {0x94510000u, 41u}, // rcf -> Latn
+    {0xA4910000u, 41u}, // rej -> Latn
+    {0xB4D10000u, 41u}, // rgn -> Latn
+    {0x81110000u, 41u}, // ria -> Latn
+    {0x95110000u, 77u}, // rif -> Tfng
+    {0x95114E4Cu, 41u}, // rif-NL -> Latn
+    {0xC9310000u, 16u}, // rjs -> Deva
+    {0xCD510000u,  7u}, // rkt -> Beng
+    {0x726D0000u, 41u}, // rm -> Latn
+    {0x95910000u, 41u}, // rmf -> Latn
+    {0xB9910000u, 41u}, // rmo -> Latn
+    {0xCD910000u,  1u}, // rmt -> Arab
+    {0xD1910000u, 41u}, // rmu -> Latn
+    {0x726E0000u, 41u}, // rn -> Latn
+    {0x99B10000u, 41u}, // rng -> Latn
+    {0x726F0000u, 41u}, // ro -> Latn
+    {0x85D10000u, 41u}, // rob -> Latn
+    {0x95D10000u, 41u}, // rof -> Latn
+    {0xB2710000u, 41u}, // rtm -> Latn
+    {0x72750000u, 15u}, // ru -> Cyrl
+    {0x92910000u, 15u}, // rue -> Cyrl
+    {0x9A910000u, 41u}, // rug -> Latn
+    {0x72770000u, 41u}, // rw -> Latn
+    {0xAAD10000u, 41u}, // rwk -> Latn
+    {0xD3110000u, 33u}, // ryu -> Kana
+    {0x73610000u, 16u}, // sa -> Deva
+    {0x94120000u, 41u}, // saf -> Latn
+    {0x9C120000u, 15u}, // sah -> Cyrl
+    {0xC0120000u, 41u}, // saq -> Latn
+    {0xC8120000u, 41u}, // sas -> Latn
+    {0xCC120000u, 41u}, // sat -> Latn
+    {0xE4120000u, 67u}, // saz -> Saur
+    {0xBC320000u, 41u}, // sbp -> Latn
+    {0x73630000u, 41u}, // sc -> Latn
+    {0xA8520000u, 16u}, // sck -> Deva
+    {0xB4520000u, 41u}, // scn -> Latn
+    {0xB8520000u, 41u}, // sco -> Latn
+    {0xC8520000u, 41u}, // scs -> Latn
+    {0x73640000u,  1u}, // sd -> Arab
+    {0x88720000u, 41u}, // sdc -> Latn
+    {0x9C720000u,  1u}, // sdh -> Arab
+    {0x73650000u, 41u}, // se -> Latn
+    {0x94920000u, 41u}, // sef -> Latn
+    {0x9C920000u, 41u}, // seh -> Latn
+    {0xA0920000u, 41u}, // sei -> Latn
+    {0xC8920000u, 41u}, // ses -> Latn
+    {0x73670000u, 41u}, // sg -> Latn
+    {0x80D20000u, 56u}, // sga -> Ogam
+    {0xC8D20000u, 41u}, // sgs -> Latn
+    {0x73680000u, 41u}, // sh -> Latn
+    {0xA0F20000u, 77u}, // shi -> Tfng
+    {0xB4F20000u, 53u}, // shn -> Mymr
+    {0x73690000u, 69u}, // si -> Sinh
+    {0x8D120000u, 41u}, // sid -> Latn
+    {0x736B0000u, 41u}, // sk -> Latn
+    {0xC5520000u,  1u}, // skr -> Arab
+    {0x736C0000u, 41u}, // sl -> Latn
+    {0xA1720000u, 41u}, // sli -> Latn
+    {0xE1720000u, 41u}, // sly -> Latn
+    {0x736D0000u, 41u}, // sm -> Latn
+    {0x81920000u, 41u}, // sma -> Latn
+    {0xA5920000u, 41u}, // smj -> Latn
+    {0xB5920000u, 41u}, // smn -> Latn
+    {0xBD920000u, 65u}, // smp -> Samr
+    {0xC9920000u, 41u}, // sms -> Latn
+    {0x736E0000u, 41u}, // sn -> Latn
+    {0xA9B20000u, 41u}, // snk -> Latn
+    {0x736F0000u, 41u}, // so -> Latn
+    {0xD1D20000u, 79u}, // sou -> Thai
+    {0x73710000u, 41u}, // sq -> Latn
+    {0x73720000u, 15u}, // sr -> Cyrl
+    {0x73724D45u, 41u}, // sr-ME -> Latn
+    {0x7372524Fu, 41u}, // sr-RO -> Latn
+    {0x73725255u, 41u}, // sr-RU -> Latn
+    {0x73725452u, 41u}, // sr-TR -> Latn
+    {0x86320000u, 70u}, // srb -> Sora
+    {0xB6320000u, 41u}, // srn -> Latn
+    {0xC6320000u, 41u}, // srr -> Latn
+    {0xDE320000u, 16u}, // srx -> Deva
+    {0x73730000u, 41u}, // ss -> Latn
+    {0xE2520000u, 41u}, // ssy -> Latn
+    {0x73740000u, 41u}, // st -> Latn
+    {0xC2720000u, 41u}, // stq -> Latn
+    {0x73750000u, 41u}, // su -> Latn
+    {0xAA920000u, 41u}, // suk -> Latn
+    {0xCA920000u, 41u}, // sus -> Latn
+    {0x73760000u, 41u}, // sv -> Latn
+    {0x73770000u, 41u}, // sw -> Latn
+    {0x86D20000u,  1u}, // swb -> Arab
+    {0x8AD20000u, 41u}, // swc -> Latn
+    {0x9AD20000u, 41u}, // swg -> Latn
+    {0xD6D20000u, 16u}, // swv -> Deva
+    {0xB6F20000u, 41u}, // sxn -> Latn
+    {0xAF120000u,  7u}, // syl -> Beng
+    {0xC7120000u, 71u}, // syr -> Syrc
+    {0xAF320000u, 41u}, // szl -> Latn
+    {0x74610000u, 74u}, // ta -> Taml
+    {0xA4130000u, 16u}, // taj -> Deva
+    {0xD8330000u, 41u}, // tbw -> Latn
+    {0xE0530000u, 36u}, // tcy -> Knda
+    {0x8C730000u, 72u}, // tdd -> Tale
+    {0x98730000u, 16u}, // tdg -> Deva
+    {0x9C730000u, 16u}, // tdh -> Deva
+    {0x74650000u, 76u}, // te -> Telu
+    {0xB0930000u, 41u}, // tem -> Latn
+    {0xB8930000u, 41u}, // teo -> Latn
+    {0xCC930000u, 41u}, // tet -> Latn
+    {0x74670000u, 15u}, // tg -> Cyrl
+    {0x7467504Bu,  1u}, // tg-PK -> Arab
+    {0x74680000u, 79u}, // th -> Thai
+    {0xACF30000u, 16u}, // thl -> Deva
+    {0xC0F30000u, 16u}, // thq -> Deva
+    {0xC4F30000u, 16u}, // thr -> Deva
+    {0x74690000u, 18u}, // ti -> Ethi
+    {0x99130000u, 18u}, // tig -> Ethi
+    {0xD5130000u, 41u}, // tiv -> Latn
+    {0x746B0000u, 41u}, // tk -> Latn
+    {0xAD530000u, 41u}, // tkl -> Latn
+    {0xC5530000u, 41u}, // tkr -> Latn
+    {0xCD530000u, 16u}, // tkt -> Deva
+    {0x746C0000u, 41u}, // tl -> Latn
+    {0xE1730000u, 41u}, // tly -> Latn
+    {0x9D930000u, 41u}, // tmh -> Latn
+    {0x746E0000u, 41u}, // tn -> Latn
+    {0x746F0000u, 41u}, // to -> Latn
+    {0x99D30000u, 41u}, // tog -> Latn
+    {0xA1F30000u, 41u}, // tpi -> Latn
+    {0x74720000u, 41u}, // tr -> Latn
+    {0xD2330000u, 41u}, // tru -> Latn
+    {0xD6330000u, 41u}, // trv -> Latn
+    {0x74730000u, 41u}, // ts -> Latn
+    {0x8E530000u, 21u}, // tsd -> Grek
+    {0x96530000u, 16u}, // tsf -> Deva
+    {0x9A530000u, 41u}, // tsg -> Latn
+    {0xA6530000u, 80u}, // tsj -> Tibt
+    {0x74740000u, 15u}, // tt -> Cyrl
+    {0xA6730000u, 41u}, // ttj -> Latn
+    {0xCA730000u, 79u}, // tts -> Thai
+    {0xCE730000u, 41u}, // ttt -> Latn
+    {0xB2930000u, 41u}, // tum -> Latn
+    {0xAEB30000u, 41u}, // tvl -> Latn
+    {0xC2D30000u, 41u}, // twq -> Latn
+    {0x74790000u, 41u}, // ty -> Latn
+    {0xD7130000u, 15u}, // tyv -> Cyrl
+    {0xB3330000u, 41u}, // tzm -> Latn
+    {0xB0740000u, 15u}, // udm -> Cyrl
+    {0x75670000u,  1u}, // ug -> Arab
+    {0x75674B5Au, 15u}, // ug-KZ -> Cyrl
+    {0x75674D4Eu, 15u}, // ug-MN -> Cyrl
+    {0x80D40000u, 81u}, // uga -> Ugar
+    {0x756B0000u, 15u}, // uk -> Cyrl
+    {0xA1740000u, 41u}, // uli -> Latn
+    {0x85940000u, 41u}, // umb -> Latn
+    {0xC5B40000u,  7u}, // unr -> Beng
+    {0xC5B44E50u, 16u}, // unr-NP -> Deva
+    {0xDDB40000u,  7u}, // unx -> Beng
+    {0x75720000u,  1u}, // ur -> Arab
+    {0x757A0000u, 41u}, // uz -> Latn
+    {0x757A4146u,  1u}, // uz-AF -> Arab
+    {0x757A434Eu, 15u}, // uz-CN -> Cyrl
+    {0xA0150000u, 82u}, // vai -> Vaii
+    {0x76650000u, 41u}, // ve -> Latn
+    {0x88950000u, 41u}, // vec -> Latn
+    {0xBC950000u, 41u}, // vep -> Latn
+    {0x76690000u, 41u}, // vi -> Latn
+    {0x89150000u, 41u}, // vic -> Latn
+    {0xC9750000u, 41u}, // vls -> Latn
+    {0x95950000u, 41u}, // vmf -> Latn
+    {0xD9950000u, 41u}, // vmw -> Latn
+    {0x766F0000u, 41u}, // vo -> Latn
+    {0xCDD50000u, 41u}, // vot -> Latn
+    {0xBA350000u, 41u}, // vro -> Latn
+    {0xB6950000u, 41u}, // vun -> Latn
+    {0x77610000u, 41u}, // wa -> Latn
+    {0x90160000u, 41u}, // wae -> Latn
+    {0xAC160000u, 18u}, // wal -> Ethi
+    {0xC4160000u, 41u}, // war -> Latn
+    {0xBC360000u, 41u}, // wbp -> Latn
+    {0xC0360000u, 76u}, // wbq -> Telu
+    {0xC4360000u, 16u}, // wbr -> Deva
+    {0xC9760000u, 41u}, // wls -> Latn
+    {0xA1B60000u,  1u}, // wni -> Arab
+    {0x776F0000u, 41u}, // wo -> Latn
+    {0xB2760000u, 16u}, // wtm -> Deva
+    {0xD2960000u, 24u}, // wuu -> Hans
+    {0xD4170000u, 41u}, // xav -> Latn
+    {0xC4570000u, 10u}, // xcr -> Cari
+    {0x78680000u, 41u}, // xh -> Latn
+    {0x89770000u, 45u}, // xlc -> Lyci
+    {0x8D770000u, 46u}, // xld -> Lydi
+    {0x95970000u, 19u}, // xmf -> Geor
+    {0xB5970000u, 48u}, // xmn -> Mani
+    {0xC5970000u, 49u}, // xmr -> Merc
+    {0x81B70000u, 54u}, // xna -> Narb
+    {0xC5B70000u, 16u}, // xnr -> Deva
+    {0x99D70000u, 41u}, // xog -> Latn
+    {0xC5F70000u, 63u}, // xpr -> Prti
+    {0x82570000u, 66u}, // xsa -> Sarb
+    {0xC6570000u, 16u}, // xsr -> Deva
+    {0xB8180000u, 41u}, // yao -> Latn
+    {0xBC180000u, 41u}, // yap -> Latn
+    {0xD4180000u, 41u}, // yav -> Latn
+    {0x84380000u, 41u}, // ybb -> Latn
+    {0x79690000u, 27u}, // yi -> Hebr
+    {0x796F0000u, 41u}, // yo -> Latn
+    {0xAE380000u, 41u}, // yrl -> Latn
+    {0x82980000u, 41u}, // yua -> Latn
+    {0x7A610000u, 41u}, // za -> Latn
+    {0x98190000u, 41u}, // zag -> Latn
+    {0xA4790000u,  1u}, // zdj -> Arab
+    {0x80990000u, 41u}, // zea -> Latn
+    {0x9CD90000u, 77u}, // zgh -> Tfng
+    {0x7A680000u, 24u}, // zh -> Hans
+    {0x7A684155u, 25u}, // zh-AU -> Hant
+    {0x7A68424Eu, 25u}, // zh-BN -> Hant
+    {0x7A684742u, 25u}, // zh-GB -> Hant
+    {0x7A684746u, 25u}, // zh-GF -> Hant
+    {0x7A68484Bu, 25u}, // zh-HK -> Hant
+    {0x7A684944u, 25u}, // zh-ID -> Hant
+    {0x7A684D4Fu, 25u}, // zh-MO -> Hant
+    {0x7A684D59u, 25u}, // zh-MY -> Hant
+    {0x7A685041u, 25u}, // zh-PA -> Hant
+    {0x7A685046u, 25u}, // zh-PF -> Hant
+    {0x7A685048u, 25u}, // zh-PH -> Hant
+    {0x7A685352u, 25u}, // zh-SR -> Hant
+    {0x7A685448u, 25u}, // zh-TH -> Hant
+    {0x7A685457u, 25u}, // zh-TW -> Hant
+    {0x7A685553u, 25u}, // zh-US -> Hant
+    {0x7A68564Eu, 25u}, // zh-VN -> Hant
+    {0xA1990000u, 41u}, // zmi -> Latn
+    {0x7A750000u, 41u}, // zu -> Latn
+    {0x83390000u, 41u}, // zza -> Latn
+});
+
+std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({
+    0x616145544C61746Ellu, // aa_Latn_ET
+    0x616247454379726Cllu, // ab_Cyrl_GE
+    0xC42047484C61746Ellu, // abr_Latn_GH
+    0x904049444C61746Ellu, // ace_Latn_ID
+    0x9C4055474C61746Ellu, // ach_Latn_UG
+    0x806047484C61746Ellu, // ada_Latn_GH
+    0xE06052554379726Cllu, // ady_Cyrl_RU
+    0x6165495241767374llu, // ae_Avst_IR
+    0x8480544E41726162llu, // aeb_Arab_TN
+    0x61665A414C61746Ellu, // af_Latn_ZA
+    0xC0C0434D4C61746Ellu, // agq_Latn_CM
+    0xB8E0494E41686F6Dllu, // aho_Ahom_IN
+    0x616B47484C61746Ellu, // ak_Latn_GH
+    0xA940495158737578llu, // akk_Xsux_IQ
+    0xB560584B4C61746Ellu, // aln_Latn_XK
+    0xCD6052554379726Cllu, // alt_Cyrl_RU
+    0x616D455445746869llu, // am_Ethi_ET
+    0xB9804E474C61746Ellu, // amo_Latn_NG
+    0xE5C049444C61746Ellu, // aoz_Latn_ID
+    0x6172454741726162llu, // ar_Arab_EG
+    0x8A20495241726D69llu, // arc_Armi_IR
+    0x8A204A4F4E626174llu, // arc_Nbat_JO
+    0x8A20535950616C6Dllu, // arc_Palm_SY
+    0xB620434C4C61746Ellu, // arn_Latn_CL
+    0xBA20424F4C61746Ellu, // aro_Latn_BO
+    0xC220445A41726162llu, // arq_Arab_DZ
+    0xE2204D4141726162llu, // ary_Arab_MA
+    0xE620454741726162llu, // arz_Arab_EG
+    0x6173494E42656E67llu, // as_Beng_IN
+    0x8240545A4C61746Ellu, // asa_Latn_TZ
+    0x9240555353676E77llu, // ase_Sgnw_US
+    0xCE4045534C61746Ellu, // ast_Latn_ES
+    0xA66043414C61746Ellu, // atj_Latn_CA
+    0x617652554379726Cllu, // av_Cyrl_RU
+    0x82C0494E44657661llu, // awa_Deva_IN
+    0x6179424F4C61746Ellu, // ay_Latn_BO
+    0x617A495241726162llu, // az_Arab_IR
+    0x617A415A4C61746Ellu, // az_Latn_AZ
+    0x626152554379726Cllu, // ba_Cyrl_RU
+    0xAC01504B41726162llu, // bal_Arab_PK
+    0xB40149444C61746Ellu, // ban_Latn_ID
+    0xBC014E5044657661llu, // bap_Deva_NP
+    0xC40141544C61746Ellu, // bar_Latn_AT
+    0xC801434D4C61746Ellu, // bas_Latn_CM
+    0xDC01434D42616D75llu, // bax_Bamu_CM
+    0x882149444C61746Ellu, // bbc_Latn_ID
+    0xA421434D4C61746Ellu, // bbj_Latn_CM
+    0xA04143494C61746Ellu, // bci_Latn_CI
+    0x626542594379726Cllu, // be_Cyrl_BY
+    0xA481534441726162llu, // bej_Arab_SD
+    0xB0815A4D4C61746Ellu, // bem_Latn_ZM
+    0xD88149444C61746Ellu, // bew_Latn_ID
+    0xE481545A4C61746Ellu, // bez_Latn_TZ
+    0x8CA1434D4C61746Ellu, // bfd_Latn_CM
+    0xC0A1494E54616D6Cllu, // bfq_Taml_IN
+    0xCCA1504B41726162llu, // bft_Arab_PK
+    0xE0A1494E44657661llu, // bfy_Deva_IN
+    0x626742474379726Cllu, // bg_Cyrl_BG
+    0x88C1494E44657661llu, // bgc_Deva_IN
+    0xB4C1504B41726162llu, // bgn_Arab_PK
+    0xDCC154524772656Bllu, // bgx_Grek_TR
+    0x6268494E4B746869llu, // bh_Kthi_IN
+    0x84E1494E44657661llu, // bhb_Deva_IN
+    0xA0E1494E44657661llu, // bhi_Deva_IN
+    0xA8E150484C61746Ellu, // bhk_Latn_PH
+    0xB8E1494E44657661llu, // bho_Deva_IN
+    0x626956554C61746Ellu, // bi_Latn_VU
+    0xA90150484C61746Ellu, // bik_Latn_PH
+    0xB5014E474C61746Ellu, // bin_Latn_NG
+    0xA521494E44657661llu, // bjj_Deva_IN
+    0xB52149444C61746Ellu, // bjn_Latn_ID
+    0xB141434D4C61746Ellu, // bkm_Latn_CM
+    0xD14150484C61746Ellu, // bku_Latn_PH
+    0xCD61564E54617674llu, // blt_Tavt_VN
+    0x626D4D4C4C61746Ellu, // bm_Latn_ML
+    0xC1814D4C4C61746Ellu, // bmq_Latn_ML
+    0x626E424442656E67llu, // bn_Beng_BD
+    0x626F434E54696274llu, // bo_Tibt_CN
+    0xE1E1494E42656E67llu, // bpy_Beng_IN
+    0xA201495241726162llu, // bqi_Arab_IR
+    0xD60143494C61746Ellu, // bqv_Latn_CI
+    0x627246524C61746Ellu, // br_Latn_FR
+    0x8221494E44657661llu, // bra_Deva_IN
+    0x9E21504B41726162llu, // brh_Arab_PK
+    0xDE21494E44657661llu, // brx_Deva_IN
+    0x627342414C61746Ellu, // bs_Latn_BA
+    0xC2414C5242617373llu, // bsq_Bass_LR
+    0xCA41434D4C61746Ellu, // bss_Latn_CM
+    0xBA6150484C61746Ellu, // bto_Latn_PH
+    0xD661504B44657661llu, // btv_Deva_PK
+    0x828152554379726Cllu, // bua_Cyrl_RU
+    0x8A8159544C61746Ellu, // buc_Latn_YT
+    0x9A8149444C61746Ellu, // bug_Latn_ID
+    0xB281434D4C61746Ellu, // bum_Latn_CM
+    0x86A147514C61746Ellu, // bvb_Latn_GQ
+    0xB701455245746869llu, // byn_Ethi_ER
+    0xD701434D4C61746Ellu, // byv_Latn_CM
+    0x93214D4C4C61746Ellu, // bze_Latn_ML
+    0x636145534C61746Ellu, // ca_Latn_ES
+    0x9C424E474C61746Ellu, // cch_Latn_NG
+    0xBC42494E42656E67llu, // ccp_Beng_IN
+    0xBC42424443616B6Dllu, // ccp_Cakm_BD
+    0x636552554379726Cllu, // ce_Cyrl_RU
+    0x848250484C61746Ellu, // ceb_Latn_PH
+    0x98C255474C61746Ellu, // cgg_Latn_UG
+    0x636847554C61746Ellu, // ch_Latn_GU
+    0xA8E2464D4C61746Ellu, // chk_Latn_FM
+    0xB0E252554379726Cllu, // chm_Cyrl_RU
+    0xB8E255534C61746Ellu, // cho_Latn_US
+    0xBCE243414C61746Ellu, // chp_Latn_CA
+    0xC4E2555343686572llu, // chr_Cher_US
+    0x81224B4841726162llu, // cja_Arab_KH
+    0xB122564E4368616Dllu, // cjm_Cham_VN
+    0x8542495141726162llu, // ckb_Arab_IQ
+    0x636F46524C61746Ellu, // co_Latn_FR
+    0xBDC24547436F7074llu, // cop_Copt_EG
+    0xC9E250484C61746Ellu, // cps_Latn_PH
+    0x6372434143616E73llu, // cr_Cans_CA
+    0xA622434143616E73llu, // crj_Cans_CA
+    0xAA22434143616E73llu, // crk_Cans_CA
+    0xAE22434143616E73llu, // crl_Cans_CA
+    0xB222434143616E73llu, // crm_Cans_CA
+    0xCA2253434C61746Ellu, // crs_Latn_SC
+    0x6373435A4C61746Ellu, // cs_Latn_CZ
+    0x8642504C4C61746Ellu, // csb_Latn_PL
+    0xDA42434143616E73llu, // csw_Cans_CA
+    0x8E624D4D50617563llu, // ctd_Pauc_MM
+    0x637552554379726Cllu, // cu_Cyrl_RU
+    0x63754247476C6167llu, // cu_Glag_BG
+    0x637652554379726Cllu, // cv_Cyrl_RU
+    0x637947424C61746Ellu, // cy_Latn_GB
+    0x6461444B4C61746Ellu, // da_Latn_DK
+    0xA80355534C61746Ellu, // dak_Latn_US
+    0xC40352554379726Cllu, // dar_Cyrl_RU
+    0xD4034B454C61746Ellu, // dav_Latn_KE
+    0x8843494E41726162llu, // dcc_Arab_IN
+    0x646544454C61746Ellu, // de_Latn_DE
+    0xB48343414C61746Ellu, // den_Latn_CA
+    0xC4C343414C61746Ellu, // dgr_Latn_CA
+    0x91234E454C61746Ellu, // dje_Latn_NE
+    0xA5A343494C61746Ellu, // dnj_Latn_CI
+    0xA1C3494E41726162llu, // doi_Arab_IN
+    0x864344454C61746Ellu, // dsb_Latn_DE
+    0xB2634D4C4C61746Ellu, // dtm_Latn_ML
+    0xBE634D594C61746Ellu, // dtp_Latn_MY
+    0x8283434D4C61746Ellu, // dua_Latn_CM
+    0x64764D5654686161llu, // dv_Thaa_MV
+    0xBB03534E4C61746Ellu, // dyo_Latn_SN
+    0xD30342464C61746Ellu, // dyu_Latn_BF
+    0x647A425454696274llu, // dz_Tibt_BT
+    0xD0244B454C61746Ellu, // ebu_Latn_KE
+    0x656547484C61746Ellu, // ee_Latn_GH
+    0xA0A44E474C61746Ellu, // efi_Latn_NG
+    0xACC449544C61746Ellu, // egl_Latn_IT
+    0xE0C4454745677970llu, // egy_Egyp_EG
+    0xE1444D4D4B616C69llu, // eky_Kali_MM
+    0x656C47524772656Bllu, // el_Grek_GR
+    0x656E47424C61746Ellu, // en_Latn_GB
+    0x656E55534C61746Ellu, // en_Latn_US
+    0x656E474253686177llu, // en_Shaw_GB
+    0x657345534C61746Ellu, // es_Latn_ES
+    0x65734D584C61746Ellu, // es_Latn_MX
+    0x657355534C61746Ellu, // es_Latn_US
+    0xD24455534C61746Ellu, // esu_Latn_US
+    0x657445454C61746Ellu, // et_Latn_EE
+    0xCE6449544974616Cllu, // ett_Ital_IT
+    0x657545534C61746Ellu, // eu_Latn_ES
+    0xBAC4434D4C61746Ellu, // ewo_Latn_CM
+    0xCEE445534C61746Ellu, // ext_Latn_ES
+    0x6661495241726162llu, // fa_Arab_IR
+    0xB40547514C61746Ellu, // fan_Latn_GQ
+    0x6666534E4C61746Ellu, // ff_Latn_SN
+    0xB0A54D4C4C61746Ellu, // ffm_Latn_ML
+    0x666946494C61746Ellu, // fi_Latn_FI
+    0x8105534441726162llu, // fia_Arab_SD
+    0xAD0550484C61746Ellu, // fil_Latn_PH
+    0xCD0553454C61746Ellu, // fit_Latn_SE
+    0x666A464A4C61746Ellu, // fj_Latn_FJ
+    0x666F464F4C61746Ellu, // fo_Latn_FO
+    0xB5C5424A4C61746Ellu, // fon_Latn_BJ
+    0x667246524C61746Ellu, // fr_Latn_FR
+    0x8A2555534C61746Ellu, // frc_Latn_US
+    0xBE2546524C61746Ellu, // frp_Latn_FR
+    0xC62544454C61746Ellu, // frr_Latn_DE
+    0xCA2544454C61746Ellu, // frs_Latn_DE
+    0x8E8557464C61746Ellu, // fud_Latn_WF
+    0xC2854E454C61746Ellu, // fuq_Latn_NE
+    0xC68549544C61746Ellu, // fur_Latn_IT
+    0xD6854E474C61746Ellu, // fuv_Latn_NG
+    0xC6A553444C61746Ellu, // fvr_Latn_SD
+    0x66794E4C4C61746Ellu, // fy_Latn_NL
+    0x676149454C61746Ellu, // ga_Latn_IE
+    0x800647484C61746Ellu, // gaa_Latn_GH
+    0x98064D444C61746Ellu, // gag_Latn_MD
+    0xB406434E48616E73llu, // gan_Hans_CN
+    0xE00649444C61746Ellu, // gay_Latn_ID
+    0xB026494E44657661llu, // gbm_Deva_IN
+    0xE426495241726162llu, // gbz_Arab_IR
+    0xC44647464C61746Ellu, // gcr_Latn_GF
+    0x676447424C61746Ellu, // gd_Latn_GB
+    0xE486455445746869llu, // gez_Ethi_ET
+    0xB4C64E5044657661llu, // ggn_Deva_NP
+    0xAD064B494C61746Ellu, // gil_Latn_KI
+    0xA926504B41726162llu, // gjk_Arab_PK
+    0xD126504B41726162llu, // gju_Arab_PK
+    0x676C45534C61746Ellu, // gl_Latn_ES
+    0xA966495241726162llu, // glk_Arab_IR
+    0x676E50594C61746Ellu, // gn_Latn_PY
+    0xB1C6494E44657661llu, // gom_Deva_IN
+    0xB5C6494E54656C75llu, // gon_Telu_IN
+    0xC5C649444C61746Ellu, // gor_Latn_ID
+    0xC9C64E4C4C61746Ellu, // gos_Latn_NL
+    0xCDC65541476F7468llu, // got_Goth_UA
+    0x8A26435943707274llu, // grc_Cprt_CY
+    0x8A2647524C696E62llu, // grc_Linb_GR
+    0xCE26494E42656E67llu, // grt_Beng_IN
+    0xDA4643484C61746Ellu, // gsw_Latn_CH
+    0x6775494E47756A72llu, // gu_Gujr_IN
+    0x868642524C61746Ellu, // gub_Latn_BR
+    0x8A86434F4C61746Ellu, // guc_Latn_CO
+    0xC68647484C61746Ellu, // gur_Latn_GH
+    0xE6864B454C61746Ellu, // guz_Latn_KE
+    0x6776494D4C61746Ellu, // gv_Latn_IM
+    0xC6A64E5044657661llu, // gvr_Deva_NP
+    0xA2C643414C61746Ellu, // gwi_Latn_CA
+    0x68614E474C61746Ellu, // ha_Latn_NG
+    0xA807434E48616E73llu, // hak_Hans_CN
+    0xD80755534C61746Ellu, // haw_Latn_US
+    0xE407414641726162llu, // haz_Arab_AF
+    0x6865494C48656272llu, // he_Hebr_IL
+    0x6869494E44657661llu, // hi_Deva_IN
+    0x9507464A4C61746Ellu, // hif_Latn_FJ
+    0xAD0750484C61746Ellu, // hil_Latn_PH
+    0xD1675452486C7577llu, // hlu_Hluw_TR
+    0x8D87434E506C7264llu, // hmd_Plrd_CN
+    0x8DA7504B41726162llu, // hnd_Arab_PK
+    0x91A7494E44657661llu, // hne_Deva_IN
+    0xA5A74C41486D6E67llu, // hnj_Hmng_LA
+    0xB5A750484C61746Ellu, // hnn_Latn_PH
+    0xB9A7504B41726162llu, // hno_Arab_PK
+    0x686F50474C61746Ellu, // ho_Latn_PG
+    0x89C7494E44657661llu, // hoc_Deva_IN
+    0xA5C7494E44657661llu, // hoj_Deva_IN
+    0x687248524C61746Ellu, // hr_Latn_HR
+    0x864744454C61746Ellu, // hsb_Latn_DE
+    0xB647434E48616E73llu, // hsn_Hans_CN
+    0x687448544C61746Ellu, // ht_Latn_HT
+    0x687548554C61746Ellu, // hu_Latn_HU
+    0x6879414D41726D6Ellu, // hy_Armn_AM
+    0x687A4E414C61746Ellu, // hz_Latn_NA
+    0x696146524C61746Ellu, // ia_Latn_FR
+    0x80284D594C61746Ellu, // iba_Latn_MY
+    0x84284E474C61746Ellu, // ibb_Latn_NG
+    0x696449444C61746Ellu, // id_Latn_ID
+    0x69674E474C61746Ellu, // ig_Latn_NG
+    0x6969434E59696969llu, // ii_Yiii_CN
+    0x696B55534C61746Ellu, // ik_Latn_US
+    0xCD4843414C61746Ellu, // ikt_Latn_CA
+    0xB96850484C61746Ellu, // ilo_Latn_PH
+    0x696E49444C61746Ellu, // in_Latn_ID
+    0x9DA852554379726Cllu, // inh_Cyrl_RU
+    0x697349534C61746Ellu, // is_Latn_IS
+    0x697449544C61746Ellu, // it_Latn_IT
+    0x6975434143616E73llu, // iu_Cans_CA
+    0x6977494C48656272llu, // iw_Hebr_IL
+    0x9F2852554C61746Ellu, // izh_Latn_RU
+    0x6A614A504A70616Ellu, // ja_Jpan_JP
+    0xB0094A4D4C61746Ellu, // jam_Latn_JM
+    0xB8C9434D4C61746Ellu, // jgo_Latn_CM
+    0x6A69554148656272llu, // ji_Hebr_UA
+    0x8989545A4C61746Ellu, // jmc_Latn_TZ
+    0xAD894E5044657661llu, // jml_Deva_NP
+    0xCE89444B4C61746Ellu, // jut_Latn_DK
+    0x6A7649444C61746Ellu, // jv_Latn_ID
+    0x6A7749444C61746Ellu, // jw_Latn_ID
+    0x6B61474547656F72llu, // ka_Geor_GE
+    0x800A555A4379726Cllu, // kaa_Cyrl_UZ
+    0x840A445A4C61746Ellu, // kab_Latn_DZ
+    0x880A4D4D4C61746Ellu, // kac_Latn_MM
+    0xA40A4E474C61746Ellu, // kaj_Latn_NG
+    0xB00A4B454C61746Ellu, // kam_Latn_KE
+    0xB80A4D4C4C61746Ellu, // kao_Latn_ML
+    0x8C2A52554379726Cllu, // kbd_Cyrl_RU
+    0x984A4E474C61746Ellu, // kcg_Latn_NG
+    0xA84A5A574C61746Ellu, // kck_Latn_ZW
+    0x906A545A4C61746Ellu, // kde_Latn_TZ
+    0xCC6A544854686169llu, // kdt_Thai_TH
+    0x808A43564C61746Ellu, // kea_Latn_CV
+    0xB48A434D4C61746Ellu, // ken_Latn_CM
+    0xB8AA43494C61746Ellu, // kfo_Latn_CI
+    0xC4AA494E44657661llu, // kfr_Deva_IN
+    0xE0AA494E44657661llu, // kfy_Deva_IN
+    0x6B6743444C61746Ellu, // kg_Latn_CD
+    0x90CA49444C61746Ellu, // kge_Latn_ID
+    0xBCCA42524C61746Ellu, // kgp_Latn_BR
+    0x80EA494E4C61746Ellu, // kha_Latn_IN
+    0x84EA434E54616C75llu, // khb_Talu_CN
+    0xB4EA494E44657661llu, // khn_Deva_IN
+    0xC0EA4D4C4C61746Ellu, // khq_Latn_ML
+    0xCCEA494E4D796D72llu, // kht_Mymr_IN
+    0xD8EA504B41726162llu, // khw_Arab_PK
+    0x6B694B454C61746Ellu, // ki_Latn_KE
+    0xD10A54524C61746Ellu, // kiu_Latn_TR
+    0x6B6A4E414C61746Ellu, // kj_Latn_NA
+    0x992A4C414C616F6Fllu, // kjg_Laoo_LA
+    0x6B6B434E41726162llu, // kk_Arab_CN
+    0x6B6B4B5A4379726Cllu, // kk_Cyrl_KZ
+    0xA54A434D4C61746Ellu, // kkj_Latn_CM
+    0x6B6C474C4C61746Ellu, // kl_Latn_GL
+    0xB56A4B454C61746Ellu, // kln_Latn_KE
+    0x6B6D4B484B686D72llu, // km_Khmr_KH
+    0x858A414F4C61746Ellu, // kmb_Latn_AO
+    0x6B6E494E4B6E6461llu, // kn_Knda_IN
+    0x6B6F4B524B6F7265llu, // ko_Kore_KR
+    0xA1CA52554379726Cllu, // koi_Cyrl_RU
+    0xA9CA494E44657661llu, // kok_Deva_IN
+    0xC9CA464D4C61746Ellu, // kos_Latn_FM
+    0x91EA4C524C61746Ellu, // kpe_Latn_LR
+    0x8A2A52554379726Cllu, // krc_Cyrl_RU
+    0xA22A534C4C61746Ellu, // kri_Latn_SL
+    0xA62A50484C61746Ellu, // krj_Latn_PH
+    0xAE2A52554C61746Ellu, // krl_Latn_RU
+    0xD22A494E44657661llu, // kru_Deva_IN
+    0x6B73494E41726162llu, // ks_Arab_IN
+    0x864A545A4C61746Ellu, // ksb_Latn_TZ
+    0x964A434D4C61746Ellu, // ksf_Latn_CM
+    0x9E4A44454C61746Ellu, // ksh_Latn_DE
+    0x6B75495141726162llu, // ku_Arab_IQ
+    0x6B7554524C61746Ellu, // ku_Latn_TR
+    0xB28A52554379726Cllu, // kum_Cyrl_RU
+    0x6B7652554379726Cllu, // kv_Cyrl_RU
+    0xC6AA49444C61746Ellu, // kvr_Latn_ID
+    0xDEAA504B41726162llu, // kvx_Arab_PK
+    0x6B7747424C61746Ellu, // kw_Latn_GB
+    0xB2EA544854686169llu, // kxm_Thai_TH
+    0xBEEA504B41726162llu, // kxp_Arab_PK
+    0x6B79434E41726162llu, // ky_Arab_CN
+    0x6B794B474379726Cllu, // ky_Cyrl_KG
+    0x6B7954524C61746Ellu, // ky_Latn_TR
+    0x6C6156414C61746Ellu, // la_Latn_VA
+    0x840B47524C696E61llu, // lab_Lina_GR
+    0x8C0B494C48656272llu, // lad_Hebr_IL
+    0x980B545A4C61746Ellu, // lag_Latn_TZ
+    0x9C0B504B41726162llu, // lah_Arab_PK
+    0xA40B55474C61746Ellu, // laj_Latn_UG
+    0x6C624C554C61746Ellu, // lb_Latn_LU
+    0x902B52554379726Cllu, // lbe_Cyrl_RU
+    0xD82B49444C61746Ellu, // lbw_Latn_ID
+    0xBC4B434E54686169llu, // lcp_Thai_CN
+    0xBC8B494E4C657063llu, // lep_Lepc_IN
+    0xE48B52554379726Cllu, // lez_Cyrl_RU
+    0x6C6755474C61746Ellu, // lg_Latn_UG
+    0x6C694E4C4C61746Ellu, // li_Latn_NL
+    0x950B4E5044657661llu, // lif_Deva_NP
+    0x950B494E4C696D62llu, // lif_Limb_IN
+    0xA50B49544C61746Ellu, // lij_Latn_IT
+    0xC90B434E4C697375llu, // lis_Lisu_CN
+    0xBD2B49444C61746Ellu, // ljp_Latn_ID
+    0xA14B495241726162llu, // lki_Arab_IR
+    0xCD4B55534C61746Ellu, // lkt_Latn_US
+    0xB58B494E54656C75llu, // lmn_Telu_IN
+    0xB98B49544C61746Ellu, // lmo_Latn_IT
+    0x6C6E43444C61746Ellu, // ln_Latn_CD
+    0x6C6F4C414C616F6Fllu, // lo_Laoo_LA
+    0xADCB43444C61746Ellu, // lol_Latn_CD
+    0xE5CB5A4D4C61746Ellu, // loz_Latn_ZM
+    0x8A2B495241726162llu, // lrc_Arab_IR
+    0x6C744C544C61746Ellu, // lt_Latn_LT
+    0x9A6B4C564C61746Ellu, // ltg_Latn_LV
+    0x6C7543444C61746Ellu, // lu_Latn_CD
+    0x828B43444C61746Ellu, // lua_Latn_CD
+    0xBA8B4B454C61746Ellu, // luo_Latn_KE
+    0xE28B4B454C61746Ellu, // luy_Latn_KE
+    0xE68B495241726162llu, // luz_Arab_IR
+    0x6C764C564C61746Ellu, // lv_Latn_LV
+    0xAECB544854686169llu, // lwl_Thai_TH
+    0x9F2B434E48616E73llu, // lzh_Hans_CN
+    0xE72B54524C61746Ellu, // lzz_Latn_TR
+    0x8C0C49444C61746Ellu, // mad_Latn_ID
+    0x940C434D4C61746Ellu, // maf_Latn_CM
+    0x980C494E44657661llu, // mag_Deva_IN
+    0xA00C494E44657661llu, // mai_Deva_IN
+    0xA80C49444C61746Ellu, // mak_Latn_ID
+    0xB40C474D4C61746Ellu, // man_Latn_GM
+    0xB40C474E4E6B6F6Fllu, // man_Nkoo_GN
+    0xC80C4B454C61746Ellu, // mas_Latn_KE
+    0xE40C4D584C61746Ellu, // maz_Latn_MX
+    0x946C52554379726Cllu, // mdf_Cyrl_RU
+    0x9C6C50484C61746Ellu, // mdh_Latn_PH
+    0xC46C49444C61746Ellu, // mdr_Latn_ID
+    0xB48C534C4C61746Ellu, // men_Latn_SL
+    0xC48C4B454C61746Ellu, // mer_Latn_KE
+    0x80AC544841726162llu, // mfa_Arab_TH
+    0x90AC4D554C61746Ellu, // mfe_Latn_MU
+    0x6D674D474C61746Ellu, // mg_Latn_MG
+    0x9CCC4D5A4C61746Ellu, // mgh_Latn_MZ
+    0xB8CC434D4C61746Ellu, // mgo_Latn_CM
+    0xBCCC4E5044657661llu, // mgp_Deva_NP
+    0xE0CC545A4C61746Ellu, // mgy_Latn_TZ
+    0x6D684D484C61746Ellu, // mh_Latn_MH
+    0x6D694E5A4C61746Ellu, // mi_Latn_NZ
+    0xB50C49444C61746Ellu, // min_Latn_ID
+    0xC90C495148617472llu, // mis_Hatr_IQ
+    0x6D6B4D4B4379726Cllu, // mk_Cyrl_MK
+    0x6D6C494E4D6C796Dllu, // ml_Mlym_IN
+    0xC96C53444C61746Ellu, // mls_Latn_SD
+    0x6D6E4D4E4379726Cllu, // mn_Cyrl_MN
+    0x6D6E434E4D6F6E67llu, // mn_Mong_CN
+    0xA1AC494E42656E67llu, // mni_Beng_IN
+    0xD9AC4D4D4D796D72llu, // mnw_Mymr_MM
+    0x91CC43414C61746Ellu, // moe_Latn_CA
+    0x9DCC43414C61746Ellu, // moh_Latn_CA
+    0xC9CC42464C61746Ellu, // mos_Latn_BF
+    0x6D72494E44657661llu, // mr_Deva_IN
+    0x8E2C4E5044657661llu, // mrd_Deva_NP
+    0xA62C52554379726Cllu, // mrj_Cyrl_RU
+    0xD22C42444D726F6Fllu, // mru_Mroo_BD
+    0x6D734D594C61746Ellu, // ms_Latn_MY
+    0x6D744D544C61746Ellu, // mt_Latn_MT
+    0xC66C494E44657661llu, // mtr_Deva_IN
+    0x828C434D4C61746Ellu, // mua_Latn_CM
+    0xCA8C55534C61746Ellu, // mus_Latn_US
+    0xE2AC504B41726162llu, // mvy_Arab_PK
+    0xAACC4D4C4C61746Ellu, // mwk_Latn_ML
+    0xC6CC494E44657661llu, // mwr_Deva_IN
+    0xD6CC49444C61746Ellu, // mwv_Latn_ID
+    0x8AEC5A574C61746Ellu, // mxc_Latn_ZW
+    0x6D794D4D4D796D72llu, // my_Mymr_MM
+    0xD70C52554379726Cllu, // myv_Cyrl_RU
+    0xDF0C55474C61746Ellu, // myx_Latn_UG
+    0xE70C49524D616E64llu, // myz_Mand_IR
+    0xB72C495241726162llu, // mzn_Arab_IR
+    0x6E614E524C61746Ellu, // na_Latn_NR
+    0xB40D434E48616E73llu, // nan_Hans_CN
+    0xBC0D49544C61746Ellu, // nap_Latn_IT
+    0xC00D4E414C61746Ellu, // naq_Latn_NA
+    0x6E624E4F4C61746Ellu, // nb_Latn_NO
+    0x9C4D4D584C61746Ellu, // nch_Latn_MX
+    0x6E645A574C61746Ellu, // nd_Latn_ZW
+    0x886D4D5A4C61746Ellu, // ndc_Latn_MZ
+    0xC86D44454C61746Ellu, // nds_Latn_DE
+    0x6E654E5044657661llu, // ne_Deva_NP
+    0xD88D4E5044657661llu, // new_Deva_NP
+    0x6E674E414C61746Ellu, // ng_Latn_NA
+    0xACCD4D5A4C61746Ellu, // ngl_Latn_MZ
+    0x90ED4D584C61746Ellu, // nhe_Latn_MX
+    0xD8ED4D584C61746Ellu, // nhw_Latn_MX
+    0xA50D49444C61746Ellu, // nij_Latn_ID
+    0xD10D4E554C61746Ellu, // niu_Latn_NU
+    0xB92D494E4C61746Ellu, // njo_Latn_IN
+    0x6E6C4E4C4C61746Ellu, // nl_Latn_NL
+    0x998D434D4C61746Ellu, // nmg_Latn_CM
+    0x6E6E4E4F4C61746Ellu, // nn_Latn_NO
+    0x9DAD434D4C61746Ellu, // nnh_Latn_CM
+    0x6E6F4E4F4C61746Ellu, // no_Latn_NO
+    0x8DCD54484C616E61llu, // nod_Lana_TH
+    0x91CD494E44657661llu, // noe_Deva_IN
+    0xB5CD534552756E72llu, // non_Runr_SE
+    0xBA0D474E4E6B6F6Fllu, // nqo_Nkoo_GN
+    0x6E725A414C61746Ellu, // nr_Latn_ZA
+    0xAA4D434143616E73llu, // nsk_Cans_CA
+    0xBA4D5A414C61746Ellu, // nso_Latn_ZA
+    0xCA8D53534C61746Ellu, // nus_Latn_SS
+    0x6E7655534C61746Ellu, // nv_Latn_US
+    0xC2ED434E4C61746Ellu, // nxq_Latn_CN
+    0x6E794D574C61746Ellu, // ny_Latn_MW
+    0xB30D545A4C61746Ellu, // nym_Latn_TZ
+    0xB70D55474C61746Ellu, // nyn_Latn_UG
+    0xA32D47484C61746Ellu, // nzi_Latn_GH
+    0x6F6346524C61746Ellu, // oc_Latn_FR
+    0x6F6D45544C61746Ellu, // om_Latn_ET
+    0x6F72494E4F727961llu, // or_Orya_IN
+    0x6F7347454379726Cllu, // os_Cyrl_GE
+    0xAA6E4D4E4F726B68llu, // otk_Orkh_MN
+    0x7061504B41726162llu, // pa_Arab_PK
+    0x7061494E47757275llu, // pa_Guru_IN
+    0x980F50484C61746Ellu, // pag_Latn_PH
+    0xAC0F495250686C69llu, // pal_Phli_IR
+    0xAC0F434E50686C70llu, // pal_Phlp_CN
+    0xB00F50484C61746Ellu, // pam_Latn_PH
+    0xBC0F41574C61746Ellu, // pap_Latn_AW
+    0xD00F50574C61746Ellu, // pau_Latn_PW
+    0x8C4F46524C61746Ellu, // pcd_Latn_FR
+    0xB04F4E474C61746Ellu, // pcm_Latn_NG
+    0x886F55534C61746Ellu, // pdc_Latn_US
+    0xCC6F43414C61746Ellu, // pdt_Latn_CA
+    0xB88F49525870656Fllu, // peo_Xpeo_IR
+    0xACAF44454C61746Ellu, // pfl_Latn_DE
+    0xB4EF4C4250686E78llu, // phn_Phnx_LB
+    0x814F494E42726168llu, // pka_Brah_IN
+    0xB94F4B454C61746Ellu, // pko_Latn_KE
+    0x706C504C4C61746Ellu, // pl_Latn_PL
+    0xC98F49544C61746Ellu, // pms_Latn_IT
+    0xCDAF47524772656Bllu, // pnt_Grek_GR
+    0xB5CF464D4C61746Ellu, // pon_Latn_FM
+    0x822F504B4B686172llu, // pra_Khar_PK
+    0x8E2F495241726162llu, // prd_Arab_IR
+    0x7073414641726162llu, // ps_Arab_AF
+    0x707442524C61746Ellu, // pt_Latn_BR
+    0xD28F47414C61746Ellu, // puu_Latn_GA
+    0x717550454C61746Ellu, // qu_Latn_PE
+    0x8A9047544C61746Ellu, // quc_Latn_GT
+    0x9A9045434C61746Ellu, // qug_Latn_EC
+    0xA411494E44657661llu, // raj_Deva_IN
+    0x945152454C61746Ellu, // rcf_Latn_RE
+    0xA49149444C61746Ellu, // rej_Latn_ID
+    0xB4D149544C61746Ellu, // rgn_Latn_IT
+    0x8111494E4C61746Ellu, // ria_Latn_IN
+    0x95114D4154666E67llu, // rif_Tfng_MA
+    0xC9314E5044657661llu, // rjs_Deva_NP
+    0xCD51424442656E67llu, // rkt_Beng_BD
+    0x726D43484C61746Ellu, // rm_Latn_CH
+    0x959146494C61746Ellu, // rmf_Latn_FI
+    0xB99143484C61746Ellu, // rmo_Latn_CH
+    0xCD91495241726162llu, // rmt_Arab_IR
+    0xD19153454C61746Ellu, // rmu_Latn_SE
+    0x726E42494C61746Ellu, // rn_Latn_BI
+    0x99B14D5A4C61746Ellu, // rng_Latn_MZ
+    0x726F524F4C61746Ellu, // ro_Latn_RO
+    0x85D149444C61746Ellu, // rob_Latn_ID
+    0x95D1545A4C61746Ellu, // rof_Latn_TZ
+    0xB271464A4C61746Ellu, // rtm_Latn_FJ
+    0x727552554379726Cllu, // ru_Cyrl_RU
+    0x929155414379726Cllu, // rue_Cyrl_UA
+    0x9A9153424C61746Ellu, // rug_Latn_SB
+    0x727752574C61746Ellu, // rw_Latn_RW
+    0xAAD1545A4C61746Ellu, // rwk_Latn_TZ
+    0xD3114A504B616E61llu, // ryu_Kana_JP
+    0x7361494E44657661llu, // sa_Deva_IN
+    0x941247484C61746Ellu, // saf_Latn_GH
+    0x9C1252554379726Cllu, // sah_Cyrl_RU
+    0xC0124B454C61746Ellu, // saq_Latn_KE
+    0xC81249444C61746Ellu, // sas_Latn_ID
+    0xCC12494E4C61746Ellu, // sat_Latn_IN
+    0xE412494E53617572llu, // saz_Saur_IN
+    0xBC32545A4C61746Ellu, // sbp_Latn_TZ
+    0x736349544C61746Ellu, // sc_Latn_IT
+    0xA852494E44657661llu, // sck_Deva_IN
+    0xB45249544C61746Ellu, // scn_Latn_IT
+    0xB85247424C61746Ellu, // sco_Latn_GB
+    0xC85243414C61746Ellu, // scs_Latn_CA
+    0x7364504B41726162llu, // sd_Arab_PK
+    0x7364494E44657661llu, // sd_Deva_IN
+    0x7364494E4B686F6Allu, // sd_Khoj_IN
+    0x7364494E53696E64llu, // sd_Sind_IN
+    0x887249544C61746Ellu, // sdc_Latn_IT
+    0x9C72495241726162llu, // sdh_Arab_IR
+    0x73654E4F4C61746Ellu, // se_Latn_NO
+    0x949243494C61746Ellu, // sef_Latn_CI
+    0x9C924D5A4C61746Ellu, // seh_Latn_MZ
+    0xA0924D584C61746Ellu, // sei_Latn_MX
+    0xC8924D4C4C61746Ellu, // ses_Latn_ML
+    0x736743464C61746Ellu, // sg_Latn_CF
+    0x80D249454F67616Dllu, // sga_Ogam_IE
+    0xC8D24C544C61746Ellu, // sgs_Latn_LT
+    0xA0F24D4154666E67llu, // shi_Tfng_MA
+    0xB4F24D4D4D796D72llu, // shn_Mymr_MM
+    0x73694C4B53696E68llu, // si_Sinh_LK
+    0x8D1245544C61746Ellu, // sid_Latn_ET
+    0x736B534B4C61746Ellu, // sk_Latn_SK
+    0xC552504B41726162llu, // skr_Arab_PK
+    0x736C53494C61746Ellu, // sl_Latn_SI
+    0xA172504C4C61746Ellu, // sli_Latn_PL
+    0xE17249444C61746Ellu, // sly_Latn_ID
+    0x736D57534C61746Ellu, // sm_Latn_WS
+    0x819253454C61746Ellu, // sma_Latn_SE
+    0xA59253454C61746Ellu, // smj_Latn_SE
+    0xB59246494C61746Ellu, // smn_Latn_FI
+    0xBD92494C53616D72llu, // smp_Samr_IL
+    0xC99246494C61746Ellu, // sms_Latn_FI
+    0x736E5A574C61746Ellu, // sn_Latn_ZW
+    0xA9B24D4C4C61746Ellu, // snk_Latn_ML
+    0x736F534F4C61746Ellu, // so_Latn_SO
+    0xD1D2544854686169llu, // sou_Thai_TH
+    0x7371414C4C61746Ellu, // sq_Latn_AL
+    0x737252534379726Cllu, // sr_Cyrl_RS
+    0x737252534C61746Ellu, // sr_Latn_RS
+    0x8632494E536F7261llu, // srb_Sora_IN
+    0xB63253524C61746Ellu, // srn_Latn_SR
+    0xC632534E4C61746Ellu, // srr_Latn_SN
+    0xDE32494E44657661llu, // srx_Deva_IN
+    0x73735A414C61746Ellu, // ss_Latn_ZA
+    0xE25245524C61746Ellu, // ssy_Latn_ER
+    0x73745A414C61746Ellu, // st_Latn_ZA
+    0xC27244454C61746Ellu, // stq_Latn_DE
+    0x737549444C61746Ellu, // su_Latn_ID
+    0xAA92545A4C61746Ellu, // suk_Latn_TZ
+    0xCA92474E4C61746Ellu, // sus_Latn_GN
+    0x737653454C61746Ellu, // sv_Latn_SE
+    0x7377545A4C61746Ellu, // sw_Latn_TZ
+    0x86D2595441726162llu, // swb_Arab_YT
+    0x8AD243444C61746Ellu, // swc_Latn_CD
+    0x9AD244454C61746Ellu, // swg_Latn_DE
+    0xD6D2494E44657661llu, // swv_Deva_IN
+    0xB6F249444C61746Ellu, // sxn_Latn_ID
+    0xAF12424442656E67llu, // syl_Beng_BD
+    0xC712495153797263llu, // syr_Syrc_IQ
+    0xAF32504C4C61746Ellu, // szl_Latn_PL
+    0x7461494E54616D6Cllu, // ta_Taml_IN
+    0xA4134E5044657661llu, // taj_Deva_NP
+    0xD83350484C61746Ellu, // tbw_Latn_PH
+    0xE053494E4B6E6461llu, // tcy_Knda_IN
+    0x8C73434E54616C65llu, // tdd_Tale_CN
+    0x98734E5044657661llu, // tdg_Deva_NP
+    0x9C734E5044657661llu, // tdh_Deva_NP
+    0x7465494E54656C75llu, // te_Telu_IN
+    0xB093534C4C61746Ellu, // tem_Latn_SL
+    0xB89355474C61746Ellu, // teo_Latn_UG
+    0xCC93544C4C61746Ellu, // tet_Latn_TL
+    0x7467504B41726162llu, // tg_Arab_PK
+    0x7467544A4379726Cllu, // tg_Cyrl_TJ
+    0x7468544854686169llu, // th_Thai_TH
+    0xACF34E5044657661llu, // thl_Deva_NP
+    0xC0F34E5044657661llu, // thq_Deva_NP
+    0xC4F34E5044657661llu, // thr_Deva_NP
+    0x7469455445746869llu, // ti_Ethi_ET
+    0x9913455245746869llu, // tig_Ethi_ER
+    0xD5134E474C61746Ellu, // tiv_Latn_NG
+    0x746B544D4C61746Ellu, // tk_Latn_TM
+    0xAD53544B4C61746Ellu, // tkl_Latn_TK
+    0xC553415A4C61746Ellu, // tkr_Latn_AZ
+    0xCD534E5044657661llu, // tkt_Deva_NP
+    0x746C50484C61746Ellu, // tl_Latn_PH
+    0xE173415A4C61746Ellu, // tly_Latn_AZ
+    0x9D934E454C61746Ellu, // tmh_Latn_NE
+    0x746E5A414C61746Ellu, // tn_Latn_ZA
+    0x746F544F4C61746Ellu, // to_Latn_TO
+    0x99D34D574C61746Ellu, // tog_Latn_MW
+    0xA1F350474C61746Ellu, // tpi_Latn_PG
+    0x747254524C61746Ellu, // tr_Latn_TR
+    0xD23354524C61746Ellu, // tru_Latn_TR
+    0xD63354574C61746Ellu, // trv_Latn_TW
+    0x74735A414C61746Ellu, // ts_Latn_ZA
+    0x8E5347524772656Bllu, // tsd_Grek_GR
+    0x96534E5044657661llu, // tsf_Deva_NP
+    0x9A5350484C61746Ellu, // tsg_Latn_PH
+    0xA653425454696274llu, // tsj_Tibt_BT
+    0x747452554379726Cllu, // tt_Cyrl_RU
+    0xA67355474C61746Ellu, // ttj_Latn_UG
+    0xCA73544854686169llu, // tts_Thai_TH
+    0xCE73415A4C61746Ellu, // ttt_Latn_AZ
+    0xB2934D574C61746Ellu, // tum_Latn_MW
+    0xAEB354564C61746Ellu, // tvl_Latn_TV
+    0xC2D34E454C61746Ellu, // twq_Latn_NE
+    0x747950464C61746Ellu, // ty_Latn_PF
+    0xD71352554379726Cllu, // tyv_Cyrl_RU
+    0xB3334D414C61746Ellu, // tzm_Latn_MA
+    0xB07452554379726Cllu, // udm_Cyrl_RU
+    0x7567434E41726162llu, // ug_Arab_CN
+    0x75674B5A4379726Cllu, // ug_Cyrl_KZ
+    0x80D4535955676172llu, // uga_Ugar_SY
+    0x756B55414379726Cllu, // uk_Cyrl_UA
+    0xA174464D4C61746Ellu, // uli_Latn_FM
+    0x8594414F4C61746Ellu, // umb_Latn_AO
+    0xC5B4494E42656E67llu, // unr_Beng_IN
+    0xC5B44E5044657661llu, // unr_Deva_NP
+    0xDDB4494E42656E67llu, // unx_Beng_IN
+    0x7572504B41726162llu, // ur_Arab_PK
+    0x757A414641726162llu, // uz_Arab_AF
+    0x757A555A4C61746Ellu, // uz_Latn_UZ
+    0xA0154C5256616969llu, // vai_Vaii_LR
+    0x76655A414C61746Ellu, // ve_Latn_ZA
+    0x889549544C61746Ellu, // vec_Latn_IT
+    0xBC9552554C61746Ellu, // vep_Latn_RU
+    0x7669564E4C61746Ellu, // vi_Latn_VN
+    0x891553584C61746Ellu, // vic_Latn_SX
+    0xC97542454C61746Ellu, // vls_Latn_BE
+    0x959544454C61746Ellu, // vmf_Latn_DE
+    0xD9954D5A4C61746Ellu, // vmw_Latn_MZ
+    0xCDD552554C61746Ellu, // vot_Latn_RU
+    0xBA3545454C61746Ellu, // vro_Latn_EE
+    0xB695545A4C61746Ellu, // vun_Latn_TZ
+    0x776142454C61746Ellu, // wa_Latn_BE
+    0x901643484C61746Ellu, // wae_Latn_CH
+    0xAC16455445746869llu, // wal_Ethi_ET
+    0xC41650484C61746Ellu, // war_Latn_PH
+    0xBC3641554C61746Ellu, // wbp_Latn_AU
+    0xC036494E54656C75llu, // wbq_Telu_IN
+    0xC436494E44657661llu, // wbr_Deva_IN
+    0xC97657464C61746Ellu, // wls_Latn_WF
+    0xA1B64B4D41726162llu, // wni_Arab_KM
+    0x776F534E4C61746Ellu, // wo_Latn_SN
+    0xB276494E44657661llu, // wtm_Deva_IN
+    0xD296434E48616E73llu, // wuu_Hans_CN
+    0xD41742524C61746Ellu, // xav_Latn_BR
+    0xC457545243617269llu, // xcr_Cari_TR
+    0x78685A414C61746Ellu, // xh_Latn_ZA
+    0x897754524C796369llu, // xlc_Lyci_TR
+    0x8D7754524C796469llu, // xld_Lydi_TR
+    0x9597474547656F72llu, // xmf_Geor_GE
+    0xB597434E4D616E69llu, // xmn_Mani_CN
+    0xC59753444D657263llu, // xmr_Merc_SD
+    0x81B753414E617262llu, // xna_Narb_SA
+    0xC5B7494E44657661llu, // xnr_Deva_IN
+    0x99D755474C61746Ellu, // xog_Latn_UG
+    0xC5F7495250727469llu, // xpr_Prti_IR
+    0x8257594553617262llu, // xsa_Sarb_YE
+    0xC6574E5044657661llu, // xsr_Deva_NP
+    0xB8184D5A4C61746Ellu, // yao_Latn_MZ
+    0xBC18464D4C61746Ellu, // yap_Latn_FM
+    0xD418434D4C61746Ellu, // yav_Latn_CM
+    0x8438434D4C61746Ellu, // ybb_Latn_CM
+    0x796F4E474C61746Ellu, // yo_Latn_NG
+    0xAE3842524C61746Ellu, // yrl_Latn_BR
+    0x82984D584C61746Ellu, // yua_Latn_MX
+    0x7A61434E4C61746Ellu, // za_Latn_CN
+    0x981953444C61746Ellu, // zag_Latn_SD
+    0xA4794B4D41726162llu, // zdj_Arab_KM
+    0x80994E4C4C61746Ellu, // zea_Latn_NL
+    0x9CD94D4154666E67llu, // zgh_Tfng_MA
+    0x7A685457426F706Fllu, // zh_Bopo_TW
+    0x7A68434E48616E73llu, // zh_Hans_CN
+    0x7A68545748616E74llu, // zh_Hant_TW
+    0xA1994D594C61746Ellu, // zmi_Latn_MY
+    0x7A755A414C61746Ellu, // zu_Latn_ZA
+    0x833954524C61746Ellu, // zza_Latn_TR
+});
+
+const std::unordered_map<uint32_t, uint32_t> ARAB_PARENTS({
+    {0x6172445Au, 0x61729420u}, // ar-DZ -> ar-015
+    {0x61724548u, 0x61729420u}, // ar-EH -> ar-015
+    {0x61724C59u, 0x61729420u}, // ar-LY -> ar-015
+    {0x61724D41u, 0x61729420u}, // ar-MA -> ar-015
+    {0x6172544Eu, 0x61729420u}, // ar-TN -> ar-015
+});
+
+const std::unordered_map<uint32_t, uint32_t> HANT_PARENTS({
+    {0x7A684D4Fu, 0x7A68484Bu}, // zh-Hant-MO -> zh-Hant-HK
+});
+
+const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({
+    {0x656E80A1u, 0x656E8400u}, // en-150 -> en-001
+    {0x656E4147u, 0x656E8400u}, // en-AG -> en-001
+    {0x656E4149u, 0x656E8400u}, // en-AI -> en-001
+    {0x656E4154u, 0x656E80A1u}, // en-AT -> en-150
+    {0x656E4155u, 0x656E8400u}, // en-AU -> en-001
+    {0x656E4242u, 0x656E8400u}, // en-BB -> en-001
+    {0x656E4245u, 0x656E8400u}, // en-BE -> en-001
+    {0x656E424Du, 0x656E8400u}, // en-BM -> en-001
+    {0x656E4253u, 0x656E8400u}, // en-BS -> en-001
+    {0x656E4257u, 0x656E8400u}, // en-BW -> en-001
+    {0x656E425Au, 0x656E8400u}, // en-BZ -> en-001
+    {0x656E4341u, 0x656E8400u}, // en-CA -> en-001
+    {0x656E4343u, 0x656E8400u}, // en-CC -> en-001
+    {0x656E4348u, 0x656E80A1u}, // en-CH -> en-150
+    {0x656E434Bu, 0x656E8400u}, // en-CK -> en-001
+    {0x656E434Du, 0x656E8400u}, // en-CM -> en-001
+    {0x656E4358u, 0x656E8400u}, // en-CX -> en-001
+    {0x656E4359u, 0x656E8400u}, // en-CY -> en-001
+    {0x656E4445u, 0x656E80A1u}, // en-DE -> en-150
+    {0x656E4447u, 0x656E8400u}, // en-DG -> en-001
+    {0x656E444Bu, 0x656E80A1u}, // en-DK -> en-150
+    {0x656E444Du, 0x656E8400u}, // en-DM -> en-001
+    {0x656E4552u, 0x656E8400u}, // en-ER -> en-001
+    {0x656E4649u, 0x656E80A1u}, // en-FI -> en-150
+    {0x656E464Au, 0x656E8400u}, // en-FJ -> en-001
+    {0x656E464Bu, 0x656E8400u}, // en-FK -> en-001
+    {0x656E464Du, 0x656E8400u}, // en-FM -> en-001
+    {0x656E4742u, 0x656E8400u}, // en-GB -> en-001
+    {0x656E4744u, 0x656E8400u}, // en-GD -> en-001
+    {0x656E4747u, 0x656E8400u}, // en-GG -> en-001
+    {0x656E4748u, 0x656E8400u}, // en-GH -> en-001
+    {0x656E4749u, 0x656E8400u}, // en-GI -> en-001
+    {0x656E474Du, 0x656E8400u}, // en-GM -> en-001
+    {0x656E4759u, 0x656E8400u}, // en-GY -> en-001
+    {0x656E484Bu, 0x656E8400u}, // en-HK -> en-001
+    {0x656E4945u, 0x656E8400u}, // en-IE -> en-001
+    {0x656E494Cu, 0x656E8400u}, // en-IL -> en-001
+    {0x656E494Du, 0x656E8400u}, // en-IM -> en-001
+    {0x656E494Eu, 0x656E8400u}, // en-IN -> en-001
+    {0x656E494Fu, 0x656E8400u}, // en-IO -> en-001
+    {0x656E4A45u, 0x656E8400u}, // en-JE -> en-001
+    {0x656E4A4Du, 0x656E8400u}, // en-JM -> en-001
+    {0x656E4B45u, 0x656E8400u}, // en-KE -> en-001
+    {0x656E4B49u, 0x656E8400u}, // en-KI -> en-001
+    {0x656E4B4Eu, 0x656E8400u}, // en-KN -> en-001
+    {0x656E4B59u, 0x656E8400u}, // en-KY -> en-001
+    {0x656E4C43u, 0x656E8400u}, // en-LC -> en-001
+    {0x656E4C52u, 0x656E8400u}, // en-LR -> en-001
+    {0x656E4C53u, 0x656E8400u}, // en-LS -> en-001
+    {0x656E4D47u, 0x656E8400u}, // en-MG -> en-001
+    {0x656E4D4Fu, 0x656E8400u}, // en-MO -> en-001
+    {0x656E4D53u, 0x656E8400u}, // en-MS -> en-001
+    {0x656E4D54u, 0x656E8400u}, // en-MT -> en-001
+    {0x656E4D55u, 0x656E8400u}, // en-MU -> en-001
+    {0x656E4D57u, 0x656E8400u}, // en-MW -> en-001
+    {0x656E4D59u, 0x656E8400u}, // en-MY -> en-001
+    {0x656E4E41u, 0x656E8400u}, // en-NA -> en-001
+    {0x656E4E46u, 0x656E8400u}, // en-NF -> en-001
+    {0x656E4E47u, 0x656E8400u}, // en-NG -> en-001
+    {0x656E4E4Cu, 0x656E80A1u}, // en-NL -> en-150
+    {0x656E4E52u, 0x656E8400u}, // en-NR -> en-001
+    {0x656E4E55u, 0x656E8400u}, // en-NU -> en-001
+    {0x656E4E5Au, 0x656E8400u}, // en-NZ -> en-001
+    {0x656E5047u, 0x656E8400u}, // en-PG -> en-001
+    {0x656E5048u, 0x656E8400u}, // en-PH -> en-001
+    {0x656E504Bu, 0x656E8400u}, // en-PK -> en-001
+    {0x656E504Eu, 0x656E8400u}, // en-PN -> en-001
+    {0x656E5057u, 0x656E8400u}, // en-PW -> en-001
+    {0x656E5257u, 0x656E8400u}, // en-RW -> en-001
+    {0x656E5342u, 0x656E8400u}, // en-SB -> en-001
+    {0x656E5343u, 0x656E8400u}, // en-SC -> en-001
+    {0x656E5344u, 0x656E8400u}, // en-SD -> en-001
+    {0x656E5345u, 0x656E80A1u}, // en-SE -> en-150
+    {0x656E5347u, 0x656E8400u}, // en-SG -> en-001
+    {0x656E5348u, 0x656E8400u}, // en-SH -> en-001
+    {0x656E5349u, 0x656E80A1u}, // en-SI -> en-150
+    {0x656E534Cu, 0x656E8400u}, // en-SL -> en-001
+    {0x656E5353u, 0x656E8400u}, // en-SS -> en-001
+    {0x656E5358u, 0x656E8400u}, // en-SX -> en-001
+    {0x656E535Au, 0x656E8400u}, // en-SZ -> en-001
+    {0x656E5443u, 0x656E8400u}, // en-TC -> en-001
+    {0x656E544Bu, 0x656E8400u}, // en-TK -> en-001
+    {0x656E544Fu, 0x656E8400u}, // en-TO -> en-001
+    {0x656E5454u, 0x656E8400u}, // en-TT -> en-001
+    {0x656E5456u, 0x656E8400u}, // en-TV -> en-001
+    {0x656E545Au, 0x656E8400u}, // en-TZ -> en-001
+    {0x656E5547u, 0x656E8400u}, // en-UG -> en-001
+    {0x656E5643u, 0x656E8400u}, // en-VC -> en-001
+    {0x656E5647u, 0x656E8400u}, // en-VG -> en-001
+    {0x656E5655u, 0x656E8400u}, // en-VU -> en-001
+    {0x656E5753u, 0x656E8400u}, // en-WS -> en-001
+    {0x656E5A41u, 0x656E8400u}, // en-ZA -> en-001
+    {0x656E5A4Du, 0x656E8400u}, // en-ZM -> en-001
+    {0x656E5A57u, 0x656E8400u}, // en-ZW -> en-001
+    {0x65734152u, 0x6573A424u}, // es-AR -> es-419
+    {0x6573424Fu, 0x6573A424u}, // es-BO -> es-419
+    {0x6573434Cu, 0x6573A424u}, // es-CL -> es-419
+    {0x6573434Fu, 0x6573A424u}, // es-CO -> es-419
+    {0x65734352u, 0x6573A424u}, // es-CR -> es-419
+    {0x65734355u, 0x6573A424u}, // es-CU -> es-419
+    {0x6573444Fu, 0x6573A424u}, // es-DO -> es-419
+    {0x65734543u, 0x6573A424u}, // es-EC -> es-419
+    {0x65734754u, 0x6573A424u}, // es-GT -> es-419
+    {0x6573484Eu, 0x6573A424u}, // es-HN -> es-419
+    {0x65734D58u, 0x6573A424u}, // es-MX -> es-419
+    {0x65734E49u, 0x6573A424u}, // es-NI -> es-419
+    {0x65735041u, 0x6573A424u}, // es-PA -> es-419
+    {0x65735045u, 0x6573A424u}, // es-PE -> es-419
+    {0x65735052u, 0x6573A424u}, // es-PR -> es-419
+    {0x65735059u, 0x6573A424u}, // es-PY -> es-419
+    {0x65735356u, 0x6573A424u}, // es-SV -> es-419
+    {0x65735553u, 0x6573A424u}, // es-US -> es-419
+    {0x65735559u, 0x6573A424u}, // es-UY -> es-419
+    {0x65735645u, 0x6573A424u}, // es-VE -> es-419
+    {0x7074414Fu, 0x70745054u}, // pt-AO -> pt-PT
+    {0x70744356u, 0x70745054u}, // pt-CV -> pt-PT
+    {0x70744757u, 0x70745054u}, // pt-GW -> pt-PT
+    {0x70744D4Fu, 0x70745054u}, // pt-MO -> pt-PT
+    {0x70744D5Au, 0x70745054u}, // pt-MZ -> pt-PT
+    {0x70745354u, 0x70745054u}, // pt-ST -> pt-PT
+    {0x7074544Cu, 0x70745054u}, // pt-TL -> pt-PT
+});
+
+const struct {
+    const char script[4];
+    const std::unordered_map<uint32_t, uint32_t>* map;
+} SCRIPT_PARENTS[] = {
+    {{'A', 'r', 'a', 'b'}, &ARAB_PARENTS},
+    {{'H', 'a', 'n', 't'}, &HANT_PARENTS},
+    {{'L', 'a', 't', 'n'}, &LATN_PARENTS},
+};
+
+const size_t MAX_PARENT_DEPTH = 3;
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 44f92c7..71e9c92 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -1868,7 +1868,10 @@
     }
 
     // The language & region are equal, so compare the scripts and variants.
-    int script = memcmp(l.localeScript, r.localeScript, sizeof(l.localeScript));
+    const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'};
+    const char *lScript = l.localeScriptWasProvided ? l.localeScript : emptyScript;
+    const char *rScript = r.localeScriptWasProvided ? r.localeScript : emptyScript;
+    int script = memcmp(lScript, rScript, sizeof(l.localeScript));
     if (script) {
         return script;
     }
@@ -2012,10 +2015,10 @@
     // scripts since it seems more useful to do so. We will consider
     // "en-US-POSIX" to be more specific than "en-Latn-US".
 
-    const int score = ((localeScript[0] != 0) ? 1 : 0) +
+    const int score = (localeScriptWasProvided ? 1 : 0) +
         ((localeVariant[0] != 0) ? 2 : 0);
 
-    const int oScore = ((o.localeScript[0] != 0) ? 1 : 0) +
+    const int oScore = (o.localeScriptWasProvided ? 1 : 0) +
         ((o.localeVariant[0] != 0) ? 2 : 0);
 
     return score - oScore;
@@ -2165,6 +2168,63 @@
     return false;
 }
 
+bool ResTable_config::isLocaleBetterThan(const ResTable_config& o,
+        const ResTable_config* requested) const {
+    if (requested->locale == 0) {
+        // The request doesn't have a locale, so no resource is better
+        // than the other.
+        return false;
+    }
+
+    if (locale == 0 && o.locale == 0) {
+        // The locales parts of both resources are empty, so no one is better
+        // than the other.
+        return false;
+    }
+
+    // Non-matching locales have been filtered out, so both resources
+    // match the requested locale.
+    //
+    // Because of the locale-related checks in match() and the checks, we know
+    // that:
+    // 1) The resource languages are either empty or match the request;
+    // and
+    // 2) If the request's script is known, the resource scripts are either
+    //    unknown or match the request.
+
+    if (language[0] != o.language[0]) {
+        // The languages of the two resources are not the same. We can only
+        // assume that one of the two resources matched the request because one
+        // doesn't have a language and the other has a matching language.
+        return (language[0] != 0);
+    }
+
+    // If we are here, both the resources have the same non-empty language as
+    // the request.
+    //
+    // Because the languages are the same, computeScript() always
+    // returns a non-empty script for languages it knows about, and we have passed
+    // the script checks in match(), the scripts are either all unknown or are
+    // all the same. So we can't gain anything by checking the scripts. We need
+    // to check the region and variant.
+
+    // See if any of the regions is better than the other
+    const int region_comparison = localeDataCompareRegions(
+            country, o.country,
+            language, localeScript, requested->country);
+    if (region_comparison != 0) {
+        return (region_comparison > 0);
+    }
+
+    // The regions are the same. Try the variant.
+    if (requested->localeVariant[0] != '\0'
+            && strncmp(localeVariant, requested->localeVariant, sizeof(localeVariant)) == 0) {
+        return (strncmp(o.localeVariant, requested->localeVariant, sizeof(localeVariant)) != 0);
+    }
+
+    return false;
+}
+
 bool ResTable_config::isBetterThan(const ResTable_config& o,
         const ResTable_config* requested) const {
     if (requested) {
@@ -2178,26 +2238,8 @@
             }
         }
 
-        if (locale || o.locale) {
-            if ((language[0] != o.language[0]) && requested->language[0]) {
-                return (language[0]);
-            }
-
-            if ((country[0] != o.country[0]) && requested->country[0]) {
-                return (country[0]);
-            }
-        }
-
-        if (localeScript[0] || o.localeScript[0]) {
-            if (localeScript[0] != o.localeScript[0] && requested->localeScript[0]) {
-                return localeScript[0];
-            }
-        }
-
-        if (localeVariant[0] || o.localeVariant[0]) {
-            if (localeVariant[0] != o.localeVariant[0] && requested->localeVariant[0]) {
-                return localeVariant[0];
-            }
+        if (isLocaleBetterThan(o, requested)) {
+            return true;
         }
 
         if (screenLayout || o.screenLayout) {
@@ -2445,20 +2487,33 @@
         }
     }
     if (locale != 0) {
-        // Don't consider the script & variants when deciding matches.
+        // Don't consider country and variants when deciding matches.
+        // (Theoretically, the variant can also affect the script. For
+        // example, "ar-alalc97" probably implies the Latin script, but since
+        // CLDR doesn't support getting likely scripts for that, we'll assume
+        // the variant doesn't change the script.)
         //
-        // If we two configs differ only in their script or language, they
-        // can be weeded out in the isMoreSpecificThan test.
-        if (language[0] != 0
-            && (language[0] != settings.language[0]
-                || language[1] != settings.language[1])) {
+        // If two configs differ only in their country and variant,
+        // they can be weeded out in the isMoreSpecificThan test.
+        if (language[0] != settings.language[0] || language[1] != settings.language[1]) {
             return false;
         }
 
-        if (country[0] != 0
-            && (country[0] != settings.country[0]
-                || country[1] != settings.country[1])) {
-            return false;
+        // For backward compatibility and supporting private-use locales, we
+        // fall back to old behavior if we couldn't determine the script for
+        // either of the desired locale or the provided locale.
+        if (localeScript[0] == '\0' || localeScript[1] == '\0') {
+            if (country[0] != '\0'
+                && (country[0] != settings.country[0]
+                    || country[1] != settings.country[1])) {
+                return false;
+            }
+        } else {
+            // But if we could determine the scripts, they should be the same
+            // for the locales to match.
+            if (memcmp(localeScript, settings.localeScript, sizeof(localeScript)) != 0) {
+                return false;
+            }
         }
     }
 
@@ -2587,7 +2642,7 @@
         return;
     }
 
-    if (!localeScript[0] && !localeVariant[0]) {
+    if (!localeScriptWasProvided && !localeVariant[0]) {
         // Legacy format.
         if (out.size() > 0) {
             out.append("-");
@@ -2605,7 +2660,7 @@
         return;
     }
 
-    // We are writing the modified bcp47 tag.
+    // We are writing the modified BCP 47 tag.
     // It starts with 'b+' and uses '+' as a separator.
 
     if (out.size() > 0) {
@@ -2617,7 +2672,7 @@
     size_t len = unpackLanguage(buf);
     out.append(buf, len);
 
-    if (localeScript[0]) {
+    if (localeScriptWasProvided) {
         out.append("+");
         out.append(localeScript, sizeof(localeScript));
     }
@@ -2630,7 +2685,7 @@
 
     if (localeVariant[0]) {
         out.append("+");
-        out.append(localeVariant, sizeof(localeVariant));
+        out.append(localeVariant, strnlen(localeVariant, sizeof(localeVariant)));
     }
 }
 
@@ -2648,7 +2703,7 @@
         charsWritten += unpackLanguage(str);
     }
 
-    if (localeScript[0]) {
+    if (localeScriptWasProvided) {
         if (charsWritten) {
             str[charsWritten++] = '-';
         }
@@ -2682,11 +2737,16 @@
            config->language[0] ? config->packRegion(start) : config->packLanguage(start);
            break;
        case 4:
-           config->localeScript[0] = toupper(start[0]);
-           for (size_t i = 1; i < 4; ++i) {
-               config->localeScript[i] = tolower(start[i]);
+           if ('0' <= start[0] && start[0] <= '9') {
+               // this is a variant, so fall through
+           } else {
+               config->localeScript[0] = toupper(start[0]);
+               for (size_t i = 1; i < 4; ++i) {
+                   config->localeScript[i] = tolower(start[i]);
+               }
+               config->localeScriptWasProvided = true;
+               break;
            }
-           break;
        case 5:
        case 6:
        case 7:
@@ -2704,6 +2764,7 @@
 
 void ResTable_config::setBcp47Locale(const char* in) {
     locale = 0;
+    localeScriptWasProvided = false;
     memset(localeScript, 0, sizeof(localeScript));
     memset(localeVariant, 0, sizeof(localeVariant));
 
@@ -2720,6 +2781,9 @@
 
     const size_t size = in + strlen(in) - start;
     assignLocaleComponent(this, start, size);
+    if (localeScript[0] == '\0') {
+        computeScript();
+    };
 }
 
 String8 ResTable_config::toString() const {
diff --git a/libs/androidfw/tests/ConfigLocale_test.cpp b/libs/androidfw/tests/ConfigLocale_test.cpp
index 4999594..1941563 100644
--- a/libs/androidfw/tests/ConfigLocale_test.cpp
+++ b/libs/androidfw/tests/ConfigLocale_test.cpp
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#include <androidfw/LocaleData.h>
 #include <androidfw/ResourceTypes.h>
 #include <utils/Log.h>
 #include <utils/String8.h>
@@ -28,7 +29,7 @@
      EXPECT_EQ('e', config.language[0]);
      EXPECT_EQ('n', config.language[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackLanguage(out);
      EXPECT_EQ('e', out[0]);
      EXPECT_EQ('n', out[1]);
@@ -51,7 +52,7 @@
      EXPECT_EQ('U', config.country[0]);
      EXPECT_EQ('S', config.country[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackRegion(out);
      EXPECT_EQ('U', out[0]);
      EXPECT_EQ('S', out[1]);
@@ -67,7 +68,7 @@
      EXPECT_EQ('\x99', config.language[0]);
      EXPECT_EQ('\xA4', config.language[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackLanguage(out);
      EXPECT_EQ('e', out[0]);
      EXPECT_EQ('n', out[1]);
@@ -91,7 +92,7 @@
      EXPECT_EQ(char(0xbc), config.language[0]);
      EXPECT_EQ(char(0xd3), config.language[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackLanguage(out);
      EXPECT_EQ('t', out[0]);
      EXPECT_EQ('g', out[1]);
@@ -103,7 +104,7 @@
      ResTable_config config;
      config.packRegion("419");
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackRegion(out);
 
      EXPECT_EQ('4', out[0]);
@@ -124,6 +125,10 @@
 
      if (script != NULL) {
          memcpy(out->localeScript, script, 4);
+         out->localeScriptWasProvided = true;
+     } else {
+         out->computeScript();
+         out->localeScriptWasProvided = false;
      }
 
      if (variant != NULL) {
@@ -177,11 +182,12 @@
     EXPECT_EQ('n', test.language[1]);
     EXPECT_EQ('U', test.country[0]);
     EXPECT_EQ('S', test.country[1]);
-    EXPECT_EQ(0, test.localeScript[0]);
+    EXPECT_FALSE(test.localeScriptWasProvided);
+    EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
     EXPECT_EQ(0, test.localeVariant[0]);
 
     test.setBcp47Locale("eng-419");
-    char out[4] = { 1, 1, 1, 1};
+    char out[4] = {1, 1, 1, 1};
     test.unpackLanguage(out);
     EXPECT_EQ('e', out[0]);
     EXPECT_EQ('n', out[1]);
@@ -193,17 +199,397 @@
     EXPECT_EQ('1', out[1]);
     EXPECT_EQ('9', out[2]);
 
-
     test.setBcp47Locale("en-Latn-419");
-    memset(out, 1, 4);
     EXPECT_EQ('e', test.language[0]);
     EXPECT_EQ('n', test.language[1]);
-
     EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
+    EXPECT_TRUE(test.localeScriptWasProvided);
+    memset(out, 1, 4);
     test.unpackRegion(out);
     EXPECT_EQ('4', out[0]);
     EXPECT_EQ('1', out[1]);
     EXPECT_EQ('9', out[2]);
+
+    test.setBcp47Locale("de-1901");
+    memset(out, 1, 4);
+    test.unpackLanguage(out);
+    EXPECT_EQ('d', out[0]);
+    EXPECT_EQ('e', out[1]);
+    EXPECT_EQ('\0', out[2]);
+    EXPECT_FALSE(test.localeScriptWasProvided);
+    EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
+    memset(out, 1, 4);
+    test.unpackRegion(out);
+    EXPECT_EQ('\0', out[0]);
+    EXPECT_EQ(0, strcmp("1901", test.localeVariant));
+
+    test.setBcp47Locale("de-Latn-1901");
+    memset(out, 1, 4);
+    test.unpackLanguage(out);
+    EXPECT_EQ('d', out[0]);
+    EXPECT_EQ('e', out[1]);
+    EXPECT_EQ('\0', out[2]);
+    EXPECT_TRUE(test.localeScriptWasProvided);
+    EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
+    memset(out, 1, 4);
+    test.unpackRegion(out);
+    EXPECT_EQ('\0', out[0]);
+    EXPECT_EQ(0, strcmp("1901", test.localeVariant));
 }
 
-}  // namespace android.
+TEST(ConfigLocaleTest, computeScript) {
+    ResTable_config config;
+
+    fillIn(NULL, NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("\0\0\0\0", config.localeScript, 4));
+
+    fillIn("zh", "TW", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Hant", config.localeScript, 4));
+
+    fillIn("zh", "CN", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Hans", config.localeScript, 4));
+
+    fillIn("az", NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Latn", config.localeScript, 4));
+
+    fillIn("az", "AZ", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Latn", config.localeScript, 4));
+
+    fillIn("az", "IR", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Arab", config.localeScript, 4));
+
+    fillIn("peo", NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Xpeo", config.localeScript, 4));
+
+    fillIn("qaa", NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("\0\0\0\0", config.localeScript, 4));
+}
+
+TEST(ConfigLocaleTest, getBcp47Locale_script) {
+    ResTable_config config;
+    fillIn("en", NULL, "Latn", NULL, &config);
+
+    char out[RESTABLE_MAX_LOCALE_LEN];
+    config.localeScriptWasProvided = true;
+    config.getBcp47Locale(out);
+    EXPECT_EQ(0, strcmp("en-Latn", out));
+
+    config.localeScriptWasProvided = false;
+    config.getBcp47Locale(out);
+    EXPECT_EQ(0, strcmp("en", out));
+}
+
+TEST(ConfigLocaleTest, match) {
+    ResTable_config supported, requested;
+
+    fillIn(NULL, NULL, NULL, NULL, &supported);
+    fillIn("fr", "CA", NULL, NULL, &requested);
+    // Empty locale matches everything (as a default).
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("en", "CA", NULL, NULL, &supported);
+    fillIn("fr", "CA", NULL, NULL, &requested);
+    // Different languages don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", "FR", NULL, NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // If we can't infer the scripts, different regions don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", "FR", "Latn", NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // If we can't infer any of the scripts, different regions don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", "FR", NULL, NULL, &supported);
+    fillIn("qaa", "CA", "Latn", NULL, &requested);
+    // If we can't infer any of the scripts, different regions don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", NULL, NULL, NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // language-only resources still support language+region requests, even if we can't infer the
+    // script.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("qaa", "CA", NULL, NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // Even if we can't infer the scripts, exactly equal locales match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("az", NULL, NULL, NULL, &supported);
+    fillIn("az", NULL, "Latn", NULL, &requested);
+    // If the resolved scripts are the same, it doesn't matter if they were explicitly provided
+    // or not, and they match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("az", NULL, NULL, NULL, &supported);
+    fillIn("az", NULL, "Cyrl", NULL, &requested);
+    // If the resolved scripts are different, they don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("az", NULL, NULL, NULL, &supported);
+    fillIn("az", "IR", NULL, NULL, &requested);
+    // If the resolved scripts are different, they don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("az", "IR", NULL, NULL, &supported);
+    fillIn("az", NULL, "Arab", NULL, &requested);
+    // If the resolved scripts are the same, it doesn't matter if they were explicitly provided
+    // or not, and they match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("en", NULL, NULL, NULL, &supported);
+    fillIn("en", "XA", NULL, NULL, &requested);
+    // en-XA is a pseudo-locale, and English resources are not a match for it.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("en", "XA", NULL, NULL, &supported);
+    fillIn("en", NULL, NULL, NULL, &requested);
+    // en-XA is a pseudo-locale, and its resources don't support English locales.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("en", "XA", NULL, NULL, &supported);
+    fillIn("en", "XA", NULL, NULL, &requested);
+    // Even if they are pseudo-locales, exactly equal locales match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("ar", NULL, NULL, NULL, &supported);
+    fillIn("ar", "XB", NULL, NULL, &requested);
+    // ar-XB is a pseudo-locale, and Arabic resources are not a match for it.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("ar", "XB", NULL, NULL, &supported);
+    fillIn("ar", NULL, NULL, NULL, &requested);
+    // ar-XB is a pseudo-locale, and its resources don't support Arabic locales.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("ar", "XB", NULL, NULL, &supported);
+    fillIn("ar", "XB", NULL, NULL, &requested);
+    // Even if they are pseudo-locales, exactly equal locales match.
+    EXPECT_TRUE(supported.match(requested));
+}
+
+TEST(ConfigLocaleTest, isLocaleBetterThan_basics) {
+    ResTable_config config1, config2, request;
+
+    fillIn(NULL, NULL, NULL, NULL, &request);
+    fillIn("fr", "FR", NULL, NULL, &config1);
+    fillIn("fr", "CA", NULL, NULL, &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("fr", "CA", NULL, NULL, &request);
+    fillIn(NULL, NULL, NULL, NULL, &config1);
+    fillIn(NULL, NULL, NULL, NULL, &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("fr", "CA", NULL, NULL, &request);
+    fillIn("fr", "FR", NULL, NULL, &config1);
+    fillIn(NULL, NULL, NULL, NULL, &config2);
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("de", "DE", NULL, NULL, &request);
+    fillIn("de", "DE", NULL, "1901", &config1);
+    fillIn("de", "DE", NULL, "1996", &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("de", "DE", NULL, "1901", &request);
+    fillIn("de", "DE", NULL, "1901", &config1);
+    fillIn("de", "DE", NULL, NULL, &config2);
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("de", "DE", NULL, "1901", &request);
+    fillIn("de", "DE", NULL, "1996", &config1);
+    fillIn("de", "DE", NULL, NULL, &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+}
+
+TEST(ConfigLocaleTest, isLocaleBetterThan_regionComparison) {
+    ResTable_config config1, config2, request;
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "419", NULL, NULL, &config1);
+    fillIn("es", "419", NULL, NULL, &config2);
+    // Both supported locales are the same, so none is better than the other.
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "AR", NULL, NULL, &config1);
+    fillIn("es", "419", NULL, NULL, &config2);
+    // An exact locale match is better than a parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "419", NULL, NULL, &config1);
+    fillIn("es", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "419", NULL, NULL, &config1);
+    fillIn("es", "ES", NULL, NULL, &config2);
+    // A parent is better than a non-parent representative locale.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", NULL, NULL, NULL, &config1);
+    fillIn("es", "ES", NULL, NULL, &config2);
+    // A parent is better than a non-parent representative locale.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "PE", NULL, NULL, &config1);
+    fillIn("es", "ES", NULL, NULL, &config2);
+    // A closer locale is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "MX", NULL, NULL, &config1);
+    fillIn("es", "BO", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "US", NULL, NULL, &config1);
+    fillIn("es", "BO", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "MX", NULL, NULL, &config1);
+    fillIn("es", "US", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "GQ", NULL, NULL, &request);
+    fillIn("es", "IC", NULL, NULL, &config1);
+    fillIn("es", "419", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better and
+    // letters are better than numbers.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "GB", NULL, NULL, &request);
+    fillIn("en", "001", NULL, NULL, &config1);
+    fillIn("en", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "PR", NULL, NULL, &request);
+    fillIn("en", NULL, NULL, NULL, &config1);
+    fillIn("en", "001", NULL, NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "DE", NULL, NULL, &request);
+    fillIn("en", "150", NULL, NULL, &config1);
+    fillIn("en", "001", NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "IN", NULL, NULL, &request);
+    fillIn("en", "AU", NULL, NULL, &config1);
+    fillIn("en", "US", NULL, NULL, &config2);
+    // A closer locale is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "PR", NULL, NULL, &request);
+    fillIn("en", "001", NULL, NULL, &config1);
+    fillIn("en", "GB", NULL, NULL, &config2);
+    // A closer locale is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "IN", NULL, NULL, &request);
+    fillIn("en", "GB", NULL, NULL, &config1);
+    fillIn("en", "AU", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "IN", NULL, NULL, &request);
+    fillIn("en", "AU", NULL, NULL, &config1);
+    fillIn("en", "CA", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("pt", "MZ", NULL, NULL, &request);
+    fillIn("pt", "PT", NULL, NULL, &config1);
+    fillIn("pt", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("pt", "MZ", NULL, NULL, &request);
+    fillIn("pt", "PT", NULL, NULL, &config1);
+    fillIn("pt", "BR", NULL, NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("zh", "MO", "Hant", NULL, &request);
+    fillIn("zh", "HK", "Hant", NULL, &config1);
+    fillIn("zh", "TW", "Hant", NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("zh", "US", "Hant", NULL, &request);
+    fillIn("zh", "TW", "Hant", NULL, &config1);
+    fillIn("zh", "HK", "Hant", NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "DZ", NULL, NULL, &request);
+    fillIn("ar", "015", NULL, NULL, &config1);
+    fillIn("ar", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "EG", NULL, NULL, &request);
+    fillIn("ar", NULL, NULL, NULL, &config1);
+    fillIn("ar", "015", NULL, NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "QA", NULL, NULL, &request);
+    fillIn("ar", "EG", NULL, NULL, &config1);
+    fillIn("ar", "BH", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "QA", NULL, NULL, &request);
+    fillIn("ar", "SA", NULL, NULL, &config1);
+    fillIn("ar", "015", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better and
+    // letters are better than numbers.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+}
+
+}  // namespace android
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index 1bfa308..db017fe 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -1630,6 +1630,7 @@
 
     Texture* texture = entry ? entry->texture : mCaches.textureCache.get(bitmap);
     if (!texture) return;
+    const AutoTexture autoCleanup(texture);
 
     // 9 patches are built for stretching - always filter
     int textureFillFlags = TextureFillFlags::ForceFilter;
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index 793df92..3e20608 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -31,7 +31,7 @@
 
 const int Tree::MAX_CACHED_BITMAP_SIZE = 2048;
 
-void Path::draw(Canvas* outCanvas, const SkMatrix& groupStackedMatrix, float scaleX, float scaleY) {
+void Path::draw(SkCanvas* outCanvas, const SkMatrix& groupStackedMatrix, float scaleX, float scaleY) {
     float matrixScale = getMatrixScale(groupStackedMatrix);
     if (matrixScale == 0) {
         // When either x or y is scaled to 0, we don't need to draw anything.
@@ -186,7 +186,7 @@
     return SkColorSetA(color, alphaBytes * alpha);
 }
 
-void FullPath::drawPath(Canvas* outCanvas, const SkPath& renderPath, float strokeScale){
+void FullPath::drawPath(SkCanvas* outCanvas, const SkPath& renderPath, float strokeScale){
     // Draw path's fill, if fill color isn't transparent.
     if (mFillColor != SK_ColorTRANSPARENT) {
         mPaint.setStyle(SkPaint::Style::kFill_Style);
@@ -287,9 +287,9 @@
     return true;
 }
 
-void ClipPath::drawPath(Canvas* outCanvas, const SkPath& renderPath,
+void ClipPath::drawPath(SkCanvas* outCanvas, const SkPath& renderPath,
         float strokeScale){
-    outCanvas->clipPath(&renderPath, SkRegion::kIntersect_Op);
+    outCanvas->clipPath(renderPath, SkRegion::kIntersect_Op);
 }
 
 Group::Group(const Group& group) : Node(group) {
@@ -302,7 +302,7 @@
     mTranslateY = group.mTranslateY;
 }
 
-void Group::draw(Canvas* outCanvas, const SkMatrix& currentMatrix, float scaleX,
+void Group::draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix, float scaleX,
         float scaleY) {
     // TODO: Try apply the matrix to the canvas instead of passing it down the tree
 
@@ -315,7 +315,7 @@
     stackedMatrix.postConcat(currentMatrix);
 
     // Save the current clip information, which is local to this group.
-    outCanvas->save(SkCanvas::kMatrixClip_SaveFlag);
+    outCanvas->save();
     // Draw the group tree in the same order as the XML file.
     for (Node* child : mChildren) {
         child->draw(outCanvas, stackedMatrix, scaleX, scaleY);
@@ -465,10 +465,10 @@
 
 void Tree::updateCachedBitmap(int width, int height) {
     mCachedBitmap.eraseColor(SK_ColorTRANSPARENT);
-    Canvas* outCanvas = Canvas::create_canvas(mCachedBitmap);
+    SkCanvas outCanvas(mCachedBitmap);
     float scaleX = width / mViewportWidth;
     float scaleY = height / mViewportHeight;
-    mRootNode->draw(outCanvas, SkMatrix::I(), scaleX, scaleY);
+    mRootNode->draw(&outCanvas, SkMatrix::I(), scaleX, scaleY);
     mCacheDirty = false;
 }
 
diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h
index 6c84b05..5ae5f6a 100644
--- a/libs/hwui/VectorDrawable.h
+++ b/libs/hwui/VectorDrawable.h
@@ -20,6 +20,7 @@
 #include "Canvas.h"
 #include <SkBitmap.h>
 #include <SkColor.h>
+#include <SkCanvas.h>
 #include <SkMatrix.h>
 #include <SkPaint.h>
 #include <SkPath.h>
@@ -56,7 +57,7 @@
         mName = node.mName;
     }
     Node() {}
-    virtual void draw(Canvas* outCanvas, const SkMatrix& currentMatrix,
+    virtual void draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix,
             float scaleX, float scaleY) = 0;
     virtual void dump() = 0;
     void setName(const char* name) {
@@ -85,7 +86,7 @@
     void dump() override;
     bool canMorph(const Data& path);
     bool canMorph(const Path& path);
-    void draw(Canvas* outCanvas, const SkMatrix& groupStackedMatrix,
+    void draw(SkCanvas* outCanvas, const SkMatrix& groupStackedMatrix,
             float scaleX, float scaleY) override;
     void setPath(const char* path, size_t strLength);
     void setPathData(const Data& data);
@@ -93,7 +94,7 @@
 
 protected:
     virtual const SkPath& getUpdatedPath();
-    virtual void drawPath(Canvas *outCanvas, const SkPath& renderPath,
+    virtual void drawPath(SkCanvas *outCanvas, const SkPath& renderPath,
             float strokeScale) = 0;
     Data mData;
     SkPath mSkPath;
@@ -163,7 +164,7 @@
 
 protected:
     const SkPath& getUpdatedPath() override;
-    void drawPath(Canvas* outCanvas, const SkPath& renderPath,
+    void drawPath(SkCanvas* outCanvas, const SkPath& renderPath,
             float strokeScale) override;
 
 private:
@@ -193,7 +194,7 @@
     ClipPath(const Data& nodes) : Path(nodes) {}
 
 protected:
-    void drawPath(Canvas* outCanvas, const SkPath& renderPath,
+    void drawPath(SkCanvas* outCanvas, const SkPath& renderPath,
             float strokeScale) override;
 };
 
@@ -243,7 +244,7 @@
     void setTranslateY(float translateY) {
         mTranslateY = translateY;
     }
-    virtual void draw(Canvas* outCanvas, const SkMatrix& currentMatrix,
+    virtual void draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix,
             float scaleX, float scaleY) override;
     void updateLocalMatrix(float rotate, float pivotX, float pivotY,
             float scaleX, float scaleY, float translateX, float translateY);
diff --git a/libs/hwui/renderstate/RenderState.cpp b/libs/hwui/renderstate/RenderState.cpp
index e71d6ee..75dcf16 100644
--- a/libs/hwui/renderstate/RenderState.cpp
+++ b/libs/hwui/renderstate/RenderState.cpp
@@ -340,6 +340,9 @@
     SkiaShader::apply(*mCaches, fill.skiaShaderData);
 
     GL_CHECKPOINT();
+    Texture* texture = (fill.skiaShaderData.skiaShaderType & kBitmap_SkiaShaderType) ?
+            fill.skiaShaderData.bitmapData.bitmapTexture : nullptr;
+    const AutoTexture autoCleanup(texture);
 
     // ------------------------------------
     // ---------- GL state setup ----------
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 5ad6b08..dc534be 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -2163,7 +2163,7 @@
      * audio service.
      */
     private final ServiceEventHandlerDelegate mServiceEventHandlerDelegate =
-            new ServiceEventHandlerDelegate();
+            new ServiceEventHandlerDelegate(null);
 
     /**
      * Event types
@@ -2177,10 +2177,14 @@
     private class ServiceEventHandlerDelegate {
         private final Handler mHandler;
 
-        ServiceEventHandlerDelegate() {
+        ServiceEventHandlerDelegate(Handler handler) {
             Looper looper;
-            if ((looper = Looper.myLooper()) == null) {
-                looper = Looper.getMainLooper();
+            if (handler == null) {
+                if ((looper = Looper.myLooper()) == null) {
+                    looper = Looper.getMainLooper();
+                }
+            } else {
+                looper = handler.getLooper();
             }
 
             if (looper != null) {
@@ -2201,27 +2205,9 @@
                                 }
                                 break;
                             case MSSG_RECORDING_CONFIG_CHANGE:
-                                // optimizing for the case of a single callback
-                                AudioRecordingCallback singleCallback = null;
-                                ArrayList<AudioRecordingCallback> multipleCallbacks = null;
-                                synchronized(mRecordCallbackLock) {
-                                    if ((mRecordCallbackList != null)
-                                            && (mRecordCallbackList.size() != 0)) {
-                                        if (mRecordCallbackList.size() == 1) {
-                                            singleCallback = mRecordCallbackList.get(0);
-                                        } else {
-                                            multipleCallbacks =
-                                                    new ArrayList<AudioRecordingCallback>(
-                                                            mRecordCallbackList);
-                                        }
-                                    }
-                                }
-                                if (singleCallback != null) {
-                                    singleCallback.onRecordConfigChanged();
-                                } else if (multipleCallbacks != null) {
-                                    for (int i=0 ; i < multipleCallbacks.size() ; i++) {
-                                        multipleCallbacks.get(i).onRecordConfigChanged();
-                                    }
+                                final AudioRecordingCallback cb = (AudioRecordingCallback) msg.obj;
+                                if (cb != null) {
+                                    cb.onRecordConfigChanged();
                                 }
                                 break;
                             default:
@@ -2798,34 +2784,51 @@
     //====================================================================
     // Recording configuration
     /**
-     * @hide
-     * candidate for public API
+     * Interface for receiving update notifications about the recording configuration. Extend
+     * this abstract class and register it with
+     * {@link AudioManager#registerAudioRecordingCallback(AudioRecordingCallback, Handler)}
+     * to be notified.
+     * Use {@link AudioManager#getActiveRecordConfigurations()} to query the current configuration.
      */
     public static abstract class AudioRecordingCallback {
         /**
-         * @hide
-         * candidate for public API
+         * Called whenever the device recording configuration has changed.
          */
         public void onRecordConfigChanged() {}
     }
 
+    private static class AudioRecordingCallbackInfo {
+        final AudioRecordingCallback mCb;
+        final Handler mHandler;
+        AudioRecordingCallbackInfo(AudioRecordingCallback cb, Handler handler) {
+            mCb = cb;
+            mHandler = handler;
+        }
+    }
+
     /**
-     * @hide
-     * candidate for public API
-     * @param non-null callback
+     * Register a callback to be notified of audio recording changes through
+     * {@link AudioRecordingCallback}
+     * @param cb non-null callback to register
+     * @param handler the {@link Handler} object for the thread on which to execute
+     * the callback. If <code>null</code>, the {@link Handler} associated with the main
+     * {@link Looper} will be used.
      */
-    public void registerAudioRecordingCallback(@NonNull AudioRecordingCallback cb) {
+    public void registerAudioRecordingCallback(@NonNull AudioRecordingCallback cb, Handler handler)
+    {
         if (cb == null) {
             throw new IllegalArgumentException("Illegal null AudioRecordingCallback argument");
         }
+
         synchronized(mRecordCallbackLock) {
             // lazy initialization of the list of recording callbacks
             if (mRecordCallbackList == null) {
-                mRecordCallbackList = new ArrayList<AudioRecordingCallback>();
+                mRecordCallbackList = new ArrayList<AudioRecordingCallbackInfo>();
             }
             final int oldCbCount = mRecordCallbackList.size();
-            if (!mRecordCallbackList.contains(cb)) {
-                mRecordCallbackList.add(cb);
+            if (!hasRecordCallback_sync(cb)) {
+                mRecordCallbackList.add(new AudioRecordingCallbackInfo(cb,
+                        new ServiceEventHandlerDelegate(handler).getHandler()));
                 final int newCbCount = mRecordCallbackList.size();
                 if ((oldCbCount == 0) && (newCbCount > 0)) {
                     // register binder for callbacks
@@ -2844,9 +2847,9 @@
     }
 
     /**
-     * @hide
-     * candidate for public API
-     * @param non-null callback
+     * Unregister an audio recording callback previously registered with
+     * {@link #registerAudioRecordingCallback(AudioRecordingCallback, Handler)}.
+     * @param cb non-null callback to unregister
      */
     public void unregisterAudioRecordingCallback(@NonNull AudioRecordingCallback cb) {
         if (cb == null) {
@@ -2857,7 +2860,7 @@
                 return;
             }
             final int oldCbCount = mRecordCallbackList.size();
-            if (mRecordCallbackList.remove(cb)) {
+            if (removeRecordCallback_sync(cb)) {
                 final int newCbCount = mRecordCallbackList.size();
                 if ((oldCbCount > 0) && (newCbCount == 0)) {
                     // unregister binder for callbacks
@@ -2876,8 +2879,7 @@
     }
 
     /**
-     * @hide
-     * candidate for public API
+     * Returns the current active audio recording configurations of the device.
      * @return a non-null array of recording configurations. An array of length 0 indicates there is
      *     no recording active when queried.
      */
@@ -2902,18 +2904,57 @@
 
     /**
      * All operations on this list are sync'd on mRecordCallbackLock.
-     * List is lazy-initialized in {@link #registerAudioRecordingCallback(AudioRecordingCallback)}.
+     * List is lazy-initialized in
+     * {@link #registerAudioRecordingCallback(AudioRecordingCallback, Handler)}.
      * List can be null.
      */
-    private List<AudioRecordingCallback> mRecordCallbackList;
+    private List<AudioRecordingCallbackInfo> mRecordCallbackList;
     private final Object mRecordCallbackLock = new Object();
 
+    /**
+     * Must be called synchronized on mRecordCallbackLock
+     */
+    private boolean hasRecordCallback_sync(@NonNull AudioRecordingCallback cb) {
+        if (mRecordCallbackList != null) {
+            for (int i=0 ; i < mRecordCallbackList.size() ; i++) {
+                if (cb.equals(mRecordCallbackList.get(i).mCb)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Must be called synchronized on mRecordCallbackLock
+     */
+    private boolean removeRecordCallback_sync(@NonNull AudioRecordingCallback cb) {
+        if (mRecordCallbackList != null) {
+            for (int i=0 ; i < mRecordCallbackList.size() ; i++) {
+                if (cb.equals(mRecordCallbackList.get(i).mCb)) {
+                    mRecordCallbackList.remove(i);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private final IRecordingConfigDispatcher mRecCb = new IRecordingConfigDispatcher.Stub() {
 
         public void dispatchRecordingConfigChange() {
-            final Message m = mServiceEventHandlerDelegate.getHandler().obtainMessage(
-                    MSSG_RECORDING_CONFIG_CHANGE/*what*/);
-            mServiceEventHandlerDelegate.getHandler().sendMessage(m);
+            synchronized(mRecordCallbackLock) {
+                if (mRecordCallbackList != null) {
+                    for (int i=0 ; i < mRecordCallbackList.size() ; i++) {
+                        final AudioRecordingCallbackInfo arci = mRecordCallbackList.get(i);
+                        if (arci.mHandler != null) {
+                            final Message m = arci.mHandler.obtainMessage(
+                                    MSSG_RECORDING_CONFIG_CHANGE/*what*/, arci.mCb/*obj*/);
+                            arci.mHandler.sendMessage(m);
+                        }
+                    }
+                }
+            }
         }
 
     };
diff --git a/media/java/android/media/AudioRecordConfiguration.java b/media/java/android/media/AudioRecordConfiguration.java
index aefe692..c7d219d 100644
--- a/media/java/android/media/AudioRecordConfiguration.java
+++ b/media/java/android/media/AudioRecordConfiguration.java
@@ -22,8 +22,9 @@
 import java.util.Objects;
 
 /**
- * @hide
- * Candidate for public API, see AudioManager.getActiveRecordConfiguration()
+ * The AudioRecordConfiguration class collects the information describing an audio recording
+ * session. This information is returned through the 
+ * {@link AudioManager#getActiveRecordConfigurations()} method.
  *
  */
 public class AudioRecordConfiguration implements Parcelable {
@@ -41,19 +42,23 @@
     }
 
     /**
-     * @return one of AudioSource.MIC, AudioSource.VOICE_UPLINK,
-     *       AudioSource.VOICE_DOWNLINK, AudioSource.VOICE_CALL,
-     *       AudioSource.CAMCORDER, AudioSource.VOICE_RECOGNITION,
-     *       AudioSource.VOICE_COMMUNICATION.
+     * Returns the audio source being used for the recording.
+     * @return one of {@link MediaRecorder.AudioSource#MIC},
+     *       {@link MediaRecorder.AudioSource#VOICE_UPLINK},
+     *       {@link MediaRecorder.AudioSource#VOICE_DOWNLINK},
+     *       {@link MediaRecorder.AudioSource#VOICE_CALL},
+     *       {@link MediaRecorder.AudioSource#CAMCORDER},
+     *       {@link MediaRecorder.AudioSource#VOICE_RECOGNITION},
+     *       {@link MediaRecorder.AudioSource#VOICE_COMMUNICATION}.
      */
     public int getClientAudioSource() { return mClientSource; }
 
     /**
-     * @return the session number of the recorder.
+     * Returns the session number of the recording, see {@link AudioRecord#getAudioSessionId()}.
+     * @return the session number.
      */
     public int getAudioSessionId() { return mSessionId; }
 
-
     public static final Parcelable.Creator<AudioRecordConfiguration> CREATOR
             = new Parcelable.Creator<AudioRecordConfiguration>() {
         /**
diff --git a/native/android/Android.mk b/native/android/Android.mk
index 1742bee..5386e6f 100644
--- a/native/android/Android.mk
+++ b/native/android/Android.mk
@@ -7,6 +7,7 @@
 #
 LOCAL_SRC_FILES:= \
     asset_manager.cpp \
+    choreographer.cpp \
     configuration.cpp \
     input.cpp \
     looper.cpp \
@@ -43,4 +44,7 @@
 
 LOCAL_CFLAGS += -Wall -Werror -Wunused -Wunreachable-code
 
+# Required because of b/25642296
+LOCAL_CLANG_arm64 := false
+
 include $(BUILD_SHARED_LIBRARY)
diff --git a/native/android/choreographer.cpp b/native/android/choreographer.cpp
new file mode 100644
index 0000000..cc2fd77
--- /dev/null
+++ b/native/android/choreographer.cpp
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Choreographer"
+//#define LOG_NDEBUG 0
+
+#include <cinttypes>
+#include <queue>
+#include <thread>
+
+#include <android/choreographer.h>
+#include <androidfw/DisplayEventDispatcher.h>
+#include <gui/ISurfaceComposer.h>
+#include <utils/Looper.h>
+#include <utils/Mutex.h>
+#include <utils/Timers.h>
+
+namespace android {
+
+static inline const char* toString(bool value) {
+    return value ? "true" : "false";
+}
+
+struct FrameCallback {
+    AChoreographer_frameCallback callback;
+    void* data;
+    nsecs_t dueTime;
+
+    inline bool operator<(const FrameCallback& rhs) const {
+        // Note that this is intentionally flipped because we want callbacks due sooner to be at
+        // the head of the queue
+        return dueTime > rhs.dueTime;
+    }
+};
+
+
+class Choreographer : public DisplayEventDispatcher, public MessageHandler {
+public:
+    void postFrameCallback(AChoreographer_frameCallback cb, void* data);
+    void postFrameCallbackDelayed(AChoreographer_frameCallback cb, void* data, nsecs_t delay);
+
+    enum {
+        MSG_SCHEDULE_CALLBACKS = 0,
+        MSG_SCHEDULE_VSYNC = 1
+    };
+    virtual void handleMessage(const Message& message) override;
+
+    static Choreographer* getForThread();
+
+protected:
+    virtual ~Choreographer() = default;
+
+private:
+    Choreographer(const sp<Looper>& looper);
+    Choreographer(const Choreographer&) = delete;
+
+    virtual void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count);
+    virtual void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected);
+
+    void scheduleCallbacks();
+
+    // Protected by mLock
+    std::priority_queue<FrameCallback> mCallbacks;
+
+    mutable Mutex mLock;
+
+    const sp<Looper> mLooper;
+    const std::thread::id mThreadId;
+};
+
+
+thread_local Choreographer* gChoreographer;
+Choreographer* Choreographer::getForThread() {
+    if (gChoreographer == nullptr) {
+        sp<Looper> looper = Looper::getForThread();
+        if (!looper.get()) {
+            ALOGW("No looper prepared for thread");
+            return nullptr;
+        }
+        gChoreographer = new Choreographer(looper);
+        status_t result = gChoreographer->initialize();
+        if (result != OK) {
+            ALOGW("Failed to initialize");
+            return nullptr;
+        }
+    }
+    return gChoreographer;
+}
+
+Choreographer::Choreographer(const sp<Looper>& looper) :
+    DisplayEventDispatcher(looper), mLooper(looper), mThreadId(std::this_thread::get_id()) {
+}
+
+void Choreographer::postFrameCallback(AChoreographer_frameCallback cb, void* data) {
+    postFrameCallbackDelayed(cb, data, 0);
+}
+
+void Choreographer::postFrameCallbackDelayed(
+        AChoreographer_frameCallback cb, void* data, nsecs_t delay) {
+    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+    FrameCallback callback{cb, data, now + delay};
+    {
+        AutoMutex _l{mLock};
+        mCallbacks.push(callback);
+    }
+    if (callback.dueTime <= now) {
+        if (std::this_thread::get_id() != mThreadId) {
+            Message m{MSG_SCHEDULE_VSYNC};
+            mLooper->sendMessage(this, m);
+        } else {
+            scheduleVsync();
+        }
+    } else {
+        Message m{MSG_SCHEDULE_CALLBACKS};
+        mLooper->sendMessageDelayed(delay, this, m);
+    }
+}
+
+void Choreographer::scheduleCallbacks() {
+    AutoMutex _{mLock};
+    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+    if (mCallbacks.top().dueTime <= now) {
+        ALOGV("choreographer %p ~ scheduling vsync", this);
+        scheduleVsync();
+        return;
+    }
+}
+
+
+void Choreographer::dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t) {
+    if (id != ISurfaceComposer::eDisplayIdMain) {
+        ALOGV("choreographer %p ~ ignoring vsync signal for non-main display (id=%d)", this, id);
+        scheduleVsync();
+        return;
+    }
+    std::vector<FrameCallback> callbacks{};
+    {
+        AutoMutex _l{mLock};
+        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+        while (!mCallbacks.empty() && mCallbacks.top().dueTime < now) {
+            callbacks.push_back(mCallbacks.top());
+            mCallbacks.pop();
+        }
+    }
+    for (const auto& cb : callbacks) {
+        cb.callback(timestamp, cb.data);
+    }
+}
+
+void Choreographer::dispatchHotplug(nsecs_t, int32_t id, bool connected) {
+    ALOGV("choreographer %p ~ received hotplug event (id=%" PRId32 ", connected=%s), ignoring.",
+            this, id, toString(connected));
+}
+
+void Choreographer::handleMessage(const Message& message) {
+    switch (message.what) {
+    case MSG_SCHEDULE_CALLBACKS:
+        scheduleCallbacks();
+        break;
+    case MSG_SCHEDULE_VSYNC:
+        scheduleVsync();
+        break;
+    }
+}
+
+}
+
+/* Glue for the NDK interface */
+
+using android::Choreographer;
+
+static inline Choreographer* AChoreographer_to_Choreographer(AChoreographer* choreographer) {
+    return reinterpret_cast<Choreographer*>(choreographer);
+}
+
+static inline AChoreographer* Choreographer_to_AChoreographer(Choreographer* choreographer) {
+    return reinterpret_cast<AChoreographer*>(choreographer);
+}
+
+AChoreographer* AChoreographer_getInstance() {
+    return Choreographer_to_AChoreographer(Choreographer::getForThread());
+}
+
+void AChoreographer_postFrameCallback(AChoreographer* choreographer,
+        AChoreographer_frameCallback callback, void* data) {
+    AChoreographer_to_Choreographer(choreographer)->postFrameCallback(callback, data);
+}
+void AChoreographer_postFrameCallbackDelayed(AChoreographer* choreographer,
+        AChoreographer_frameCallback callback, void* data, long delayMillis) {
+    AChoreographer_to_Choreographer(choreographer)->postFrameCallbackDelayed(
+            callback, data, ms2ns(delayMillis));
+}
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 016657e..05c43b2 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -87,7 +87,7 @@
     <!-- Button label that hides the error bar [CHAR LIMIT=24] -->
     <string name="button_dismiss">Dismiss</string>
     <string name="button_retry">Try Again</string>
-    
+
     <!-- Mode that sorts documents by their display name alphabetically [CHAR LIMIT=24] -->
     <string name="sort_name">By name</string>
     <!-- Mode that sorts documents by their last modified time in descending order; most recent first [CHAR LIMIT=24] -->
@@ -158,6 +158,8 @@
     <string name="copy_preparing">Preparing for copy\u2026</string>
     <!-- Text shown on the notification while DocumentsUI performs setup in preparation for moving files [CHAR LIMIT=32] -->
     <string name="move_preparing">Preparing for move\u2026</string>
+    <!-- Text shown on the notification while DocumentsUI performs setup in preparation for deleting files [CHAR LIMIT=32] -->
+    <string name="delete_preparing">Preparing for delete\u2026</string>
     <!-- Title of the copy error notification [CHAR LIMIT=48] -->
     <plurals name="copy_error_notification_title">
         <item quantity="one">Couldn\'t copy <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
@@ -168,6 +170,11 @@
         <item quantity="one">Couldn\'t move <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
         <item quantity="other">Couldn\'t move <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
     </plurals>
+    <!-- Title of the delete error notification [CHAR LIMIT=48] -->
+    <plurals name="delete_error_notification_title">
+        <item quantity="one">Couldn\'t delete <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
+        <item quantity="other">Couldn\'t delete <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
+    </plurals>
     <!-- Second line for notifications saying that more information will be shown after touching [CHAR LIMIT=48] -->
     <string name="notification_touch_for_details">Touch to view details</string>
     <!-- Label of a dialog button for retrying a failed operation [CHAR LIMIT=24] -->
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java
index 1b5b60de..10a78b9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Events.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java
@@ -111,17 +111,15 @@
 
     public static final class MotionInputEvent implements InputEvent {
         private final MotionEvent mEvent;
-        private final RecyclerView mView;
         private final int mPosition;
 
         public MotionInputEvent(MotionEvent event, RecyclerView view) {
             mEvent = event;
-            mView = view;
 
             // Consider determining position lazily as an optimization.
-            View child = mView.findChildViewUnder(mEvent.getX(), mEvent.getY());
-            mPosition = (child != null)
-                    ? mView.getChildAdapterPosition(child)
+            View child = view.findChildViewUnder(mEvent.getX(), mEvent.getY());
+            mPosition = (child!= null)
+                    ? view.getChildAdapterPosition(child)
                     : RecyclerView.NO_POSITION;
         }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 580e2d8..d220f40 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -84,6 +84,7 @@
 import com.android.documentsui.DocumentsActivity;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.Events;
+import com.android.documentsui.Events.MotionInputEvent;
 import com.android.documentsui.Menus;
 import com.android.documentsui.MessageBar;
 import com.android.documentsui.MimePredicate;
@@ -124,11 +125,13 @@
 
     public static final int REQUEST_COPY_DESTINATION = 1;
 
-    private static final String TAG = "DirectoryFragment";
-
-    private static final int LOADER_ID = 42;
     static final boolean DEBUG_ENABLE_DND = true;
 
+    private static final String TAG = "DirectoryFragment";
+    private static final int LOADER_ID = 42;
+    private static final int DELETE_UNDO_TIMEOUT = 5000;
+    private static final int DELETE_JOB_DELAY = 5500;
+
     private static final String EXTRA_TYPE = "type";
     private static final String EXTRA_ROOT = "root";
     private static final String EXTRA_DOC = "doc";
@@ -138,7 +141,7 @@
     private Model mModel;
     private MultiSelectManager mSelectionManager;
     private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
-    private ItemClickListener mItemClickListener = new ItemClickListener();
+    private ItemEventListener mItemEventListener = new ItemEventListener();
 
     private IconHelper mIconHelper;
 
@@ -297,19 +300,7 @@
 
         mRecView.setAdapter(mAdapter);
 
-        GestureDetector.SimpleOnGestureListener listener =
-                new GestureDetector.SimpleOnGestureListener() {
-                    @Override
-                    public boolean onSingleTapUp(MotionEvent e) {
-                        return DirectoryFragment.this.onSingleTapUp(e);
-                    }
-                    @Override
-                    public boolean onDoubleTap(MotionEvent e) {
-                        Log.d(TAG, "Handling double tap.");
-                        return DirectoryFragment.this.onDoubleTap(e);
-                    }
-                };
-
+        GestureListener listener = new GestureListener();
         final GestureDetector detector = new GestureDetector(this.getContext(), listener);
         detector.setOnDoubleTapListener(listener);
 
@@ -339,7 +330,7 @@
                     : MultiSelectManager.MODE_SINGLE);
         mSelectionManager.addCallback(new SelectionModeListener());
 
-        mModel = new Model(context);
+        mModel = new Model();
         mModel.addUpdateListener(mAdapter);
         mModel.addUpdateListener(mModelUpdateListener);
 
@@ -466,22 +457,8 @@
                 operationType);
     }
 
-    private boolean onSingleTapUp(MotionEvent e) {
-        // Only respond to touch events.  Single-click mouse events are selection events and are
-        // handled by the selection manager.  Tap events that occur while the selection manager is
-        // active are also selection events.
-        if (Events.isTouchEvent(e) && !mSelectionManager.hasSelection()) {
-            String id = getModelId(e);
-            if (id != null) {
-                return handleViewItem(id);
-            }
-        }
-        return false;
-    }
-
     protected boolean onDoubleTap(MotionEvent e) {
         if (Events.isMouseEvent(e)) {
-            Log.d(TAG, "Handling double tap from mouse.");
             String id = getModelId(e);
             if (id != null) {
                 return handleViewItem(id);
@@ -852,16 +829,32 @@
     }
 
     private void deleteDocuments(final Selection selected) {
-        Context context = getActivity();
-        String message = Shared.getQuantityString(context, R.plurals.deleting, selected.size());
 
-        // Hide the files in the UI.
-        final SparseArray<String> toDelete = mAdapter.hide(selected.getAll());
+            checkArgument(!selected.isEmpty());
+            new GetDocumentsTask() {
+                @Override
+                void onDocumentsReady(List<DocumentInfo> docs) {
+                    // Hide the files in the UI.
+                    final SparseArray<String> hidden = mAdapter.hide(selected.getAll());
+
+                    checkState(DELETE_JOB_DELAY > DELETE_UNDO_TIMEOUT);
+                    String operationId = FileOperations.delete(
+                            getActivity(), docs, getDisplayState().stack,
+                            DELETE_JOB_DELAY);
+                    showDeleteSnackbar(hidden, operationId);
+                }
+            }.execute(selected);
+    }
+
+    private void showDeleteSnackbar(final SparseArray<String> hidden, final String jobId) {
+
+        Context context = getActivity();
+        String message = Shared.getQuantityString(context, R.plurals.deleting, hidden.size());
 
         // Show a snackbar informing the user that files will be deleted, and give them an option to
         // cancel.
         final Activity activity = getActivity();
-        Snackbars.makeSnackbar(activity, message, Snackbar.LENGTH_LONG)
+        Snackbars.makeSnackbar(activity, message, DELETE_UNDO_TIMEOUT)
                 .setAction(
                         R.string.undo,
                         new View.OnClickListener() {
@@ -874,22 +867,8 @@
                             public void onDismissed(Snackbar snackbar, int event) {
                                 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
                                     // If the delete was cancelled, just unhide the files.
-                                    mAdapter.unhide(toDelete);
-                                } else {
-                                    // Actually kick off the delete.
-                                    mModel.delete(
-                                            selected,
-                                            new Model.DeletionListener() {
-                                                @Override
-                                                  public void onError() {
-                                                      Snackbars.makeSnackbar(
-                                                              activity,
-                                                              R.string.toast_failed_delete,
-                                                              Snackbar.LENGTH_LONG)
-                                                              .show();
-
-                                                  }
-                                            });
+                                    FileOperations.cancel(activity, jobId);
+                                    mAdapter.unhide(hidden);
                                 }
                             }
                         })
@@ -926,7 +905,7 @@
 
     @Override
     public void initDocumentHolder(DocumentHolder holder) {
-        holder.addClickListener(mItemClickListener);
+        holder.addEventListener(mItemEventListener);
         holder.addOnKeyListener(mSelectionManager);
     }
 
@@ -1330,15 +1309,18 @@
         return mSelectionManager.getSelection().contains(modelId);
     }
 
-    private class ItemClickListener implements DocumentHolder.ClickListener {
+    private class ItemEventListener implements DocumentHolder.EventListener {
         @Override
-        public void onClick(DocumentHolder doc) {
-            if (mSelectionManager.hasSelection()) {
-                mSelectionManager.toggleSelection(doc.modelId);
-                mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
-            } else {
-                handleViewItem(doc.modelId);
-            }
+        public boolean onActivate(DocumentHolder doc) {
+            handleViewItem(doc.modelId);
+            return true;
+        }
+
+        @Override
+        public boolean onSelect(DocumentHolder doc) {
+            mSelectionManager.toggleSelection(doc.modelId);
+            mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
+            return true;
         }
     }
 
@@ -1366,4 +1348,54 @@
             showErrorView();
         }
     }
+
+    /**
+     * The gesture listener for items in the list/grid view. Interprets gestures and sends the
+     * events to the target DocumentHolder, whence they are routed to the appropriate listener.
+     */
+    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            // Single tap logic:
+            // If the selection manager is active, it gets first whack at handling tap
+            // events. Otherwise, tap events are routed to the target DocumentHolder.
+            boolean handled = mSelectionManager.onSingleTapUp(
+                        new MotionInputEvent(e, mRecView));
+
+            if (handled) {
+                return handled;
+            }
+
+            // Give the DocumentHolder a crack at the event.
+            DocumentHolder holder = getTarget(e);
+            if (holder != null) {
+                handled = holder.onSingleTapUp(e);
+            }
+
+            return handled;
+        }
+
+        @Override
+        public void onLongPress(MotionEvent e) {
+            // Long-press events get routed directly to the selection manager. They can be
+            // changed to route through the DocumentHolder if necessary.
+            mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            // Double-tap events are handled directly by the DirectoryFragment. They can be changed
+            // to route through the DocumentHolder if necessary.
+            return DirectoryFragment.this.onDoubleTap(e);
+        }
+
+        private @Nullable DocumentHolder getTarget(MotionEvent e) {
+            View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
+            if (childView != null) {
+                return (DocumentHolder) mRecView.getChildViewHolder(childView);
+            } else {
+                return null;
+            }
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
index 9ac9057..8acf1af 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -16,17 +16,21 @@
 
 package com.android.documentsui.dirlist;
 
+import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.Preconditions.checkState;
 
 import android.content.Context;
 import android.database.Cursor;
+import android.graphics.Rect;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.documentsui.Events;
 import com.android.documentsui.R;
 import com.android.documentsui.State;
 
@@ -41,8 +45,9 @@
     final boolean mAlwaysShowSummary;
     final Context mContext;
 
-    private ListDocumentHolder.ClickListener mClickListener;
+    DocumentHolder.EventListener mEventListener;
     private View.OnKeyListener mKeyListener;
+    private View mSelectionHotspot;
 
     public DocumentHolder(Context context, ViewGroup parent, int layout) {
         this(context, inflateLayout(context, parent, layout));
@@ -58,6 +63,8 @@
         mDefaultItemColor = context.getColor(R.color.item_doc_background);
         mSelectedItemColor = context.getColor(R.color.item_doc_background_selected);
         mAlwaysShowSummary = context.getResources().getBoolean(R.bool.always_show_summary);
+
+        mSelectionHotspot = itemView.findViewById(R.id.icon_check);
     }
 
     /**
@@ -75,23 +82,21 @@
 
     @Override
     public boolean onKey(View v, int keyCode, KeyEvent event) {
+        // Event listener should always be set.
+        checkNotNull(mEventListener);
         // Intercept enter key-up events, and treat them as clicks.  Forward other events.
-        if (event.getAction() == KeyEvent.ACTION_UP &&
-                keyCode == KeyEvent.KEYCODE_ENTER) {
-            if (mClickListener != null) {
-                mClickListener.onClick(this);
-            }
-            return true;
+        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) {
+            return mEventListener.onActivate(this);
         } else if (mKeyListener != null) {
             return mKeyListener.onKey(v, keyCode, event);
         }
         return false;
     }
 
-    public void addClickListener(ListDocumentHolder.ClickListener listener) {
+    public void addEventListener(DocumentHolder.EventListener listener) {
         // Just handle one for now; switch to a list if necessary.
-        checkState(mClickListener == null);
-        mClickListener = listener;
+        checkState(mEventListener == null);
+        mEventListener = listener;
     }
 
     public void addOnKeyListener(View.OnKeyListener listener) {
@@ -104,6 +109,33 @@
         setEnabledRecursive(itemView, enabled);
     }
 
+    public boolean onSingleTapUp(MotionEvent event) {
+        if (Events.isMouseEvent(event)) {
+            // Mouse clicks select.
+            // TODO:  && input.isPrimaryButtonPressed(), but it is returning false.
+            if (mEventListener != null) {
+                return mEventListener.onSelect(this);
+            }
+        } else if (Events.isTouchEvent(event)) {
+            // Touch events select if they occur in the selection hotspot, otherwise they activate.
+            if (mEventListener == null) {
+                return false;
+            }
+
+            // Do everything in global coordinates - it makes things simpler.
+            Rect rect = new Rect();
+            mSelectionHotspot.getGlobalVisibleRect(rect);
+
+            // If the tap occurred within the icon rect, consider it a selection.
+            if (rect.contains((int)event.getRawX(), (int)event.getRawY())) {
+                return mEventListener.onSelect(this);
+            } else {
+                return mEventListener.onActivate(this);
+            }
+        }
+        return false;
+    }
+
     static void setEnabledRecursive(View itemView, boolean enabled) {
         if (itemView == null) return;
         if (itemView.isEnabled() == enabled) return;
@@ -122,7 +154,20 @@
         return inflater.inflate(layout, parent, false);
     }
 
-    interface ClickListener {
-        public void onClick(DocumentHolder doc);
+    /**
+     * Implement this in order to be able to respond to events coming from DocumentHolders.
+     */
+    interface EventListener {
+        /**
+         * @param doc The target DocumentHolder
+         * @return Whether the event was handled.
+         */
+        public boolean onActivate(DocumentHolder doc);
+
+        /**
+         * @param doc The target DocumentHolder
+         * @return Whether the event was handled.
+         */
+        public boolean onSelect(DocumentHolder doc);
     }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
index cf21d15..075b3ea 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
@@ -24,11 +24,7 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 import static com.android.internal.util.Preconditions.checkNotNull;
 
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
 import android.database.Cursor;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Looper;
 import android.provider.DocumentsContract;
@@ -39,7 +35,6 @@
 
 import com.android.documentsui.BaseActivity.SiblingProvider;
 import com.android.documentsui.DirectoryResult;
-import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.RootCursorWrapper;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
@@ -56,7 +51,6 @@
 public class Model implements SiblingProvider {
     private static final String TAG = "Model";
 
-    private Context mContext;
     private boolean mIsLoading;
     private List<UpdateListener> mUpdateListeners = new ArrayList<>();
     @Nullable private Cursor mCursor;
@@ -73,10 +67,6 @@
     @Nullable String info;
     @Nullable String error;
 
-    Model(Context context) {
-        mContext = context;
-    }
-
     /**
      * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
      * string that can be used to identify the document referred to by the cursor.
@@ -406,91 +396,6 @@
         return mCursor;
     }
 
-    public void delete(Selection selected, DeletionListener listener) {
-        final ContentResolver resolver = mContext.getContentResolver();
-        new DeleteFilesTask(resolver, listener).execute(selected);
-    }
-
-    /**
-     * A Task which collects the DocumentInfo for documents that have been marked for deletion,
-     * and actually deletes them.
-     */
-    private class DeleteFilesTask extends AsyncTask<Selection, Void, Void> {
-        private ContentResolver mResolver;
-        private DeletionListener mListener;
-        private boolean mHadTrouble;
-
-        /**
-         * @param resolver A ContentResolver for performing the actual file deletions.
-         * @param errorCallback A Runnable that is executed in the event that one or more errors
-         *     occurred while copying files.  Execution will occur on the UI thread.
-         */
-        public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) {
-            mResolver = resolver;
-            mListener = listener;
-        }
-
-        @Override
-        protected Void doInBackground(Selection... selected) {
-            List<DocumentInfo> toDelete = null;
-            try {
-                toDelete = getDocuments(selected[0]);
-            } catch (NullPointerException e) {
-                Log.w(TAG, "Failed to retrieve documents for delete.");
-                mHadTrouble = true;
-                return null;
-            }
-
-            for (DocumentInfo doc : toDelete) {
-                if (!doc.isDeleteSupported()) {
-                    Log.w(TAG, doc + " could not be deleted.  Skipping...");
-                    mHadTrouble = true;
-                    continue;
-                }
-
-                ContentProviderClient client = null;
-                try {
-                    if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
-                    client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                        mResolver, doc.derivedUri.getAuthority());
-                    DocumentsContract.deleteDocument(client, doc.derivedUri);
-                } catch (Exception e) {
-                    Log.w(TAG, "Failed to delete " + doc, e);
-                    mHadTrouble = true;
-                } finally {
-                    ContentProviderClient.releaseQuietly(client);
-                }
-            }
-
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void _) {
-            if (mHadTrouble) {
-                // TODO show which files failed? b/23720103
-                mListener.onError();
-                if (DEBUG) Log.d(TAG, "Deletion task completed.  Some deletions failed.");
-            } else {
-                if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
-            }
-
-            mListener.onCompletion();
-        }
-    }
-
-    static class DeletionListener {
-        /**
-         * Called when deletion has completed (regardless of whether an error occurred).
-         */
-        void onCompletion() {}
-
-        /**
-         * Called at the end of a deletion operation that produced one or more errors.
-         */
-        void onError() {}
-    }
-
     void addUpdateListener(UpdateListener listener) {
         mUpdateListeners.add(listener);
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
index d868fb4..71e87cb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -17,6 +17,8 @@
 package com.android.documentsui.dirlist;
 
 import static com.android.documentsui.Shared.DEBUG;
+import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
+import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.Preconditions.checkState;
@@ -32,7 +34,6 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
-import android.view.GestureDetector;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -90,29 +91,10 @@
             mBandManager = new BandController();
         }
 
-        GestureDetector.SimpleOnGestureListener listener =
-                new GestureDetector.SimpleOnGestureListener() {
-                    @Override
-                    public boolean onSingleTapUp(MotionEvent e) {
-                        return MultiSelectManager.this.onSingleTapUp(
-                                new MotionInputEvent(e, recyclerView));
-                    }
-                    @Override
-                    public void onLongPress(MotionEvent e) {
-                        MultiSelectManager.this.onLongPress(
-                                new MotionInputEvent(e, recyclerView));
-                    }
-                };
-
-        final GestureDetector detector = new GestureDetector(recyclerView.getContext(), listener);
-        detector.setOnDoubleTapListener(listener);
-
         recyclerView.addOnItemTouchListener(
                 new RecyclerView.OnItemTouchListener() {
                     @Override
                     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
-                        detector.onTouchEvent(e);
-
                         if (mBandManager != null) {
                             return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
                         }
@@ -287,13 +269,7 @@
     boolean onSingleTapUp(InputEvent input) {
         if (DEBUG) Log.d(TAG, "Processing tap event.");
         if (!hasSelection()) {
-            // if this is a mouse click on an item, start selection mode.
-            // TODO:  && input.isPrimaryButtonPressed(), but it is returning false.
-            if (input.isOverItem() && input.isMouseEvent()) {
-                int position = input.getItemPosition();
-                toggleSelection(position);
-                setSelectionRangeBegin(position);
-            }
+            // No selection active - do nothing.
             return false;
         }
 
@@ -814,6 +790,10 @@
         int getChildCount();
         int getVisibleChildCount();
         void focusItem(int position);
+        /**
+         * Layout items are excluded from the GridModel.
+         */
+        boolean isLayoutItem(int adapterPosition);
     }
 
     /** Recycler view facade implementation backed by good ol' RecyclerView. */
@@ -970,6 +950,20 @@
                     });
             }
         }
+
+        @Override
+        public boolean isLayoutItem(int pos) {
+            // The band selection model only operates on documents and directories. Exclude other
+            // types of adapter items (e.g. whitespace items like dividers).
+            RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
+            switch (vh.getItemViewType()) {
+                case ITEM_TYPE_DOCUMENT:
+                case ITEM_TYPE_DIRECTORY:
+                    return false;
+                default:
+                    return true;
+            }
+        }
     }
 
     public interface Callback {
@@ -1433,7 +1427,8 @@
         private void recordVisibleChildren() {
             for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
                 int adapterPosition = mHelper.getAdapterPositionAt(i);
-                if (!mKnownPositions.get(adapterPosition)) {
+                if (!mHelper.isLayoutItem(adapterPosition) &&
+                        !mKnownPositions.get(adapterPosition)) {
                     mKnownPositions.put(adapterPosition, true);
                     recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
                 }
@@ -1519,31 +1514,29 @@
          * @param rect Rectangle including all covered items.
          */
         private void updateSelection(Rect rect) {
-            int columnStartIndex =
+            int columnStart =
                     Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
-            checkState(columnStartIndex >= 0);
-            int columnEndIndex = columnStartIndex;
+            checkState(columnStart >= 0);
+            int columnEnd = columnStart;
 
-            for (int i = columnStartIndex; i < mColumnBounds.size()
+            for (int i = columnStart; i < mColumnBounds.size()
                     && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
-                columnEndIndex = i;
+                columnEnd = i;
             }
 
-            SparseIntArray firstColumn =
-                    mColumns.get(mColumnBounds.get(columnStartIndex).lowerLimit);
-            int rowStartIndex = firstColumn.indexOfKey(rect.top);
-            if (rowStartIndex < 0) {
+            int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
+            if (rowStart < 0) {
                 mPositionNearestOrigin = NOT_SET;
                 return;
             }
 
-            int rowEndIndex = rowStartIndex;
-            for (int i = rowStartIndex;
-                    i < firstColumn.size() && firstColumn.keyAt(i) <= rect.bottom; i++) {
-                rowEndIndex = i;
+            int rowEnd = rowStart;
+            for (int i = rowStart; i < mRowBounds.size()
+                    && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
+                rowEnd = i;
             }
 
-            updateSelection(columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex);
+            updateSelection(columnStart, columnEnd, rowStart, rowEnd);
         }
 
         /**
@@ -1552,13 +1545,17 @@
          */
         private void updateSelection(
                 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+            if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
+                    columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
+
             mSelection.clear();
             for (int column = columnStartIndex; column <= columnEndIndex; column++) {
                 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
                 for (int row = rowStartIndex; row <= rowEndIndex; row++) {
                     // The default return value for SparseIntArray.get is 0, which is a valid
                     // position. Use a sentry value to prevent erroneously selecting item 0.
-                    int position = items.get(items.keyAt(row), NOT_SET);
+                    final int rowKey = mRowBounds.get(row).lowerLimit;
+                    int position = items.get(rowKey, NOT_SET);
                     if (position != NOT_SET) {
                         String id = mAdapter.getModelId(position);
                         if (id != null) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
index b1932b8..f3195a7 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
@@ -21,7 +21,6 @@
 import static android.provider.DocumentsContract.buildDocumentUri;
 import static android.provider.DocumentsContract.getDocumentId;
 import static android.provider.DocumentsContract.isChildDocument;
-import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
 import static com.android.documentsui.Shared.DEBUG;
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
@@ -62,12 +61,7 @@
 class CopyJob extends Job {
     private static final String TAG = "CopyJob";
     private static final int PROGRESS_INTERVAL_MILLIS = 1000;
-    final List<DocumentInfo> mSrcFiles;
-
-    // Provider clients are acquired for the duration of each copy job. Note that there is an
-    // implicit assumption that all srcs come from the same authority.
-    ContentProviderClient srcClient;
-    ContentProviderClient dstClient;
+    final List<DocumentInfo> mSrcs;
 
     private long mStartTime = -1;
     private long mBatchSize;
@@ -86,11 +80,11 @@
      * @param srcs List of files to be copied.
      */
     CopyJob(Context service, Context appContext, Listener listener,
-            String id, DocumentStack destination, List<DocumentInfo> srcs) {
-        super(service, appContext, listener, OPERATION_COPY, id, destination);
+            String id, DocumentStack stack, List<DocumentInfo> srcs) {
+        super(service, appContext, listener, OPERATION_COPY, id, stack);
 
         checkArgument(!srcs.isEmpty());
-        this.mSrcFiles = srcs;
+        this.mSrcs = srcs;
     }
 
     /**
@@ -103,7 +97,7 @@
         super(service, appContext, listener, opType, id, destination);
 
         checkArgument(!srcs.isEmpty());
-        this.mSrcFiles = srcs;
+        this.mSrcs = srcs;
     }
 
     @Override
@@ -185,21 +179,13 @@
     void start() throws RemoteException {
         mStartTime = elapsedRealtime();
 
-        // Acquire content providers.
-        srcClient = acquireUnstableProviderOrThrow(
-                getContentResolver(),
-                mSrcFiles.get(0).authority);
-        dstClient = acquireUnstableProviderOrThrow(
-                getContentResolver(),
-                stack.peek().authority);
-
         // client
-        mBatchSize = calculateSize(srcClient, mSrcFiles);
+        mBatchSize = calculateSize(mSrcs);
 
         DocumentInfo srcInfo;
         DocumentInfo dstInfo;
-        for (int i = 0; i < mSrcFiles.size() && !isCanceled(); ++i) {
-            srcInfo = mSrcFiles.get(i);
+        for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
+            srcInfo = mSrcs.get(i);
             dstInfo = stack.peek();
 
             // Guard unsupported recursive operation.
@@ -233,24 +219,24 @@
     /**
      * Copies a the given document to the given location.
      *
-     * @param srcInfo DocumentInfos for the documents to copy.
+     * @param src DocumentInfos for the documents to copy.
      * @param dstDirInfo The destination directory.
      * @return True on success, false on failure.
      * @throws RemoteException
      */
-    boolean processDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
+    boolean processDocument(DocumentInfo src, DocumentInfo dstDirInfo) throws RemoteException {
 
         // TODO: When optimized copy kicks in, we'll not making any progress updates.
         // For now. Local storage isn't using optimized copy.
 
         // When copying within the same provider, try to use optimized copying.
         // If not supported, then fallback to byte-by-byte copy/move.
-        if (srcInfo.authority.equals(dstDirInfo.authority)) {
-            if ((srcInfo.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
-                if (DocumentsContract.copyDocument(srcClient, srcInfo.derivedUri,
+        if (src.authority.equals(dstDirInfo.authority)) {
+            if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
+                if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
                         dstDirInfo.derivedUri) == null) {
-                    onFileFailed(srcInfo,
-                            "Provider side copy failed for documents: " + srcInfo.derivedUri + ".");
+                    onFileFailed(src,
+                            "Provider side copy failed for documents: " + src.derivedUri + ".");
                     return false;
                 }
                 return true;
@@ -258,44 +244,44 @@
         }
 
         // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
-        return byteCopyDocument(srcInfo, dstDirInfo);
+        return byteCopyDocument(src, dstDirInfo);
     }
 
-    boolean byteCopyDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo)
+    boolean byteCopyDocument(DocumentInfo src, DocumentInfo dest)
             throws RemoteException {
         final String dstMimeType;
         final String dstDisplayName;
 
-        if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + srcInfo);
+        if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
         // If the file is virtual, but can be converted to another format, then try to copy it
         // as such format. Also, append an extension for the target mime type (if known).
-        if (srcInfo.isVirtualDocument()) {
+        if (src.isVirtualDocument()) {
             final String[] streamTypes = getContentResolver().getStreamTypes(
-                    srcInfo.derivedUri, "*/*");
+                    src.derivedUri, "*/*");
             if (streamTypes != null && streamTypes.length > 0) {
                 dstMimeType = streamTypes[0];
                 final String extension = MimeTypeMap.getSingleton().
                         getExtensionFromMimeType(dstMimeType);
-                dstDisplayName = srcInfo.displayName +
-                        (extension != null ? "." + extension : srcInfo.displayName);
+                dstDisplayName = src.displayName +
+                        (extension != null ? "." + extension : src.displayName);
             } else {
-                onFileFailed(srcInfo, "Cannot copy virtual file. No streamable formats available.");
+                onFileFailed(src, "Cannot copy virtual file. No streamable formats available.");
                 return false;
             }
         } else {
-            dstMimeType = srcInfo.mimeType;
-            dstDisplayName = srcInfo.displayName;
+            dstMimeType = src.mimeType;
+            dstDisplayName = src.displayName;
         }
 
         // Create the target document (either a file or a directory), then copy recursively the
         // contents (bytes or children).
-        final Uri dstUri = DocumentsContract.createDocument(dstClient,
-                dstDirInfo.derivedUri, dstMimeType, dstDisplayName);
+        final Uri dstUri = DocumentsContract.createDocument(
+                getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
         if (dstUri == null) {
             // If this is a directory, the entire subdir will not be copied over.
-            onFileFailed(srcInfo,
+            onFileFailed(src,
                     "Couldn't create destination document " + dstDisplayName
-                    + " in directory " + dstDirInfo.displayName + ".");
+                    + " in directory " + dest.displayName + ".");
             return false;
         }
 
@@ -303,16 +289,16 @@
         try {
             dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
         } catch (FileNotFoundException e) {
-            onFileFailed(srcInfo,
+            onFileFailed(src,
                     "Could not load DocumentInfo for newly created file: " + dstUri + ".");
             return false;
         }
 
         final boolean success;
-        if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) {
-            success = copyDirectoryHelper(srcInfo, dstInfo);
+        if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
+            success = copyDirectoryHelper(src, dstInfo);
         } else {
-            success = copyFileHelper(srcInfo, dstInfo, dstMimeType);
+            success = copyFileHelper(src, dstInfo, dstMimeType);
         }
 
         return success;
@@ -322,13 +308,13 @@
      * Handles recursion into a directory and copying its contents. Note that in linux terms, this
      * does the equivalent of "cp src/* dst", not "cp -r src dst".
      *
-     * @param srcDirInfo Info of the directory to copy from. The routine will copy the directory's
+     * @param srcDir Info of the directory to copy from. The routine will copy the directory's
      *            contents, not the directory itself.
-     * @param dstDirInfo Info of the directory to copy to. Must be created beforehand.
+     * @param destDir Info of the directory to copy to. Must be created beforehand.
      * @return True on success, false if some of the children failed to copy.
      * @throws RemoteException
      */
-    private boolean copyDirectoryHelper(DocumentInfo srcDirInfo, DocumentInfo dstDirInfo)
+    private boolean copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
             throws RemoteException {
         // Recurse into directories. Copy children into the new subdirectory.
         final String queryColumns[] = new String[] {
@@ -342,13 +328,11 @@
         boolean success = true;
         try {
             // Iterate over srcs in the directory; copy to the destination directory.
-            final Uri queryUri = DocumentsContract.buildChildDocumentsUri(srcDirInfo.authority,
-                    srcDirInfo.documentId);
-            cursor = srcClient.query(queryUri, queryColumns, null, null, null);
-            DocumentInfo srcInfo;
+            final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
+            cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
             while (cursor.moveToNext() && !isCanceled()) {
-                srcInfo = DocumentInfo.fromCursor(cursor, srcDirInfo.authority);
-                success &= processDocument(srcInfo, dstDirInfo);
+                DocumentInfo src = DocumentInfo.fromCursor(cursor, srcDir.authority);
+                success &= processDocument(src, destDir);
             }
         } finally {
             IoUtils.closeQuietly(cursor);
@@ -366,50 +350,49 @@
      * @return True on success, false on error.
      * @throws RemoteException
      */
-    private boolean copyFileHelper(DocumentInfo srcInfo, DocumentInfo dstInfo, String mimeType)
+    private boolean copyFileHelper(DocumentInfo src, DocumentInfo dest, String mimeType)
             throws RemoteException {
-        // Copy an individual file.
         CancellationSignal canceller = new CancellationSignal();
         ParcelFileDescriptor srcFile = null;
         ParcelFileDescriptor dstFile = null;
-        InputStream src = null;
-        OutputStream dst = null;
+        InputStream in = null;
+        OutputStream out = null;
 
         boolean success = true;
         try {
             // If the file is virtual, but can be converted to another format, then try to copy it
             // as such format.
-            if (srcInfo.isVirtualDocument()) {
+            if (src.isVirtualDocument()) {
                 final AssetFileDescriptor srcFileAsAsset =
-                        srcClient.openTypedAssetFileDescriptor(
-                                srcInfo.derivedUri, mimeType, null, canceller);
+                        getClient(src).openTypedAssetFileDescriptor(
+                                src.derivedUri, mimeType, null, canceller);
                 srcFile = srcFileAsAsset.getParcelFileDescriptor();
-                src = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
+                in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
             } else {
-                srcFile = srcClient.openFile(srcInfo.derivedUri, "r", canceller);
-                src = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
+                srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
+                in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
             }
 
-            dstFile = dstClient.openFile(dstInfo.derivedUri, "w", canceller);
-            dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
+            dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
+            out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
 
             byte[] buffer = new byte[32 * 1024];
             int len;
-            while ((len = src.read(buffer)) != -1) {
+            while ((len = in.read(buffer)) != -1) {
                 if (isCanceled()) {
                     if (DEBUG) Log.d(TAG, "Canceled copy mid-copy. Id:" + id);
                     success = false;
                     break;
                 }
-                dst.write(buffer, 0, len);
+                out.write(buffer, 0, len);
                 makeCopyProgress(len);
             }
 
             srcFile.checkError();
         } catch (IOException e) {
             success = false;
-            onFileFailed(srcInfo, "Exception thrown while copying from "
-                    + srcInfo.derivedUri + " to " + dstInfo.derivedUri + ".");
+            onFileFailed(src, "Exception thrown while copying from "
+                    + src.derivedUri + " to " + dest.derivedUri + ".");
 
             if (dstFile != null) {
                 try {
@@ -420,20 +403,20 @@
             }
         } finally {
             // This also ensures the file descriptors are closed.
-            IoUtils.closeQuietly(src);
-            IoUtils.closeQuietly(dst);
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
         }
 
         if (!success) {
             if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
             canceller.cancel();
             try {
-                DocumentsContract.deleteDocument(dstClient, dstInfo.derivedUri);
+                DocumentsContract.deleteDocument(getClient(dest), dest.derivedUri);
             } catch (RemoteException e) {
                 // RemoteExceptions usually signal that the connection is dead, so there's no
                 // point attempting to continue. Propagate the exception up so the copy job is
                 // cancelled.
-                Log.w(TAG, "Failed to cleanup after copy error: " + srcInfo.derivedUri, e);
+                Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
                 throw e;
             }
         }
@@ -449,14 +432,14 @@
      * @return Size in bytes.
      * @throws RemoteException
      */
-    private static long calculateSize(ContentProviderClient client, List<DocumentInfo> srcs)
+    private long calculateSize(List<DocumentInfo> srcs)
             throws RemoteException {
         long result = 0;
 
         for (DocumentInfo src : srcs) {
             if (src.isDirectory()) {
                 // Directories need to be recursed into.
-                result += calculateFileSizesRecursively(client, src.derivedUri);
+                result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
             } else {
                 result += src.size;
             }
@@ -503,20 +486,14 @@
         return result;
     }
 
-    @Override
-    void cleanup() {
-        ContentProviderClient.releaseQuietly(srcClient);
-        ContentProviderClient.releaseQuietly(dstClient);
-    }
-
     /**
      * Returns true if {@code doc} is a descendant of {@code parentDoc}.
      * @throws RemoteException
      */
-    boolean isDescendentOf(DocumentInfo doc, DocumentInfo parentDoc)
+    boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
             throws RemoteException {
-        if (parentDoc.isDirectory() && doc.authority.equals(parentDoc.authority)) {
-            return isChildDocument(dstClient, doc.derivedUri, parentDoc.derivedUri);
+        if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
+            return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
         }
         return false;
     }
@@ -525,4 +502,16 @@
         Log.w(TAG, msg);
         onFileFailed(file);
     }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("CopyJob")
+                .append("{")
+                .append("id=" + id)
+                .append("srcs=" + mSrcs)
+                .append(", destination=" + stack)
+                .append("}")
+                .toString();
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
new file mode 100644
index 0000000..6a2a794
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.services;
+
+import static com.android.documentsui.Shared.DEBUG;
+import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
+
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.documentsui.R;
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+
+import java.util.List;
+
+final class DeleteJob extends Job {
+
+    private static final String TAG = "DeleteJob";
+    private List<DocumentInfo> mSrcs;
+
+    /**
+     * Moves files to a destination identified by {@code destination}.
+     * Performs most work by delegating to CopyJob, then deleting
+     * a file after it has been copied.
+     *
+     * @see @link {@link Job} constructor for most param descriptions.
+     *
+     * @param srcs List of files to delete
+     */
+    DeleteJob(Context service, Context appContext, Listener listener,
+            String id, DocumentStack stack, List<DocumentInfo> srcs) {
+        super(service, appContext, listener, OPERATION_DELETE, id, stack);
+        this.mSrcs = srcs;
+    }
+
+    @Override
+    Builder createProgressBuilder() {
+        return super.createProgressBuilder(
+                service.getString(R.string.move_notification_title),
+                R.drawable.ic_menu_copy,
+                service.getString(android.R.string.cancel),
+                R.drawable.ic_cab_cancel);
+    }
+
+    @Override
+    public Notification getSetupNotification() {
+        return getSetupNotification(service.getString(R.string.delete_preparing));
+    }
+
+    @Override
+    Notification getFailureNotification() {
+        return getFailureNotification(
+                R.plurals.delete_error_notification_title, R.drawable.ic_menu_delete);
+    }
+
+    @Override
+    void start() throws RemoteException {
+        for (DocumentInfo doc : mSrcs) {
+            if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
+            if (!deleteDocument(doc)) {
+                Log.w(TAG, "Failed to delete document @ " + doc.derivedUri);
+                onFileFailed(doc);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("DeleteJob")
+                .append("{")
+                .append("id=" + id)
+                .append("srcs=" + mSrcs)
+                .append(", location=" + stack)
+                .append("}")
+                .toString();
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
index 1df20ac..aca2d7a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
@@ -27,6 +27,7 @@
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.PowerManager;
+import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 
@@ -51,9 +52,11 @@
 
     private static final int DEFAULT_DELAY = 0;
     private static final int MAX_DELAY = 10 * 1000;  // ten seconds
+    private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".
+    private static final int NOTIFICATION_ID_PROGRESS = 0;
+    private static final int NOTIFICATION_ID_FAILURE = 1;
 
     public static final String TAG = "FileOperationService";
-    private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".
 
     public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
     public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
@@ -92,7 +95,7 @@
     @GuardedBy("mRunning")
     private Map<String, JobRecord> mRunning = new HashMap<>();
 
-    private int mLastStarted;
+    private int mLastServiceId;
 
     @Override
     public void onCreate() {
@@ -111,7 +114,18 @@
     }
 
     @Override
-    public int onStartCommand(Intent intent, int flags, int startTime) {
+    public void onDestroy() {
+        if (DEBUG) Log.d(TAG, "Shutting down executor.");
+        List<Runnable> unfinished = executor.shutdownNow();
+        if (!unfinished.isEmpty()) {
+            Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
+        }
+        executor = null;
+        if (DEBUG) Log.d(TAG, "Destroyed.");
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int serviceId) {
         // TODO: Ensure we're not being called with retry or redeliver.
         // checkArgument(flags == 0);  // retry and redeliver are not supported.
 
@@ -123,17 +137,17 @@
             handleCancel(intent);
         } else {
             checkArgument(operationType != OPERATION_UNKNOWN);
-            handleOperation(intent, startTime, jobId, operationType);
+            handleOperation(intent, serviceId, jobId, operationType);
         }
 
         return START_NOT_STICKY;
     }
 
-    private void handleOperation(Intent intent, int startTime, String jobId, int operationType) {
-        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with start time " + startTime);
+    private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
+        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
 
-        // Track start time so we can stop the service once we're out of work to do.
-        mLastStarted = startTime;
+        // Track the service supplied id so we can stop the service once we're out of work to do.
+        mLastServiceId = serviceId;
 
         Job job = null;
         synchronized (mRunning) {
@@ -147,12 +161,18 @@
 
             job = createJob(operationType, jobId, srcs, stack);
 
+            if (job == null) {
+                return;
+            }
+
             mWakeLock.acquire();
         }
 
         checkState(job != null);
         int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
         checkArgument(delay <= MAX_DELAY);
+        if (DEBUG) Log.d(
+                TAG, "Scheduling job " + job.id + " to run in " + delay + " milliseconds.");
         ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
         mRunning.put(jobId, new JobRecord(job, future));
     }
@@ -191,16 +211,24 @@
         // interactivity for the user in case the copy loop is stalled.
         // Try to cancel it even if we don't have a job id...in case there is some sad
         // orphan notification.
-        mNotificationManager.cancel(jobId, 0);
+        mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
 
         // TODO: Guarantee the job is being finalized
     }
 
+    /**
+     * Creates a new job. Returns null if a job with {@code id} already exists.
+     * @return
+     */
     @GuardedBy("mRunning")
-    private Job createJob(
+    private @Nullable Job createJob(
             @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentStack stack) {
 
-        checkArgument(!mRunning.containsKey(id));
+        if (mRunning.containsKey(id)) {
+            Log.w(TAG, "Duplicate job id: " + id
+                    + ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
+            return null;
+        }
 
         Job job = null;
         switch (operationType) {
@@ -211,7 +239,8 @@
                 job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs);
                 break;
             case OPERATION_DELETE:
-                throw new UnsupportedOperationException();
+                job = jobFactory.createDelete(this, getApplicationContext(), this, id, stack, srcs);
+                break;
             default:
                 throw new UnsupportedOperationException();
         }
@@ -234,20 +263,21 @@
 
     /**
      * Most likely shuts down. Won't shut down if service has a pending
-     * message.
+     * message. Thread pool is deal with in onDestroy.
      */
     private void shutdown() {
-        if (DEBUG) Log.d(TAG, "Shutting down. Last start time: " + mLastStarted);
+        if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
         mWakeLock.release();
         mWakeLock = null;
-        boolean gonnaStop = stopSelfResult(mLastStarted);
+
+        // Turns out, for us, stopSelfResult always returns false in tests,
+        // so we can't guard executor shutdown. For this reason we move
+        // executor shutdown to #onDestroy.
+        boolean gonnaStop = stopSelfResult(mLastServiceId);
         if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
         if (!gonnaStop) {
             Log.w(TAG, "Service should be stopping, but reports otherwise.");
         }
-        // Sadly "gonnaStop" is always false in tests, so we can't guard executor shutdown.
-        List<Runnable> unfinished = executor.shutdownNow();
-        checkState(unfinished.isEmpty());
     }
 
     @VisibleForTesting
@@ -258,7 +288,7 @@
     @Override
     public void onStart(Job job) {
         if (DEBUG) Log.d(TAG, "onStart: " + job.id);
-        mNotificationManager.notify(job.id, 0, job.getSetupNotification());
+        mNotificationManager.notify(job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
     }
 
     @Override
@@ -266,7 +296,7 @@
         if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
 
         // Dismiss the ongoing copy notification when the copy is done.
-        mNotificationManager.cancel(job.id, 0);
+        mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
 
         synchronized (mRunning) {
             deleteJob(job);
@@ -276,7 +306,8 @@
     @Override
     public void onProgress(CopyJob job) {
         if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
-        mNotificationManager.notify(job.id, 0, job.getProgressNotification());
+        mNotificationManager.notify(
+                job.id, NOTIFICATION_ID_PROGRESS, job.getProgressNotification());
     }
 
     @Override
@@ -284,8 +315,8 @@
         if (DEBUG) Log.d(TAG, "onFailed: " + job.id);
         checkArgument(job.failed());
         Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
-        mNotificationManager.notify(job.id, 0, job.getFailureNotification());
-        onFinished(job);  // failed jobs don't call finished, so we do.
+        mNotificationManager.notify(job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
+        onFinished(job);  // Failed jobs don't call finished, so we do.
     }
 
     private static final class JobRecord {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
index 0f1730a3..f59a32a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
@@ -22,6 +22,7 @@
 import static com.android.documentsui.Shared.asArrayList;
 import static com.android.documentsui.Shared.getQuantityString;
 import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
+import static com.android.documentsui.services.FileOperationService.EXTRA_DELAY;
 import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
 import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
 import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
@@ -52,10 +53,12 @@
 
     private static final String TAG = "FileOperations";
 
+    private static final IdBuilder idBuilder = new IdBuilder();
+
     private FileOperations() {}
 
     public static String createJobId() {
-        return String.valueOf(elapsedRealtime());
+        return idBuilder.getNext();
     }
 
     /**
@@ -73,7 +76,7 @@
             case OPERATION_MOVE:
                 return FileOperations.move(activity, srcDocs, stack);
             case OPERATION_DELETE:
-                return FileOperations.delete(activity, srcDocs, stack);
+                throw new UnsupportedOperationException("Delete isn't currently supported.");
             default:
                 throw new UnsupportedOperationException("Unknown operation: " + operationType);
         }
@@ -151,14 +154,17 @@
      * @param jobId A unique jobid for this job.
      *     Use {@link #createJobId} if you don't have one handy.
      * @param srcDocs A list of src files to copy.
+     * @param delay Number of milliseconds to wait before executing the job.
      * @return Id of the job.
      */
     public static String delete(
-            Activity activity, List<DocumentInfo> srcDocs, DocumentStack location) {
+            Activity activity, List<DocumentInfo> srcDocs, DocumentStack location, int delay) {
         String jobId = createJobId();
-        if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id: " + jobId);
+        if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id " + jobId
+                + " delayed by " + delay + " milliseconds.");
 
         Intent intent = createBaseIntent(OPERATION_DELETE, activity, jobId, srcDocs, location);
+        intent.putExtra(EXTRA_DELAY, delay);
         activity.startService(intent);
 
         return jobId;
@@ -193,4 +199,24 @@
                 getQuantityString(activity, contentId, fileCount),
                 Snackbar.LENGTH_SHORT);
     }
+
+    private static final class IdBuilder {
+
+        // Remember last job time so we can guard against collisions.
+        private long mLastJobTime;
+
+        // If we detect a collision, use subId to make distinct.
+        private int mSubId;
+
+        public synchronized String getNext() {
+            long time = elapsedRealtime();
+            if (time == mLastJobTime) {
+                mSubId++;
+            } else {
+                mSubId = 0;
+            }
+            mLastJobTime = time;
+            return String.valueOf(mLastJobTime) + "-" + String.valueOf(mSubId);
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
index c7939eb..f351df9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui.services;
 
+import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
 import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
 import static com.android.documentsui.services.FileOperationService.EXTRA_FAILURE;
 import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
@@ -24,18 +25,21 @@
 import static com.android.documentsui.services.FileOperationService.FAILURE_COPY;
 import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
 import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
 
 import android.annotation.DrawableRes;
 import android.annotation.PluralsRes;
 import android.app.Notification;
 import android.app.Notification.Builder;
 import android.app.PendingIntent;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.provider.DocumentsContract;
+import android.util.Log;
 
 import com.android.documentsui.FilesActivity;
 import com.android.documentsui.R;
@@ -45,7 +49,9 @@
 import com.android.documentsui.services.FileOperationService.OpType;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
@@ -53,6 +59,7 @@
  */
 abstract class Job implements Runnable {
 
+    private static final String TAG = "Job";
     final Context service;
     final Context appContext;
     final Listener listener;
@@ -64,6 +71,7 @@
     final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
     final Notification.Builder mProgressBuilder;
 
+    private final Map<String, ContentProviderClient> mClients = new HashMap<>();
     private volatile boolean mCanceled;
 
     /**
@@ -118,14 +126,31 @@
 
     abstract void start() throws RemoteException;
 
-    // Service will call this when it is done with the job.
-    abstract void cleanup();
-
     abstract Notification getSetupNotification();
     // TODO: Progress notification for deletes.
     // abstract Notification getProgressNotification(long bytesCopied);
     abstract Notification getFailureNotification();
 
+    ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
+        ContentProviderClient client = mClients.get(doc.authority);
+        if (client == null) {
+            // Acquire content providers.
+            client = acquireUnstableProviderOrThrow(
+                    getContentResolver(),
+                    doc.authority);
+
+            mClients.put(doc.authority, client);
+        }
+
+        return checkNotNull(client);
+    }
+
+    final void cleanup() {
+        for (ContentProviderClient client : mClients.values()) {
+            ContentProviderClient.releaseQuietly(client);
+        }
+    }
+
     final void cancel() {
         mCanceled = true;
     }
@@ -146,6 +171,17 @@
         return !failedFiles.isEmpty();
     }
 
+    final boolean deleteDocument(DocumentInfo doc) {
+        try {
+            DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to delete file: " + doc.derivedUri, e);
+            return false;
+        }
+
+        return true;  // victory dance!
+    }
+
     Notification getSetupNotification(String content) {
         mProgressBuilder.setProgress(0, 0, true);
         mProgressBuilder.setContentText(content);
@@ -215,6 +251,16 @@
         return cancelIntent;
     }
 
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("Job")
+                .append("{")
+                .append("id=" + id)
+                .append("}")
+                .toString();
+    }
+
     /**
      * Factory class that facilitates our testing FileOperationService.
      */
@@ -231,6 +277,11 @@
                 String id, DocumentStack stack, List<DocumentInfo> srcs) {
             return new MoveJob(service, appContext, listener, id, stack, srcs);
         }
+
+        Job createDelete(Context service, Context appContext, Listener listener,
+                String id, DocumentStack stack, List<DocumentInfo> srcs) {
+            return new DeleteJob(service, appContext, listener, id, stack, srcs);
+        }
     }
 
     /**
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
index f46f234..7f6b1e1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
@@ -24,7 +24,6 @@
 import android.os.RemoteException;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
-import android.util.Log;
 
 import com.android.documentsui.R;
 import com.android.documentsui.model.DocumentInfo;
@@ -76,17 +75,17 @@
     }
 
     @Override
-    boolean processDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
+    boolean processDocument(DocumentInfo src, DocumentInfo dest) throws RemoteException {
 
         // TODO: When optimized move kicks in, we're not making any progress updates. FIX IT!
 
         // When moving within the same provider, try to use optimized moving.
         // If not supported, then fallback to byte-by-byte copy/move.
-        if (srcInfo.authority.equals(dstDirInfo.authority)) {
-            if ((srcInfo.flags & Document.FLAG_SUPPORTS_MOVE) != 0) {
-                if (DocumentsContract.moveDocument(srcClient, srcInfo.derivedUri,
-                        dstDirInfo.derivedUri) == null) {
-                    onFileFailed(srcInfo);
+        if (src.authority.equals(dest.authority)) {
+            if ((src.flags & Document.FLAG_SUPPORTS_MOVE) != 0) {
+                if (DocumentsContract.moveDocument(getClient(src), src.derivedUri,
+                        dest.derivedUri) == null) {
+                    onFileFailed(src);
                     return false;
                 }
                 return true;
@@ -94,22 +93,20 @@
         }
 
         // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
-        boolean copied = byteCopyDocument(srcInfo, dstDirInfo);
+        boolean copied = byteCopyDocument(src, dest);
 
-        return copied && !isCanceled() && deleteSrcDocument(srcInfo);
+        return copied && !isCanceled() && deleteDocument(src);
     }
 
-    private boolean deleteSrcDocument(DocumentInfo srcInfo) {
-        // This is racey. We should make sure that we never delete a directory after
-        // it changed, so we don't remove a file which had not been copied earlier
-        // to the target location.
-        try {
-            DocumentsContract.deleteDocument(srcClient, srcInfo.derivedUri);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to delete source after copy: " + srcInfo.derivedUri, e);
-            return false;
-        }
-
-        return true;  // victory dance!
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("MoveJob")
+                .append("{")
+                .append("id=" + id)
+                .append("srcs=" + mSrcs)
+                .append(", destination=" + stack)
+                .append("}")
+                .toString();
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
new file mode 100644
index 0000000..16efc6e
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+import com.android.documentsui.R;
+import com.android.documentsui.State;
+
+@SmallTest
+public class DocumentHolderTest extends AndroidTestCase {
+
+    DocumentHolder mHolder;
+    TestListener mListener;
+
+    public void setUp() throws Exception {
+        Context context = getContext();
+        LayoutInflater inflater = LayoutInflater.from(context);
+        mHolder = new DocumentHolder(getContext(), inflater.inflate(R.layout.item_doc_list, null)) {
+            @Override
+            public void bind(Cursor cursor, String modelId, State state) {}
+        };
+
+        mListener = new TestListener();
+        mHolder.addEventListener(mListener);
+
+        mHolder.itemView.requestLayout();
+        mHolder.itemView.invalidate();
+    }
+
+    public void testClickActivates() {
+        click();
+        mListener.assertSelected();
+    }
+
+    public void testTapActivates() {
+        tap();
+        mListener.assertActivated();
+    }
+
+    public void click() {
+        mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_MOUSE));
+    }
+
+    public void tap() {
+        mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_FINGER));
+    }
+
+    public MotionEvent createEvent(int tooltype) {
+        long time = SystemClock.uptimeMillis();
+
+        PointerProperties properties[] = new PointerProperties[] {
+                new PointerProperties()
+        };
+        properties[0].toolType = tooltype;
+
+        PointerCoords coords[] = new PointerCoords[] {
+                new PointerCoords()
+        };
+
+        Rect rect = new Rect();
+        mHolder.itemView.getHitRect(rect);
+        coords[0].x = rect.left;
+        coords[0].y = rect.top;
+
+        return MotionEvent.obtain(
+                time, // down time
+                time, // event time
+                MotionEvent.ACTION_UP, // action
+                1, // pointer count
+                properties, // pointer properties
+                coords, // pointer coords
+                0, // metastate
+                0, // button state
+                0, // xprecision
+                0, // yprecision
+                0, // deviceid
+                0, // edgeflags
+                0, // source
+                0 // flags
+                );
+    }
+
+    private class TestListener implements DocumentHolder.EventListener {
+        private boolean mActivated = false;
+        private boolean mSelected = false;
+
+        public void assertActivated() {
+            assertTrue(mActivated);
+            assertFalse(mSelected);
+        }
+
+        public void assertSelected() {
+            assertTrue(mSelected);
+            assertFalse(mActivated);
+        }
+
+        @Override
+        public boolean onActivate(DocumentHolder doc) {
+            mActivated = true;
+            return true;
+        }
+
+        @Override
+        public boolean onSelect(DocumentHolder doc) {
+            mSelected = true;
+            return true;
+        }
+
+    }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index 5ce1823..2244be9 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -51,7 +51,7 @@
     public void setUp() {
 
         final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
-        mModel = new TestModel(testContext, AUTHORITY);
+        mModel = new TestModel(AUTHORITY);
         mModel.update(NAMES);
 
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java
index a5f0656..83299f0 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java
@@ -38,7 +38,6 @@
 import java.util.List;
 import java.util.Random;
 import java.util.Set;
-import java.util.concurrent.CountDownLatch;
 
 @SmallTest
 public class ModelTest extends AndroidTestCase {
@@ -96,7 +95,7 @@
         r.cursor = cursor;
 
         // Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
-        model = new Model(context);
+        model = new Model();
         model.addUpdateListener(new DummyListener());
         model.update(r);
     }
@@ -303,16 +302,6 @@
         }
     }
 
-    // Tests that Model.delete works correctly.
-    public void testDelete() throws Exception {
-        // Simulate deleting 2 files.
-        List<DocumentInfo> docsBefore = getDocumentInfo(2, 3);
-        delete(2, 3);
-
-        provider.assertWasDeleted(docsBefore.get(0));
-        provider.assertWasDeleted(docsBefore.get(1));
-    }
-
     private void setupTestContext() {
         final MockContentResolver resolver = new MockContentResolver();
         context = new ContextWrapper(getContext()) {
@@ -335,29 +324,6 @@
         return s;
     }
 
-    private void delete(int... positions) throws InterruptedException {
-        Selection s = positionToSelection(positions);
-        final CountDownLatch latch = new CountDownLatch(1);
-
-        model.delete(
-                s,
-                new Model.DeletionListener() {
-                    @Override
-                    public void onError() {
-                        latch.countDown();
-                    }
-                    @Override
-                    void onCompletion() {
-                        latch.countDown();
-                    }
-                });
-        latch.await();
-    }
-
-    private List<DocumentInfo> getDocumentInfo(int... positions) {
-        return model.getDocuments(positionToSelection(positions));
-    }
-
     private static class DummyListener implements Model.UpdateListener {
         public void onModelUpdate(Model model) {}
         public void onModelUpdateFailed(Exception e) {}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
index 7a3b6d4..d3ef9aa 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
@@ -23,7 +23,6 @@
 
 import com.android.documentsui.TestInputEvent;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
-
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -55,13 +54,21 @@
         mManager.addCallback(mCallback);
     }
 
-    public void testMouseClick_StartsSelectionMode() {
-        click(7);
+    public void testSelection() {
+        // Check selection.
+        mManager.toggleSelection(items.get(7));
         assertSelection(items.get(7));
+        // Check deselection.
+        mManager.toggleSelection(items.get(7));
+        assertSelectionSize(0);
     }
 
-    public void testMouseClick_NotifiesSelectionChanged() {
-        click(7);
+    public void testSelection_NotifiesSelectionChanged() {
+        // Selection should notify.
+        mManager.toggleSelection(items.get(7));
+        mCallback.assertSelectionChanged();
+        // Deselection should notify.
+        mManager.toggleSelection(items.get(7));
         mCallback.assertSelectionChanged();
     }
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
index 5c04db9..7920c50 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
@@ -334,5 +334,10 @@
         public void focusItem(int i) {
             throw new UnsupportedOperationException();
         }
+
+        @Override
+        public boolean isLayoutItem(int adapterPosition) {
+            return false;
+        }
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
index 398885c..7c324e7 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
@@ -23,15 +23,12 @@
 import android.support.v7.widget.RecyclerView;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.SparseArray;
 import android.view.ViewGroup;
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.RootCursorWrapper;
 import com.android.documentsui.State;
 
-import java.util.List;
-
 @SmallTest
 public class SectionBreakDocumentsAdapterWrapperTest extends AndroidTestCase {
 
@@ -57,7 +54,7 @@
         final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
 
-        mModel = new TestModel(testContext, AUTHORITY);
+        mModel = new TestModel(AUTHORITY);
         mAdapter = new SectionBreakDocumentsAdapterWrapper(
             env,
             new ModelBackedDocumentsAdapter(
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java
index f9cd3b2..d8c29db 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java
@@ -16,16 +16,13 @@
 
 package com.android.documentsui.dirlist;
 
-import android.content.Context;
 import android.database.MatrixCursor;
 import android.provider.DocumentsContract.Document;
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.RootCursorWrapper;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 
 import java.util.Random;
-import java.util.Set;
 
 public class TestModel extends Model {
 
@@ -39,14 +36,9 @@
     };
 
     private final String mAuthority;
-    private Set<String> mDeleted;
 
-    /**
-     * Creates a new context. context must be configured with provider for authority.
-     * @see TestContext#createStorageTestContext(Context, String).
-     */
-    public TestModel(Context context, String authority) {
-        super(context);
+    public TestModel(String authority) {
+        super();
         mAuthority = authority;
     }
 
@@ -75,12 +67,4 @@
     String idForPosition(int p) {
         return createModelId(mAuthority, Integer.toString(p));
     }
-
-    @Override
-    public void delete(Selection selected, DeletionListener listener) {
-        for (String id : selected.getAll()) {
-            mDeleted.add(id);
-        }
-        listener.onCompletion();
-    }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
index c4cfd3a..0e79561 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
@@ -110,4 +110,9 @@
     @Override
     public void focusItem(int position) {
     }
+
+    @Override
+    public boolean isLayoutItem(int adapterPosition) {
+        return false;
+    }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
similarity index 72%
rename from packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java
rename to packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
index f57ce53..ec21150 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
@@ -16,72 +16,18 @@
 
 package com.android.documentsui.services;
 
-import static com.android.documentsui.StubProvider.ROOT_0_ID;
-import static com.android.documentsui.StubProvider.ROOT_1_ID;
 import static com.google.common.collect.Lists.newArrayList;
 
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
 import android.net.Uri;
-import android.os.RemoteException;
 import android.provider.DocumentsContract;
-import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
-import com.android.documentsui.DocumentsProviderHelper;
-import com.android.documentsui.StubProvider;
 import com.android.documentsui.model.DocumentInfo;
-import com.android.documentsui.model.RootInfo;
 
 import java.util.List;
 
 @MediumTest
-public abstract class BaseCopyJobTest extends AndroidTestCase {
-
-    static String AUTHORITY = StubProvider.DEFAULT_AUTHORITY;
-    static final byte[] HAM_BYTES = "ham and cheese".getBytes();
-    static final byte[] FRUITY_BYTES = "I love fruit cakes!".getBytes();
-
-    Context mContext;
-    ContentResolver mResolver;
-    ContentProviderClient mClient;
-    DocumentsProviderHelper mDocs;
-    TestJobListener mJobListener;
-    RootInfo mSrcRoot;
-    RootInfo mDestRoot;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mJobListener = new TestJobListener();
-
-        // NOTE: Must be the "target" context, else security checks in content provider will fail.
-        mContext = getContext();
-        mResolver = mContext.getContentResolver();
-
-        mClient = mResolver.acquireContentProviderClient(AUTHORITY);
-        mDocs = new DocumentsProviderHelper(AUTHORITY, mClient);
-
-        initTestFiles();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        resetStorage();
-        mClient.release();
-        super.tearDown();
-    }
-
-    private void resetStorage() throws RemoteException {
-        mClient.call("clear", null, null);
-    }
-
-    private void initTestFiles() throws RemoteException {
-        mSrcRoot = mDocs.getRoot(ROOT_0_ID);
-        mDestRoot = mDocs.getRoot(ROOT_1_ID);
-    }
+public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJobTest<T> {
 
     public void runCopyFilesTest() throws Exception {
         Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
@@ -174,9 +120,9 @@
 
     public void runNoCopyDirToDescendentTest() throws Exception {
         Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
-        Uri descDir = mDocs.createFolder(testDir, "theDescendent");
+        Uri destDir = mDocs.createFolder(testDir, "theDescendent");
 
-        createJob(newArrayList(testDir), descDir).run();
+        createJob(newArrayList(testDir), destDir).run();
 
         mJobListener.waitForFinished();
         mJobListener.assertFailed();
@@ -201,10 +147,11 @@
         mDocs.assertChildCount(mDestRoot, 0);
     }
 
-    final CopyJob createJob(List<Uri> srcs) throws Exception {
+    /**
+     * Creates a job with a stack consisting to the default destination.
+     */
+    final T createJob(List<Uri> srcs) throws Exception {
         Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
         return createJob(srcs, destination);
     }
-
-    abstract CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception;
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
new file mode 100644
index 0000000..691af6a
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.services;
+
+import static com.android.documentsui.StubProvider.ROOT_0_ID;
+import static com.android.documentsui.StubProvider.ROOT_1_ID;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.DocumentsContract;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.documentsui.DocumentsProviderHelper;
+import com.android.documentsui.StubProvider;
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.model.RootInfo;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+@MediumTest
+public abstract class AbstractJobTest<T extends Job> extends AndroidTestCase {
+
+    static String AUTHORITY = StubProvider.DEFAULT_AUTHORITY;
+    static final byte[] HAM_BYTES = "ham and cheese".getBytes();
+    static final byte[] FRUITY_BYTES = "I love fruit cakes!".getBytes();
+
+    Context mContext;
+    ContentResolver mResolver;
+    ContentProviderClient mClient;
+    DocumentsProviderHelper mDocs;
+    TestJobListener mJobListener;
+    RootInfo mSrcRoot;
+    RootInfo mDestRoot;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mJobListener = new TestJobListener();
+
+        // NOTE: Must be the "target" context, else security checks in content provider will fail.
+        mContext = getContext();
+        mResolver = mContext.getContentResolver();
+
+        mClient = mResolver.acquireContentProviderClient(AUTHORITY);
+        mDocs = new DocumentsProviderHelper(AUTHORITY, mClient);
+
+        initTestFiles();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        resetStorage();
+        mClient.release();
+        super.tearDown();
+    }
+
+    private void resetStorage() throws RemoteException {
+        mClient.call("clear", null, null);
+    }
+
+    private void initTestFiles() throws RemoteException {
+        mSrcRoot = mDocs.getRoot(ROOT_0_ID);
+        mDestRoot = mDocs.getRoot(ROOT_1_ID);
+    }
+
+    final T createJob(List<Uri> srcs, Uri destination) throws Exception {
+        DocumentStack stack = new DocumentStack();
+        stack.push(DocumentInfo.fromUri(mResolver, destination));
+
+        List<DocumentInfo> srcDocs = Lists.newArrayList();
+        for (Uri src : srcs) {
+            srcDocs.add(DocumentInfo.fromUri(mResolver, src));
+        }
+
+        return createJob(srcDocs, stack);
+    }
+
+    abstract T createJob(List<DocumentInfo> srcs, DocumentStack destination) throws Exception;
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
index c0ce993..1acf2dc 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
@@ -16,18 +16,15 @@
 
 package com.android.documentsui.services;
 
-import android.net.Uri;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 
-import com.google.common.collect.Lists;
-
 import java.util.List;
 
 @MediumTest
-public class CopyJobTest extends BaseCopyJobTest {
+public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
 
     public void testCopyFiles() throws Exception {
         runCopyFilesTest();
@@ -62,16 +59,8 @@
     }
 
     @Override
-    CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
-        DocumentStack stack = new DocumentStack();
-        stack.push(DocumentInfo.fromUri(mResolver, destination));
-
-        List<DocumentInfo> srcDocs = Lists.newArrayList();
-        for (Uri src : srcs) {
-            srcDocs.add(DocumentInfo.fromUri(mResolver, src));
-        }
-
+    CopyJob createJob(List<DocumentInfo> srcs, DocumentStack stack) throws Exception {
         return new CopyJob(
-                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
+                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
new file mode 100644
index 0000000..d6d10239
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.services;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+
+import java.util.List;
+
+@MediumTest
+public class DeleteJobTest extends AbstractJobTest<DeleteJob> {
+
+    public void testDeleteFiles() throws Exception {
+        Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
+        mDocs.writeDocument(testFile1, HAM_BYTES);
+
+        Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
+        mDocs.writeDocument(testFile2, FRUITY_BYTES);
+
+        createJob(newArrayList(testFile1, testFile2)).run();
+        mJobListener.waitForFinished();
+
+        mDocs.assertChildCount(mSrcRoot, 0);
+    }
+
+    /**
+     * Creates a job with a stack consisting to the default src directory.
+     */
+    private final DeleteJob createJob(List<Uri> srcs) throws Exception {
+        Uri stack = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
+        return createJob(srcs, stack);
+    }
+
+    @Override
+    DeleteJob createJob(List<DocumentInfo> srcs, DocumentStack stack) throws Exception {
+        return new DeleteJob(
+                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
+    }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
index d55b6f0..4d5392e 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
@@ -114,8 +114,11 @@
     public void testShutdownStopsExecutor_AfterSuccess() throws Exception {
         startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
 
-        mExecutor.isAlive();
+        mExecutor.assertAlive();
+
         mExecutor.runAll();
+        shutdownService();
+
         mExecutor.assertShutdown();
     }
 
@@ -126,6 +129,8 @@
         mJobFactory.jobs.get(0).fail(ALPHA_DOC);
 
         mExecutor.runAll();
+        shutdownService();
+
         mExecutor.assertShutdown();
     }
 
@@ -137,6 +142,8 @@
         mJobFactory.jobs.get(1).fail(GAMMA_DOC);
 
         mExecutor.runAll();
+        shutdownService();
+
         mExecutor.assertShutdown();
     }
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
index 5e41524..7edfcdb 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
@@ -16,18 +16,15 @@
 
 package com.android.documentsui.services;
 
-import android.net.Uri;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 
-import com.google.common.collect.Lists;
-
 import java.util.List;
 
 @MediumTest
-public class MoveJobTest extends BaseCopyJobTest {
+public class MoveJobTest extends AbstractCopyJobTest<MoveJob> {
 
     public void testMoveFiles() throws Exception {
         runCopyFilesTest();
@@ -82,16 +79,8 @@
     }
 
     @Override
-    CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
-        DocumentStack stack = new DocumentStack();
-        stack.push(DocumentInfo.fromUri(mResolver, destination));
-
-        List<DocumentInfo> srcDocs = Lists.newArrayList();
-        for (Uri src : srcs) {
-            srcDocs.add(DocumentInfo.fromUri(mResolver, src));
-        }
-
+    MoveJob createJob(List<DocumentInfo> srcs, DocumentStack stack) throws Exception {
         return new MoveJob(
-                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
+                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
index 72da9a1..9c58780 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
@@ -46,9 +46,6 @@
         assertTrue(mStarted);
     }
 
-    @Override
-    void cleanup() {}
-
     void fail(DocumentInfo doc) {
         onFileFailed(doc);
     }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java
index 5c39b78..4d417cf 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java
@@ -146,7 +146,7 @@
         scheduled.get(taskIndex).runnable.run();
     }
 
-    public void isAlive() {
+    public void assertAlive() {
         assertFalse(isShutdown());
     }
 
diff --git a/packages/SystemUI/res/layout/signal_cluster_view.xml b/packages/SystemUI/res/layout/signal_cluster_view.xml
index 198e658..c634cd6 100644
--- a/packages/SystemUI/res/layout/signal_cluster_view.xml
+++ b/packages/SystemUI/res/layout/signal_cluster_view.xml
@@ -77,7 +77,7 @@
     </FrameLayout>
     <View
         android:id="@+id/wifi_signal_spacer"
-        android:layout_width="4dp"
+        android:layout_width="@dimen/status_bar_wifi_signal_spacer_width"
         android:layout_height="4dp"
         android:visibility="gone"
         />
@@ -112,7 +112,7 @@
     </FrameLayout>
     <View
         android:id="@+id/wifi_airplane_spacer"
-        android:layout_width="4dp"
+        android:layout_width="@dimen/status_bar_airplane_spacer_width"
         android:layout_height="4dp"
         android:visibility="gone"
         />
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index bea9f78..39c16d7 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -70,7 +70,8 @@
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:singleLine="true"
-                android:paddingStart="7dp"
+                android:paddingStart="@dimen/status_bar_clock_starting_padding"
+                android:paddingEnd="@dimen/status_bar_clock_end_padding"
                 android:gravity="center_vertical|start"
                 />
         </com.android.keyguard.AlphaOptimizedLinearLayout>
diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml
index 943e846..e9448db 100644
--- a/packages/SystemUI/res/layout/system_icons.xml
+++ b/packages/SystemUI/res/layout/system_icons.xml
@@ -30,11 +30,11 @@
         android:id="@+id/signal_cluster"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginStart="2.5dp"/>
+        android:layout_marginStart="@dimen/signal_cluster_margin_start"/>
 
     <!-- battery must be padded below to match assets -->
     <com.android.systemui.BatteryMeterView android:id="@+id/battery"
-        android:layout_height="14.5dp"
-        android:layout_width="9.5dp"
+        android:layout_height="@dimen/status_bar_battery_icon_height"
+        android:layout_width="@dimen/status_bar_battery_icon_width"
         android:layout_marginBottom="@dimen/battery_margin_bottom"/>
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index fe92e4e..d7bd86f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -33,9 +33,30 @@
     <!-- Height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
 
-    <!-- The font size for the clock -->
+    <!-- Height of the battery icon in the status bar. -->
+    <dimen name="status_bar_battery_icon_height">14.5dp</dimen>
+
+    <!-- Width of the battery icon in the status bar. -->
+    <dimen name="status_bar_battery_icon_width">9.5dp</dimen>
+
+    <!-- The font size for the clock in the status bar. -->
     <dimen name="status_bar_clock_size">14sp</dimen>
 
+    <!-- The starting padding for the clock in the status bar. -->
+    <dimen name="status_bar_clock_starting_padding">7dp</dimen>
+
+    <!-- The end padding for the clock in the status bar. -->
+    <dimen name="status_bar_clock_end_padding">0dp</dimen>
+
+    <!-- Spacing after the wifi signals that is present if there are any icons following it. -->
+    <dimen name="status_bar_wifi_signal_spacer_width">4dp</dimen>
+
+    <!-- Spacing before the airplane mode icon if there are any icons preceding it. -->
+    <dimen name="status_bar_airplane_spacer_width">4dp</dimen>
+
+    <!-- The amount to scale each of the status bar icons by. A value of 1 means no scaling. -->
+    <item name="status_bar_icon_scale_factor" format="float" type="dimen">1.0</item>
+
     <!-- Height of a small notification in the status bar-->
     <dimen name="notification_min_height">84dp</dimen>
 
@@ -521,6 +542,9 @@
 
     <dimen name="fake_shadow_size">8dp</dimen>
 
+    <!-- Starting margin before the signal cluster -->
+    <dimen name="signal_cluster_margin_start">2.5dp</dimen>
+
     <!-- Padding between signal cluster and battery icon -->
     <dimen name="signal_cluster_battery_padding">7dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScalingDrawableWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScalingDrawableWrapper.java
new file mode 100644
index 0000000..24277e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScalingDrawableWrapper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+
+/**
+ * An extension of {@link DrawableWrapper} that will take a given Drawable and scale it by
+ * the given factor.
+ */
+class ScalingDrawableWrapper extends DrawableWrapper {
+    private float mScaleFactor;
+
+    public ScalingDrawableWrapper(Drawable drawable, float scaleFactor) {
+        super(drawable);
+        mScaleFactor = scaleFactor;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return (int) (super.getIntrinsicWidth() * mScaleFactor);
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return (int) (super.getIntrinsicHeight() * mScaleFactor);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
index 68e483c..6801e5f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
@@ -16,16 +16,18 @@
 
 package com.android.systemui.statusbar;
 
+import android.annotation.DrawableRes;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Resources;
 import android.graphics.Color;
-import android.graphics.PorterDuff;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
 import android.telephony.SubscriptionInfo;
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -86,10 +88,11 @@
     View mWifiSignalSpacer;
     LinearLayout mMobileSignalGroup;
 
-    private int mWideTypeIconStartPadding;
-    private int mSecondaryTelephonyPadding;
-    private int mEndPadding;
-    private int mEndPaddingNothingVisible;
+    private final int mWideTypeIconStartPadding;
+    private final int mSecondaryTelephonyPadding;
+    private final int mEndPadding;
+    private final int mEndPaddingNothingVisible;
+    private final float mIconScaleFactor;
 
     private boolean mBlockAirplane;
     private boolean mBlockMobile;
@@ -106,6 +109,17 @@
 
     public SignalClusterView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+
+        Resources res = getResources();
+        mWideTypeIconStartPadding = res.getDimensionPixelSize(R.dimen.wide_type_icon_start_padding);
+        mSecondaryTelephonyPadding = res.getDimensionPixelSize(R.dimen.secondary_telephony_padding);
+        mEndPadding = res.getDimensionPixelSize(R.dimen.signal_cluster_battery_padding);
+        mEndPaddingNothingVisible = res.getDimensionPixelSize(
+                R.dimen.no_signal_cluster_battery_padding);
+
+        TypedValue typedValue = new TypedValue();
+        res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        mIconScaleFactor = typedValue.getFloat();
     }
 
     @Override
@@ -146,19 +160,6 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mWideTypeIconStartPadding = getContext().getResources().getDimensionPixelSize(
-                R.dimen.wide_type_icon_start_padding);
-        mSecondaryTelephonyPadding = getContext().getResources().getDimensionPixelSize(
-                R.dimen.secondary_telephony_padding);
-        mEndPadding = getContext().getResources().getDimensionPixelSize(
-                R.dimen.signal_cluster_battery_padding);
-        mEndPaddingNothingVisible = getContext().getResources().getDimensionPixelSize(
-                R.dimen.no_signal_cluster_battery_padding);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
 
         mVpn            = (ImageView) findViewById(R.id.vpn);
         mEthernetGroup  = (ViewGroup) findViewById(R.id.ethernet_combo);
@@ -174,6 +175,32 @@
         mWifiAirplaneSpacer =         findViewById(R.id.wifi_airplane_spacer);
         mWifiSignalSpacer =           findViewById(R.id.wifi_signal_spacer);
         mMobileSignalGroup = (LinearLayout) findViewById(R.id.mobile_signal_group);
+
+        maybeScaleVpnAndNoSimsIcons();
+    }
+
+    /**
+     * Extracts the icon off of the VPN and no sims views and maybe scale them by
+     * {@link #mIconScaleFactor}. Note that the other icons are not scaled here because they are
+     * dynamic. As such, they need to be scaled each time the icon changes in {@link #apply()}.
+     */
+    private void maybeScaleVpnAndNoSimsIcons() {
+        if (mIconScaleFactor == 1.f) {
+            return;
+        }
+
+        mVpn.setImageDrawable(new ScalingDrawableWrapper(mVpn.getDrawable(), mIconScaleFactor));
+
+        mNoSims.setImageDrawable(
+                new ScalingDrawableWrapper(mNoSims.getDrawable(), mIconScaleFactor));
+        mNoSimsDark.setImageDrawable(
+                new ScalingDrawableWrapper(mNoSimsDark.getDrawable(), mIconScaleFactor));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
         for (PhoneState state : mPhoneStates) {
             mMobileSignalGroup.addView(state.mMobileGroup);
         }
@@ -185,14 +212,7 @@
 
     @Override
     protected void onDetachedFromWindow() {
-        mVpn            = null;
-        mEthernetGroup  = null;
-        mEthernet       = null;
-        mWifiGroup      = null;
-        mWifi           = null;
-        mAirplane       = null;
         mMobileSignalGroup.removeAllViews();
-        mMobileSignalGroup = null;
         TunerService.get(mContext).removeTunable(this);
 
         super.onDetachedFromWindow();
@@ -383,8 +403,8 @@
 
         if (mEthernetVisible) {
             if (mLastEthernetIconId != mEthernetIconId) {
-                mEthernet.setImageResource(mEthernetIconId);
-                mEthernetDark.setImageResource(mEthernetIconId);
+                setIconForView(mEthernet, mEthernetIconId);
+                setIconForView(mEthernetDark, mEthernetIconId);
                 mLastEthernetIconId = mEthernetIconId;
             }
             mEthernetGroup.setContentDescription(mEthernetDescription);
@@ -397,11 +417,10 @@
                 String.format("ethernet: %s",
                     (mEthernetVisible ? "VISIBLE" : "GONE")));
 
-
         if (mWifiVisible) {
             if (mWifiStrengthId != mLastWifiStrengthId) {
-                mWifi.setImageResource(mWifiStrengthId);
-                mWifiDark.setImageResource(mWifiStrengthId);
+                setIconForView(mWifi, mWifiStrengthId);
+                setIconForView(mWifiDark, mWifiStrengthId);
                 mLastWifiStrengthId = mWifiStrengthId;
             }
             mWifiGroup.setContentDescription(mWifiDescription);
@@ -428,7 +447,7 @@
 
         if (mIsAirplaneMode) {
             if (mLastAirplaneIconId != mAirplaneIconId) {
-                mAirplane.setImageResource(mAirplaneIconId);
+                setIconForView(mAirplane, mAirplaneIconId);
                 mLastAirplaneIconId = mAirplaneIconId;
             }
             mAirplane.setContentDescription(mAirplaneContentDescription);
@@ -456,6 +475,21 @@
         setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0);
     }
 
+    /**
+     * Sets the given drawable id on the view. This method will also scale the icon by
+     * {@link #mIconScaleFactor} if appropriate.
+     */
+    private void setIconForView(ImageView imageView, @DrawableRes int iconId) {
+        // Using the imageView's context to retrieve the Drawable so that theme is preserved.
+        Drawable icon = imageView.getContext().getDrawable(iconId);
+
+        if (mIconScaleFactor == 1.f) {
+            imageView.setImageDrawable(icon);
+        } else {
+            imageView.setImageDrawable(new ScalingDrawableWrapper(icon, mIconScaleFactor));
+        }
+    }
+
     public void setIconTint(int tint, float darkIntensity) {
         boolean changed = tint != mIconTint || darkIntensity != mDarkIntensity;
         mIconTint = tint;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index de7a8db..5a7cf86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -24,10 +24,13 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.graphics.drawable.ScaleDrawable;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
 import android.view.ViewDebug;
 import android.view.accessibility.AccessibilityEvent;
 import com.android.internal.statusbar.StatusBarIcon;
@@ -189,12 +192,24 @@
      * @return Drawable for this item, or null if the package or item could not
      *         be found
      */
-    public static Drawable getIcon(Context context, StatusBarIcon icon) {
-        int userId = icon.user.getIdentifier();
+    public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
+        int userId = statusBarIcon.user.getIdentifier();
         if (userId == UserHandle.USER_ALL) {
             userId = UserHandle.USER_SYSTEM;
         }
-        return icon.icon.loadDrawableAsUser(context, userId);
+
+        Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
+
+        TypedValue typedValue = new TypedValue();
+        context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        float scaleFactor = typedValue.getFloat();
+
+        // No need to scale the icon, so return it as is.
+        if (scaleFactor == 1.f) {
+            return icon;
+        }
+
+        return new ScalingDrawableWrapper(icon, scaleFactor);
     }
 
     public StatusBarIcon getStatusBarIcon() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
index 2db0804..8bf4572 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
@@ -162,11 +162,16 @@
         mDockWindowTouchSlopExceeded = false;
         mTouchDownX = (int) event.getX();
         mTouchDownY = (int) event.getY();
-        View recentsButton = mNavigationBarView.getRecentsButton();
-        mDownOnRecents = mTouchDownX >= recentsButton.getLeft()
-                && mTouchDownX <= recentsButton.getRight()
-                && mTouchDownY >= recentsButton.getTop()
-                && mTouchDownY <= recentsButton.getBottom();
+
+        if (mNavigationBarView != null) {
+            View recentsButton = mNavigationBarView.getRecentsButton();
+            if (recentsButton != null) {
+                mDownOnRecents = mTouchDownX >= recentsButton.getLeft()
+                        && mTouchDownX <= recentsButton.getRight()
+                        && mTouchDownY >= recentsButton.getTop()
+                        && mTouchDownY <= recentsButton.getBottom();
+            }
+        }
     }
 
     private boolean handleDragActionMoveEvent(MotionEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index efa8f5b..5309903 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -21,6 +21,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
+import android.annotation.Nullable;
 import android.app.ActivityManagerNative;
 import android.app.StatusBarManager;
 import android.content.Context;
@@ -131,12 +132,15 @@
         }
 
         public void onBackAltCleared() {
+            View backButton = getBackButton();
+            View homeButton = getHomeButton();
+
             // When dismissing ime during unlock, force the back button to run the same appearance
             // animation as home (if we catch this condition early enough).
-            if (!mBackTransitioning && getBackButton().getVisibility() == VISIBLE
-                    && mHomeAppearing && getHomeButton().getAlpha() == 0) {
+            if (backButton != null && !mBackTransitioning && backButton.getVisibility() == VISIBLE
+                    && mHomeAppearing && homeButton != null && getHomeButton().getAlpha() == 0) {
                 getBackButton().setAlpha(0);
-                ValueAnimator a = ObjectAnimator.ofFloat(getBackButton(), "alpha", 0, 1);
+                ValueAnimator a = ObjectAnimator.ofFloat(backButton, "alpha", 0, 1);
                 a.setStartDelay(mStartDelay);
                 a.setDuration(mDuration);
                 a.setInterpolator(mInterpolator);
@@ -222,7 +226,10 @@
     }
 
     public void abortCurrentGesture() {
-        getHomeButton().abortCurrentGesture();
+        View homeButton = getHomeButton();
+        if (homeButton != null) {
+            getHomeButton().abortCurrentGesture();
+        }
     }
 
     private H mHandler = new H();
@@ -235,26 +242,34 @@
         return mRotatedViews;
     }
 
+    // The following Buttons can possibly return null if NavigationBarView is extended to provide
+    // a different layout and the buttons do not exist in that new layout.
+    @Nullable
     public KeyButtonView getRecentsButton() {
         return (KeyButtonView) getCurrentView().findViewById(R.id.recent_apps);
     }
 
+    @Nullable
     public View getMenuButton() {
         return getCurrentView().findViewById(R.id.menu);
     }
 
+    @Nullable
     public View getBackButton() {
         return getCurrentView().findViewById(R.id.back);
     }
 
+    @Nullable
     public KeyButtonView getHomeButton() {
         return (KeyButtonView) getCurrentView().findViewById(R.id.home);
     }
 
+    @Nullable
     public View getImeSwitchButton() {
         return getCurrentView().findViewById(R.id.ime_switcher);
     }
 
+    @Nullable
     public View getAppShelf() {
         return getCurrentView().findViewById(R.id.app_shelf);
     }
@@ -329,19 +344,27 @@
                 ? getBackIconWithAlt(mCarMode, mVertical)
                 : getBackIcon(mCarMode, mVertical);
 
-        ((ImageView) getBackButton()).setImageDrawable(backIcon);
+        View backButton = getBackButton();
+        if (backButton != null && backButton instanceof ImageView) {
+            ((ImageView) backButton).setImageDrawable(backIcon);
+        }
 
-        ((ImageView) getRecentsButton()).setImageDrawable(
-                mVertical ? mRecentLandIcon : mRecentIcon);
+        ImageView recentsButton = getRecentsButton();
+        if (recentsButton != null) {
+            recentsButton.setImageDrawable(mVertical ? mRecentLandIcon : mRecentIcon);
+        }
 
-        if (mCarMode) {
-            ((ImageView) getHomeButton()).setImageDrawable(mHomeCarModeIcon);
-        } else {
-            ((ImageView) getHomeButton()).setImageDrawable(mHomeDefaultIcon);
+        ImageView homeButton = getHomeButton();
+        if (homeButton != null) {
+            homeButton.setImageDrawable(mCarMode ? mHomeCarModeIcon : mHomeDefaultIcon);
         }
 
         final boolean showImeButton = ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0);
-        getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
+        View imeSwitchButton = getImeSwitchButton();
+        if (imeSwitchButton != null) {
+            imeSwitchButton.setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
+        }
+
         // Update menu button in case the IME state has changed.
         setMenuVisibility(mShowMenu, true);
 
@@ -382,9 +405,20 @@
             disableRecent = false;
         }
 
-        getBackButton()   .setVisibility(disableBack       ? View.INVISIBLE : View.VISIBLE);
-        getHomeButton()   .setVisibility(disableHome       ? View.INVISIBLE : View.VISIBLE);
-        getRecentsButton().setVisibility(disableRecent     ? View.INVISIBLE : View.VISIBLE);
+        View backButton = getBackButton();
+        if (backButton != null) {
+            backButton.setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE);
+        }
+
+        View homeButton = getHomeButton();
+        if (homeButton != null) {
+            homeButton.setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE);
+        }
+
+        View recentsButton = getRecentsButton();
+        if (recentsButton != null) {
+            recentsButton.setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
+        }
 
         // The app shelf, if it exists, follows the visibility of the home button.
         View appShelf = getAppShelf();
@@ -475,7 +509,11 @@
         // Only show Menu if IME switcher not shown.
         final boolean shouldShow = mShowMenu &&
                 ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0);
-        getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
+
+        View menuButton = getMenuButton();
+        if (menuButton != null) {
+            menuButton.setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
+        }
     }
 
     @Override
@@ -489,7 +527,10 @@
 
         mCurrentView = mRotatedViews[Surface.ROTATION_0];
 
-        getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
+        View imeSwitchButton = getImeSwitchButton();
+        if (imeSwitchButton != null) {
+            imeSwitchButton.setOnClickListener(mImeSwitcherClickListener);
+        }
 
         updateRTLOrder();
 
@@ -515,9 +556,13 @@
     }
 
     private void updateRecentsIcon(boolean dockedStackExists) {
-        getRecentsButton().setImageResource(dockedStackExists
-                ? R.drawable.ic_sysbar_docked
-                : R.drawable.ic_sysbar_recent);
+        ImageView recentsButton = getRecentsButton();
+
+        if (recentsButton != null) {
+            recentsButton.setImageResource(dockedStackExists
+                    ? R.drawable.ic_sysbar_docked
+                    : R.drawable.ic_sysbar_recent);
+        }
     }
 
     public boolean isVertical() {
@@ -533,7 +578,10 @@
         mCurrentView.setVisibility(View.VISIBLE);
         updateLayoutTransitionsEnabled();
 
-        getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
+        View imeSwitchButton = getImeSwitchButton();
+        if (imeSwitchButton != null) {
+            imeSwitchButton.setOnClickListener(mImeSwitcherClickListener);
+        }
 
         mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 8a5cf26..7a4adbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -1184,14 +1184,26 @@
     private void prepareNavigationBarView() {
         mNavigationBarView.reorient();
 
-        mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener);
-        mNavigationBarView.getRecentsButton().setOnTouchListener(mRecentsPreloadOnTouchListener);
-        mNavigationBarView.getRecentsButton().setLongClickable(true);
-        mNavigationBarView.getRecentsButton().setOnLongClickListener(mRecentsLongClickListener);
-        mNavigationBarView.getBackButton().setLongClickable(true);
-        mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackListener);
-        mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener);
-        mNavigationBarView.getHomeButton().setOnLongClickListener(mLongPressHomeListener);
+        View recentsButton = mNavigationBarView.getRecentsButton();
+        if (recentsButton != null) {
+            recentsButton.setOnClickListener(mRecentsClickListener);
+            recentsButton.setOnTouchListener(mRecentsPreloadOnTouchListener);
+            recentsButton.setLongClickable(true);
+            recentsButton.setOnLongClickListener(mRecentsLongClickListener);
+        }
+
+        View backButton = mNavigationBarView.getBackButton();
+        if (backButton != null) {
+            backButton.setLongClickable(true);
+            backButton.setOnLongClickListener(mLongPressBackListener);
+        }
+
+        View homeButton = mNavigationBarView.getHomeButton();
+        if (homeButton != null) {
+            homeButton.setOnTouchListener(mHomeActionListener);
+            homeButton.setOnLongClickListener(mLongPressHomeListener);
+        }
+
         mAssistManager.onConfigurationChanged();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index b16c98e..64fb066 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -20,6 +20,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
@@ -28,6 +29,7 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
@@ -122,7 +124,10 @@
         notificationIconArea.addView(mNotificationIconAreaInner);
 
         mStatusIconsKeyguard = (LinearLayout) keyguardStatusBar.findViewById(R.id.statusIcons);
+
         mBatteryMeterView = (BatteryMeterView) statusBar.findViewById(R.id.battery);
+        maybeScaleBatteryMeterView(context);
+
         mClock = (TextView) statusBar.findViewById(R.id.clock);
         mLinearOutSlowIn = AnimationUtils.loadInterpolator(mContext,
                 android.R.interpolator.linear_out_slow_in);
@@ -136,6 +141,30 @@
         TunerService.get(mContext).addTunable(this, ICON_BLACKLIST);
     }
 
+    /**
+     * Looks up the scale factor for status bar icons and scales the battery view by that amount
+     * if appropriate.
+     */
+    private void maybeScaleBatteryMeterView(Context context) {
+        Resources res = context.getResources();
+        TypedValue typedValue = new TypedValue();
+
+        res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        float iconScaleFactor = typedValue.getFloat();
+
+        if (iconScaleFactor == 1.f) {
+            return;
+        }
+
+        float batteryHeight = res.getDimension(R.dimen.status_bar_battery_icon_height);
+        float batteryWidth = res.getDimension(R.dimen.status_bar_battery_icon_width);
+
+        LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
+                (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
+
+        mBatteryMeterView.setLayoutParams(scaledLayoutParams);
+    }
+
     @Override
     public void onTuningChanged(String key, String newValue) {
         if (!ICON_BLACKLIST.equals(key)) {
@@ -174,8 +203,12 @@
         boolean blocked = mIconBlacklist.contains(slot);
         StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked);
         view.set(icon);
-        mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));
+
+        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+        lp.setMargins(mIconHPadding, 0, mIconHPadding, 0);
+        mStatusIcons.addView(view, viewIndex, lp);
+
         view = new StatusBarIconView(mContext, slot, null, blocked);
         view.set(icon);
         mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams(
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index d9dd77d..1fc674b 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -2251,7 +2251,10 @@
             // If we didn't actual do a relaunch (indicated by kept==true meaning we kept the old
             // window), we need to clear the replace window settings. Otherwise, we schedule a
             // timeout to remove the old window if the replacing window is not coming in time.
-            mWindowManager.scheduleClearReplacingWindowIfNeeded(topActivity.appToken, !kept);
+            // In case of the pinned stack we don't resize the task during the move, but we will
+            // resize the stack soon after so we want to retain the replacing window.
+            mWindowManager.scheduleClearReplacingWindowIfNeeded(topActivity.appToken,
+                    !kept || stackId == PINNED_STACK_ID);
         }
 
         // The task might have already been running and its visibility needs to be synchronized with
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 482217c..bce7223 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -82,7 +82,7 @@
      * {@link PackageManagerService#mInstallLock}.
      */
     int performDexOpt(PackageParser.Package pkg, String[] instructionSets,
-            boolean inclDependencies, String volumeUuid, boolean useProfiles, boolean extractOnly) {
+            boolean inclDependencies, boolean useProfiles, boolean extractOnly) {
         ArraySet<String> done;
         if (inclDependencies && (pkg.usesLibraries != null || pkg.usesOptionalLibraries != null)) {
             done = new ArraySet<String>();
@@ -97,7 +97,7 @@
                 mDexoptWakeLock.acquire();
             }
             try {
-                return performDexOptLI(pkg, instructionSets, done, volumeUuid, useProfiles,
+                return performDexOptLI(pkg, instructionSets, done, useProfiles,
                         extractOnly);
             } finally {
                 if (useLock) {
@@ -108,7 +108,7 @@
     }
 
     private int performDexOptLI(PackageParser.Package pkg, String[] targetInstructionSets,
-            ArraySet<String> done, String volumeUuid, boolean useProfiles, boolean extractOnly) {
+            ArraySet<String> done, boolean useProfiles, boolean extractOnly) {
         final String[] instructionSets = targetInstructionSets != null ?
                 targetInstructionSets : getAppDexInstructionSets(pkg.applicationInfo);
 
@@ -185,7 +185,7 @@
                 try {
                     mPackageManagerService.mInstaller.dexopt(path, sharedGid,
                             pkg.packageName, dexCodeInstructionSet, dexoptNeeded, oatDir,
-                            dexFlags, volumeUuid, useProfiles);
+                            dexFlags, pkg.volumeUuid, useProfiles);
                     performedDexOpt = true;
                 } catch (InstallerException e) {
                     Slog.w(TAG, "Failed to dexopt", e);
@@ -252,8 +252,7 @@
             if (libPkg != null && !done.contains(libName)) {
                 // TODO: Analyze and investigate if we (should) profile libraries.
                 // Currently this will do a full compilation of the library.
-                performDexOptLI(libPkg, instructionSets, done,
-                        StorageManager.UUID_PRIVATE_INTERNAL, /*useProfiles*/ false,
+                performDexOptLI(libPkg, instructionSets, done, /*useProfiles*/ false,
                         /* extractOnly */ false);
             }
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9d17f8f..f05e45f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6717,7 +6717,7 @@
             synchronized (mInstallLock) {
                 final String[] instructionSets = new String[] { targetInstructionSet };
                 int result = mPackageDexOptimizer.performDexOpt(p, instructionSets,
-                        true /* inclDependencies */, p.volumeUuid, useProfiles, extractOnly);
+                        true /* inclDependencies */, useProfiles, extractOnly);
                 return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
             }
         } finally {
@@ -6762,7 +6762,7 @@
             // Whoever is calling forceDexOpt wants a fully compiled package.
             // Don't use profiles since that may cause compilation to be skipped.
             final int res = mPackageDexOptimizer.performDexOpt(pkg, instructionSets,
-                    true /* inclDependencies */, pkg.volumeUuid, false /* useProfiles */,
+                    true /* inclDependencies */, false /* useProfiles */,
                     false /* extractOnly */);
 
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
@@ -13024,7 +13024,7 @@
                 // Do not run PackageDexOptimizer through the local performDexOpt
                 // method because `pkg` is not in `mPackages` yet.
                 int result = mPackageDexOptimizer.performDexOpt(pkg, null /* instructionSets */,
-                        false /* inclDependencies */, volumeUuid, false /* useProfiles */,
+                        false /* inclDependencies */, false /* useProfiles */,
                         true /* extractOnly */);
                 Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                 if (result == PackageDexOptimizer.DEX_OPT_FAILED) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 3248fe6..1e057aa 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -730,23 +730,15 @@
     @Override
     public void setUserIcon(int userId, Bitmap bitmap) {
         checkManageUsersPermission("update users");
-        long ident = Binder.clearCallingIdentity();
-        try {
-            synchronized (mPackagesLock) {
-                UserData userData = getUserDataNoChecks(userId);
-                if (userData == null || userData.info.partial) {
-                    Slog.w(LOG_TAG, "setUserIcon: unknown user #" + userId);
-                    return;
-                }
-                writeBitmapLP(userData.info, bitmap);
-                writeUserLP(userData);
-            }
-            sendUserInfoChangedBroadcast(userId);
-        } finally {
-            Binder.restoreCallingIdentity(ident);
+        if (hasUserRestriction(UserManager.DISALLOW_SET_USER_ICON, userId)) {
+            Log.w(LOG_TAG, "Cannot set user icon. DISALLOW_SET_USER_ICON is enabled.");
+            return;
         }
+        mLocalService.setUserIcon(userId, bitmap);
     }
 
+
+
     private void sendUserInfoChangedBroadcast(int userId) {
         Intent changedIntent = new Intent(Intent.ACTION_USER_INFO_CHANGED);
         changedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
@@ -2901,6 +2893,25 @@
                 mIsUserManaged.put(userId, isManaged);
             }
         }
+
+        @Override
+        public void setUserIcon(int userId, Bitmap bitmap) {
+            long ident = Binder.clearCallingIdentity();
+            try {
+                synchronized (mPackagesLock) {
+                    UserData userData = getUserDataNoChecks(userId);
+                    if (userData == null || userData.info.partial) {
+                        Slog.w(LOG_TAG, "setUserIcon: unknown user #" + userId);
+                        return;
+                    }
+                    writeBitmapLP(userData.info, bitmap);
+                    writeUserLP(userData);
+                }
+                sendUserInfoChangedBroadcast(userId);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
     }
 
     private class Shell extends ShellCommand {
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index f0ed790..87f505d 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -92,7 +92,8 @@
             UserManager.DISALLOW_RECORD_AUDIO,
             UserManager.DISALLOW_CAMERA,
             UserManager.DISALLOW_RUN_IN_BACKGROUND,
-            UserManager.DISALLOW_DATA_ROAMING
+            UserManager.DISALLOW_DATA_ROAMING,
+            UserManager.DISALLOW_SET_USER_ICON
     );
 
     /**
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 785f166..2732821 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -302,6 +302,13 @@
         }
     }
 
+    void markSurfacesExiting() {
+        for (int i = allAppWindows.size() - 1; i >= 0; i--) {
+            WindowState win = allAppWindows.get(i);
+            win.mExiting = true;
+        }
+    }
+
     /**
      * Checks whether we should save surfaces for this app.
      *
@@ -329,15 +336,14 @@
         if (!hasSavedSurface()) {
             return;
         }
-
-        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG_WM,
-                "Restoring saved surfaces: " + this + ", allDrawn=" + allDrawn);
-
         mAnimatingWithSavedSurface = true;
         for (int i = windows.size() - 1; i >= 0; i--) {
             WindowState ws = windows.get(i);
             ws.restoreSavedSurface();
         }
+        // Mark the app allDrawn since it must be allDrawn at the time
+        // it was first saved.
+        allDrawn = true;
     }
 
     void destroySavedSurfaces() {
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 7b0a8d7..65b91f7 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -360,8 +360,6 @@
         mCurrentX = x;
         mCurrentY = y;
 
-        final int myPid = Process.myPid();
-
         // Move the surface to the given touch
         if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                 TAG_WM, ">>> OPEN TRANSACTION notifyMoveLw");
@@ -376,7 +374,10 @@
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                     TAG_WM, "<<< CLOSE TRANSACTION notifyMoveLw");
         }
+        notifyLocationLw(x, y);
+    }
 
+    void notifyLocationLw(float x, float y) {
         // Tell the affected window
         WindowState touchedWin = getTouchedWinAtPointLw(x, y);
         if (touchedWin == null) {
@@ -392,6 +393,8 @@
             }
         }
         try {
+            final int myPid = Process.myPid();
+
             // have we dragged over a new window?
             if ((touchedWin != mTargetWindow) && (mTargetWindow != null)) {
                 if (DEBUG_DRAG) {
@@ -399,7 +402,7 @@
                 }
                 // force DRAG_EXITED_EVENT if appropriate
                 DragEvent evt = obtainDragEvent(mTargetWindow, DragEvent.ACTION_DRAG_EXITED,
-                        x, y, null, null, null, null, false);
+                        0, 0, null, null, null, null, false);
                 mTargetWindow.mClient.dispatchDragEvent(evt);
                 if (myPid != mTargetWindow.mSession.mPid) {
                     evt.recycle();
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 8aaf430..d8cbbab 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -357,6 +357,8 @@
                 if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                         TAG_WM, "<<< CLOSE TRANSACTION performDrag");
             }
+
+            mService.mDragState.notifyLocationLw(touchX, touchY);
         }
 
         return true;    // success!
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 62d4f36..722c3b4 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -281,8 +281,7 @@
             final int flags = win.mAttrs.flags;
             boolean canBeForceHidden = mPolicy.canBeForceHidden(win, win.mAttrs);
             boolean shouldBeForceHidden = shouldForceHide(win);
-            if (winAnimator.mSurfaceController != null
-                    && winAnimator.mSurfaceController.hasSurface()) {
+            if (winAnimator.hasSurface()) {
                 final boolean wasAnimating = winAnimator.mWasAnimating;
                 final boolean nowAnimating = winAnimator.stepAnimationLocked(mCurrentTime);
                 winAnimator.mWasAnimating = nowAnimating;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4b0015e..d2e2639 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2679,20 +2679,11 @@
             } else {
                 winAnimator.mEnterAnimationPending = false;
                 winAnimator.mEnteringAnimation = false;
-                if (winAnimator.mSurfaceController != null &&
-                        winAnimator.mSurfaceController.hasSurface()) {
+                if (winAnimator.hasSurface() && !win.mExiting) {
                     if (DEBUG_VISIBILITY) Slog.i(TAG_WM, "Relayout invis " + win
                             + ": mExiting=" + win.mExiting);
-                    // If we are using a saved surface to do enter animation, just let the
-                    // animation run and don't destroy the surface. This could happen when
-                    // the app sets visibility to invisible for the first time after resume,
-                    // or when the user exits immediately after a resume. In both cases, we
-                    // don't want to destroy the saved surface.
                     // If we are not currently running the exit animation, we
                     // need to see about starting one.
-                    final boolean notExitingOrAnimating =
-                            !win.mExiting && !win.isAnimatingWithSavedSurface();
-                    result |= notExitingOrAnimating ? RELAYOUT_RES_SURFACE_CHANGED : 0;
                     // We don't want to animate visibility of windows which are pending
                     // replacement. In the case of activity relaunch child windows
                     // could request visibility changes as they are detached from the main
@@ -2700,11 +2691,11 @@
                     // these visibility changes though, we would cause a visual glitch
                     // hiding the window before it's replacement was available.
                     // So we just do nothing on our side.
-                    if (notExitingOrAnimating && win.mWillReplaceWindow == false) {
-                        focusMayChange = tryStartingAnimation(win, winAnimator, isDefaultDisplay,
-                                focusMayChange);
-
+                    if (!win.mWillReplaceWindow) {
+                        focusMayChange = tryStartExitingAnimation(
+                                win, winAnimator, isDefaultDisplay, focusMayChange);
                     }
+                    result |= RELAYOUT_RES_SURFACE_CHANGED;
                 }
 
                 outSurface.release();
@@ -2787,7 +2778,7 @@
         return result;
     }
 
-    private boolean tryStartingAnimation(WindowState win, WindowStateAnimator winAnimator,
+    private boolean tryStartExitingAnimation(WindowState win, WindowStateAnimator winAnimator,
             boolean isDefaultDisplay, boolean focusMayChange) {
         // Try starting an animation; if there isn't one, we
         // can destroy the surface right away.
@@ -4240,6 +4231,7 @@
                         }
                     }
                 } else {
+                    wtoken.markSurfacesExiting();
                     mClosingApps.add(wtoken);
                     wtoken.mEnteringAnimation = false;
                 }
@@ -10278,10 +10270,6 @@
         return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
     }
 
-    void scheduleSurfaceDestroy(WindowState win) {
-        mDestroySurface.add(win);
-    }
-
     @Override
     public void registerDockedStackListener(IDockedStackListener listener) {
         if (!checkCallingPermission(android.Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS,
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index e8a02b0..dca7735 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1768,45 +1768,69 @@
         return mAppToken != null && mAppToken.mAnimatingWithSavedSurface;
     }
 
-    // Returns true if the surface is saved.
-    boolean destroyOrSaveSurface() {
-        Task task = getTask();
+    private boolean shouldSaveSurface() {
         if (ActivityManager.isLowRamDeviceStatic()) {
             // Don't save surfaces on Svelte devices.
-            mSurfaceSaved = false;
-        } else if (task == null || task.inHomeStack()
-                || task.getTopVisibleAppToken() != mAppToken) {
+            return false;
+        }
+
+        if (isChildWindow()) {
+            return false;
+        }
+
+        Task task = getTask();
+        if (task == null || task.inHomeStack()) {
             // Don't save surfaces for home stack apps. These usually resume and draw
             // first frame very fast. Saving surfaces are mostly a waste of memory.
-            // Don't save if the window is not the topmost window.
-            mSurfaceSaved = false;
-        } else if (isChildWindow()) {
-            mSurfaceSaved = false;
-        } else {
-            mSurfaceSaved = mAppToken.shouldSaveSurface();
+            return false;
         }
-        if (mSurfaceSaved == false) {
+
+        final AppWindowToken taskTop = task.getTopVisibleAppToken();
+        if (taskTop != null && taskTop != mAppToken) {
+            // Don't save if the window is not the topmost window.
+            return false;
+        }
+
+        return mAppToken.shouldSaveSurface();
+    }
+
+    void destroyOrSaveSurface() {
+        mSurfaceSaved = shouldSaveSurface();
+        if (mSurfaceSaved) {
+            if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
+                Slog.v(TAG, "Saving surface: " + this);
+            }
+
+            mWinAnimator.hide("saved surface");
+            mWinAnimator.mDrawState = WindowStateAnimator.NO_SURFACE;
+            setHasSurface(false);
+        } else {
             mWinAnimator.destroySurfaceLocked();
         }
-        return mSurfaceSaved;
     }
 
     public void destroySavedSurface() {
-        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG, "Destroying saved surface: " + this);
         if (mSurfaceSaved) {
+            if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
+                Slog.v(TAG, "Destroying saved surface: " + this);
+            }
             mWinAnimator.destroySurfaceLocked();
         }
     }
 
+    public void restoreSavedSurface() {
+        mSurfaceSaved = false;
+        setHasSurface(true);
+        mWinAnimator.mDrawState = WindowStateAnimator.READY_TO_SHOW;
+        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
+            Slog.v(TAG, "Restoring saved surface: " + this);
+        }
+    }
+
     public boolean hasSavedSurface() {
         return mSurfaceSaved;
     }
 
-    public void restoreSavedSurface() {
-        mSurfaceSaved = false;
-        mWinAnimator.mDrawState = WindowStateAnimator.READY_TO_SHOW;
-    }
-
     @Override
     public boolean isDefaultDisplay() {
         final DisplayContent displayContent = getDisplayContent();
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index da4e191..cffcc5d 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -468,7 +468,7 @@
         if (WindowManagerService.localLOGV) Slog.v(
                 TAG, "Exit animation finished in " + this
                 + ": remove=" + mWin.mRemoveOnExit);
-        if (mSurfaceController != null && mSurfaceController.hasSurface()) {
+        if (hasSurface()) {
             mService.mDestroySurface.add(mWin);
             mWin.mDestroying = true;
             hide("finishExit");
@@ -734,6 +734,11 @@
         mTmpSize.bottom += scale * (attrs.surfaceInsets.top + attrs.surfaceInsets.bottom);
     }
 
+    boolean hasSurface() {
+        return !mWin.mSurfaceSaved
+                && mSurfaceController != null && mSurfaceController.hasSurface();
+    }
+
     void destroySurfaceLocked() {
         final AppWindowToken wtoken = mWin.mAppToken;
         if (wtoken != null) {
@@ -1229,7 +1234,7 @@
 
     void prepareSurfaceLocked(final boolean recoveringMemory) {
         final WindowState w = mWin;
-        if (mSurfaceController == null || !mSurfaceController.hasSurface()) {
+        if (!hasSurface()) {
             if (w.mOrientationChanging) {
                 if (DEBUG_ORIENTATION) {
                     Slog.v(TAG, "Orientation change skips hidden " + w);
@@ -1311,7 +1316,7 @@
                     w.mOrientationChanging = false;
                 }
             }
-            if (mSurfaceController != null && mSurfaceController.hasSurface()) {
+            if (hasSurface()) {
                 w.mToken.hasVisible = true;
             }
         } else {
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index ab6667a..9012b98 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -1161,6 +1161,9 @@
                 appAnimator.animation = null;
             }
             wtoken.inPendingTransaction = false;
+
+            wtoken.restoreSavedSurfaces();
+
             if (!mService.setTokenVisibilityLocked(
                     wtoken, animLp, true, transit, false, voiceInteraction)){
                 // This token isn't going to be animating. Add it to the list of tokens to
@@ -1217,8 +1220,6 @@
             if (mService.mAppTransition.isNextAppTransitionThumbnailUp()) {
                 createThumbnailAppAnimator(transit, wtoken, topOpeningLayer, topClosingLayer);
             }
-
-            wtoken.restoreSavedSurfaces();
         }
         return topOpeningApp;
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index a23e74b..eb75914 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -7032,7 +7032,7 @@
             int userId = UserHandle.getCallingUserId();
             long id = mInjector.binderClearCallingIdentity();
             try {
-                mUserManager.setUserIcon(userId, icon);
+                mUserManagerInternal.setUserIcon(userId, icon);
             } finally {
                 mInjector.binderRestoreCallingIdentity(id);
             }
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index d45b26f..3e30188 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -298,6 +298,16 @@
     public static final String METADATA_IN_CALL_SERVICE_UI = "android.telecom.IN_CALL_SERVICE_UI";
 
     /**
+     * A boolean meta-data value indicating whether an {@link InCallService} implements an
+     * in-call user interface to be used while the device is in car-mode (see
+     * {@link android.content.res.Configuration.UI_MODE_TYPE_CAR}).
+     *
+     * @hide
+     */
+    public static final String METADATA_IN_CALL_SERVICE_CAR_MODE_UI =
+            "android.telecom.IN_CALL_SERVICE_CAR_MODE_UI";
+
+    /**
      * The dual tone multi-frequency signaling character sent to indicate the dialing system should
      * pause for a predefined period.
      */
diff --git a/tools/aapt/AaptAssets.cpp b/tools/aapt/AaptAssets.cpp
index d346731..3b01827 100644
--- a/tools/aapt/AaptAssets.cpp
+++ b/tools/aapt/AaptAssets.cpp
@@ -235,7 +235,7 @@
          setRegion(part2.string());
      } else if (part2.length() == 4 && isAlpha(part2)) {
          setScript(part2.string());
-     } else if (part2.length() >= 5 && part2.length() <= 8) {
+     } else if (part2.length() >= 4 && part2.length() <= 8) {
          setVariant(part2.string());
      } else {
          valid = false;
@@ -250,7 +250,7 @@
      if (((part3.length() == 2 && isAlpha(part3)) ||
          (part3.length() == 3 && isNumber(part3))) && script[0]) {
          setRegion(part3.string());
-     } else if (part3.length() >= 5 && part3.length() <= 8) {
+     } else if (part3.length() >= 4 && part3.length() <= 8) {
          setVariant(part3.string());
      } else {
          valid = false;
@@ -261,7 +261,7 @@
      }
 
      const String8& part4 = parts[3];
-     if (part4.length() >= 5 && part4.length() <= 8) {
+     if (part4.length() >= 4 && part4.length() <= 8) {
          setVariant(part4.string());
      } else {
          valid = false;
@@ -280,7 +280,7 @@
 
     String8 part = parts[currentIndex];
     if (part[0] == 'b' && part[1] == '+') {
-        // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags,
+        // This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags,
         // except that the separator is "+" and not "-".
         Vector<String8> subtags = AaptUtil::splitAndLowerCase(part, '+');
         subtags.removeItemsAt(0);
@@ -296,8 +296,11 @@
                     setRegion(subtags[1]);
                     break;
                 case 4:
-                    setScript(subtags[1]);
-                    break;
+                    if (isAlpha(subtags[1])) {
+                        setScript(subtags[1]);
+                        break;
+                    }
+                    // This is not alphabetical, so we fall through to variant
                 case 5:
                 case 6:
                 case 7:
@@ -305,7 +308,7 @@
                     setVariant(subtags[1]);
                     break;
                 default:
-                    fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name %s\n",
+                    fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name %s\n",
                             part.string());
                     return -1;
             }
@@ -322,13 +325,13 @@
                 setRegion(subtags[1]);
                 hasRegion = true;
             } else {
-                fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name %s\n", part.string());
+                fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name %s\n", part.string());
                 return -1;
             }
 
             // The third tag can either be a region code (if the second tag was
             // a script), else a variant code.
-            if (subtags[2].size() > 4) {
+            if (subtags[2].size() >= 4) {
                 setVariant(subtags[2]);
             } else {
                 setRegion(subtags[2]);
@@ -339,7 +342,7 @@
             setRegion(subtags[2]);
             setVariant(subtags[3]);
         } else {
-            fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name: %s\n", part.string());
+            fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name: %s\n", part.string());
             return -1;
         }
 
@@ -370,7 +373,7 @@
 void AaptLocaleValue::initFromResTable(const ResTable_config& config) {
     config.unpackLanguage(language);
     config.unpackRegion(region);
-    if (config.localeScript[0]) {
+    if (config.localeScriptWasProvided) {
         memcpy(script, config.localeScript, sizeof(config.localeScript));
     }
 
@@ -385,6 +388,10 @@
 
     if (script[0]) {
         memcpy(out->localeScript, script, sizeof(out->localeScript));
+        out->localeScriptWasProvided = true;
+    } else {
+        out->computeScript();
+        out->localeScriptWasProvided = false;
     }
 
     if (variant[0]) {
diff --git a/tools/aapt2/Locale.cpp b/tools/aapt2/Locale.cpp
index 20a2d0c..0369156 100644
--- a/tools/aapt2/Locale.cpp
+++ b/tools/aapt2/Locale.cpp
@@ -96,7 +96,7 @@
          setRegion(part2.c_str());
      } else if (part2.length() == 4 && isAlpha(part2)) {
          setScript(part2.c_str());
-     } else if (part2.length() >= 5 && part2.length() <= 8) {
+     } else if (part2.length() >= 4 && part2.length() <= 8) {
          setVariant(part2.c_str());
      } else {
          valid = false;
@@ -111,7 +111,7 @@
      if (((part3.length() == 2 && isAlpha(part3)) ||
          (part3.length() == 3 && isNumber(part3))) && script[0]) {
          setRegion(part3.c_str());
-     } else if (part3.length() >= 5 && part3.length() <= 8) {
+     } else if (part3.length() >= 4 && part3.length() <= 8) {
          setVariant(part3.c_str());
      } else {
          valid = false;
@@ -122,7 +122,7 @@
      }
 
      const std::string& part4 = parts[3];
-     if (part4.length() >= 5 && part4.length() <= 8) {
+     if (part4.length() >= 4 && part4.length() <= 8) {
          setVariant(part4.c_str());
      } else {
          valid = false;
@@ -141,7 +141,7 @@
 
     std::string& part = *iter;
     if (part[0] == 'b' && part[1] == '+') {
-        // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags,
+        // This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags,
         // except that the separator is "+" and not "-".
         std::vector<std::string> subtags = util::splitAndLowercase(part, '+');
         subtags.erase(subtags.begin());
@@ -157,8 +157,12 @@
                     setRegion(subtags[1].c_str());
                     break;
                 case 4:
-                    setScript(subtags[1].c_str());
-                    break;
+                    if ('0' <= subtags[1][0] && subtags[1][0] <= '9') {
+                        // This is a variant: fall through
+                    } else {
+                        setScript(subtags[1].c_str());
+                        break;
+                    }
                 case 5:
                 case 6:
                 case 7:
@@ -184,7 +188,7 @@
 
             // The third tag can either be a region code (if the second tag was
             // a script), else a variant code.
-            if (subtags[2].size() > 4) {
+            if (subtags[2].size() >= 4) {
                 setVariant(subtags[2].c_str());
             } else {
                 setRegion(subtags[2].c_str());
@@ -249,7 +253,7 @@
 void LocaleValue::initFromResTable(const ResTable_config& config) {
     config.unpackLanguage(language);
     config.unpackRegion(region);
-    if (config.localeScript[0]) {
+    if (config.localeScriptWasProvided) {
         memcpy(script, config.localeScript, sizeof(config.localeScript));
     }
 
@@ -264,6 +268,10 @@
 
     if (script[0]) {
         memcpy(out->localeScript, script, sizeof(out->localeScript));
+        out->localeScriptWasProvided = true;
+    } else {
+        out->computeScript();
+        out->localeScriptWasProvided = false;
     }
 
     if (variant[0]) {
diff --git a/tools/aapt2/io/ZipArchive.cpp b/tools/aapt2/io/ZipArchive.cpp
index bf0f4aa..329dac9 100644
--- a/tools/aapt2/io/ZipArchive.cpp
+++ b/tools/aapt2/io/ZipArchive.cpp
@@ -75,11 +75,19 @@
 
 std::unique_ptr<ZipFileCollection> ZipFileCollection::create(const StringPiece& path,
                                                              std::string* outError) {
+    constexpr static const int32_t kEmptyArchive = -6;
+
     std::unique_ptr<ZipFileCollection> collection = std::unique_ptr<ZipFileCollection>(
             new ZipFileCollection());
 
     int32_t result = OpenArchive(path.data(), &collection->mHandle);
     if (result != 0) {
+        // If a zip is empty, result will be an error code. This is fine and we should
+        // return an empty ZipFileCollection.
+        if (result == kEmptyArchive) {
+            return collection;
+        }
+
         if (outError) *outError = ErrorCodeString(result);
         return {};
     }
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index 51b9cec..364a55b 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -551,12 +551,13 @@
             if (resourceFile) {
                 return mergeCompiledFile(file, std::move(resourceFile), override);
             }
-        } else {
-            // Ignore non .flat files. This could be classes.dex or something else that happens
-            // to be in an archive.
+
+            return false;
         }
 
-        return false;
+        // Ignore non .flat files. This could be classes.dex or something else that happens
+        // to be in an archive.
+        return true;
     }
 
     int run(const std::vector<std::string>& inputFiles) {
diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py
new file mode 100755
index 0000000..b071093
--- /dev/null
+++ b/tools/localedata/extract_icu_data.py
@@ -0,0 +1,286 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 The Android Open Source Project. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+"""Generate a C++ data table containing locale data."""
+
+import collections
+import glob
+import os.path
+import sys
+
+
+def get_locale_parts(locale):
+    """Split a locale into three parts, for langauge, script, and region."""
+    parts = locale.split('_')
+    if len(parts) == 1:
+        return (parts[0], None, None)
+    elif len(parts) == 2:
+        if len(parts[1]) == 4:  # parts[1] is a script
+            return (parts[0], parts[1], None)
+        else:
+            return (parts[0], None, parts[1])
+    else:
+        assert len(parts) == 3
+        return tuple(parts)
+
+
+def read_likely_subtags(input_file_name):
+    """Read and parse ICU's likelySubtags.txt."""
+    with open(input_file_name) as input_file:
+        likely_script_dict = {
+            # Android's additions for pseudo-locales. These internal codes make
+            # sure that the pseudo-locales would not match other English or
+            # Arabic locales. (We can't use private-use ISO 15924 codes, since
+            # they may be used by apps for other purposes.)
+            "en_XA": "~~~A",
+            "ar_XB": "~~~B",
+        }
+        representative_locales = {
+            # Android's additions
+            "en_Latn_GB", # representative for en_Latn_001
+            "es_Latn_MX", # representative for es_Latn_419
+            "es_Latn_US", # representative for es_Latn_419 (not the best idea,
+                          # but Android has been shipping with it for quite a
+                          # while. Fortunately, MX < US, so if both exist, MX
+                          # would be chosen.)
+        }
+        for line in input_file:
+            line = unicode(line, 'UTF-8').strip(u' \n\uFEFF').encode('UTF-8')
+            if line.startswith('//'):
+                continue
+            if '{' in line and '}' in line:
+                from_locale = line[:line.index('{')]
+                to_locale = line[line.index('"')+1:line.rindex('"')]
+                from_lang, from_scr, from_region = get_locale_parts(from_locale)
+                _, to_scr, to_region = get_locale_parts(to_locale)
+                if from_lang == 'und':
+                    continue  # not very useful for our purposes
+                if from_region is None and to_region != '001':
+                    representative_locales.add(to_locale)
+                if from_scr is None:
+                    likely_script_dict[from_locale] = to_scr
+        return likely_script_dict, frozenset(representative_locales)
+
+
+# From packLanguageOrRegion() in ResourceTypes.cpp
+def pack_language_or_region(inp, base):
+    """Pack langauge or region in a two-byte tuple."""
+    if inp is None:
+        return (0, 0)
+    elif len(inp) == 2:
+        return ord(inp[0]), ord(inp[1])
+    else:
+        assert len(inp) == 3
+        base = ord(base)
+        first = ord(inp[0]) - base
+        second = ord(inp[1]) - base
+        third = ord(inp[2]) - base
+
+        return (0x80 | (third << 2) | (second >>3),
+                ((second << 5) | first) & 0xFF)
+
+
+# From packLanguage() in ResourceTypes.cpp
+def pack_language(language):
+    """Pack language in a two-byte tuple."""
+    return pack_language_or_region(language, 'a')
+
+
+# From packRegion() in ResourceTypes.cpp
+def pack_region(region):
+    """Pack region in a two-byte tuple."""
+    return pack_language_or_region(region, '0')
+
+
+def pack_to_uint32(locale):
+    """Pack language+region of locale into a 32-bit unsigned integer."""
+    lang, _, region = get_locale_parts(locale)
+    plang = pack_language(lang)
+    pregion = pack_region(region)
+    return (plang[0] << 24) | (plang[1] << 16) | (pregion[0] << 8) | pregion[1]
+
+
+def dump_script_codes(all_scripts):
+    """Dump the SCRIPT_CODES table."""
+    print 'const char SCRIPT_CODES[][4] = {'
+    for index, script in enumerate(all_scripts):
+        print "    /* %-2d */ {'%c', '%c', '%c', '%c'}," % (
+            index, script[0], script[1], script[2], script[3])
+    print '};'
+    print
+
+
+def dump_script_data(likely_script_dict, all_scripts):
+    """Dump the script data."""
+    print
+    print 'const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({'
+    for locale in sorted(likely_script_dict.keys()):
+        script = likely_script_dict[locale]
+        print '    {0x%08Xu, %2du}, // %s -> %s' % (
+            pack_to_uint32(locale),
+            all_scripts.index(script),
+            locale.replace('_', '-'),
+            script)
+    print '});'
+
+
+def pack_to_uint64(locale):
+    """Pack a full locale into a 64-bit unsigned integer."""
+    _, script, _ = get_locale_parts(locale)
+    return ((pack_to_uint32(locale) << 32) |
+            (ord(script[0]) << 24) |
+            (ord(script[1]) << 16) |
+            (ord(script[2]) << 8) |
+            ord(script[3]))
+
+
+def dump_representative_locales(representative_locales):
+    """Dump the set of representative locales."""
+    print
+    print 'std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({'
+    for locale in sorted(representative_locales):
+        print '    0x%08Xllu, // %s' % (
+            pack_to_uint64(locale),
+            locale)
+    print '});'
+
+
+def read_and_dump_likely_data(icu_data_dir):
+    """Read and dump the likely-script data."""
+    likely_subtags_txt = os.path.join(icu_data_dir, 'misc', 'likelySubtags.txt')
+    likely_script_dict, representative_locales = read_likely_subtags(
+        likely_subtags_txt)
+
+    all_scripts = list(set(likely_script_dict.values()))
+    assert len(all_scripts) <= 256
+    all_scripts.sort()
+
+    dump_script_codes(all_scripts)
+    dump_script_data(likely_script_dict, all_scripts)
+    dump_representative_locales(representative_locales)
+    return likely_script_dict
+
+
+def read_parent_data(icu_data_dir):
+    """Read locale parent data from ICU data files."""
+    all_icu_data_files = glob.glob(os.path.join(icu_data_dir, '*', '*.txt'))
+    parent_dict = {}
+    for data_file in all_icu_data_files:
+        locale = os.path.splitext(os.path.basename(data_file))[0]
+        with open(data_file) as input_file:
+            for line in input_file:
+                if '%%Parent' in line:
+                    parent = line[line.index('"')+1:line.rindex('"')]
+                    if locale in parent_dict:
+                        # Different files shouldn't have different parent info
+                        assert parent_dict[locale] == parent
+                    else:
+                        parent_dict[locale] = parent
+                elif locale.startswith('ar_') and 'default{"latn"}' in line:
+                    # Arabic parent overrides for ASCII digits. Since
+                    # Unicode extensions are not supported in ResourceTypes,
+                    # we will use ar-015 (Arabic, Northern Africa) instead
+                    # of the more correct ar-u-nu-latn.
+                    parent_dict[locale] = 'ar_015'
+    return parent_dict
+
+
+def get_likely_script(locale, likely_script_dict):
+    """Find the likely script for a locale, given the likely-script dictionary.
+    """
+    if locale.count('_') == 2:
+        # it already has a script
+        return locale.split('_')[1]
+    elif locale in likely_script_dict:
+        return likely_script_dict[locale]
+    else:
+        language = locale.split('_')[0]
+        return likely_script_dict[language]
+
+
+def dump_parent_data(script_organized_dict):
+    """Dump information for parents of locales."""
+    sorted_scripts = sorted(script_organized_dict.keys())
+    print
+    for script in sorted_scripts:
+        parent_dict = script_organized_dict[script]
+        print ('const std::unordered_map<uint32_t, uint32_t> %s_PARENTS({'
+            % script.upper())
+        for locale in sorted(parent_dict.keys()):
+            parent = parent_dict[locale]
+            print '    {0x%08Xu, 0x%08Xu}, // %s -> %s' % (
+                pack_to_uint32(locale),
+                pack_to_uint32(parent),
+                locale.replace('_', '-'),
+                parent.replace('_', '-'))
+        print '});'
+        print
+
+    print 'const struct {'
+    print '    const char script[4];'
+    print '    const std::unordered_map<uint32_t, uint32_t>* map;'
+    print '} SCRIPT_PARENTS[] = {'
+    for script in sorted_scripts:
+        print "    {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % (
+            script[0], script[1], script[2], script[3],
+            script.upper())
+    print '};'
+
+
+def dump_parent_tree_depth(parent_dict):
+    """Find and dump the depth of the parent tree."""
+    max_depth = 1
+    for locale, _ in parent_dict.items():
+        depth = 1
+        while locale in parent_dict:
+            locale = parent_dict[locale]
+            depth += 1
+        max_depth = max(max_depth, depth)
+    assert max_depth < 5 # Our algorithms assume small max_depth
+    print
+    print 'const size_t MAX_PARENT_DEPTH = %d;' % max_depth
+
+
+def read_and_dump_parent_data(icu_data_dir, likely_script_dict):
+    """Read parent data from ICU and dump it."""
+    parent_dict = read_parent_data(icu_data_dir)
+    script_organized_dict = collections.defaultdict(dict)
+    for locale in parent_dict:
+        parent = parent_dict[locale]
+        if parent == 'root':
+            continue
+        script = get_likely_script(locale, likely_script_dict)
+        script_organized_dict[script][locale] = parent_dict[locale]
+    dump_parent_data(script_organized_dict)
+    dump_parent_tree_depth(parent_dict)
+
+
+def main():
+    """Read the data files from ICU and dump the output to a C++ file."""
+    source_root = sys.argv[1]
+    icu_data_dir = os.path.join(
+        source_root,
+        'external', 'icu', 'icu4c', 'source', 'data')
+
+    print '// Auto-generated by %s' % sys.argv[0]
+    print
+    likely_script_dict = read_and_dump_likely_data(icu_data_dir)
+    read_and_dump_parent_data(icu_data_dir, likely_script_dict)
+
+
+if __name__ == '__main__':
+    main()