Import Android SDK Platform P [4719250]

/google/data/ro/projects/android/fetch_artifact \
    --bid 4719250 \
    --target sdk_phone_armv7-win_sdk \
    sdk-repo-linux-sources-4719250.zip

AndroidVersion.ApiLevel has been modified to appear as 28

Change-Id: I9ec0a12c9251b8449dba0d86b0cfdbcca16b0a7c
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java
index 0a4541b..829a944 100644
--- a/android/accessibilityservice/AccessibilityService.java
+++ b/android/accessibilityservice/AccessibilityService.java
@@ -537,7 +537,7 @@
      * anything behind it, then only the modal window will be reported
      * (assuming it is the top one). For convenience the returned windows
      * are ordered in a descending layer order, which is the windows that
-     * are higher in the Z-order are reported first. Since the user can always
+     * are on top are reported first. Since the user can always
      * interact with the window that has input focus by typing, the focused
      * window is always returned (even if covered by a modal window).
      * <p>
diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java
index 09dcbf2..ecd99a7 100644
--- a/android/app/ActivityOptions.java
+++ b/android/app/ActivityOptions.java
@@ -1139,7 +1139,8 @@
      * {@link android.app.admin.DevicePolicyManager} can run in LockTask mode. Therefore, if
      * {@link android.app.admin.DevicePolicyManager#isLockTaskPermitted(String)} returns
      * {@code false} for the package of the target activity, a {@link SecurityException} will be
-     * thrown during {@link Context#startActivity(Intent, Bundle)}.
+     * thrown during {@link Context#startActivity(Intent, Bundle)}. This method doesn't affect
+     * activities that are already running — relaunch the activity to run in lock task mode.
      *
      * Defaults to {@code false} if not set.
      *
diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java
index 50a4398..82c3383 100644
--- a/android/app/ActivityThread.java
+++ b/android/app/ActivityThread.java
@@ -5873,7 +5873,7 @@
         } finally {
             // If the app targets < O-MR1, or doesn't change the thread policy
             // during startup, clobber the policy to maintain behavior of b/36951662
-            if (data.appInfo.targetSdkVersion <= Build.VERSION_CODES.O
+            if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1
                     || StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
                 StrictMode.setThreadPolicy(savedPolicy);
             }
diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java
index a68136b..1084b42 100644
--- a/android/app/ApplicationPackageManager.java
+++ b/android/app/ApplicationPackageManager.java
@@ -2155,19 +2155,10 @@
     public String[] setPackagesSuspended(String[] packageNames, boolean suspended,
             PersistableBundle appExtras, PersistableBundle launcherExtras,
             String dialogMessage) {
-        // TODO (b/75332201): Pass in the dialogMessage and use it in the interceptor dialog
         try {
             return mPM.setPackagesSuspendedAsUser(packageNames, suspended, appExtras,
-                    launcherExtras, mContext.getOpPackageName(), mContext.getUserId());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @Override
-    public PersistableBundle getSuspendedPackageAppExtras(String packageName) {
-        try {
-            return mPM.getSuspendedPackageAppExtras(packageName, mContext.getUserId());
+                    launcherExtras, dialogMessage, mContext.getOpPackageName(),
+                    mContext.getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2175,17 +2166,14 @@
 
     @Override
     public Bundle getSuspendedPackageAppExtras() {
-        final PersistableBundle extras = getSuspendedPackageAppExtras(mContext.getOpPackageName());
-        return extras != null ? new Bundle(extras.deepCopy()) : null;
-    }
-
-    @Override
-    public void setSuspendedPackageAppExtras(String packageName, PersistableBundle appExtras) {
+        final PersistableBundle extras;
         try {
-            mPM.setSuspendedPackageAppExtras(packageName, appExtras, mContext.getUserId());
+            extras = mPM.getSuspendedPackageAppExtras(mContext.getOpPackageName(),
+                    mContext.getUserId());
         } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+            throw e.rethrowFromSystemServer();
         }
+        return extras != null ? new Bundle(extras.deepCopy()) : null;
     }
 
     @Override
@@ -2199,8 +2187,12 @@
 
     /** @hide */
     @Override
-    public boolean isPackageSuspended(String packageName) {
-        return isPackageSuspendedForUser(packageName, mContext.getUserId());
+    public boolean isPackageSuspended(String packageName) throws NameNotFoundException {
+        try {
+            return isPackageSuspendedForUser(packageName, mContext.getUserId());
+        } catch (IllegalArgumentException ie) {
+            throw new NameNotFoundException(packageName);
+        }
     }
 
     @Override
diff --git a/android/app/BroadcastOptions.java b/android/app/BroadcastOptions.java
index b6cff38..69c3632 100644
--- a/android/app/BroadcastOptions.java
+++ b/android/app/BroadcastOptions.java
@@ -32,6 +32,7 @@
     private long mTemporaryAppWhitelistDuration;
     private int mMinManifestReceiverApiLevel = 0;
     private int mMaxManifestReceiverApiLevel = Build.VERSION_CODES.CUR_DEVELOPMENT;
+    private boolean mDontSendToRestrictedApps = false;
 
     /**
      * How long to temporarily put an app on the power whitelist when executing this broadcast
@@ -52,6 +53,12 @@
     static final String KEY_MAX_MANIFEST_RECEIVER_API_LEVEL
             = "android:broadcast.maxManifestReceiverApiLevel";
 
+    /**
+     * Corresponds to {@link #setMaxManifestReceiverApiLevel}.
+     */
+    static final String KEY_DONT_SEND_TO_RESTRICTED_APPS =
+            "android:broadcast.dontSendToRestrictedApps";
+
     public static BroadcastOptions makeBasic() {
         BroadcastOptions opts = new BroadcastOptions();
         return opts;
@@ -66,6 +73,7 @@
         mMinManifestReceiverApiLevel = opts.getInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, 0);
         mMaxManifestReceiverApiLevel = opts.getInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL,
                 Build.VERSION_CODES.CUR_DEVELOPMENT);
+        mDontSendToRestrictedApps = opts.getBoolean(KEY_DONT_SEND_TO_RESTRICTED_APPS, false);
     }
 
     /**
@@ -123,6 +131,23 @@
     }
 
     /**
+     * Sets whether pending intent can be sent for an application with background restrictions
+     * @param dontSendToRestrictedApps if true, pending intent will not be sent for an application
+     * with background restrictions. Default value is {@code false}
+     */
+    public void setDontSendToRestrictedApps(boolean dontSendToRestrictedApps) {
+        mDontSendToRestrictedApps = dontSendToRestrictedApps;
+    }
+
+    /**
+     * @hide
+     * @return #setDontSendToRestrictedApps
+     */
+    public boolean isDontSendToRestrictedApps() {
+        return mDontSendToRestrictedApps;
+    }
+
+    /**
      * Returns the created options as a Bundle, which can be passed to
      * {@link android.content.Context#sendBroadcast(android.content.Intent)
      * Context.sendBroadcast(Intent)} and related methods.
@@ -141,6 +166,9 @@
         if (mMaxManifestReceiverApiLevel != Build.VERSION_CODES.CUR_DEVELOPMENT) {
             b.putInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL, mMaxManifestReceiverApiLevel);
         }
+        if (mDontSendToRestrictedApps) {
+            b.putBoolean(KEY_DONT_SEND_TO_RESTRICTED_APPS, true);
+        }
         return b.isEmpty() ? null : b;
     }
 }
diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java
index 71b88fa..9a491bc 100644
--- a/android/app/ContextImpl.java
+++ b/android/app/ContextImpl.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
@@ -88,11 +89,12 @@
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicInteger;
 
 class ReceiverRestrictedContext extends ContextWrapper {
     ReceiverRestrictedContext(Context base) {
@@ -212,13 +214,24 @@
     static final int STATE_UNINITIALIZED = 0;
     static final int STATE_INITIALIZING = 1;
     static final int STATE_READY = 2;
+    static final int STATE_NOT_FOUND = 3;
+
+    /** @hide */
+    @IntDef(prefix = { "STATE_" }, value = {
+            STATE_UNINITIALIZED,
+            STATE_INITIALIZING,
+            STATE_READY,
+            STATE_NOT_FOUND,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ServiceInitializationState {}
 
     /**
      * Initialization state for each service. Any of {@link #STATE_UNINITIALIZED},
      * {@link #STATE_INITIALIZING} or {@link #STATE_READY},
      */
-    final AtomicInteger[] mServiceInitializationStateArray =
-            SystemServiceRegistry.createServiceInitializationStateArray();
+    @ServiceInitializationState
+    final int[] mServiceInitializationStateArray = new int[mServiceCache.length];
 
     static ContextImpl getImpl(Context context) {
         Context nextContext;
diff --git a/android/app/Notification.java b/android/app/Notification.java
index 4326ee3..2b4f420 100644
--- a/android/app/Notification.java
+++ b/android/app/Notification.java
@@ -3902,7 +3902,7 @@
          * @deprecated use {@link #addPerson(Person)}
          */
         public Builder addPerson(String uri) {
-            addPerson(new Person().setUri(uri));
+            addPerson(new Person.Builder().setUri(uri).build());
             return this;
         }
 
@@ -4588,10 +4588,18 @@
                 bindHeaderChronometerAndTime(contentView);
                 bindProfileBadge(contentView);
             }
+            bindActivePermissions(contentView);
             bindExpandButton(contentView);
             mN.mUsesStandardHeader = true;
         }
 
+        private void bindActivePermissions(RemoteViews contentView) {
+            int color = isColorized() ? getPrimaryTextColor() : getSecondaryTextColor();
+            contentView.setDrawableTint(R.id.camera, false, color, PorterDuff.Mode.SRC_ATOP);
+            contentView.setDrawableTint(R.id.mic, false, color, PorterDuff.Mode.SRC_ATOP);
+            contentView.setDrawableTint(R.id.overlay, false, color, PorterDuff.Mode.SRC_ATOP);
+        }
+
         private void bindExpandButton(RemoteViews contentView) {
             int color = isColorized() ? getPrimaryTextColor() : getSecondaryTextColor();
             contentView.setDrawableTint(R.id.expand_button, false, color,
@@ -6384,7 +6392,7 @@
          * @deprecated use {@code MessagingStyle(Person)}
          */
         public MessagingStyle(@NonNull CharSequence userDisplayName) {
-            this(new Person().setName(userDisplayName));
+            this(new Person.Builder().setName(userDisplayName).build());
         }
 
         /**
@@ -6431,6 +6439,7 @@
         /**
          * @return the user to be displayed for any replies sent by the user
          */
+        @NonNull
         public Person getUser() {
             return mUser;
         }
@@ -6489,7 +6498,7 @@
          */
         public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) {
             return addMessage(text, timestamp,
-                    sender == null ? null : new Person().setName(sender));
+                    sender == null ? null : new Person.Builder().setName(sender).build());
         }
 
         /**
@@ -6505,7 +6514,8 @@
          *
          * @return this object for method chaining
          */
-        public MessagingStyle addMessage(CharSequence text, long timestamp, Person sender) {
+        public MessagingStyle addMessage(@NonNull CharSequence text, long timestamp,
+                @Nullable Person sender) {
             return addMessage(new Message(text, timestamp, sender));
         }
 
@@ -6661,7 +6671,7 @@
             mUser = extras.getParcelable(EXTRA_MESSAGING_PERSON);
             if (mUser == null) {
                 CharSequence displayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
-                mUser = new Person().setName(displayName);
+                mUser = new Person.Builder().setName(displayName).build();
             }
             mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
             Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
@@ -6678,7 +6688,8 @@
         public RemoteViews makeContentView(boolean increasedHeight) {
             mBuilder.mOriginalActions = mBuilder.mActions;
             mBuilder.mActions = new ArrayList<>();
-            RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */);
+            RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */,
+                    true /* showReplyIcon */);
             mBuilder.mActions = mBuilder.mOriginalActions;
             mBuilder.mOriginalActions = null;
             return remoteViews;
@@ -6765,11 +6776,19 @@
          */
         @Override
         public RemoteViews makeBigContentView() {
-            return makeMessagingView(false /* isCollapsed */);
+            return makeMessagingView(false /* displayImagesAtEnd */, false /* showReplyIcon */);
         }
 
+        /**
+         * Create a messaging layout.
+         *
+         * @param displayImagesAtEnd should images be displayed at the end of the content instead
+         *                           of inline.
+         * @param showReplyIcon Should the reply affordance be shown at the end of the notification
+         * @return the created remoteView.
+         */
         @NonNull
-        private RemoteViews makeMessagingView(boolean isCollapsed) {
+        private RemoteViews makeMessagingView(boolean displayImagesAtEnd, boolean showReplyIcon) {
             CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
                     ? super.mBigContentTitle
                     : mConversationTitle;
@@ -6780,24 +6799,24 @@
                 nameReplacement = conversationTitle;
                 conversationTitle = null;
             }
-            boolean hideLargeIcon = !isCollapsed || isOneToOne;
+            boolean hideLargeIcon = !showReplyIcon || isOneToOne;
             RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
                     mBuilder.getMessagingLayoutResource(),
                     mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null)
                             .hideLargeIcon(hideLargeIcon)
                             .headerTextSecondary(conversationTitle)
-                            .alwaysShowReply(isCollapsed));
+                            .alwaysShowReply(showReplyIcon));
             addExtras(mBuilder.mN.extras);
             // also update the end margin if there is an image
             int endMargin = R.dimen.notification_content_margin_end;
-            if (isCollapsed) {
+            if (showReplyIcon) {
                 endMargin = R.dimen.notification_content_plus_picture_margin_end;
             }
             contentView.setViewLayoutMarginEndDimen(R.id.notification_main_column, endMargin);
             contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
                     mBuilder.resolveContrastColor());
-            contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsCollapsed",
-                    isCollapsed);
+            contentView.setBoolean(R.id.status_bar_latest_event_content, "setDisplayImagesAtEnd",
+                    displayImagesAtEnd);
             contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon",
                     mBuilder.mN.mLargeIcon);
             contentView.setCharSequence(R.id.status_bar_latest_event_content, "setNameReplacement",
@@ -6864,7 +6883,8 @@
          */
         @Override
         public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
-            RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */);
+            RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */,
+                    false /* showReplyIcon */);
             remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1);
             return remoteViews;
         }
@@ -6906,7 +6926,8 @@
              *  @deprecated use {@code Message(CharSequence, long, Person)}
              */
             public Message(CharSequence text, long timestamp, CharSequence sender){
-                this(text, timestamp, sender == null ? null : new Person().setName(sender));
+                this(text, timestamp, sender == null ? null
+                        : new Person.Builder().setName(sender).build());
             }
 
             /**
@@ -6917,13 +6938,14 @@
              * Should be <code>null</code> for messages by the current user, in which case
              * the platform will insert the user set in {@code MessagingStyle(Person)}.
              * <p>
-             * The person provided should contain an Icon, set with {@link Person#setIcon(Icon)}
-             * and also have a name provided with {@link Person#setName(CharSequence)}. If multiple
-             * users have the same name, consider providing a key with {@link Person#setKey(String)}
-             * in order to differentiate between the different users.
+             * The person provided should contain an Icon, set with
+             * {@link Person.Builder#setIcon(Icon)} and also have a name provided
+             * with {@link Person.Builder#setName(CharSequence)}. If multiple users have the same
+             * name, consider providing a key with {@link Person.Builder#setKey(String)} in order
+             * to differentiate between the different users.
              * </p>
              */
-            public Message(CharSequence text, long timestamp, @Nullable Person sender){
+            public Message(@NonNull CharSequence text, long timestamp, @Nullable Person sender) {
                 mText = text;
                 mTimestamp = timestamp;
                 mSender = sender;
@@ -7082,7 +7104,7 @@
                             // the native api instead
                             CharSequence senderName = bundle.getCharSequence(KEY_SENDER);
                             if (senderName != null) {
-                                senderPerson = new Person().setName(senderName);
+                                senderPerson = new Person.Builder().setName(senderName).build();
                             }
                         }
                         Message message = new Message(bundle.getCharSequence(KEY_TEXT),
@@ -7777,217 +7799,6 @@
         }
     }
 
-    /**
-     * A Person associated with this Notification.
-     */
-    public static final class Person implements Parcelable {
-        @Nullable private CharSequence mName;
-        @Nullable private Icon mIcon;
-        @Nullable private String mUri;
-        @Nullable private String mKey;
-        private boolean mBot;
-        private boolean mImportant;
-
-        protected Person(Parcel in) {
-            mName = in.readCharSequence();
-            if (in.readInt() != 0) {
-                mIcon = Icon.CREATOR.createFromParcel(in);
-            }
-            mUri = in.readString();
-            mKey = in.readString();
-            mImportant = in.readBoolean();
-            mBot = in.readBoolean();
-        }
-
-        /**
-         * Create a new person.
-         */
-        public Person() {
-        }
-
-        /**
-         * Give this person a name.
-         *
-         * @param name the name of this person
-         */
-        public Person setName(@Nullable CharSequence name) {
-            this.mName = name;
-            return this;
-        }
-
-        /**
-         * Add an icon for this person.
-         * <br />
-         * This is currently only used for {@link MessagingStyle} notifications and should not be
-         * provided otherwise, in order to save memory. The system will prefer this icon over any
-         * images that are resolved from the URI.
-         *
-         * @param icon the icon of the person
-         */
-        public Person setIcon(@Nullable Icon icon) {
-            this.mIcon = icon;
-            return this;
-        }
-
-        /**
-         * Set a URI associated with this person.
-         *
-         * <P>
-         * Depending on user preferences, adding a URI to a Person may allow the notification to
-         * pass through interruption filters, if this notification is of
-         * category {@link #CATEGORY_CALL} or {@link #CATEGORY_MESSAGE}.
-         * The addition of people may also cause this notification to appear more prominently in
-         * the user interface.
-         * </P>
-         *
-         * <P>
-         * The person should be specified by the {@code String} representation of a
-         * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
-         * </P>
-         *
-         * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema
-         * URIs.  The path part of these URIs must exist in the contacts database, in the
-         * appropriate column, or the reference will be discarded as invalid. Telephone schema
-         * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
-         * </P>
-         *
-         * @param uri a URI for the person
-         */
-        public Person setUri(@Nullable String uri) {
-            mUri = uri;
-            return this;
-        }
-
-        /**
-         * Add a key to this person in order to uniquely identify it.
-         * This is especially useful if the name doesn't uniquely identify this person or if the
-         * display name is a short handle of the actual name.
-         *
-         * <P>If no key is provided, the name serves as as the key for the purpose of
-         * identification.</P>
-         *
-         * @param key the key that uniquely identifies this person
-         */
-        public Person setKey(@Nullable String key) {
-            mKey = key;
-            return this;
-        }
-
-        /**
-         * Sets whether this is an important person. Use this method to denote users who frequently
-         * interact with the user of this device, when it is not possible to refer to the user
-         * by {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
-         *
-         * @param isImportant {@code true} if this is an important person, {@code false} otherwise.
-         */
-        public Person setImportant(boolean isImportant) {
-            mImportant = isImportant;
-            return this;
-        }
-
-        /**
-         * Sets whether this person is a machine rather than a human.
-         *
-         * @param isBot {@code true}  if this person is a machine, {@code false} otherwise.
-         */
-        public Person setBot(boolean isBot) {
-            mBot = isBot;
-            return this;
-        }
-
-        /**
-         * @return the uri provided for this person or {@code null} if no Uri was provided
-         */
-        @Nullable
-        public String getUri() {
-            return mUri;
-        }
-
-        /**
-         * @return the name provided for this person or {@code null} if no name was provided
-         */
-        @Nullable
-        public CharSequence getName() {
-            return mName;
-        }
-
-        /**
-         * @return the icon provided for this person or {@code null} if no icon was provided
-         */
-        @Nullable
-        public Icon getIcon() {
-            return mIcon;
-        }
-
-        /**
-         * @return the key provided for this person or {@code null} if no key was provided
-         */
-        @Nullable
-        public String getKey() {
-            return mKey;
-        }
-
-        /**
-         * @return whether this Person is a machine.
-         */
-        public boolean isBot() {
-            return mBot;
-        }
-
-        /**
-         * @return whether this Person is important.
-         */
-        public boolean isImportant() {
-            return mImportant;
-        }
-
-        /**
-         * @return the URI associated with this person, or "name:mName" otherwise
-         *  @hide
-         */
-        public String resolveToLegacyUri() {
-            if (mUri != null) {
-                return mUri;
-            }
-            if (mName != null) {
-                return "name:" + mName;
-            }
-            return "";
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel dest, @WriteFlags int flags) {
-            dest.writeCharSequence(mName);
-            if (mIcon != null) {
-                dest.writeInt(1);
-                mIcon.writeToParcel(dest, 0);
-            } else {
-                dest.writeInt(0);
-            }
-            dest.writeString(mUri);
-            dest.writeString(mKey);
-            dest.writeBoolean(mImportant);
-            dest.writeBoolean(mBot);
-        }
-
-        public static final Creator<Person> CREATOR = new Creator<Person>() {
-            @Override
-            public Person createFromParcel(Parcel in) {
-                return new Person(in);
-            }
-
-            @Override
-            public Person[] newArray(int size) {
-                return new Person[size];
-            }
-        };
-    }
-
     // When adding a new Style subclass here, don't forget to update
     // Builder.getNotificationStyleClass.
 
diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java
index 4a7cf62..9e47ced 100644
--- a/android/app/NotificationChannel.java
+++ b/android/app/NotificationChannel.java
@@ -328,7 +328,8 @@
      * Group information is only used for presentation, not for behavior.
      *
      * Only modifiable before the channel is submitted to
-     * {@link NotificationManager#notify(String, int, Notification)}.
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}, unless the
+     * channel is not currently part of a group.
      *
      * @param groupId the id of a group created by
      * {@link NotificationManager#createNotificationChannelGroup(NotificationChannelGroup)}.
@@ -341,6 +342,9 @@
      * Sets whether notifications posted to this channel can appear as application icon badges
      * in a Launcher.
      *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
+     *
      * @param showBadge true if badges should be allowed to be shown.
      */
     public void setShowBadge(boolean showBadge) {
@@ -353,7 +357,7 @@
      * least {@link NotificationManager#IMPORTANCE_DEFAULT} should have a sound.
      *
      * Only modifiable before the channel is submitted to
-     * {@link NotificationManager#notify(String, int, Notification)}.
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
      */
     public void setSound(Uri sound, AudioAttributes audioAttributes) {
         this.mSound = sound;
@@ -365,7 +369,7 @@
      * on devices that support that feature.
      *
      * Only modifiable before the channel is submitted to
-     * {@link NotificationManager#notify(String, int, Notification)}.
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
      */
     public void enableLights(boolean lights) {
         this.mLights = lights;
@@ -376,7 +380,7 @@
      * {@link #enableLights(boolean) enabled} on this channel and the device supports that feature.
      *
      * Only modifiable before the channel is submitted to
-     * {@link NotificationManager#notify(String, int, Notification)}.
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
      */
     public void setLightColor(int argb) {
         this.mLightColor = argb;
@@ -387,7 +391,7 @@
      * be set with {@link #setVibrationPattern(long[])}.
      *
      * Only modifiable before the channel is submitted to
-     * {@link NotificationManager#notify(String, int, Notification)}.
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
      */
     public void enableVibration(boolean vibration) {
         this.mVibrationEnabled = vibration;
@@ -399,7 +403,7 @@
      * vibration} as well. Otherwise, vibration will be disabled.
      *
      * Only modifiable before the channel is submitted to
-     * {@link NotificationManager#notify(String, int, Notification)}.
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
      */
     public void setVibrationPattern(long[] vibrationPattern) {
         this.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0;
@@ -407,9 +411,10 @@
     }
 
     /**
-     * Sets the level of interruption of this notification channel. Only
-     * modifiable before the channel is submitted to
-     * {@link NotificationManager#notify(String, int, Notification)}.
+     * Sets the level of interruption of this notification channel.
+     *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
      *
      * @param importance the amount the user should be interrupted by
      *            notifications from this channel.
diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java
index 46d1264..757fc64 100644
--- a/android/app/NotificationManager.java
+++ b/android/app/NotificationManager.java
@@ -1145,6 +1145,21 @@
                 SUPPRESSED_EFFECT_NOTIFICATION_LIST
         };
 
+        private static final int[] SCREEN_OFF_SUPPRESSED_EFFECTS = {
+                SUPPRESSED_EFFECT_SCREEN_OFF,
+                SUPPRESSED_EFFECT_FULL_SCREEN_INTENT,
+                SUPPRESSED_EFFECT_LIGHTS,
+                SUPPRESSED_EFFECT_AMBIENT,
+        };
+
+        private static final int[] SCREEN_ON_SUPPRESSED_EFFECTS = {
+                SUPPRESSED_EFFECT_SCREEN_ON,
+                SUPPRESSED_EFFECT_PEEK,
+                SUPPRESSED_EFFECT_STATUS_BAR,
+                SUPPRESSED_EFFECT_BADGE,
+                SUPPRESSED_EFFECT_NOTIFICATION_LIST
+        };
+
         /**
          * Visual effects to suppress for a notification that is filtered by Do Not Disturb mode.
          * Bitmask of SUPPRESSED_EFFECT_* constants.
@@ -1297,6 +1312,58 @@
             return true;
         }
 
+        /**
+         * @hide
+         */
+        public static boolean areAnyScreenOffEffectsSuppressed(int effects) {
+            for (int i = 0; i < SCREEN_OFF_SUPPRESSED_EFFECTS.length; i++) {
+                final int effect = SCREEN_OFF_SUPPRESSED_EFFECTS[i];
+                if ((effects & effect) != 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * @hide
+         */
+        public static boolean areAnyScreenOnEffectsSuppressed(int effects) {
+            for (int i = 0; i < SCREEN_ON_SUPPRESSED_EFFECTS.length; i++) {
+                final int effect = SCREEN_ON_SUPPRESSED_EFFECTS[i];
+                if ((effects & effect) != 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * @hide
+         */
+        public static int toggleScreenOffEffectsSuppressed(int currentEffects, boolean suppress) {
+            return toggleEffects(currentEffects, SCREEN_OFF_SUPPRESSED_EFFECTS, suppress);
+        }
+
+        /**
+         * @hide
+         */
+        public static int toggleScreenOnEffectsSuppressed(int currentEffects, boolean suppress) {
+            return toggleEffects(currentEffects, SCREEN_ON_SUPPRESSED_EFFECTS, suppress);
+        }
+
+        private static int toggleEffects(int currentEffects, int[] effects, boolean suppress) {
+            for (int i = 0; i < effects.length; i++) {
+                final int effect = effects[i];
+                if (suppress) {
+                    currentEffects |= effect;
+                } else {
+                    currentEffects &= ~effect;
+                }
+            }
+            return currentEffects;
+        }
+
         public static String suppressedEffectsToString(int effects) {
             if (effects <= 0) return "";
             final StringBuilder sb = new StringBuilder();
diff --git a/android/app/Person.java b/android/app/Person.java
new file mode 100644
index 0000000..3884a8d
--- /dev/null
+++ b/android/app/Person.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2018 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 android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Provides an immutable reference to an entity that appears repeatedly on different surfaces of the
+ * platform. For example, this could represent the sender of a message.
+ */
+public final class Person implements Parcelable {
+
+    @Nullable private CharSequence mName;
+    @Nullable private Icon mIcon;
+    @Nullable private String mUri;
+    @Nullable private String mKey;
+    private boolean mIsBot;
+    private boolean mIsImportant;
+
+    private Person(Parcel in) {
+        mName = in.readCharSequence();
+        if (in.readInt() != 0) {
+            mIcon = Icon.CREATOR.createFromParcel(in);
+        }
+        mUri = in.readString();
+        mKey = in.readString();
+        mIsImportant = in.readBoolean();
+        mIsBot = in.readBoolean();
+    }
+
+    private Person(Builder builder) {
+        mName = builder.mName;
+        mIcon = builder.mIcon;
+        mUri = builder.mUri;
+        mKey = builder.mKey;
+        mIsBot = builder.mIsBot;
+        mIsImportant = builder.mIsImportant;
+    }
+
+    /** Creates and returns a new {@link Builder} initialized with this Person's data. */
+    public Builder toBuilder() {
+        return new Builder(this);
+    }
+
+    /**
+     * @return the uri provided for this person or {@code null} if no Uri was provided.
+     */
+    @Nullable
+    public String getUri() {
+        return mUri;
+    }
+
+    /**
+     * @return the name provided for this person or {@code null} if no name was provided.
+     */
+    @Nullable
+    public CharSequence getName() {
+        return mName;
+    }
+
+    /**
+     * @return the icon provided for this person or {@code null} if no icon was provided.
+     */
+    @Nullable
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    /**
+     * @return the key provided for this person or {@code null} if no key was provided.
+     */
+    @Nullable
+    public String getKey() {
+        return mKey;
+    }
+
+    /**
+     * @return whether this Person is a machine.
+     */
+    public boolean isBot() {
+        return mIsBot;
+    }
+
+    /**
+     * @return whether this Person is important.
+     */
+    public boolean isImportant() {
+        return mIsImportant;
+    }
+
+    /**
+     * @return the URI associated with this person, or "name:mName" otherwise
+     *  @hide
+     */
+    public String resolveToLegacyUri() {
+        if (mUri != null) {
+            return mUri;
+        }
+        if (mName != null) {
+            return "name:" + mName;
+        }
+        return "";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, @WriteFlags int flags) {
+        dest.writeCharSequence(mName);
+        if (mIcon != null) {
+            dest.writeInt(1);
+            mIcon.writeToParcel(dest, 0);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeString(mUri);
+        dest.writeString(mKey);
+        dest.writeBoolean(mIsImportant);
+        dest.writeBoolean(mIsBot);
+    }
+
+    /** Builder for the immutable {@link Person} class. */
+    public static class Builder {
+        @Nullable private CharSequence mName;
+        @Nullable private Icon mIcon;
+        @Nullable private String mUri;
+        @Nullable private String mKey;
+        private boolean mIsBot;
+        private boolean mIsImportant;
+
+        /** Creates a new, empty {@link Builder}. */
+        public Builder() {
+        }
+
+        private Builder(Person person) {
+            mName = person.mName;
+            mIcon = person.mIcon;
+            mUri = person.mUri;
+            mKey = person.mKey;
+            mIsBot = person.mIsBot;
+            mIsImportant = person.mIsImportant;
+        }
+
+        /**
+         * Give this person a name.
+         *
+         * @param name the name of this person.
+         */
+        @NonNull
+        public Person.Builder setName(@Nullable CharSequence name) {
+            this.mName = name;
+            return this;
+        }
+
+        /**
+         * Add an icon for this person.
+         * <br />
+         * The system will prefer this icon over any images that are resolved from the URI.
+         *
+         * @param icon the icon of the person.
+         */
+        @NonNull
+        public Person.Builder setIcon(@Nullable Icon icon) {
+            this.mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Set a URI associated with this person.
+         *
+         * <P>
+         * The person should be specified by the {@code String} representation of a
+         * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+         * </P>
+         *
+         * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema
+         * URIs. The path part of these URIs must exist in the contacts database, in the
+         * appropriate column, or the reference will be discarded as invalid. Telephone schema
+         * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+         * </P>
+         *
+         * @param uri a URI for the person.
+         */
+        @NonNull
+        public Person.Builder setUri(@Nullable String uri) {
+            mUri = uri;
+            return this;
+        }
+
+        /**
+         * Add a key to this person in order to uniquely identify it.
+         * This is especially useful if the name doesn't uniquely identify this person or if the
+         * display name is a short handle of the actual name.
+         *
+         * <P>If no key is provided, the name serves as the key for the purpose of
+         * identification.</P>
+         *
+         * @param key the key that uniquely identifies this person.
+         */
+        @NonNull
+        public Person.Builder setKey(@Nullable String key) {
+            mKey = key;
+            return this;
+        }
+
+        /**
+         * Sets whether this is an important person. Use this method to denote users who frequently
+         * interact with the user of this device when {@link #setUri(String)} isn't provided with
+         * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, and instead with
+         * the {@code mailto:} or {@code tel:} schemas.
+         *
+         * @param isImportant {@code true} if this is an important person, {@code false} otherwise.
+         */
+        @NonNull
+        public Person.Builder setImportant(boolean isImportant) {
+            mIsImportant = isImportant;
+            return this;
+        }
+
+        /**
+         * Sets whether this person is a machine rather than a human.
+         *
+         * @param isBot {@code true} if this person is a machine, {@code false} otherwise.
+         */
+        @NonNull
+        public Person.Builder setBot(boolean isBot) {
+            mIsBot = isBot;
+            return this;
+        }
+
+        /** Creates and returns the {@link Person} this builder represents. */
+        @NonNull
+        public Person build() {
+            return new Person(this);
+        }
+    }
+
+    public static final Creator<Person> CREATOR = new Creator<Person>() {
+        @Override
+        public Person createFromParcel(Parcel in) {
+            return new Person(in);
+        }
+
+        @Override
+        public Person[] newArray(int size) {
+            return new Person[size];
+        }
+    };
+}
diff --git a/android/app/RemoteAction.java b/android/app/RemoteAction.java
index 47741c0..c174665 100644
--- a/android/app/RemoteAction.java
+++ b/android/app/RemoteAction.java
@@ -122,6 +122,7 @@
     public RemoteAction clone() {
         RemoteAction action = new RemoteAction(mIcon, mTitle, mContentDescription, mActionIntent);
         action.setEnabled(mEnabled);
+        action.setShouldShowIcon(mShouldShowIcon);
         return action;
     }
 
diff --git a/android/app/StatsManager.java b/android/app/StatsManager.java
index 4a6fa8c..8783d94 100644
--- a/android/app/StatsManager.java
+++ b/android/app/StatsManager.java
@@ -23,6 +23,7 @@
 import android.os.IStatsManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.util.AndroidException;
 import android.util.Slog;
 
 /**
@@ -82,55 +83,74 @@
     }
 
     /**
-     * Clients can send a configuration and simultaneously registers the name of a broadcast
-     * receiver that listens for when it should request data.
+     * Adds the given configuration and associates it with the given configKey. If a config with the
+     * given configKey already exists for the caller's uid, it is replaced with the new one.
      *
      * @param configKey An arbitrary integer that allows clients to track the configuration.
-     * @param config    Wire-encoded StatsDConfig proto that specifies metrics (and all
+     * @param config    Wire-encoded StatsdConfig proto that specifies metrics (and all
      *                  dependencies eg, conditions and matchers).
-     * @return true if successful
+     * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+     * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto
      */
     @RequiresPermission(Manifest.permission.DUMP)
-    public boolean addConfiguration(long configKey, byte[] config) {
+    public void addConfig(long configKey, byte[] config) throws StatsUnavailableException {
         synchronized (this) {
             try {
                 IStatsManager service = getIStatsManagerLocked();
-                if (service == null) {
-                    Slog.e(TAG, "Failed to find statsd when adding configuration");
-                    return false;
-                }
-                return service.addConfiguration(configKey, config);
+                service.addConfiguration(configKey, config); // can throw IllegalArgumentException
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to connect to statsd when adding configuration");
-                return false;
+                throw new StatsUnavailableException("could not connect", e);
             }
         }
     }
 
     /**
+     * TODO: Temporary for backwards compatibility. Remove.
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    public boolean addConfiguration(long configKey, byte[] config) {
+        try {
+            addConfig(configKey, config);
+            return true;
+        } catch (StatsUnavailableException | IllegalArgumentException e) {
+            return false;
+        }
+    }
+
+    /**
      * Remove a configuration from logging.
      *
      * @param configKey Configuration key to remove.
-     * @return true if successful
+     * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
      */
     @RequiresPermission(Manifest.permission.DUMP)
-    public boolean removeConfiguration(long configKey) {
+    public void removeConfig(long configKey) throws StatsUnavailableException {
         synchronized (this) {
             try {
                 IStatsManager service = getIStatsManagerLocked();
-                if (service == null) {
-                    Slog.e(TAG, "Failed to find statsd when removing configuration");
-                    return false;
-                }
-                return service.removeConfiguration(configKey);
+                service.removeConfiguration(configKey);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to connect to statsd when removing configuration");
-                return false;
+                throw new StatsUnavailableException("could not connect", e);
             }
         }
     }
 
     /**
+     * TODO: Temporary for backwards compatibility. Remove.
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    public boolean removeConfiguration(long configKey) {
+        try {
+            removeConfig(configKey);
+            return true;
+        } catch (StatsUnavailableException e) {
+            return false;
+        }
+    }
+
+    /**
      * Set the PendingIntent to be used when broadcasting subscriber information to the given
      * subscriberId within the given config.
      * <p>
@@ -150,123 +170,165 @@
      * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
      * <p>
      * This function can only be called by the owner (uid) of the config. It must be called each
-     * time statsd starts. The config must have been added first (via addConfiguration()).
+     * time statsd starts. The config must have been added first (via {@link #addConfig}).
      *
-     * @param configKey     The integer naming the config to which this subscriber is attached.
-     * @param subscriberId  ID of the subscriber, as used in the config.
      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
      *                      associated with the given subscriberId. May be null, in which case
      *                      it undoes any previous setting of this subscriberId.
-     * @return true if successful
+     * @param configKey     The integer naming the config to which this subscriber is attached.
+     * @param subscriberId  ID of the subscriber, as used in the config.
+     * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    public void setBroadcastSubscriber(
+            PendingIntent pendingIntent, long configKey, long subscriberId)
+            throws StatsUnavailableException {
+        synchronized (this) {
+            try {
+                IStatsManager service = getIStatsManagerLocked();
+                if (pendingIntent != null) {
+                    // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
+                    IBinder intentSender = pendingIntent.getTarget().asBinder();
+                    service.setBroadcastSubscriber(configKey, subscriberId, intentSender);
+                } else {
+                    service.unsetBroadcastSubscriber(configKey, subscriberId);
+                }
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to connect to statsd when adding broadcast subscriber", e);
+                throw new StatsUnavailableException("could not connect", e);
+            }
+        }
+    }
+
+    /**
+     * TODO: Temporary for backwards compatibility. Remove.
      */
     @RequiresPermission(Manifest.permission.DUMP)
     public boolean setBroadcastSubscriber(
             long configKey, long subscriberId, PendingIntent pendingIntent) {
-        synchronized (this) {
-            try {
-                IStatsManager service = getIStatsManagerLocked();
-                if (service == null) {
-                    Slog.e(TAG, "Failed to find statsd when adding broadcast subscriber");
-                    return false;
-                }
-                if (pendingIntent != null) {
-                    // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
-                    IBinder intentSender = pendingIntent.getTarget().asBinder();
-                    return service.setBroadcastSubscriber(configKey, subscriberId, intentSender);
-                } else {
-                    return service.unsetBroadcastSubscriber(configKey, subscriberId);
-                }
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to connect to statsd when adding broadcast subscriber", e);
-                return false;
-            }
+        try {
+            setBroadcastSubscriber(pendingIntent, configKey, subscriberId);
+            return true;
+        } catch (StatsUnavailableException e) {
+            return false;
         }
     }
 
     /**
      * Registers the operation that is called to retrieve the metrics data. This must be called
-     * each time statsd starts. The config must have been added first (via addConfiguration(),
-     * although addConfiguration could have been called on a previous boot). This operation allows
+     * each time statsd starts. The config must have been added first (via {@link #addConfig},
+     * although addConfig could have been called on a previous boot). This operation allows
      * statsd to send metrics data whenever statsd determines that the metrics in memory are
-     * approaching the memory limits. The fetch operation should call {@link #getData} to fetch the
-     * data, which also deletes the retrieved metrics from statsd's memory.
+     * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch
+     * the data, which also deletes the retrieved metrics from statsd's memory.
      *
-     * @param configKey     The integer naming the config to which this operation is attached.
      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
      *                      associated with the given subscriberId. May be null, in which case
      *                      it removes any associated pending intent with this configKey.
-     * @return true if successful
+     * @param configKey     The integer naming the config to which this operation is attached.
+     * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
      */
     @RequiresPermission(Manifest.permission.DUMP)
-    public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
+    public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey)
+            throws StatsUnavailableException {
         synchronized (this) {
             try {
                 IStatsManager service = getIStatsManagerLocked();
-                if (service == null) {
-                    Slog.e(TAG, "Failed to find statsd when registering data listener.");
-                    return false;
-                }
                 if (pendingIntent == null) {
-                    return service.removeDataFetchOperation(configKey);
+                    service.removeDataFetchOperation(configKey);
                 } else {
                     // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
                     IBinder intentSender = pendingIntent.getTarget().asBinder();
-                    return service.setDataFetchOperation(configKey, intentSender);
+                    service.setDataFetchOperation(configKey, intentSender);
                 }
 
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to connect to statsd when registering data listener.");
-                return false;
+                throw new StatsUnavailableException("could not connect", e);
             }
         }
     }
 
     /**
-     * Clients can request data with a binder call. This getter is destructive and also clears
-     * the retrieved metrics from statsd memory.
-     *
-     * @param configKey Configuration key to retrieve data from.
-     * @return Serialized ConfigMetricsReportList proto. Returns null on failure (eg, if statsd
-     * crashed).
+     * TODO: Temporary for backwards compatibility. Remove.
      */
     @RequiresPermission(Manifest.permission.DUMP)
-    public @Nullable byte[] getData(long configKey) {
+    public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
+        try {
+            setFetchReportsOperation(pendingIntent, configKey);
+            return true;
+        } catch (StatsUnavailableException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Request the data collected for the given configKey.
+     * This getter is destructive - it also clears the retrieved metrics from statsd's memory.
+     *
+     * @param configKey Configuration key to retrieve data from.
+     * @return Serialized ConfigMetricsReportList proto.
+     * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    public byte[] getReports(long configKey) throws StatsUnavailableException {
         synchronized (this) {
             try {
                 IStatsManager service = getIStatsManagerLocked();
-                if (service == null) {
-                    Slog.e(TAG, "Failed to find statsd when getting data");
-                    return null;
-                }
                 return service.getData(configKey);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to connect to statsd when getting data");
-                return null;
+                throw new StatsUnavailableException("could not connect", e);
+            }
+        }
+    }
+
+    /**
+     * TODO: Temporary for backwards compatibility. Remove.
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    public @Nullable byte[] getData(long configKey) {
+        try {
+            return getReports(configKey);
+        } catch (StatsUnavailableException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Clients can request metadata for statsd. Will contain stats across all configurations but not
+     * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
+     * This getter is not destructive and will not reset any metrics/counters.
+     *
+     * @return Serialized StatsdStatsReport proto.
+     * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    public byte[] getStatsMetadata() throws StatsUnavailableException {
+        synchronized (this) {
+            try {
+                IStatsManager service = getIStatsManagerLocked();
+                return service.getMetadata();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to connect to statsd when getting metadata");
+                throw new StatsUnavailableException("could not connect", e);
             }
         }
     }
 
     /**
      * Clients can request metadata for statsd. Will contain stats across all configurations but not
-     * the actual metrics themselves (metrics must be collected via {@link #getData(String)}.
+     * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
      * This getter is not destructive and will not reset any metrics/counters.
      *
      * @return Serialized StatsdStatsReport proto. Returns null on failure (eg, if statsd crashed).
      */
     @RequiresPermission(Manifest.permission.DUMP)
     public @Nullable byte[] getMetadata() {
-        synchronized (this) {
-            try {
-                IStatsManager service = getIStatsManagerLocked();
-                if (service == null) {
-                    Slog.e(TAG, "Failed to find statsd when getting metadata");
-                    return null;
-                }
-                return service.getMetadata();
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to connect to statsd when getting metadata");
-                return null;
-            }
+        try {
+            return getStatsMetadata();
+        } catch (StatsUnavailableException e) {
+            return null;
         }
     }
 
@@ -279,14 +341,33 @@
         }
     }
 
-    private IStatsManager getIStatsManagerLocked() throws RemoteException {
+    private IStatsManager getIStatsManagerLocked() throws StatsUnavailableException {
         if (mService != null) {
             return mService;
         }
         mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats"));
-        if (mService != null) {
+        if (mService == null) {
+            throw new StatsUnavailableException("could not be found");
+        }
+        try {
             mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0);
+        } catch (RemoteException e) {
+            throw new StatsUnavailableException("could not connect when linkToDeath", e);
         }
         return mService;
     }
+
+    /**
+     * Exception thrown when communication with the stats service fails (eg if it is not available).
+     * This might be thrown early during boot before the stats service has started or if it crashed.
+     */
+    public static class StatsUnavailableException extends AndroidException {
+        public StatsUnavailableException(String reason) {
+            super("Failed to connect to statsd: " + reason);
+        }
+
+        public StatsUnavailableException(String reason, Throwable e) {
+            super("Failed to connect to statsd: " + reason, e);
+        }
+    }
 }
diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java
index 1776eac..246d4a3 100644
--- a/android/app/SystemServiceRegistry.java
+++ b/android/app/SystemServiceRegistry.java
@@ -18,6 +18,7 @@
 
 import android.accounts.AccountManager;
 import android.accounts.IAccountManager;
+import android.app.ContextImpl.ServiceInitializationState;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.IDevicePolicyManager;
 import android.app.job.IJobScheduler;
@@ -104,10 +105,12 @@
 import android.os.BatteryManager;
 import android.os.BatteryStats;
 import android.os.Build;
+import android.os.DeviceIdleManager;
 import android.os.DropBoxManager;
 import android.os.HardwarePropertiesManager;
 import android.os.IBatteryPropertiesRegistrar;
 import android.os.IBinder;
+import android.os.IDeviceIdleController;
 import android.os.IHardwarePropertiesManager;
 import android.os.IPowerManager;
 import android.os.IRecoverySystem;
@@ -160,7 +163,6 @@
 import com.android.internal.policy.PhoneLayoutInflater;
 
 import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Manages all of the system services that can be returned by {@link Context#getSystemService}.
@@ -279,12 +281,12 @@
             }});
 
         registerService(Context.IPSEC_SERVICE, IpSecManager.class,
-                new StaticServiceFetcher<IpSecManager>() {
+                new CachedServiceFetcher<IpSecManager>() {
             @Override
-            public IpSecManager createService() {
+            public IpSecManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                 IBinder b = ServiceManager.getService(Context.IPSEC_SERVICE);
                 IIpSecService service = IIpSecService.Stub.asInterface(b);
-                return new IpSecManager(service);
+                return new IpSecManager(ctx, service);
             }});
 
         registerService(Context.COUNTRY_DETECTOR, CountryDetector.class,
@@ -984,6 +986,17 @@
                                 ctx.mMainThread.getHandler());
                     }
             });
+
+        registerService(Context.DEVICE_IDLE_CONTROLLER, DeviceIdleManager.class,
+                new CachedServiceFetcher<DeviceIdleManager>() {
+                    @Override
+                    public DeviceIdleManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IDeviceIdleController service = IDeviceIdleController.Stub.asInterface(
+                                ServiceManager.getServiceOrThrow(
+                                        Context.DEVICE_IDLE_CONTROLLER));
+                        return new DeviceIdleManager(ctx.getOuterContext(), service);
+                    }});
     }
 
     /**
@@ -993,10 +1006,6 @@
         return new Object[sServiceCacheSize];
     }
 
-    public static AtomicInteger[] createServiceInitializationStateArray() {
-        return new AtomicInteger[sServiceCacheSize];
-    }
-
     /**
      * Gets a system service from a given context.
      */
@@ -1037,7 +1046,10 @@
     static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
         private final int mCacheIndex;
 
-        public CachedServiceFetcher() {
+        CachedServiceFetcher() {
+            // Note this class must be instantiated only by the static initializer of the
+            // outer class (SystemServiceRegistry), which already does the synchronization,
+            // so bare access to sServiceCacheSize is okay here.
             mCacheIndex = sServiceCacheSize++;
         }
 
@@ -1045,95 +1057,73 @@
         @SuppressWarnings("unchecked")
         public final T getService(ContextImpl ctx) {
             final Object[] cache = ctx.mServiceCache;
+            final int[] gates = ctx.mServiceInitializationStateArray;
 
-            // Fast path. If it's already cached, just return it.
-            Object service = cache[mCacheIndex];
-            if (service != null) {
-                return (T) service;
-            }
+            for (;;) {
+                boolean doInitialize = false;
+                synchronized (cache) {
+                    // Return it if we already have a cached instance.
+                    T service = (T) cache[mCacheIndex];
+                    if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) {
+                        return service;
+                    }
 
-            // Slow path.
-            final AtomicInteger[] gates = ctx.mServiceInitializationStateArray;
-            final AtomicInteger gate;
+                    // If we get here, there's no cached instance.
 
-            synchronized (cache) {
-                // See if it's cached or not again, with the lock held this time.
-                service = cache[mCacheIndex];
-                if (service != null) {
-                    return (T) service;
+                    // Grr... if gate is STATE_READY, then this means we initialized the service
+                    // once but someone cleared it.
+                    // We start over from STATE_UNINITIALIZED.
+                    if (gates[mCacheIndex] == ContextImpl.STATE_READY) {
+                        gates[mCacheIndex] = ContextImpl.STATE_UNINITIALIZED;
+                    }
+
+                    // It's possible for multiple threads to get here at the same time, so
+                    // use the "gate" to make sure only the first thread will call createService().
+
+                    // At this point, the gate must be either UNINITIALIZED or INITIALIZING.
+                    if (gates[mCacheIndex] == ContextImpl.STATE_UNINITIALIZED) {
+                        doInitialize = true;
+                        gates[mCacheIndex] = ContextImpl.STATE_INITIALIZING;
+                    }
                 }
 
-                // Not initialized yet. Create an atomic boolean to control which thread should
-                // instantiate the service.
-                if (gates[mCacheIndex] != null) {
-                    gate = gates[mCacheIndex];
-                } else {
-                    gate = new AtomicInteger(ContextImpl.STATE_UNINITIALIZED);
-                    gates[mCacheIndex] = gate;
-                }
-            }
+                if (doInitialize) {
+                    // Only the first thread gets here.
 
-            // Not cached yet.
-            //
-            // Note multiple threads can reach here for the same service on the same context
-            // concurrently.
-            //
-            // Now we're going to instantiate the service, but do so without the cache held;
-            // otherwise it could deadlock. (b/71882178)
-            //
-            // However we still don't want to instantiate the same service multiple times, so
-            // use the atomic integer to ensure only one thread will call createService().
-
-            if (gate.compareAndSet(
-                    ContextImpl.STATE_UNINITIALIZED, ContextImpl.STATE_INITIALIZING)) {
-                try {
-                    // This thread is the first one to get here. Instantiate the service
-                    // *without* the cache lock held.
+                    T service = null;
+                    @ServiceInitializationState int newState = ContextImpl.STATE_NOT_FOUND;
                     try {
+                        // This thread is the first one to get here. Instantiate the service
+                        // *without* the cache lock held.
                         service = createService(ctx);
+                        newState = ContextImpl.STATE_READY;
 
-                        synchronized (cache) {
-                            cache[mCacheIndex] = service;
-                        }
                     } catch (ServiceNotFoundException e) {
                         onServiceNotFound(e);
-                    }
-                } finally {
-                    // Tell the all other threads that the cache is ready now.
-                    // (But it's still be null in case of ServiceNotFoundException.)
-                    synchronized (gate) {
-                        gate.set(ContextImpl.STATE_READY);
-                        gate.notifyAll();
-                    }
-                }
-                return (T) service;
-            }
-            // Other threads will wait on the gate lock.
-            synchronized (gate) {
-                boolean interrupted = false;
 
-                // Note: We check whether "state == STATE_READY", not
-                // "cache[mCacheIndex] != null", because "cache[mCacheIndex] == null"
-                // is still a valid outcome in the ServiceNotFoundException case.
-                while (gate.get() != ContextImpl.STATE_READY) {
-                    try {
-                        gate.wait();
-                    } catch (InterruptedException e) {
-                        Log.w(TAG,  "getService() interrupted");
-                        interrupted = true;
+                    } finally {
+                        synchronized (cache) {
+                            cache[mCacheIndex] = service;
+                            gates[mCacheIndex] = newState;
+                            cache.notifyAll();
+                        }
+                    }
+                    return service;
+                }
+                // The other threads will wait for the first thread to call notifyAll(),
+                // and go back to the top and retry.
+                synchronized (cache) {
+                    while (gates[mCacheIndex] < ContextImpl.STATE_READY) {
+                        try {
+                            cache.wait();
+                        } catch (InterruptedException e) {
+                            Log.w(TAG, "getService() interrupted");
+                            Thread.currentThread().interrupt();
+                            return null;
+                        }
                     }
                 }
-                if (interrupted) {
-                    Thread.currentThread().interrupt();
-                }
             }
-            // Now the first thread has initialized it.
-            // It may still be null if ServiceNotFoundException was thrown, but that shouldn't
-            // happen, so we'll just return null here in that case.
-            synchronized (cache) {
-                service = cache[mCacheIndex];
-            }
-            return (T) service;
         }
 
         public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java
index bd4933a..c03340e 100644
--- a/android/app/UiAutomation.java
+++ b/android/app/UiAutomation.java
@@ -580,6 +580,8 @@
         // Execute the command *without* the lock being held.
         command.run();
 
+        List<AccessibilityEvent> receivedEvents = new ArrayList<>();
+
         // Acquire the lock and wait for the event.
         try {
             // Wait for the event.
@@ -600,14 +602,14 @@
                     if (filter.accept(event)) {
                         return event;
                     }
-                    event.recycle();
+                    receivedEvents.add(event);
                 }
                 // Check if timed out and if not wait.
                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
                 final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
                 if (remainingTimeMillis <= 0) {
                     throw new TimeoutException("Expected event not received within: "
-                            + timeoutMillis + " ms.");
+                            + timeoutMillis + " ms among: " + receivedEvents);
                 }
                 synchronized (mLock) {
                     if (mEventQueue.isEmpty()) {
@@ -620,6 +622,11 @@
                 }
             }
         } finally {
+            int size = receivedEvents.size();
+            for (int i = 0; i < size; i++) {
+                receivedEvents.get(i).recycle();
+            }
+
             synchronized (mLock) {
                 mWaitingForEventDelivery = false;
                 mEventQueue.clear();
diff --git a/android/app/WallpaperManager.java b/android/app/WallpaperManager.java
index 465340f..6c2fb2d 100644
--- a/android/app/WallpaperManager.java
+++ b/android/app/WallpaperManager.java
@@ -401,7 +401,8 @@
                 }
             }
             synchronized (this) {
-                if (mCachedWallpaper != null && mCachedWallpaperUserId == userId) {
+                if (mCachedWallpaper != null && mCachedWallpaperUserId == userId
+                        && !mCachedWallpaper.isRecycled()) {
                     return mCachedWallpaper;
                 }
                 mCachedWallpaper = null;
@@ -412,7 +413,7 @@
                 } catch (OutOfMemoryError e) {
                     Log.w(TAG, "Out of memory loading the current wallpaper: " + e);
                 } catch (SecurityException e) {
-                    if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {
+                    if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) {
                         Log.w(TAG, "No permission to access wallpaper, suppressing"
                                 + " exception to avoid crashing legacy app.");
                     } else {
@@ -976,7 +977,7 @@
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             } catch (SecurityException e) {
-                if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {
+                if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) {
                     Log.w(TAG, "No permission to access wallpaper, suppressing"
                             + " exception to avoid crashing legacy app.");
                     return null;
diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java
index b64aae5..c491dcc 100644
--- a/android/app/admin/DevicePolicyManager.java
+++ b/android/app/admin/DevicePolicyManager.java
@@ -1169,10 +1169,18 @@
      * Constant to indicate the feature of mandatory backups. Used as argument to
      * {@link #createAdminSupportIntent(String)}.
      * @see #setMandatoryBackupTransport(ComponentName, ComponentName)
+     * @hide
      */
     public static final String POLICY_MANDATORY_BACKUPS = "policy_mandatory_backups";
 
     /**
+     * Constant to indicate the feature of suspending app. Use it as the value of
+     * {@link #EXTRA_RESTRICTION}.
+     * @hide
+     */
+    public static final String POLICY_SUSPEND_PACKAGES = "policy_suspend_packages";
+
+    /**
      * A String indicating a specific restricted feature. Can be a user restriction from the
      * {@link UserManager}, e.g. {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the values
      * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
@@ -4211,6 +4219,15 @@
         return null;
     }
 
+    /**
+     * Returns {@code true} if the device supports attestation of device identifiers in addition
+     * to key attestation.
+     * @return {@code true} if Device ID attestation is supported.
+     */
+    public boolean isDeviceIdAttestationSupported() {
+        PackageManager pm = mContext.getPackageManager();
+        return pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ID_ATTESTATION);
+    }
 
     /**
      * Called by a device or profile owner, or delegated certificate installer, to associate
@@ -6182,6 +6199,7 @@
      * @hide
      */
      @SystemApi
+     @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
      public @Nullable List<String> getPermittedAccessibilityServices(int userId) {
         throwIfParentInstance("getPermittedAccessibilityServices");
         if (mService != null) {
@@ -6826,8 +6844,7 @@
      * @param restriction Indicates for which feature the dialog should be displayed. Can be a
      *            user restriction from {@link UserManager}, e.g.
      *            {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the constants
-     *            {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
-     *            {@link #POLICY_MANDATORY_BACKUPS}.
+     *            {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
      * @return Intent An intent to be used to start the dialog-activity if the restriction is
      *            set by an admin, or null if the restriction does not exist or no admin set it.
      */
@@ -8774,13 +8791,6 @@
      *
      * <p> Backup service is off by default when device owner is present.
      *
-     * <p> If backups are made mandatory by specifying a non-null mandatory backup transport using
-     * the {@link DevicePolicyManager#setMandatoryBackupTransport} method, the backup service is
-     * automatically enabled.
-     *
-     * <p> If the backup service is disabled using this method after the mandatory backup transport
-     * has been set, the mandatory backup transport is cleared.
-     *
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @param enabled {@code true} to enable the backup service, {@code false} to disable it.
      * @throws SecurityException if {@code admin} is not a device owner.
@@ -8818,6 +8828,8 @@
      * <p>Only device owner can call this method.
      * <p>If backups were disabled and a non-null backup transport {@link ComponentName} is
      * specified, backups will be enabled.
+     * <p> If the backup service is disabled after the mandatory backup transport has been set, the
+     * mandatory backup transport is cleared.
      *
      * <p>NOTE: The method shouldn't be called on the main thread.
      *
@@ -8825,6 +8837,7 @@
      * @param backupTransportComponent The backup transport layer to be used for mandatory backups.
      * @return {@code true} if the backup transport was successfully set; {@code false} otherwise.
      * @throws SecurityException if {@code admin} is not a device owner.
+     * @hide
      */
     @WorkerThread
     public boolean setMandatoryBackupTransport(
@@ -8844,6 +8857,7 @@
      *
      * @return a {@link ComponentName} of the backup transport layer to be used if backups are
      *         mandatory or {@code null} if backups are not mandatory.
+     * @hide
      */
     public ComponentName getMandatoryBackupTransport() {
         throwIfParentInstance("getMandatoryBackupTransport");
diff --git a/android/app/admin/FreezeInterval.java b/android/app/admin/FreezePeriod.java
similarity index 74%
rename from android/app/admin/FreezeInterval.java
rename to android/app/admin/FreezePeriod.java
index de5e21a..657f017 100644
--- a/android/app/admin/FreezeInterval.java
+++ b/android/app/admin/FreezePeriod.java
@@ -20,49 +20,88 @@
 import android.util.Pair;
 
 import java.time.LocalDate;
+import java.time.MonthDay;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 
 /**
- * An interval representing one freeze period which repeats annually. We use the number of days
- * since the start of (non-leap) year to define the start and end dates of an interval, both
- * inclusive. If the end date is smaller than the start date, the interval is considered wrapped
- * around the year-end. As far as an interval is concerned, February 29th should be treated as
- * if it were February 28th: so an interval starting or ending on February 28th are not
- * distinguishable from an interval on February 29th. When calulating interval length or
- * distance between two dates, February 29th is also disregarded.
+ * A class that represents one freeze period which repeats <em>annually</em>. A freeze period has
+ * two {@link java.time#MonthDay} values that define the start and end dates of the period, both
+ * inclusive. If the end date is earlier than the start date, the period is considered wrapped
+ * around the year-end. As far as freeze period is concerned, leap year is disregarded and February
+ * 29th should be treated as if it were February 28th: so a freeze starting or ending on February
+ * 28th is identical to a freeze starting or ending on February 29th. When calulating the length of
+ * a freeze or the distance bewteen two freee periods, February 29th is also ignored.
  *
  * @see SystemUpdatePolicy#setFreezePeriods
- * @hide
  */
-public class FreezeInterval {
-    private static final String TAG = "FreezeInterval";
+public class FreezePeriod {
+    private static final String TAG = "FreezePeriod";
 
     private static final int DUMMY_YEAR = 2001;
     static final int DAYS_IN_YEAR = 365; // 365 since DUMMY_YEAR is not a leap year
 
-    final int mStartDay; // [1,365]
-    final int mEndDay; // [1,365]
+    private final MonthDay mStart;
+    private final MonthDay mEnd;
 
-    FreezeInterval(int startDay, int endDay) {
-        if (startDay < 1 || startDay > 365 || endDay < 1 || endDay > 365) {
-            throw new RuntimeException("Bad dates for Interval: " + startDay + "," + endDay);
-        }
-        mStartDay = startDay;
-        mEndDay = endDay;
+    /*
+     * Start and end dates represented by number of days since the beginning of the year.
+     * They are internal representations of mStart and mEnd with normalized Leap year days
+     * (Feb 29 == Feb 28 == 59th day of year). All internal calclations are based on
+     * these two values so that leap year days are disregarded.
+     */
+    private final int mStartDay; // [1, 365]
+    private final int mEndDay; // [1, 365]
+
+    /**
+     * Creates a freeze period by its start and end dates. If the end date is earlier than the start
+     * date, the freeze period is considered wrapping year-end.
+     */
+    public FreezePeriod(MonthDay start, MonthDay end) {
+        mStart = start;
+        mStartDay = mStart.atYear(DUMMY_YEAR).getDayOfYear();
+        mEnd = end;
+        mEndDay = mEnd.atYear(DUMMY_YEAR).getDayOfYear();
     }
 
+    /**
+     * Returns the start date (inclusive) of this freeze period.
+     */
+    public MonthDay getStart() {
+        return mStart;
+    }
+
+    /**
+     * Returns the end date (inclusive) of this freeze period.
+     */
+    public MonthDay getEnd() {
+        return mEnd;
+    }
+
+    /**
+     * @hide
+     */
+    private FreezePeriod(int startDay, int endDay) {
+        mStartDay = startDay;
+        mStart = dayOfYearToMonthDay(startDay);
+        mEndDay = endDay;
+        mEnd = dayOfYearToMonthDay(endDay);
+    }
+
+    /** @hide */
     int getLength() {
         return getEffectiveEndDay() - mStartDay + 1;
     }
 
+    /** @hide */
     boolean isWrapped() {
         return mEndDay < mStartDay;
     }
 
     /**
      * Returns the effective end day, taking wrapping around year-end into consideration
+     * @hide
      */
     int getEffectiveEndDay() {
         if (!isWrapped()) {
@@ -72,6 +111,7 @@
         }
     }
 
+    /** @hide */
     boolean contains(LocalDate localDate) {
         final int daysOfYear = dayOfYearDisregardLeapYear(localDate);
         if (!isWrapped()) {
@@ -84,6 +124,7 @@
         }
     }
 
+    /** @hide */
     boolean after(LocalDate localDate) {
         return mStartDay > dayOfYearDisregardLeapYear(localDate);
     }
@@ -95,6 +136,7 @@
      * include now, the returned dates represents the next future interval.
      * The result will always have the same month and dayOfMonth value as the non-instantiated
      * interval itself.
+     * @hide
      */
     Pair<LocalDate, LocalDate> toCurrentOrFutureRealDates(LocalDate now) {
         final int nowDays = dayOfYearDisregardLeapYear(now);
@@ -138,14 +180,24 @@
                 + LocalDate.ofYearDay(DUMMY_YEAR, mEndDay).format(formatter);
     }
 
-    // Treat the supplied date as in a non-leap year and return its day of year.
-    static int dayOfYearDisregardLeapYear(LocalDate date) {
+    /** @hide */
+    private static MonthDay dayOfYearToMonthDay(int dayOfYear) {
+        LocalDate date = LocalDate.ofYearDay(DUMMY_YEAR, dayOfYear);
+        return MonthDay.of(date.getMonth(), date.getDayOfMonth());
+    }
+
+    /**
+     * Treat the supplied date as in a non-leap year and return its day of year.
+     * @hide
+     */
+    private static int dayOfYearDisregardLeapYear(LocalDate date) {
         return date.withYear(DUMMY_YEAR).getDayOfYear();
     }
 
     /**
      * Compute the number of days between first (inclusive) and second (exclusive),
      * treating all years in between as non-leap.
+     * @hide
      */
     public static int distanceWithoutLeapYear(LocalDate first, LocalDate second) {
         return dayOfYearDisregardLeapYear(first) - dayOfYearDisregardLeapYear(second)
@@ -165,16 +217,16 @@
      *     3. At most one wrapped Interval remains, and it will be at the end of the list
      * @hide
      */
-    protected static List<FreezeInterval> canonicalizeIntervals(List<FreezeInterval> intervals) {
+    static List<FreezePeriod> canonicalizePeriods(List<FreezePeriod> intervals) {
         boolean[] taken = new boolean[DAYS_IN_YEAR];
         // First convert the intervals into flat array
-        for (FreezeInterval interval : intervals) {
+        for (FreezePeriod interval : intervals) {
             for (int i = interval.mStartDay; i <= interval.getEffectiveEndDay(); i++) {
                 taken[(i - 1) % DAYS_IN_YEAR] = true;
             }
         }
         // Then reconstruct intervals from the array
-        List<FreezeInterval> result = new ArrayList<>();
+        List<FreezePeriod> result = new ArrayList<>();
         int i = 0;
         while (i < DAYS_IN_YEAR) {
             if (!taken[i]) {
@@ -183,14 +235,14 @@
             }
             final int intervalStart = i + 1;
             while (i < DAYS_IN_YEAR && taken[i]) i++;
-            result.add(new FreezeInterval(intervalStart, i));
+            result.add(new FreezePeriod(intervalStart, i));
         }
         // Check if the last entry can be merged to the first entry to become one single
         // wrapped interval
         final int lastIndex = result.size() - 1;
         if (lastIndex > 0 && result.get(lastIndex).mEndDay == DAYS_IN_YEAR
                 && result.get(0).mStartDay == 1) {
-            FreezeInterval wrappedInterval = new FreezeInterval(result.get(lastIndex).mStartDay,
+            FreezePeriod wrappedInterval = new FreezePeriod(result.get(lastIndex).mStartDay,
                     result.get(0).mEndDay);
             result.set(lastIndex, wrappedInterval);
             result.remove(0);
@@ -207,18 +259,18 @@
      *
      * @hide
      */
-    protected static void validatePeriods(List<FreezeInterval> periods) {
-        List<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(periods);
+    static void validatePeriods(List<FreezePeriod> periods) {
+        List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods);
         if (allPeriods.size() != periods.size()) {
             throw SystemUpdatePolicy.ValidationFailedException.duplicateOrOverlapPeriods();
         }
         for (int i = 0; i < allPeriods.size(); i++) {
-            FreezeInterval current = allPeriods.get(i);
+            FreezePeriod current = allPeriods.get(i);
             if (current.getLength() > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) {
                 throw SystemUpdatePolicy.ValidationFailedException.freezePeriodTooLong("Freeze "
                         + "period " + current + " is too long: " + current.getLength() + " days");
             }
-            FreezeInterval previous = i > 0 ? allPeriods.get(i - 1)
+            FreezePeriod previous = i > 0 ? allPeriods.get(i - 1)
                     : allPeriods.get(allPeriods.size() - 1);
             if (previous != current) {
                 final int separation;
@@ -247,7 +299,7 @@
      *
      * @hide
      */
-    protected static void validateAgainstPreviousFreezePeriod(List<FreezeInterval> periods,
+    static void validateAgainstPreviousFreezePeriod(List<FreezePeriod> periods,
             LocalDate prevPeriodStart, LocalDate prevPeriodEnd, LocalDate now) {
         if (periods.size() == 0 || prevPeriodStart == null || prevPeriodEnd == null) {
             return;
@@ -258,14 +310,14 @@
             // Clock was adjusted backwards. We can continue execution though, the separation
             // and length validation below still works under this condition.
         }
-        List<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(periods);
+        List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods);
         // Given current time now, find the freeze period that's either current, or the one
         // that's immediately afterwards. For the later case, it might be after the year-end,
         // but this can only happen if there is only one freeze period.
-        FreezeInterval curOrNextFreezePeriod = allPeriods.get(0);
-        for (FreezeInterval interval : allPeriods) {
+        FreezePeriod curOrNextFreezePeriod = allPeriods.get(0);
+        for (FreezePeriod interval : allPeriods) {
             if (interval.contains(now)
-                    || interval.mStartDay > FreezeInterval.dayOfYearDisregardLeapYear(now)) {
+                    || interval.mStartDay > FreezePeriod.dayOfYearDisregardLeapYear(now)) {
                 curOrNextFreezePeriod = interval;
                 break;
             }
@@ -282,7 +334,7 @@
         // Now validate [prevPeriodStart, prevPeriodEnd] against curOrNextFreezeDates
         final String periodsDescription = "Prev: " + prevPeriodStart + "," + prevPeriodEnd
                 + "; cur: " + curOrNextFreezeDates.first + "," + curOrNextFreezeDates.second;
-        long separation = FreezeInterval.distanceWithoutLeapYear(curOrNextFreezeDates.first,
+        long separation = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.first,
                 prevPeriodEnd) - 1;
         if (separation > 0) {
             // Two intervals do not overlap, check separation
@@ -292,7 +344,7 @@
             }
         } else {
             // Two intervals overlap, check combined length
-            long length = FreezeInterval.distanceWithoutLeapYear(curOrNextFreezeDates.second,
+            long length = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.second,
                     prevPeriodStart) + 1;
             if (length > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) {
                 throw ValidationFailedException.combinedPeriodTooLong("Combined freeze period "
diff --git a/android/app/admin/SystemUpdatePolicy.java b/android/app/admin/SystemUpdatePolicy.java
index 47b3a81..20eef6c 100644
--- a/android/app/admin/SystemUpdatePolicy.java
+++ b/android/app/admin/SystemUpdatePolicy.java
@@ -38,9 +38,11 @@
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.MonthDay;
 import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -51,7 +53,7 @@
  * @see DevicePolicyManager#setSystemUpdatePolicy
  * @see DevicePolicyManager#getSystemUpdatePolicy
  */
-public class SystemUpdatePolicy implements Parcelable {
+public final class SystemUpdatePolicy implements Parcelable {
     private static final String TAG = "SystemUpdatePolicy";
 
     /** @hide */
@@ -163,6 +165,7 @@
                 ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE,
                 ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG,
                 ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE,
+                ERROR_UNKNOWN,
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface ValidationFailureType {}
@@ -171,33 +174,38 @@
         public static final int ERROR_NONE = 0;
 
         /**
+         * Validation failed with unknown error.
+         */
+        public static final int ERROR_UNKNOWN = 1;
+
+        /**
          * The freeze periods contains duplicates, periods that overlap with each
          * other or periods whose start and end joins.
          */
-        public static final int ERROR_DUPLICATE_OR_OVERLAP = 1;
+        public static final int ERROR_DUPLICATE_OR_OVERLAP = 2;
 
         /**
          * There exists at least one freeze period whose length exceeds 90 days.
          */
-        public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 2;
+        public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 3;
 
         /**
          * There exists some freeze period which starts within 60 days of the preceding period's
          * end time.
          */
-        public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 3;
+        public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 4;
 
         /**
          * The device has been in a freeze period and when combining with the new freeze period
          * to be set, it will result in the total freeze period being longer than 90 days.
          */
-        public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 4;
+        public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 5;
 
         /**
          * The device has been in a freeze period and some new freeze period to be set is less
          * than 60 days from the end of the last freeze period the device went through.
          */
-        public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 5;
+        public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 6;
 
         @ValidationFailureType
         private final int mErrorCode;
@@ -272,7 +280,7 @@
     private int mMaintenanceWindowStart;
     private int mMaintenanceWindowEnd;
 
-    private final ArrayList<FreezeInterval> mFreezePeriods;
+    private final ArrayList<FreezePeriod> mFreezePeriods;
 
     private SystemUpdatePolicy() {
         mPolicyType = TYPE_UNKNOWN;
@@ -444,12 +452,10 @@
      *         requirement set above
      * @return this instance
      */
-    public SystemUpdatePolicy setFreezePeriods(List<Pair<Integer, Integer>> freezePeriods) {
-        List<FreezeInterval> newPeriods = freezePeriods.stream().map(
-                p -> new FreezeInterval(p.first, p.second)).collect(Collectors.toList());
-        FreezeInterval.validatePeriods(newPeriods);
+    public SystemUpdatePolicy setFreezePeriods(List<FreezePeriod> freezePeriods) {
+        FreezePeriod.validatePeriods(freezePeriods);
         mFreezePeriods.clear();
-        mFreezePeriods.addAll(newPeriods);
+        mFreezePeriods.addAll(freezePeriods);
         return this;
     }
 
@@ -458,12 +464,8 @@
      *
      * @return the list of freeze periods, or an empty list if none was set.
      */
-    public List<Pair<Integer, Integer>> getFreezePeriods() {
-        List<Pair<Integer, Integer>> result = new ArrayList<>(mFreezePeriods.size());
-        for (FreezeInterval interval : mFreezePeriods) {
-            result.add(new Pair<>(interval.mStartDay, interval.mEndDay));
-        }
-        return result;
+    public List<FreezePeriod> getFreezePeriods() {
+        return Collections.unmodifiableList(mFreezePeriods);
     }
 
     /**
@@ -472,7 +474,7 @@
      * @hide
      */
     public Pair<LocalDate, LocalDate> getCurrentFreezePeriod(LocalDate now) {
-        for (FreezeInterval interval : mFreezePeriods) {
+        for (FreezePeriod interval : mFreezePeriods) {
             if (interval.contains(now)) {
                 return interval.toCurrentOrFutureRealDates(now);
             }
@@ -485,10 +487,10 @@
      * is not within a freeze period.
      */
     private long timeUntilNextFreezePeriod(long now) {
-        List<FreezeInterval> sortedPeriods = FreezeInterval.canonicalizeIntervals(mFreezePeriods);
+        List<FreezePeriod> sortedPeriods = FreezePeriod.canonicalizePeriods(mFreezePeriods);
         LocalDate nowDate = millisToDate(now);
         LocalDate nextFreezeStart = null;
-        for (FreezeInterval interval : sortedPeriods) {
+        for (FreezePeriod interval : sortedPeriods) {
             if (interval.after(nowDate)) {
                 nextFreezeStart = interval.toCurrentOrFutureRealDates(nowDate).first;
                 break;
@@ -506,13 +508,13 @@
 
     /** @hide */
     public void validateFreezePeriods() {
-        FreezeInterval.validatePeriods(mFreezePeriods);
+        FreezePeriod.validatePeriods(mFreezePeriods);
     }
 
     /** @hide */
     public void validateAgainstPreviousFreezePeriod(LocalDate prevPeriodStart,
             LocalDate prevPeriodEnd, LocalDate now) {
-        FreezeInterval.validateAgainstPreviousFreezePeriod(mFreezePeriods, prevPeriodStart,
+        FreezePeriod.validateAgainstPreviousFreezePeriod(mFreezePeriods, prevPeriodStart,
                 prevPeriodEnd, now);
     }
 
@@ -521,10 +523,10 @@
      * updates and how long this action is valid for, given the current system update policy. Its
      * action could be one of the following
      * <ul>
-     * <li> {@code TYPE_INSTALL_AUTOMATIC} system updates should be installed immedately and without
-     * user intervention as soon as they become available.
-     * <li> {@code TYPE_POSTPONE} system updates should be postponed for a maximum of 30 days
-     * <li> {@code TYPE_PAUSE} system updates should be postponed indefinitely until further notice
+     * <li> {@link #TYPE_INSTALL_AUTOMATIC} system updates should be installed immedately and
+     * without user intervention as soon as they become available.
+     * <li> {@link #TYPE_POSTPONE} system updates should be postponed for a maximum of 30 days
+     * <li> {@link #TYPE_PAUSE} system updates should be postponed indefinitely until further notice
      * </ul>
      *
      * The effective time measures how long this installation option is valid for from the queried
@@ -535,18 +537,38 @@
      */
     @SystemApi
     public static class InstallationOption {
+        /** @hide */
+        @IntDef(prefix = { "TYPE_" }, value = {
+                TYPE_INSTALL_AUTOMATIC,
+                TYPE_PAUSE,
+                TYPE_POSTPONE
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        @interface InstallationOptionType {}
+
+        @InstallationOptionType
         private final int mType;
         private long mEffectiveTime;
 
-        InstallationOption(int type, long effectiveTime) {
+        InstallationOption(@InstallationOptionType int type, long effectiveTime) {
             this.mType = type;
             this.mEffectiveTime = effectiveTime;
         }
 
-        public int getType() {
+        /**
+         * Returns the type of the current installation option, could be one of
+         * {@link #TYPE_INSTALL_AUTOMATIC}, {@link #TYPE_POSTPONE} and {@link #TYPE_PAUSE}.
+         * @return type of installation option.
+         */
+        public @InstallationOptionType int getType() {
             return mType;
         }
 
+        /**
+         * Returns how long the current installation option in effective for, starting from the time
+         * of query.
+         * @return the effective time in milliseconds.
+         */
         public long getEffectiveTime() {
             return mEffectiveTime;
         }
@@ -667,9 +689,11 @@
         int freezeCount = mFreezePeriods.size();
         dest.writeInt(freezeCount);
         for (int i = 0; i < freezeCount; i++) {
-            FreezeInterval interval = mFreezePeriods.get(i);
-            dest.writeInt(interval.mStartDay);
-            dest.writeInt(interval.mEndDay);
+            FreezePeriod interval = mFreezePeriods.get(i);
+            dest.writeInt(interval.getStart().getMonthValue());
+            dest.writeInt(interval.getStart().getDayOfMonth());
+            dest.writeInt(interval.getEnd().getMonthValue());
+            dest.writeInt(interval.getEnd().getDayOfMonth());
         }
     }
 
@@ -686,8 +710,9 @@
                     int freezeCount = source.readInt();
                     policy.mFreezePeriods.ensureCapacity(freezeCount);
                     for (int i = 0; i < freezeCount; i++) {
-                        policy.mFreezePeriods.add(
-                                new FreezeInterval(source.readInt(), source.readInt()));
+                        MonthDay start = MonthDay.of(source.readInt(), source.readInt());
+                        MonthDay end = MonthDay.of(source.readInt(), source.readInt());
+                        policy.mFreezePeriods.add(new FreezePeriod(start, end));
                     }
                     return policy;
                 }
@@ -730,9 +755,9 @@
                     if (!parser.getName().equals(KEY_FREEZE_TAG)) {
                         continue;
                     }
-                    policy.mFreezePeriods.add(new FreezeInterval(
-                            Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_START)),
-                            Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_END))));
+                    policy.mFreezePeriods.add(new FreezePeriod(
+                            MonthDay.parse(parser.getAttributeValue(null, KEY_FREEZE_START)),
+                            MonthDay.parse(parser.getAttributeValue(null, KEY_FREEZE_END))));
                 }
                 return policy;
             }
@@ -751,10 +776,10 @@
         out.attribute(null, KEY_INSTALL_WINDOW_START, Integer.toString(mMaintenanceWindowStart));
         out.attribute(null, KEY_INSTALL_WINDOW_END, Integer.toString(mMaintenanceWindowEnd));
         for (int i = 0; i < mFreezePeriods.size(); i++) {
-            FreezeInterval interval = mFreezePeriods.get(i);
+            FreezePeriod interval = mFreezePeriods.get(i);
             out.startTag(null, KEY_FREEZE_TAG);
-            out.attribute(null, KEY_FREEZE_START, Integer.toString(interval.mStartDay));
-            out.attribute(null, KEY_FREEZE_END, Integer.toString(interval.mEndDay));
+            out.attribute(null, KEY_FREEZE_START, interval.getStart().toString());
+            out.attribute(null, KEY_FREEZE_END, interval.getEnd().toString());
             out.endTag(null, KEY_FREEZE_TAG);
         }
     }
diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java
index bf3398a..4336f18 100644
--- a/android/app/slice/Slice.java
+++ b/android/app/slice/Slice.java
@@ -21,19 +21,13 @@
 import android.annotation.StringDef;
 import android.app.PendingIntent;
 import android.app.RemoteInput;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.IContentProvider;
-import android.content.Intent;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.RemoteException;
 
 import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -575,45 +569,4 @@
         }
         return sb.toString();
     }
-
-    /**
-     * @deprecated TO BE REMOVED.
-     */
-    @Deprecated
-    public static @Nullable Slice bindSlice(ContentResolver resolver,
-            @NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
-        Preconditions.checkNotNull(uri, "uri");
-        IContentProvider provider = resolver.acquireProvider(uri);
-        if (provider == null) {
-            throw new IllegalArgumentException("Unknown URI " + uri);
-        }
-        try {
-            Bundle extras = new Bundle();
-            extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
-            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
-                    new ArrayList<>(supportedSpecs));
-            final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE,
-                    null, extras);
-            Bundle.setDefusable(res, true);
-            if (res == null) {
-                return null;
-            }
-            return res.getParcelable(SliceProvider.EXTRA_SLICE);
-        } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
-            return null;
-        } finally {
-            resolver.releaseProvider(provider);
-        }
-    }
-
-    /**
-     * @deprecated TO BE REMOVED.
-     */
-    @Deprecated
-    public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent,
-            @NonNull List<SliceSpec> supportedSpecs) {
-        return context.getSystemService(SliceManager.class).bindSlice(intent, supportedSpecs);
-    }
 }
diff --git a/android/app/slice/SliceManager.java b/android/app/slice/SliceManager.java
index 0285e9f..ad49437 100644
--- a/android/app/slice/SliceManager.java
+++ b/android/app/slice/SliceManager.java
@@ -16,6 +16,8 @@
 
 package android.app.slice;
 
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
@@ -38,6 +40,7 @@
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
 import android.os.UserHandle;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.util.Preconditions;
@@ -47,6 +50,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Class to handle interactions with {@link Slice}s.
@@ -101,22 +105,6 @@
     private final IBinder mToken = new Binder();
 
     /**
-     * Permission denied.
-     * @hide
-     */
-    public static final int PERMISSION_DENIED = -1;
-    /**
-     * Permission granted.
-     * @hide
-     */
-    public static final int PERMISSION_GRANTED = 0;
-    /**
-     * Permission just granted by the user, and should be granted uri permission as well.
-     * @hide
-     */
-    public static final int PERMISSION_USER_GRANTED = 1;
-
-    /**
      * @hide
      */
     public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
@@ -140,7 +128,7 @@
      * @see Intent#ACTION_ASSIST
      * @see Intent#CATEGORY_HOME
      */
-    public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
+    public void pinSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> specs) {
         try {
             mService.pinSlice(mContext.getPackageName(), uri,
                     specs.toArray(new SliceSpec[specs.size()]), mToken);
@@ -150,6 +138,14 @@
     }
 
     /**
+     * @deprecated TO BE REMOVED
+     */
+    @Deprecated
+    public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
+        pinSlice(uri, new ArraySet<>(specs));
+    }
+
+    /**
      * Remove a pin for a slice.
      * <p>
      * If the slice has no other pins/callbacks then the slice will be unpinned.
@@ -189,9 +185,10 @@
      * into account all clients and returns only specs supported by all.
      * @see SliceSpec
      */
-    public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) {
+    public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) {
         try {
-            return Arrays.asList(mService.getPinnedSpecs(uri, mContext.getPackageName()));
+            return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri,
+                    mContext.getPackageName())));
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -240,7 +237,7 @@
      * @return The Slice provided by the app or null if none is given.
      * @see Slice
      */
-    public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
+    public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) {
         Preconditions.checkNotNull(uri, "uri");
         ContentResolver resolver = mContext.getContentResolver();
         try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
@@ -265,6 +262,14 @@
     }
 
     /**
+     * @deprecated TO BE REMOVED
+     */
+    @Deprecated
+    public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
+        return bindSlice(uri, new ArraySet<>(supportedSpecs));
+    }
+
+    /**
      * Turns a slice intent into a slice uri. Expects an explicit intent.
      * <p>
      * This goes through a several stage resolution process to determine if any slice
@@ -272,12 +277,12 @@
      * <ol>
      *  <li> If the intent contains data that {@link ContentResolver#getType} is
      *  {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li>
-     *  <li>If the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
-     *  the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
-     *  will be returned.</li>
-     *  <li>Lastly, if the intent explicitly points at an activity, and that activity has
+     *  <li>If the intent explicitly points at an activity, and that activity has
      *  meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
      *  returned.</li>
+     *  <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
+     *  the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
+     *  will be returned.</li>
      *  <li>If no slice is found, then {@code null} is returned.</li>
      * </ol>
      * @param intent The intent associated with a slice.
@@ -287,37 +292,12 @@
      * @see Intent
      */
     public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
-        Preconditions.checkNotNull(intent, "intent");
-        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
-                || intent.getData() != null,
-                "Slice intent must be explicit %s", intent);
         ContentResolver resolver = mContext.getContentResolver();
-
-        // Check if the intent has data for the slice uri on it and use that
-        final Uri intentData = intent.getData();
-        if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
-            return intentData;
-        }
+        final Uri staticUri = resolveStatic(intent, resolver);
+        if (staticUri != null) return staticUri;
         // Otherwise ask the app
-        Intent queryIntent = new Intent(intent);
-        if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
-            queryIntent.addCategory(CATEGORY_SLICE);
-        }
-        List<ResolveInfo> providers =
-                mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
-        if (providers == null || providers.isEmpty()) {
-            // There are no providers, see if this activity has a direct link.
-            ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
-                    PackageManager.GET_META_DATA);
-            if (resolve != null && resolve.activityInfo != null
-                    && resolve.activityInfo.metaData != null
-                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
-                return Uri.parse(
-                        resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
-            }
-            return null;
-        }
-        String authority = providers.get(0).providerInfo.authority;
+        String authority = getAuthority(intent);
+        if (authority == null) return null;
         Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(authority).build();
         try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
@@ -338,10 +318,43 @@
         }
     }
 
+    private String getAuthority(Intent intent) {
+        Intent queryIntent = new Intent(intent);
+        if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+            queryIntent.addCategory(CATEGORY_SLICE);
+        }
+        List<ResolveInfo> providers =
+                mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
+        return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority
+                : null;
+    }
+
+    private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) {
+        Preconditions.checkNotNull(intent, "intent");
+        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+                || intent.getData() != null,
+                "Slice intent must be explicit %s", intent);
+
+        // Check if the intent has data for the slice uri on it and use that
+        final Uri intentData = intent.getData();
+        if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
+            return intentData;
+        }
+        // There are no providers, see if this activity has a direct link.
+        ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
+                PackageManager.GET_META_DATA);
+        if (resolve != null && resolve.activityInfo != null
+                && resolve.activityInfo.metaData != null
+                && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
+            return Uri.parse(
+                    resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
+        }
+        return null;
+    }
+
     /**
-     * Turns a slice intent into slice content. Expects an explicit intent. If there is no
-     * {@link android.content.ContentProvider} associated with the given intent this will throw
-     * {@link IllegalArgumentException}.
+     * Turns a slice intent into slice content. Is a shortcut to perform the action
+     * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, List)} at once.
      *
      * @param intent The intent associated with a slice.
      * @param supportedSpecs List of supported specs.
@@ -351,34 +364,17 @@
      * @see Intent
      */
     public @Nullable Slice bindSlice(@NonNull Intent intent,
-            @NonNull List<SliceSpec> supportedSpecs) {
+            @NonNull Set<SliceSpec> supportedSpecs) {
         Preconditions.checkNotNull(intent, "intent");
         Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
                 || intent.getData() != null,
                 "Slice intent must be explicit %s", intent);
         ContentResolver resolver = mContext.getContentResolver();
-
-        // Check if the intent has data for the slice uri on it and use that
-        final Uri intentData = intent.getData();
-        if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
-            return bindSlice(intentData, supportedSpecs);
-        }
+        final Uri staticUri = resolveStatic(intent, resolver);
+        if (staticUri != null) return bindSlice(staticUri, supportedSpecs);
         // Otherwise ask the app
-        List<ResolveInfo> providers =
-                mContext.getPackageManager().queryIntentContentProviders(intent, 0);
-        if (providers == null || providers.isEmpty()) {
-            // There are no providers, see if this activity has a direct link.
-            ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
-                    PackageManager.GET_META_DATA);
-            if (resolve != null && resolve.activityInfo != null
-                    && resolve.activityInfo.metaData != null
-                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
-                return bindSlice(Uri.parse(resolve.activityInfo.metaData
-                        .getString(SLICE_METADATA_KEY)), supportedSpecs);
-            }
-            return null;
-        }
-        String authority = providers.get(0).providerInfo.authority;
+        String authority = getAuthority(intent);
+        if (authority == null) return null;
         Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(authority).build();
         try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
@@ -387,8 +383,6 @@
             }
             Bundle extras = new Bundle();
             extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
-            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
-                    new ArrayList<>(supportedSpecs));
             final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
             if (res == null) {
                 return null;
@@ -402,6 +396,16 @@
     }
 
     /**
+     * @deprecated TO BE REMOVED.
+     */
+    @Deprecated
+    @Nullable
+    public Slice bindSlice(@NonNull Intent intent,
+            @NonNull List<SliceSpec> supportedSpecs) {
+        return bindSlice(intent, new ArraySet<>(supportedSpecs));
+    }
+
+    /**
      * Determine whether a particular process and user ID has been granted
      * permission to access a specific slice URI.
      *
@@ -417,9 +421,11 @@
      * @see #grantSlicePermission(String, Uri)
      */
     public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
-        // TODO: Switch off Uri permissions.
-        return mContext.checkUriPermission(uri, pid, uid,
-                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        try {
+            return mService.checkSlicePermission(uri, null, pid, uid, null);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -431,11 +437,11 @@
      * @see #revokeSlicePermission
      */
     public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
-        // TODO: Switch off Uri permissions.
-        mContext.grantUriPermission(toPackage, uri,
-                Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
-                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                        | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+        try {
+            mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -453,11 +459,11 @@
      * @see #grantSlicePermission
      */
     public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
-        // TODO: Switch off Uri permissions.
-        mContext.revokeUriPermission(toPackage, uri,
-                Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
-                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                        | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+        try {
+            mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -478,16 +484,6 @@
                 throw new SecurityException("User " + uid + " does not have slice permission for "
                         + uri + ".");
             }
-            if (result == PERMISSION_USER_GRANTED) {
-                // We just had a user grant of this permission and need to grant this to the app
-                // permanently.
-                mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
-                        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
-                                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                                | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
-                // Notify a change has happened because we just granted a permission.
-                mContext.getContentResolver().notifyChange(uri, null);
-            }
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java
index fe5742d..d369272 100644
--- a/android/app/slice/SliceProvider.java
+++ b/android/app/slice/SliceProvider.java
@@ -44,6 +44,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A SliceProvider allows an app to provide content to be displayed in system spaces. This content
@@ -197,6 +198,14 @@
      * @see {@link Slice}.
      * @see {@link Slice#HINT_PARTIAL}
      */
+    public Slice onBindSlice(Uri sliceUri, Set<SliceSpec> supportedSpecs) {
+        return onBindSlice(sliceUri, new ArrayList<>(supportedSpecs));
+    }
+
+    /**
+     * @deprecated TO BE REMOVED
+     */
+    @Deprecated
     public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
         return null;
     }
diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java
index 7252f02..216a4a0 100644
--- a/android/app/usage/NetworkStats.java
+++ b/android/app/usage/NetworkStats.java
@@ -237,20 +237,26 @@
                 DEFAULT_NETWORK_YES
         })
         @Retention(RetentionPolicy.SOURCE)
-        public @interface DefaultNetwork {}
+        public @interface DefaultNetworkStatus {}
 
         /**
-         * Combined usage for this network regardless of whether it was the active default network.
+         * Combined usage for this network regardless of default network status.
          */
         public static final int DEFAULT_NETWORK_ALL = -1;
 
         /**
-         * Usage that occurs while this network is not the active default network.
+         * Usage that occurs while this network is not a default network.
+         *
+         * <p>This implies that the app responsible for this usage requested that it occur on a
+         * specific network different from the one(s) the system would have selected for it.
          */
         public static final int DEFAULT_NETWORK_NO = 0x1;
 
         /**
-         * Usage that occurs while this network is the active default network.
+         * Usage that occurs while this network is a default network.
+         *
+         * <p>This implies that the app either did not select a specific network for this usage,
+         * or it selected a network that the system could have selected for app traffic.
          */
         public static final int DEFAULT_NETWORK_YES = 0x2;
 
@@ -262,7 +268,7 @@
         private int mUid;
         private int mTag;
         private int mState;
-        private int mDefaultNetwork;
+        private int mDefaultNetworkStatus;
         private int mMetered;
         private int mRoaming;
         private long mBeginTimeStamp;
@@ -323,8 +329,9 @@
             return 0;
         }
 
-        private static @DefaultNetwork int convertDefaultNetwork(int defaultNetwork) {
-            switch (defaultNetwork) {
+        private static @DefaultNetworkStatus int convertDefaultNetworkStatus(
+                int defaultNetworkStatus) {
+            switch (defaultNetworkStatus) {
                 case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL;
                 case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO;
                 case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES;
@@ -397,18 +404,15 @@
         }
 
         /**
-         * Default network state. One of the following values:<p/>
+         * Default network status. One of the following values:<p/>
          * <ul>
          * <li>{@link #DEFAULT_NETWORK_ALL}</li>
          * <li>{@link #DEFAULT_NETWORK_NO}</li>
          * <li>{@link #DEFAULT_NETWORK_YES}</li>
          * </ul>
-         * <p>Indicates whether the network usage occurred on the system default network for this
-         * type of traffic, or whether the application chose to send this traffic on a network that
-         * was not the one selected by the system.
          */
-        public @DefaultNetwork int getDefaultNetwork() {
-            return mDefaultNetwork;
+        public @DefaultNetworkStatus int getDefaultNetworkStatus() {
+            return mDefaultNetworkStatus;
         }
 
         /**
@@ -605,7 +609,7 @@
         bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid);
         bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag);
         bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set);
-        bucketOut.mDefaultNetwork = Bucket.convertDefaultNetwork(
+        bucketOut.mDefaultNetworkStatus = Bucket.convertDefaultNetworkStatus(
                 mRecycledSummaryEntry.defaultNetwork);
         bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered);
         bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming);
@@ -657,7 +661,7 @@
                 bucketOut.mUid = Bucket.convertUid(getUid());
                 bucketOut.mTag = Bucket.convertTag(mTag);
                 bucketOut.mState = mState;
-                bucketOut.mDefaultNetwork = Bucket.DEFAULT_NETWORK_ALL;
+                bucketOut.mDefaultNetworkStatus = Bucket.DEFAULT_NETWORK_ALL;
                 bucketOut.mMetered = Bucket.METERED_ALL;
                 bucketOut.mRoaming = Bucket.ROAMING_ALL;
                 bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
diff --git a/android/app/usage/NetworkStatsManager.java b/android/app/usage/NetworkStatsManager.java
index b2fe958..0b21196 100644
--- a/android/app/usage/NetworkStatsManager.java
+++ b/android/app/usage/NetworkStatsManager.java
@@ -35,6 +35,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.DataUnit;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -95,6 +96,15 @@
     /** @hide */
     public static final int CALLBACK_RELEASED = 1;
 
+    /**
+     * Minimum data usage threshold for registering usage callbacks.
+     *
+     * Requests registered with a threshold lower than this will only be triggered once this minimum
+     * is reached.
+     * @hide
+     */
+    public static final long MIN_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(2);
+
     private final Context mContext;
     private final INetworkStatsService mService;
 
@@ -305,6 +315,8 @@
      *            {@link java.lang.System#currentTimeMillis}.
      * @param uid UID of app
      * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for no tags.
+     * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
+     *            traffic from all states.
      * @return Statistics object or null if an error happened during statistics collection.
      * @throws SecurityException if permissions are insufficient to read network statistics.
      */
diff --git a/android/app/usage/TimeSparseArray.java b/android/app/usage/TimeSparseArray.java
index 9ef88e4..4ec0e9e 100644
--- a/android/app/usage/TimeSparseArray.java
+++ b/android/app/usage/TimeSparseArray.java
@@ -88,7 +88,7 @@
                 key++;
                 keyIndex++;
             }
-            if (key >= origKey + 10) {
+            if (key >= origKey + 100) {
                 Slog.w(TAG, "Value " + value + " supposed to be inserted at " + origKey
                         + " displaced to " + key);
             }
diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java
index 84f57a3..503ca6c 100644
--- a/android/app/usage/UsageEvents.java
+++ b/android/app/usage/UsageEvents.java
@@ -111,7 +111,7 @@
 
         /**
          * An event type denoting a change in App Standby Bucket. The new bucket can be
-         * retrieved by calling {@link #getStandbyBucket()}.
+         * retrieved by calling {@link #getAppStandbyBucket()}.
          *
          * @see UsageStatsManager#getAppStandbyBucket()
          */
@@ -326,13 +326,23 @@
          * Returns the standby bucket of the app, if the event is of type
          * {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0.
          * @return the standby bucket associated with the event.
-         *
+         * @hide
          */
         public int getStandbyBucket() {
             return (mBucketAndReason & 0xFFFF0000) >>> 16;
         }
 
         /**
+         * Returns the standby bucket of the app, if the event is of type
+         * {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0.
+         * @return the standby bucket associated with the event.
+         *
+         */
+        public int getAppStandbyBucket() {
+            return (mBucketAndReason & 0xFFFF0000) >>> 16;
+        }
+
+        /**
          * Returns the reason for the bucketing, if the event is of type
          * {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0. Reason values include
          * the main reason which is one of REASON_MAIN_*, OR'ed with REASON_SUB_*, if there
diff --git a/android/appwidget/AppWidgetHost.java b/android/appwidget/AppWidgetHost.java
index 37360ba..49cc498 100644
--- a/android/appwidget/AppWidgetHost.java
+++ b/android/appwidget/AppWidgetHost.java
@@ -37,6 +37,7 @@
 import android.widget.RemoteViews;
 import android.widget.RemoteViews.OnClickHandler;
 
+import com.android.internal.R;
 import com.android.internal.appwidget.IAppWidgetHost;
 import com.android.internal.appwidget.IAppWidgetService;
 
@@ -171,8 +172,9 @@
                 return;
             }
             sServiceInitialized = true;
-            if (!context.getPackageManager().hasSystemFeature(
-                    PackageManager.FEATURE_APP_WIDGETS)) {
+            PackageManager packageManager = context.getPackageManager();
+            if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)
+                    && !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) {
                 return;
             }
             IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
diff --git a/android/bluetooth/BluetoothHearingAid.java b/android/bluetooth/BluetoothHearingAid.java
index 8f8083e..159e165 100644
--- a/android/bluetooth/BluetoothHearingAid.java
+++ b/android/bluetooth/BluetoothHearingAid.java
@@ -421,29 +421,29 @@
     }
 
     /**
-     * Check whether the device is active.
+     * Get the connected physical Hearing Aid devices that are active
      *
      * <p>Requires {@link android.Manifest.permission#BLUETOOTH}
      * permission.
      *
-     * @return the connected device that is active or null if no device
-     * is active
+     * @return the list of active devices. The first element is the left active
+     * device; the second element is the right active device. If either or both side
+     * is not active, it will be null on that position. Returns empty list on error.
      * @hide
      */
     @RequiresPermission(Manifest.permission.BLUETOOTH)
-    public boolean isActiveDevice(@Nullable BluetoothDevice device) {
-        if (VDBG) log("isActiveDevice()");
+    public List<BluetoothDevice> getActiveDevices() {
+        if (VDBG) log("getActiveDevices()");
         try {
             mServiceLock.readLock().lock();
-            if (mService != null && isEnabled()
-                    && ((device == null) || isValidDevice(device))) {
-                return mService.isActiveDevice(device);
+            if (mService != null && isEnabled()) {
+                return mService.getActiveDevices();
             }
             if (mService == null) Log.w(TAG, "Proxy not attached to service");
-            return false;
+            return new ArrayList<>();
         } catch (RemoteException e) {
             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
-            return false;
+            return new ArrayList<>();
         } finally {
             mServiceLock.readLock().unlock();
         }
diff --git a/android/bluetooth/BluetoothHidDevice.java b/android/bluetooth/BluetoothHidDevice.java
index af99bf7..3bc8544 100644
--- a/android/bluetooth/BluetoothHidDevice.java
+++ b/android/bluetooth/BluetoothHidDevice.java
@@ -701,6 +701,28 @@
     }
 
     /**
+     * Gets the application name of the current HidDeviceService user.
+     *
+     * @return the current user name, or empty string if cannot get the name
+     * {@hide}
+     */
+    public String getUserAppName() {
+        final IBluetoothHidDevice service = mService;
+
+        if (service != null) {
+            try {
+                return service.getUserAppName();
+            } catch (RemoteException e) {
+                Log.e(TAG, e.toString());
+            }
+        } else {
+            Log.w(TAG, "Proxy not attached to service");
+        }
+
+        return "";
+    }
+
+    /**
      * Initiates connection to host which is currently paired with this device. If the application
      * is not registered, #connect(BluetoothDevice) will fail. The connection state should be
      * tracked by the application by handling callback from Callback#onConnectionStateChanged. The
diff --git a/android/content/ContentResolver.java b/android/content/ContentResolver.java
index 9f3df37..f7908b6 100644
--- a/android/content/ContentResolver.java
+++ b/android/content/ContentResolver.java
@@ -51,7 +51,6 @@
 import android.util.EventLog;
 import android.util.Log;
 
-import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.MimeIconUtils;
 import com.android.internal.util.Preconditions;
 
@@ -602,6 +601,8 @@
             try {
                 return provider.getType(url);
             } catch (RemoteException e) {
+                // Arbitrary and not worth documenting, as Activity
+                // Manager will kill this process shortly anyway.
                 return null;
             } catch (java.lang.Exception e) {
                 Log.w(TAG, "Failed to get type for: " + url + " (" + e.getMessage() + ")");
@@ -620,9 +621,7 @@
                     ContentProvider.getUriWithoutUserId(url), resolveUserId(url));
             return type;
         } catch (RemoteException e) {
-            // Arbitrary and not worth documenting, as Activity
-            // Manager will kill this process shortly anyway.
-            return null;
+            throw e.rethrowFromSystemServer();
         } catch (java.lang.Exception e) {
             Log.w(TAG, "Failed to get type for: " + url + " (" + e.getMessage() + ")");
             return null;
@@ -1964,6 +1963,7 @@
             getContentService().registerContentObserver(uri, notifyForDescendents,
                     observer.getContentObserver(), userHandle, mTargetSdkVersion);
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -1982,6 +1982,7 @@
                         contentObserver);
             }
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2089,6 +2090,7 @@
                     syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0,
                     userHandle, mTargetSdkVersion);
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2105,6 +2107,7 @@
                     observer != null && observer.deliverSelfNotifications(), flags,
                     userHandle, mTargetSdkVersion);
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2126,6 +2129,7 @@
                     ContentProvider.getUriWithoutUserId(uri), modeFlags, /* toPackage= */ null,
                     resolveUserId(uri));
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2141,6 +2145,7 @@
                     ContentProvider.getUriWithoutUserId(uri), modeFlags, toPackage,
                     resolveUserId(uri));
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2160,6 +2165,7 @@
                     ContentProvider.getUriWithoutUserId(uri), modeFlags, /* toPackage= */ null,
                     resolveUserId(uri));
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2178,7 +2184,7 @@
             return ActivityManager.getService()
                     .getPersistedUriPermissions(mPackageName, true).getList();
         } catch (RemoteException e) {
-            throw new RuntimeException("Activity manager has died", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2194,7 +2200,7 @@
             return ActivityManager.getService()
                     .getPersistedUriPermissions(mPackageName, false).getList();
         } catch (RemoteException e) {
-            throw new RuntimeException("Activity manager has died", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2273,7 +2279,7 @@
         try {
             getContentService().syncAsUser(request, userId);
         } catch(RemoteException e) {
-            // Shouldn't happen.
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2285,7 +2291,7 @@
         try {
             getContentService().sync(request);
         } catch(RemoteException e) {
-            // Shouldn't happen.
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2349,6 +2355,7 @@
         try {
             getContentService().cancelSync(account, authority, null);
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2360,6 +2367,7 @@
         try {
             getContentService().cancelSyncAsUser(account, authority, null, userId);
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2371,7 +2379,7 @@
         try {
             return getContentService().getSyncAdapterTypes();
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2383,7 +2391,7 @@
         try {
             return getContentService().getSyncAdapterTypesAsUser(userId);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2397,8 +2405,8 @@
         try {
             return getContentService().getSyncAdapterPackagesForAuthorityAsUser(authority, userId);
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
-        return ArrayUtils.emptyArray(String.class);
     }
 
     /**
@@ -2414,7 +2422,7 @@
         try {
             return getContentService().getSyncAutomatically(account, authority);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2427,7 +2435,7 @@
         try {
             return getContentService().getSyncAutomaticallyAsUser(account, authority, userId);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2453,8 +2461,7 @@
         try {
             getContentService().setSyncAutomaticallyAsUser(account, authority, sync, userId);
         } catch (RemoteException e) {
-            // exception ignored; if this is thrown then it means the runtime is in the midst of
-            // being restarted
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2500,8 +2507,7 @@
         try {
              getContentService().addPeriodicSync(account, authority, extras, pollFrequency);
         } catch (RemoteException e) {
-            // exception ignored; if this is thrown then it means the runtime is in the midst of
-            // being restarted
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2540,7 +2546,7 @@
         try {
             getContentService().removePeriodicSync(account, authority, extras);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2564,8 +2570,7 @@
         try {
             getContentService().cancelRequest(request);
         } catch (RemoteException e) {
-            // exception ignored; if this is thrown then it means the runtime is in the midst of
-            // being restarted
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2582,7 +2587,7 @@
         try {
             return getContentService().getPeriodicSyncs(account, authority, null);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2596,7 +2601,7 @@
         try {
             return getContentService().getIsSyncable(account, authority);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2609,7 +2614,7 @@
         try {
             return getContentService().getIsSyncableAsUser(account, authority, userId);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2623,8 +2628,7 @@
         try {
             getContentService().setIsSyncable(account, authority, syncable);
         } catch (RemoteException e) {
-            // exception ignored; if this is thrown then it means the runtime is in the midst of
-            // being restarted
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2640,7 +2644,7 @@
         try {
             return getContentService().getMasterSyncAutomatically();
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2652,7 +2656,7 @@
         try {
             return getContentService().getMasterSyncAutomaticallyAsUser(userId);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2676,8 +2680,7 @@
         try {
             getContentService().setMasterSyncAutomaticallyAsUser(sync, userId);
         } catch (RemoteException e) {
-            // exception ignored; if this is thrown then it means the runtime is in the midst of
-            // being restarted
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2701,7 +2704,7 @@
         try {
             return getContentService().isSyncActive(account, authority, null);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2727,7 +2730,7 @@
             }
             return syncs.get(0);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2744,7 +2747,7 @@
         try {
             return getContentService().getCurrentSyncs();
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2756,7 +2759,7 @@
         try {
             return getContentService().getCurrentSyncsAsUser(userId);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2771,7 +2774,7 @@
         try {
             return getContentService().getSyncStatus(account, authority, null);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2784,7 +2787,7 @@
         try {
             return getContentService().getSyncStatusAsUser(account, authority, null, userId);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2809,7 +2812,7 @@
         try {
             return getContentService().isSyncPendingAsUser(account, authority, null, userId);
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2841,7 +2844,7 @@
             getContentService().addStatusChangeListener(mask, observer);
             return observer;
         } catch (RemoteException e) {
-            throw new RuntimeException("the ContentService should always be reachable", e);
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -2856,8 +2859,7 @@
         try {
             getContentService().removeStatusChangeListener((ISyncStatusObserver.Stub) handle);
         } catch (RemoteException e) {
-            // exception ignored; if this is thrown then it means the runtime is in the midst of
-            // being restarted
+            throw e.rethrowFromSystemServer();
         }
     }
 
@@ -3027,9 +3029,7 @@
             return sContentService;
         }
         IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME);
-        if (false) Log.v("ContentService", "default service binder = " + b);
         sContentService = IContentService.Stub.asInterface(b);
-        if (false) Log.v("ContentService", "default service = " + sContentService);
         return sContentService;
     }
 
@@ -3038,7 +3038,7 @@
         return mPackageName;
     }
 
-    private static IContentService sContentService;
+    private static volatile IContentService sContentService;
     private final Context mContext;
 
     final String mPackageName;
diff --git a/android/content/Context.java b/android/content/Context.java
index 920056a..ede7ee4 100644
--- a/android/content/Context.java
+++ b/android/content/Context.java
@@ -3780,7 +3780,7 @@
     public static final String DROPBOX_SERVICE = "dropbox";
 
     /**
-     * System service name for the DeviceIdleController.  There is no Java API for this.
+     * System service name for the DeviceIdleManager.
      * @see #getSystemService(String)
      * @hide
      */
diff --git a/android/content/Intent.java b/android/content/Intent.java
index 000912c..f608fcb 100644
--- a/android/content/Intent.java
+++ b/android/content/Intent.java
@@ -40,6 +40,7 @@
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.ResultReceiver;
 import android.os.ShellCommand;
@@ -1814,8 +1815,12 @@
     public static final String EXTRA_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME";
 
     /**
-     * Intent extra: A {@link Bundle} of extras for a package being suspended. Will be sent with
-     * {@link #ACTION_MY_PACKAGE_SUSPENDED}.
+     * Intent extra: A {@link Bundle} of extras for a package being suspended. Will be sent as an
+     * extra with {@link #ACTION_MY_PACKAGE_SUSPENDED}.
+     *
+     * <p>The contents of this {@link Bundle} are a contract between the suspended app and the
+     * suspending app, i.e. any app with the permission {@code android.permission.SUSPEND_APPS}.
+     * This is meant to enable the suspended app to better handle the state of being suspended.
      *
      * @see #ACTION_MY_PACKAGE_SUSPENDED
      * @see #ACTION_MY_PACKAGE_UNSUSPENDED
@@ -2282,6 +2287,34 @@
     public static final String ACTION_MY_PACKAGE_SUSPENDED = "android.intent.action.MY_PACKAGE_SUSPENDED";
 
     /**
+     * Activity Action: Started to show more details about why an application was suspended.
+     *
+     * <p>Whenever the system detects an activity launch for a suspended app, it shows a dialog to
+     * the user to inform them of the state and present them an affordance to start this activity
+     * action to show more details about the reason for suspension.
+     *
+     * <p>Apps holding {@link android.Manifest.permission#SUSPEND_APPS} must declare an activity
+     * handling this intent and protect it with
+     * {@link android.Manifest.permission#SEND_SHOW_SUSPENDED_APP_DETAILS}.
+     *
+     * <p>Includes an extra {@link #EXTRA_PACKAGE_NAME} which is the name of the suspended package.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     *
+     * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
+     * PersistableBundle, String)
+     * @see PackageManager#isPackageSuspended()
+     * @see #ACTION_PACKAGES_SUSPENDED
+     *
+     * @hide
+     */
+    @SystemApi
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_SHOW_SUSPENDED_APP_DETAILS =
+            "android.intent.action.SHOW_SUSPENDED_APP_DETAILS";
+
+    /**
      * Broadcast Action: Sent to a package that has been unsuspended.
      *
      * <p class="note">This is a protected intent that can only be sent
@@ -6788,6 +6821,9 @@
                 case "--activity-task-on-home":
                     intent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME);
                     break;
+                case "--activity-match-external":
+                    intent.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL);
+                    break;
                 case "--receiver-registered-only":
                     intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
                     break;
@@ -6924,7 +6960,7 @@
                 "    [--activity-no-user-action] [--activity-previous-is-top]",
                 "    [--activity-reorder-to-front] [--activity-reset-task-if-needed]",
                 "    [--activity-single-top] [--activity-clear-task]",
-                "    [--activity-task-on-home]",
+                "    [--activity-task-on-home] [--activity-match-external]",
                 "    [--receiver-registered-only] [--receiver-replace-pending]",
                 "    [--receiver-foreground] [--receiver-no-abort]",
                 "    [--receiver-include-background]",
diff --git a/android/content/QuickViewConstants.java b/android/content/QuickViewConstants.java
index a25513d..132d43f 100644
--- a/android/content/QuickViewConstants.java
+++ b/android/content/QuickViewConstants.java
@@ -45,8 +45,8 @@
      * Feature to delete an individual document. Quick viewer implementations must use
      * Storage Access Framework to both verify delete permission and to delete content.
      *
-     * @see DocumentsContract#Document#FLAG_SUPPORTS_DELETE
-     * @see DocumentsContract#deleteDocument(ContentResolver resolver, Uri documentUri)
+     * @see DocumentsContract.Document#FLAG_SUPPORTS_DELETE
+     * @see DocumentsContract#deleteDocument(ContentResolver, Uri)
      */
     public static final String FEATURE_DELETE = "android:delete";
 
diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java
index e85058d..d65e051 100644
--- a/android/content/pm/ApplicationInfo.java
+++ b/android/content/pm/ApplicationInfo.java
@@ -26,6 +26,7 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Environment;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -590,26 +591,33 @@
     public static final int PRIVATE_FLAG_VIRTUAL_PRELOAD = 1 << 16;
 
     /**
-     * Value for {@linl #privateFlags}: whether this app is pre-installed on the
+     * Value for {@link #privateFlags}: whether this app is pre-installed on the
      * OEM partition of the system image.
      * @hide
      */
     public static final int PRIVATE_FLAG_OEM = 1 << 17;
 
     /**
-     * Value for {@linl #privateFlags}: whether this app is pre-installed on the
+     * Value for {@link #privateFlags}: whether this app is pre-installed on the
      * vendor partition of the system image.
      * @hide
      */
     public static final int PRIVATE_FLAG_VENDOR = 1 << 18;
 
     /**
-     * Value for {@linl #privateFlags}: whether this app is pre-installed on the
+     * Value for {@link #privateFlags}: whether this app is pre-installed on the
      * product partition of the system image.
      * @hide
      */
     public static final int PRIVATE_FLAG_PRODUCT = 1 << 19;
 
+    /**
+     * Value for {@link #privateFlags}: whether this app is signed with the
+     * platform key.
+     * @hide
+     */
+    public static final int PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY = 1 << 20;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = {
             PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE,
@@ -629,6 +637,7 @@
             PRIVATE_FLAG_PRIVILEGED,
             PRIVATE_FLAG_PRODUCT,
             PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER,
+            PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY,
             PRIVATE_FLAG_STATIC_SHARED_LIBRARY,
             PRIVATE_FLAG_VENDOR,
             PRIVATE_FLAG_VIRTUAL_PRELOAD,
@@ -904,7 +913,17 @@
      * The app's declared version code.
      * @hide
      */
-    public long versionCode;
+    public long longVersionCode;
+
+    /**
+     * An integer representation of the app's declared version code. This is being left in place as
+     * some apps were using reflection to access it before the move to long in
+     * {@link android.os.Build.VERSION_CODES#P}
+     * @deprecated Use {@link #longVersionCode} instead.
+     * @hide
+     */
+    @Deprecated
+    public int versionCode;
 
     /**
      * The user-visible SDK version (ex. 26) of the framework against which the application claims
@@ -1114,11 +1133,12 @@
      */
     public static final int HIDDEN_API_ENFORCEMENT_NONE = 0;
     /**
-     * Light grey list enforcement, the strictest option. Enforces the light grey, dark grey and
-     * black lists.
+     * No API enforcement, but enable the detection logic and warnings. Observed behaviour is the
+     * same as {@link #HIDDEN_API_ENFORCEMENT_NONE} but you may see warnings in the log when APIs
+     * are accessed.
      * @hide
      * */
-    public static final int HIDDEN_API_ENFORCEMENT_ALL_LISTS = 1;
+    public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1;
     /**
      * Dark grey list enforcement. Enforces the dark grey and black lists
      * @hide
@@ -1140,14 +1160,15 @@
     @IntDef(prefix = { "HIDDEN_API_ENFORCEMENT_" }, value = {
             HIDDEN_API_ENFORCEMENT_DEFAULT,
             HIDDEN_API_ENFORCEMENT_NONE,
-            HIDDEN_API_ENFORCEMENT_ALL_LISTS,
+            HIDDEN_API_ENFORCEMENT_JUST_WARN,
             HIDDEN_API_ENFORCEMENT_DARK_GREY_AND_BLACK,
             HIDDEN_API_ENFORCEMENT_BLACK,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface HiddenApiEnforcementPolicy {}
 
-    private boolean isValidHiddenApiEnforcementPolicy(int policy) {
+    /** @hide */
+    public static boolean isValidHiddenApiEnforcementPolicy(int policy) {
         return policy >= HIDDEN_API_ENFORCEMENT_DEFAULT && policy <= HIDDEN_API_ENFORCEMENT_MAX;
     }
 
@@ -1214,7 +1235,7 @@
         pw.println(prefix + "enabled=" + enabled
                 + " minSdkVersion=" + minSdkVersion
                 + " targetSdkVersion=" + targetSdkVersion
-                + " versionCode=" + versionCode
+                + " versionCode=" + longVersionCode
                 + " targetSandboxVersion=" + targetSandboxVersion);
         if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
             if (manageSpaceActivityName != null) {
@@ -1287,7 +1308,7 @@
         proto.write(ApplicationInfoProto.Version.ENABLED, enabled);
         proto.write(ApplicationInfoProto.Version.MIN_SDK_VERSION, minSdkVersion);
         proto.write(ApplicationInfoProto.Version.TARGET_SDK_VERSION, targetSdkVersion);
-        proto.write(ApplicationInfoProto.Version.VERSION_CODE, versionCode);
+        proto.write(ApplicationInfoProto.Version.VERSION_CODE, longVersionCode);
         proto.write(ApplicationInfoProto.Version.TARGET_SANDBOX_VERSION, targetSandboxVersion);
         proto.end(versionToken);
 
@@ -1421,7 +1442,7 @@
         uid = orig.uid;
         minSdkVersion = orig.minSdkVersion;
         targetSdkVersion = orig.targetSdkVersion;
-        versionCode = orig.versionCode;
+        setVersionCode(orig.longVersionCode);
         enabled = orig.enabled;
         enabledSetting = orig.enabledSetting;
         installLocation = orig.installLocation;
@@ -1495,7 +1516,7 @@
         dest.writeInt(uid);
         dest.writeInt(minSdkVersion);
         dest.writeInt(targetSdkVersion);
-        dest.writeLong(versionCode);
+        dest.writeLong(longVersionCode);
         dest.writeInt(enabled ? 1 : 0);
         dest.writeInt(enabledSetting);
         dest.writeInt(installLocation);
@@ -1566,7 +1587,7 @@
         uid = source.readInt();
         minSdkVersion = source.readInt();
         targetSdkVersion = source.readInt();
-        versionCode = source.readLong();
+        setVersionCode(source.readLong());
         enabled = source.readInt() != 0;
         enabledSetting = source.readInt();
         installLocation = source.readInt();
@@ -1658,17 +1679,26 @@
         return SystemConfig.getInstance().getHiddenApiWhitelistedApps().contains(packageName);
     }
 
+    private boolean isAllowedToUseHiddenApis() {
+        return isSignedWithPlatformKey()
+            || (isPackageWhitelistedForHiddenApis() && (isSystemApp() || isUpdatedSystemApp()));
+    }
+
     /**
      * @hide
      */
     public @HiddenApiEnforcementPolicy int getHiddenApiEnforcementPolicy() {
+        if (isAllowedToUseHiddenApis()) {
+            return HIDDEN_API_ENFORCEMENT_NONE;
+        }
         if (mHiddenApiPolicy != HIDDEN_API_ENFORCEMENT_DEFAULT) {
             return mHiddenApiPolicy;
         }
-        if (isPackageWhitelistedForHiddenApis() && (isSystemApp() || isUpdatedSystemApp())) {
-            return HIDDEN_API_ENFORCEMENT_NONE;
+        if (targetSdkVersion < Build.VERSION_CODES.P) {
+            return HIDDEN_API_ENFORCEMENT_BLACK;
+        } else {
+            return HIDDEN_API_ENFORCEMENT_DARK_GREY_AND_BLACK;
         }
-        return HIDDEN_API_ENFORCEMENT_BLACK;
     }
 
     /**
@@ -1682,6 +1712,39 @@
     }
 
     /**
+     * Updates the hidden API enforcement policy for this app from the given values, if appropriate.
+     *
+     * This will have no effect if this app is not subject to hidden API enforcement, i.e. if it
+     * is on the package whitelist.
+     *
+     * @param policyPreP configured policy for pre-P apps, or {@link
+     *        #HIDDEN_API_ENFORCEMENT_DEFAULT} if nothing configured.
+     * @param policyP configured policy for apps targeting P or later, or {@link
+     *        #HIDDEN_API_ENFORCEMENT_DEFAULT} if nothing configured.
+     * @hide
+     */
+    public void maybeUpdateHiddenApiEnforcementPolicy(
+            @HiddenApiEnforcementPolicy int policyPreP, @HiddenApiEnforcementPolicy int policyP) {
+        if (isPackageWhitelistedForHiddenApis()) {
+            return;
+        }
+        if (targetSdkVersion < Build.VERSION_CODES.P) {
+            setHiddenApiEnforcementPolicy(policyPreP);
+        } else if (targetSdkVersion >= Build.VERSION_CODES.P) {
+            setHiddenApiEnforcementPolicy(policyP);
+        }
+
+    }
+
+    /**
+     * @hide
+     */
+    public void setVersionCode(long newVersionCode) {
+        longVersionCode = newVersionCode;
+        versionCode = (int) newVersionCode;
+    }
+
+    /**
      * @hide
      */
     @Override
@@ -1758,6 +1821,11 @@
     }
 
     /** @hide */
+    public boolean isSignedWithPlatformKey() {
+        return (privateFlags & ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY) != 0;
+    }
+
+    /** @hide */
     @TestApi
     public boolean isPrivilegedApp() {
         return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
diff --git a/android/content/pm/LauncherApps.java b/android/content/pm/LauncherApps.java
index 9aace2e..8223363 100644
--- a/android/content/pm/LauncherApps.java
+++ b/android/content/pm/LauncherApps.java
@@ -212,7 +212,7 @@
          * an applicaton.
          *
          * <p>Note: On devices running {@link android.os.Build.VERSION_CODES#P Android P} or higher,
-         * any apps that override {@link #onPackagesSuspended(String[], Bundle, UserHandle)} will
+         * any apps that override {@link #onPackagesSuspended(String[], UserHandle, Bundle)} will
          * not receive this callback.
          *
          * @param packageNames The names of the packages that have just been
@@ -226,15 +226,20 @@
          * Indicates that one or more packages have been suspended. A device administrator or an app
          * with {@code android.permission.SUSPEND_APPS} can do this.
          *
-         * @param packageNames The names of the packages that have just been suspended.
-         * @param launcherExtras A {@link Bundle} of extras for the launcher.
-         * @param user the user for which the given packages were suspended.
+         * <p>A suspending app with the permission {@code android.permission.SUSPEND_APPS} can
+         * optionally provide a {@link Bundle} of extra information that it deems helpful for the
+         * launcher to handle the suspended state of these packages. The contents of this
+         * {@link Bundle} supposed to be a contract between the suspending app and the launcher.
          *
+         * @param packageNames The names of the packages that have just been suspended.
+         * @param user the user for which the given packages were suspended.
+         * @param launcherExtras A {@link Bundle} of extras for the launcher, if provided to the
+         *                      system, {@code null} otherwise.
          * @see PackageManager#isPackageSuspended()
          * @see #getSuspendedPackageLauncherExtras(String, UserHandle)
          */
-        public void onPackagesSuspended(String[] packageNames, @Nullable Bundle launcherExtras,
-                UserHandle user) {
+        public void onPackagesSuspended(String[] packageNames, UserHandle user,
+                @Nullable Bundle launcherExtras) {
             onPackagesSuspended(packageNames, user);
         }
 
@@ -662,6 +667,9 @@
      * {@code PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
      * PersistableBundle, String)}.
      *
+     * <p>The contents of this {@link Bundle} are supposed to be a contract between the suspending
+     * app and the launcher.
+     *
      * <p>Note: This just returns whatever extras were provided to the system, <em>which might
      * even be {@code null}.</em>
      *
@@ -670,7 +678,7 @@
      * @return A {@link Bundle} of launcher extras. Or {@code null} if the package is not currently
      *         suspended.
      *
-     * @see Callback#onPackagesSuspended(String[], Bundle, UserHandle)
+     * @see Callback#onPackagesSuspended(String[], UserHandle, Bundle)
      * @see PackageManager#isPackageSuspended()
      */
     public @Nullable Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user) {
@@ -1298,8 +1306,8 @@
                     mCallback.onPackagesUnavailable(info.packageNames, info.user, info.replacing);
                     break;
                 case MSG_SUSPENDED:
-                    mCallback.onPackagesSuspended(info.packageNames, info.launcherExtras,
-                            info.user);
+                    mCallback.onPackagesSuspended(info.packageNames, info.user, info.launcherExtras
+                    );
                     break;
                 case MSG_UNSUSPENDED:
                     mCallback.onPackagesUnsuspended(info.packageNames, info.user);
diff --git a/android/content/pm/PackageInfo.java b/android/content/pm/PackageInfo.java
index 627ceb7..5f9f8f1 100644
--- a/android/content/pm/PackageInfo.java
+++ b/android/content/pm/PackageInfo.java
@@ -244,7 +244,7 @@
      * the first position to be the same across updates.
      *
      * <strong>Deprecated</strong> This has been replaced by the
-     * {@link PackageInfo#signingCertificateHistory} field, which takes into
+     * {@link PackageInfo#signingInfo} field, which takes into
      * account signing certificate rotation.  For backwards compatibility in
      * the event of signing certificate rotation, this will return the oldest
      * reported signing certificate, so that an application will appear to
@@ -256,29 +256,15 @@
     public Signature[] signatures;
 
     /**
-     * Array of all signatures arrays read from the package file, potentially
+     * Signing information read from the package file, potentially
      * including past signing certificates no longer used after signing
-     * certificate rotation.  Though signing certificate rotation is only
-     * available for apps with a single signing certificate, this provides an
-     * array of arrays so that packages signed with multiple signing
-     * certificates can still return all signers.  This is only filled in if
+     * certificate rotation.  This is only filled in if
      * the flag {@link PackageManager#GET_SIGNING_CERTIFICATES} was set.
      *
-     * A package must be singed with at least one certificate, which is at
-     * position zero in the array.  An application may be signed by multiple
-     * certificates, which would be in the array at position zero in an
-     * indeterminate order.  A package may also have a history of certificates
-     * due to signing certificate rotation.  In this case, the array will be
-     * populated by a series of single-entry arrays corresponding to a signing
-     * certificate of the package.
-     *
-     * <strong>Note:</strong> Signature ordering is not guaranteed to be
-     * stable which means that a package signed with certificates A and B is
-     * equivalent to being signed with certificates B and A. This means that
-     * in case multiple signatures are reported you cannot assume the one at
-     * the first position will be the same across updates.
+     * Use this field instead of the deprecated {@code signatures} field.
+     * See {@link SigningInfo} for more information on its contents.
      */
-    public Signature[][] signingCertificateHistory;
+    public SigningInfo signingInfo;
 
     /**
      * Application specified preferred configuration
@@ -476,17 +462,11 @@
         dest.writeBoolean(mOverlayIsStatic);
         dest.writeInt(compileSdkVersion);
         dest.writeString(compileSdkVersionCodename);
-        writeSigningCertificateHistoryToParcel(dest, parcelableFlags);
-    }
-
-    private void writeSigningCertificateHistoryToParcel(Parcel dest, int parcelableFlags) {
-        if (signingCertificateHistory != null) {
-            dest.writeInt(signingCertificateHistory.length);
-            for (int i = 0; i < signingCertificateHistory.length; i++) {
-                dest.writeTypedArray(signingCertificateHistory[i], parcelableFlags);
-            }
+        if (signingInfo != null) {
+            dest.writeInt(1);
+            signingInfo.writeToParcel(dest, parcelableFlags);
         } else {
-            dest.writeInt(-1);
+            dest.writeInt(0);
         }
     }
 
@@ -544,7 +524,10 @@
         mOverlayIsStatic = source.readBoolean();
         compileSdkVersion = source.readInt();
         compileSdkVersionCodename = source.readString();
-        readSigningCertificateHistoryFromParcel(source);
+        int hasSigningInfo = source.readInt();
+        if (hasSigningInfo != 0) {
+            signingInfo = SigningInfo.CREATOR.createFromParcel(source);
+        }
 
         // The component lists were flattened with the redundant ApplicationInfo
         // instances omitted.  Distribute the canonical one here as appropriate.
@@ -556,16 +539,6 @@
         }
     }
 
-    private void readSigningCertificateHistoryFromParcel(Parcel source) {
-        int len = source.readInt();
-        if (len != -1) {
-            signingCertificateHistory = new Signature[len][];
-            for (int i = 0; i < len; i++) {
-                signingCertificateHistory[i] = source.createTypedArray(Signature.CREATOR);
-            }
-        }
-    }
-
     private void propagateApplicationInfo(ApplicationInfo appInfo, ComponentInfo[] components) {
         if (components != null) {
             for (ComponentInfo ci : components) {
diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java
index 491f0af..9d3b53f 100644
--- a/android/content/pm/PackageManager.java
+++ b/android/content/pm/PackageManager.java
@@ -2629,6 +2629,17 @@
             "android.hardware.strongbox_keystore";
 
     /**
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
+     * The device has a Keymaster implementation that supports Device ID attestation.
+     *
+     * @see DevicePolicyManager#isDeviceIdAttestationSupported
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_DEVICE_ID_ATTESTATION =
+            "android.software.device_id_attestation";
+
+    /**
      * Action to external storage service to clean out removed apps.
      * @hide
      */
@@ -3941,6 +3952,7 @@
      *
      * @hide
      */
+    @TestApi
     public abstract @NonNull String getServicesSystemSharedLibraryPackageName();
 
     /**
@@ -3950,6 +3962,7 @@
      *
      * @hide
      */
+    @TestApi
     public abstract @NonNull String getSharedSystemSharedLibraryPackageName();
 
     /**
@@ -5558,7 +5571,8 @@
      * @param packageName The name of the package to get the suspended status of.
      * @param userId The user id.
      * @return {@code true} if the package is suspended or {@code false} if the package is not
-     * suspended or could not be found.
+     * suspended.
+     * @throws IllegalArgumentException if the package was not found.
      * @hide
      */
     public abstract boolean isPackageSuspendedForUser(String packageName, int userId);
@@ -5567,12 +5581,13 @@
      * Query if an app is currently suspended.
      *
      * @return {@code true} if the given package is suspended, {@code false} otherwise
+     * @throws NameNotFoundException if the package could not be found.
      *
      * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String)
      * @hide
      */
     @SystemApi
-    public boolean isPackageSuspended(String packageName) {
+    public boolean isPackageSuspended(String packageName) throws NameNotFoundException {
         throw new UnsupportedOperationException("isPackageSuspended not implemented");
     }
 
@@ -5603,51 +5618,16 @@
     }
 
     /**
-     * Retrieve the {@link PersistableBundle} that was passed as {@code appExtras} when the given
-     * package was suspended.
+     * Returns a {@link Bundle} of extras that was meant to be sent to the calling app when it was
+     * suspended. An app with the permission {@code android.permission.SUSPEND_APPS} can supply this
+     * to the system at the time of suspending an app.
      *
-     * <p> The caller must hold permission {@link Manifest.permission#SUSPEND_APPS} to use this
-     * api.</p>
+     * <p>This is the same {@link Bundle} that is sent along with the broadcast
+     * {@link Intent#ACTION_MY_PACKAGE_SUSPENDED}, whenever the app is suspended. The contents of
+     * this {@link Bundle} are a contract between the suspended app and the suspending app.
      *
-     * @param packageName The package to retrieve extras for.
-     * @return The {@code appExtras} for the suspended package.
-     *
-     * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String)
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.SUSPEND_APPS)
-    public @Nullable PersistableBundle getSuspendedPackageAppExtras(String packageName) {
-        throw new UnsupportedOperationException("getSuspendedPackageAppExtras not implemented");
-    }
-
-    /**
-     * Set the app extras for a suspended package. This method can be used to update the appExtras
-     * for a package that was earlier suspended using
-     * {@link #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle,
-     * String)}
-     * Does nothing if the given package is not already in a suspended state.
-     *
-     * @param packageName The package for which the appExtras need to be updated
-     * @param appExtras The new appExtras for the given package
-     *
-     * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String)
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.SUSPEND_APPS)
-    public void setSuspendedPackageAppExtras(String packageName,
-            @Nullable PersistableBundle appExtras) {
-        throw new UnsupportedOperationException("setSuspendedPackageAppExtras not implemented");
-    }
-
-    /**
-     * Returns any extra information supplied as {@code appExtras} to the system when the calling
-     * app was suspended.
-     *
-     * <p>Note: If no extras were supplied to the system, this method will return {@code null}, even
-     * when the calling app has been suspended.</p>
+     * <p>Note: These extras are optional, so if no extras were supplied to the system, this method
+     * will return {@code null}, even when the calling app has been suspended.
      *
      * @return A {@link Bundle} containing the extras for the app, or {@code null} if the
      * package is not currently suspended.
@@ -5655,6 +5635,7 @@
      * @see #isPackageSuspended()
      * @see Intent#ACTION_MY_PACKAGE_UNSUSPENDED
      * @see Intent#ACTION_MY_PACKAGE_SUSPENDED
+     * @see Intent#EXTRA_SUSPENDED_PACKAGE_EXTRAS
      */
     public @Nullable Bundle getSuspendedPackageAppExtras() {
         throw new UnsupportedOperationException("getSuspendedPackageAppExtras not implemented");
@@ -6118,7 +6099,9 @@
      * signed.  This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
      * since it takes into account the possibility of signing certificate rotation, except in the
      * case of packages that are signed by multiple certificates, for which signing certificate
-     * rotation is not supported.
+     * rotation is not supported.  This method is analogous to using {@code getPackageInfo} with
+     * {@code GET_SIGNING_CERTIFICATES} and then searching through the resulting {@code
+     * signingCertificateHistory} field to see if the desired certificate is present.
      *
      * @param packageName package whose signing certificates to check
      * @param certificate signing certificate for which to search
@@ -6132,13 +6115,19 @@
     }
 
     /**
-     * Searches the set of signing certificates by which the given uid has proven to have been
-     * signed.  This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
+     * Searches the set of signing certificates by which the package(s) for the given uid has proven
+     * to have been signed.  For multiple packages sharing the same uid, this will return the
+     * signing certificates found in the signing history of the "newest" package, where "newest"
+     * indicates the package with the newest signing certificate in the shared uid group.  This
+     * method should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
      * since it takes into account the possibility of signing certificate rotation, except in the
      * case of packages that are signed by multiple certificates, for which signing certificate
-     * rotation is not supported.
+     * rotation is not supported. This method is analogous to using {@code getPackagesForUid}
+     * followed by {@code getPackageInfo} with {@code GET_SIGNING_CERTIFICATES}, selecting the
+     * {@code PackageInfo} of the newest-signed bpackage , and finally searching through the
+     * resulting {@code signingCertificateHistory} field to see if the desired certificate is there.
      *
-     * @param uid package whose signing certificates to check
+     * @param uid uid whose signing certificates to check
      * @param certificate signing certificate for which to search
      * @param type representation of the {@code certificate}
      * @return true if this package was or is signed by exactly the certificate {@code certificate}
diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java
index c9b78c0..a9d0911 100644
--- a/android/content/pm/PackageManagerInternal.java
+++ b/android/content/pm/PackageManagerInternal.java
@@ -191,10 +191,10 @@
     /**
      * Retrieve launcher extras for a suspended package provided to the system in
      * {@link PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
-     * PersistableBundle, String)}
+     * PersistableBundle, String)}.
      *
      * @param packageName The package for which to return launcher extras.
-     * @param userId The user for which to check,
+     * @param userId The user for which to check.
      * @return The launcher extras.
      *
      * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
@@ -205,6 +205,38 @@
             int userId);
 
     /**
+     * Internal api to query the suspended state of a package.
+     * @param packageName The package to check.
+     * @param userId The user id to check for.
+     * @return {@code true} if the package is suspended, {@code false} otherwise.
+     * @see PackageManager#isPackageSuspended(String)
+     */
+    public abstract boolean isPackageSuspended(String packageName, int userId);
+
+    /**
+     * Get the name of the package that suspended the given package. Packages can be suspended by
+     * device administrators or apps holding {@link android.Manifest.permission#MANAGE_USERS} or
+     * {@link android.Manifest.permission#SUSPEND_APPS}.
+     *
+     * @param suspendedPackage The package that has been suspended.
+     * @param userId The user for which to check.
+     * @return Name of the package that suspended the given package. Returns {@code null} if the
+     * given package is not currently suspended and the platform package name - i.e.
+     * {@code "android"} - if the package was suspended by a device admin.
+     */
+    public abstract String getSuspendingPackage(String suspendedPackage, int userId);
+
+    /**
+     * Get the dialog message to be shown to the user when they try to launch a suspended
+     * application.
+     *
+     * @param suspendedPackage The package that has been suspended.
+     * @param userId The user for which to check.
+     * @return The dialog message to be shown to the user.
+     */
+    public abstract String getSuspendedDialogMessage(String suspendedPackage, int userId);
+
+    /**
      * Do a straight uid lookup for the given package/application in the given user.
      * @see PackageManager#getPackageUidAsUser(String, int, int)
      * @return The app's uid, or < 0 if the package was not found in that user
@@ -429,7 +461,7 @@
      * Resolves an activity intent, allowing instant apps to be resolved.
      */
     public abstract ResolveInfo resolveIntent(Intent intent, String resolvedType,
-            int flags, int userId, boolean resolveForStart);
+            int flags, int userId, boolean resolveForStart, int filterCallingUid);
 
     /**
     * Resolves a service intent, allowing instant apps to be resolved.
diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java
index 2f0faf2..453a74a 100644
--- a/android/content/pm/PackageParser.java
+++ b/android/content/pm/PackageParser.java
@@ -810,21 +810,11 @@
 
         // replacement for GET_SIGNATURES
         if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) {
-            if (p.mSigningDetails.hasPastSigningCertificates()) {
-                // Package has included signing certificate rotation information.  Convert each
-                // entry to an array
-                int numberOfSigs = p.mSigningDetails.pastSigningCertificates.length;
-                pi.signingCertificateHistory = new Signature[numberOfSigs][];
-                for (int i = 0; i < numberOfSigs; i++) {
-                    pi.signingCertificateHistory[i] =
-                            new Signature[] { p.mSigningDetails.pastSigningCertificates[i] };
-                }
-            } else if (p.mSigningDetails.hasSignatures()) {
-                // otherwise keep old behavior
-                int numberOfSigs = p.mSigningDetails.signatures.length;
-                pi.signingCertificateHistory = new Signature[1][numberOfSigs];
-                System.arraycopy(p.mSigningDetails.signatures, 0,
-                        pi.signingCertificateHistory[0], 0, numberOfSigs);
+            if (p.mSigningDetails != SigningDetails.UNKNOWN) {
+                // only return a valid SigningInfo if there is signing information to report
+                pi.signingInfo = new SigningInfo(p.mSigningDetails);
+            } else {
+                pi.signingInfo = null;
             }
         }
         return pi;
@@ -1918,7 +1908,7 @@
                 com.android.internal.R.styleable.AndroidManifest_versionCode, 0);
         pkg.mVersionCodeMajor = sa.getInteger(
                 com.android.internal.R.styleable.AndroidManifest_versionCodeMajor, 0);
-        pkg.applicationInfo.versionCode = pkg.getLongVersionCode();
+        pkg.applicationInfo.setVersionCode(pkg.getLongVersionCode());
         pkg.baseRevisionCode = sa.getInteger(
                 com.android.internal.R.styleable.AndroidManifest_revisionCode, 0);
         pkg.mVersionName = sa.getNonConfigurationString(
@@ -2726,7 +2716,7 @@
 
         // Fot apps targeting O-MR1 we require explicit enumeration of all certs.
         String[] additionalCertSha256Digests = EmptyArray.STRING;
-        if (pkg.applicationInfo.targetSdkVersion > Build.VERSION_CODES.O) {
+        if (pkg.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O_MR1) {
             additionalCertSha256Digests = parseAdditionalCertificates(res, parser, outError);
             if (additionalCertSha256Digests == null) {
                 return false;
diff --git a/android/content/pm/PackageSharedLibraryUpdater.java b/android/content/pm/PackageSharedLibraryUpdater.java
index fa89432..b14b321 100644
--- a/android/content/pm/PackageSharedLibraryUpdater.java
+++ b/android/content/pm/PackageSharedLibraryUpdater.java
@@ -62,7 +62,7 @@
 
     static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(PackageParser.Package pkg) {
         int targetSdkVersion = pkg.applicationInfo.targetSdkVersion;
-        return targetSdkVersion <= Build.VERSION_CODES.O_MR1;
+        return targetSdkVersion < Build.VERSION_CODES.P;
     }
 
     /**
diff --git a/android/content/pm/PackageUserState.java b/android/content/pm/PackageUserState.java
index f7b6e09..f471a1d 100644
--- a/android/content/pm/PackageUserState.java
+++ b/android/content/pm/PackageUserState.java
@@ -34,6 +34,7 @@
 import com.android.internal.util.ArrayUtils;
 
 import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * Per-user state information about a package.
@@ -47,6 +48,7 @@
     public boolean hidden; // Is the app restricted by owner / admin
     public boolean suspended;
     public String suspendingPackage;
+    public String dialogMessage; // Message to show when a suspended package launch attempt is made
     public PersistableBundle suspendedAppExtras;
     public PersistableBundle suspendedLauncherExtras;
     public boolean instantApp;
@@ -82,6 +84,7 @@
         hidden = o.hidden;
         suspended = o.suspended;
         suspendingPackage = o.suspendingPackage;
+        dialogMessage = o.dialogMessage;
         suspendedAppExtras = o.suspendedAppExtras;
         suspendedLauncherExtras = o.suspendedLauncherExtras;
         instantApp = o.instantApp;
@@ -208,6 +211,9 @@
                     || !suspendingPackage.equals(oldState.suspendingPackage)) {
                 return false;
             }
+            if (!Objects.equals(dialogMessage, oldState.dialogMessage)) {
+                return false;
+            }
             if (!BaseBundle.kindofEquals(suspendedAppExtras,
                     oldState.suspendedAppExtras)) {
                 return false;
diff --git a/android/content/pm/SigningInfo.java b/android/content/pm/SigningInfo.java
new file mode 100644
index 0000000..ef87403
--- /dev/null
+++ b/android/content/pm/SigningInfo.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018 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 android.content.pm;
+
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information pertaining to the signing certificates used to sign a package.
+ */
+public final class SigningInfo implements Parcelable {
+
+    @NonNull
+    private final PackageParser.SigningDetails mSigningDetails;
+
+    public SigningInfo() {
+        mSigningDetails = PackageParser.SigningDetails.UNKNOWN;
+    }
+
+    /**
+     * @hide only packagemanager should be populating this
+     */
+    public SigningInfo(PackageParser.SigningDetails signingDetails) {
+        mSigningDetails = new PackageParser.SigningDetails(signingDetails);
+    }
+
+    public SigningInfo(SigningInfo orig) {
+        mSigningDetails = new PackageParser.SigningDetails(orig.mSigningDetails);
+    }
+
+    private SigningInfo(Parcel source) {
+        mSigningDetails = PackageParser.SigningDetails.CREATOR.createFromParcel(source);
+    }
+
+    /**
+     * Although relatively uncommon, packages may be signed by more than one signer, in which case
+     * their identity is viewed as being the set of all signers, not just any one.
+     */
+    public boolean hasMultipleSigners() {
+        return mSigningDetails.signatures != null && mSigningDetails.signatures.length > 1;
+    }
+
+    /**
+     * APK Signature Scheme v3 enables packages to provide a proof-of-rotation record that the
+     * platform verifies, and uses, to allow the use of new signing certificates.  This is only
+     * available to packages that are not signed by multiple signers.  In the event of a change to a
+     * new signing certificate, the package's past signing certificates are presented as well.  Any
+     * check of a package's signing certificate should also include a search through its entire
+     * signing history, since it could change to a new signing certificate at any time.
+     */
+    public boolean hasPastSigningCertificates() {
+        return mSigningDetails.signatures != null
+                && mSigningDetails.pastSigningCertificates != null;
+    }
+
+    /**
+     * Returns the signing certificates this package has proven it is authorized to use. This
+     * includes both the signing certificate associated with the signer of the package and the past
+     * signing certificates it included as its proof of signing certificate rotation.  This method
+     * is the preferred replacement for the {@code GET_SIGNATURES} flag used with {@link
+     * PackageManager#getPackageInfo(String, int)}.  When determining if a package is signed by a
+     * desired certificate, the returned array should be checked to determine if it is one of the
+     * entries.
+     *
+     * <note>
+     *     This method returns null if the package is signed by multiple signing certificates, as
+     *     opposed to being signed by one current signer and also providing the history of past
+     *     signing certificates.  {@link #hasMultipleSigners()} may be used to determine if this
+     *     package is signed by multiple signers.  Packages which are signed by multiple signers
+     *     cannot change their signing certificates and their {@code Signature} array should be
+     *     checked to make sure that every entry matches the looked-for signing certificates.
+     * </note>
+     */
+    public Signature[] getSigningCertificateHistory() {
+        if (hasMultipleSigners()) {
+            return null;
+        } else if (!hasPastSigningCertificates()) {
+
+            // this package is only signed by one signer with no history, return it
+            return mSigningDetails.signatures;
+        } else {
+
+            // this package has provided proof of past signing certificates, include them
+            return mSigningDetails.pastSigningCertificates;
+        }
+    }
+
+    /**
+     * Returns the signing certificates used to sign the APK contents of this application.  Not
+     * including any past signing certificates the package proved it is authorized to use.
+     * <note>
+     *     This method should not be used unless {@link #hasMultipleSigners()} returns true,
+     *     indicating that {@link #getSigningCertificateHistory()} cannot be used, otherwise {@link
+     *     #getSigningCertificateHistory()} should be preferred.
+     * </note>
+     */
+    public Signature[] getApkContentsSigners() {
+        return mSigningDetails.signatures;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int parcelableFlags) {
+        mSigningDetails.writeToParcel(dest, parcelableFlags);
+    }
+
+    public static final Parcelable.Creator<SigningInfo> CREATOR =
+            new Parcelable.Creator<SigningInfo>() {
+        @Override
+        public SigningInfo createFromParcel(Parcel source) {
+            return new SigningInfo(source);
+        }
+
+        @Override
+        public SigningInfo[] newArray(int size) {
+            return new SigningInfo[size];
+        }
+    };
+}
diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java
index 506eab5..098f100 100644
--- a/android/graphics/ImageDecoder.java
+++ b/android/graphics/ImageDecoder.java
@@ -16,36 +16,177 @@
 
 package android.graphics;
 
+import static android.system.OsConstants.SEEK_CUR;
+import static android.system.OsConstants.SEEK_SET;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.AnyThread;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.Px;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
 import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
 import android.content.res.AssetManager.AssetInputStream;
 import android.content.res.Resources;
 import android.graphics.drawable.AnimatedImageDrawable;
-import android.graphics.drawable.Drawable;
 import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
 import android.net.Uri;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.util.DisplayMetrics;
 import android.util.Size;
 import android.util.TypedValue;
 
-import java.nio.ByteBuffer;
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
 import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.ArrayIndexOutOfBoundsException;
-import java.lang.AutoCloseable;
-import java.lang.NullPointerException;
 import java.lang.annotation.Retention;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- *  Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
+ *  <p>A class for converting encoded images (like {@code PNG}, {@code JPEG},
+ *  {@code WEBP}, {@code GIF}, or {@code HEIF}) into {@link Drawable} or
+ *  {@link Bitmap} objects.
+ *
+ *  <p>To use it, first create a {@link Source Source} using one of the
+ *  {@code createSource} overloads. For example, to decode from a {@link File}, call
+ *  {@link #createSource(File)} and pass the result to {@link #decodeDrawable(Source)}
+ *  or {@link #decodeBitmap(Source)}:
+ *
+ *  <pre class="prettyprint">
+ *  File file = new File(...);
+ *  ImageDecoder.Source source = ImageDecoder.createSource(file);
+ *  Drawable drawable = ImageDecoder.decodeDrawable(source);
+ *  </pre>
+ *
+ *  <p>To change the default settings, pass the {@link Source Source} and an
+ *  {@link OnHeaderDecodedListener OnHeaderDecodedListener} to
+ *  {@link #decodeDrawable(Source, OnHeaderDecodedListener)} or
+ *  {@link #decodeBitmap(Source, OnHeaderDecodedListener)}. For example, to
+ *  create a sampled image with half the width and height of the original image,
+ *  call {@link #setTargetSampleSize setTargetSampleSize(2)} inside
+ *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}:
+ *
+ *  <pre class="prettyprint">
+ *  OnHeaderDecodedListener listener = new OnHeaderDecodedListener() {
+ *      public void onHeaderDecoded(ImageDecoder decoder, ImageInfo info, Source source) {
+ *          decoder.setTargetSampleSize(2);
+ *      }
+ *  };
+ *  Drawable drawable = ImageDecoder.decodeDrawable(source, listener);
+ *  </pre>
+ *
+ *  <p>The {@link ImageInfo ImageInfo} contains information about the encoded image, like
+ *  its width and height, and the {@link Source Source} can be used to match to a particular
+ *  {@link Source Source} if a single {@link OnHeaderDecodedListener OnHeaderDecodedListener}
+ *  is used with multiple {@link Source Source} objects.
+ *
+ *  <p>The {@link OnHeaderDecodedListener OnHeaderDecodedListener} can also be implemented
+ *  as a lambda:
+ *
+ *  <pre class="prettyprint">
+ *  Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ *      decoder.setTargetSampleSize(2);
+ *  });
+ *  </pre>
+ *
+ *  <p>If the encoded image is an animated {@code GIF} or {@code WEBP},
+ *  {@link #decodeDrawable decodeDrawable} will return an {@link AnimatedImageDrawable}. To
+ *  start its animation, call {@link AnimatedImageDrawable#start AnimatedImageDrawable.start()}:
+ *
+ *  <pre class="prettyprint">
+ *  Drawable drawable = ImageDecoder.decodeDrawable(source);
+ *  if (drawable instanceof AnimatedImageDrawable) {
+ *      ((AnimatedImageDrawable) drawable).start();
+ *  }
+ *  </pre>
+ *
+ *  <p>By default, a {@link Bitmap} created by {@link ImageDecoder} (including
+ *  one that is inside a {@link Drawable}) will be immutable (i.e.
+ *  {@link Bitmap#isMutable Bitmap.isMutable()} returns {@code false}), and it
+ *  will typically have {@code Config} {@link Bitmap.Config#HARDWARE}. Although
+ *  these properties can be changed with {@link #setMutableRequired setMutableRequired(true)}
+ *  (which is only compatible with {@link #decodeBitmap(Source)} and
+ *  {@link #decodeBitmap(Source, OnHeaderDecodedListener)}) and {@link #setAllocator},
+ *  it is also possible to apply custom effects regardless of the mutability of
+ *  the final returned object by passing a {@link PostProcessor} to
+ *  {@link #setPostProcessor setPostProcessor}. A {@link PostProcessor} can also be a lambda:
+ *
+ *  <pre class="prettyprint">
+ *  Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ *      decoder.setPostProcessor((canvas) -&gt; {
+ *              // This will create rounded corners.
+ *              Path path = new Path();
+ *              path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ *              int width = canvas.getWidth();
+ *              int height = canvas.getHeight();
+ *              path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
+ *              Paint paint = new Paint();
+ *              paint.setAntiAlias(true);
+ *              paint.setColor(Color.TRANSPARENT);
+ *              paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ *              canvas.drawPath(path, paint);
+ *              return PixelFormat.TRANSLUCENT;
+ *      });
+ *  });
+ *  </pre>
+ *
+ *  <p>If the encoded image is incomplete or contains an error, or if an
+ *  {@link Exception} occurs during decoding, a {@link DecodeException DecodeException}
+ *  will be thrown. In some cases, the {@link ImageDecoder} may have decoded part of
+ *  the image. In order to display the partial image, an
+ *  {@link OnPartialImageListener OnPartialImageListener} must be passed to
+ *  {@link #setOnPartialImageListener setOnPartialImageListener}. For example:
+ *
+ *  <pre class="prettyprint">
+ *  Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ *      decoder.setOnPartialImageListener((DecodeException e) -&gt; {
+ *              // Returning true indicates to create a Drawable or Bitmap even
+ *              // if the whole image could not be decoded. Any remaining lines
+ *              // will be blank.
+ *              return true;
+ *      });
+ *  });
+ *  </pre>
  */
 public final class ImageDecoder implements AutoCloseable {
+    /** @hide **/
+    public static int sApiLevel;
 
     /**
-     *  Source of the encoded image data.
+     *  Source of encoded image data.
+     *
+     *  <p>References the data that will be used to decode a {@link Drawable}
+     *  or {@link Bitmap} in {@link #decodeDrawable decodeDrawable} or
+     *  {@link #decodeBitmap decodeBitmap}. Constructing a {@code Source} (with
+     *  one of the overloads of {@code createSource}) can be done on any thread
+     *  because the construction simply captures values. The real work is done
+     *  in {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap}.
+     *
+     *  <p>A {@code Source} object can be reused to create multiple versions of the
+     *  same image. For example, to decode a full size image and its thumbnail,
+     *  the same {@code Source} can be used once with no
+     *  {@link OnHeaderDecodedListener OnHeaderDecodedListener} and once with an
+     *  implementation of {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}
+     *  that calls {@link #setTargetSize} with smaller dimensions. One {@code Source}
+     *  even used simultaneously in multiple threads.</p>
      */
     public static abstract class Source {
         private Source() {}
@@ -58,7 +199,7 @@
         int getDensity() { return Bitmap.DENSITY_NONE; }
 
         /* @hide */
-        int computeDstDensity() {
+        final int computeDstDensity() {
             Resources res = getResources();
             if (res == null) {
                 return Bitmap.getDefaultDensity();
@@ -84,7 +225,7 @@
 
         @Override
         public ImageDecoder createImageDecoder() throws IOException {
-            return new ImageDecoder();
+            return nCreate(mData, mOffset, mLength, this);
         }
     }
 
@@ -96,27 +237,126 @@
 
         @Override
         public ImageDecoder createImageDecoder() throws IOException {
-            return new ImageDecoder();
+            if (!mBuffer.isDirect() && mBuffer.hasArray()) {
+                int offset = mBuffer.arrayOffset() + mBuffer.position();
+                int length = mBuffer.limit() - mBuffer.position();
+                return nCreate(mBuffer.array(), offset, length, this);
+            }
+            ByteBuffer buffer = mBuffer.slice();
+            return nCreate(buffer, buffer.position(), buffer.limit(), this);
         }
     }
 
     private static class ContentResolverSource extends Source {
-        ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri) {
+        ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri,
+                @Nullable Resources res) {
             mResolver = resolver;
             mUri = uri;
+            mResources = res;
         }
 
         private final ContentResolver mResolver;
         private final Uri mUri;
+        private final Resources mResources;
+
+        @Nullable
+        Resources getResources() { return mResources; }
 
         @Override
         public ImageDecoder createImageDecoder() throws IOException {
-            return new ImageDecoder();
+            AssetFileDescriptor assetFd = null;
+            try {
+                if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) {
+                    assetFd = mResolver.openTypedAssetFileDescriptor(mUri,
+                            "image/*", null);
+                } else {
+                    assetFd = mResolver.openAssetFileDescriptor(mUri, "r");
+                }
+            } catch (FileNotFoundException e) {
+                // Some images cannot be opened as AssetFileDescriptors (e.g.
+                // bmp, ico). Open them as InputStreams.
+                InputStream is = mResolver.openInputStream(mUri);
+                if (is == null) {
+                    throw new FileNotFoundException(mUri.toString());
+                }
+
+                return createFromStream(is, true, this);
+            }
+
+            final FileDescriptor fd = assetFd.getFileDescriptor();
+            final long offset = assetFd.getStartOffset();
+
+            ImageDecoder decoder = null;
+            try {
+                try {
+                    Os.lseek(fd, offset, SEEK_SET);
+                    decoder = nCreate(fd, this);
+                } catch (ErrnoException e) {
+                    decoder = createFromStream(new FileInputStream(fd), true, this);
+                }
+            } finally {
+                if (decoder == null) {
+                    IoUtils.closeQuietly(assetFd);
+                } else {
+                    decoder.mAssetFd = assetFd;
+                }
+            }
+            return decoder;
         }
     }
 
+    @NonNull
+    private static ImageDecoder createFromFile(@NonNull File file,
+            @NonNull Source source) throws IOException {
+        FileInputStream stream = new FileInputStream(file);
+        FileDescriptor fd = stream.getFD();
+        try {
+            Os.lseek(fd, 0, SEEK_CUR);
+        } catch (ErrnoException e) {
+            return createFromStream(stream, true, source);
+        }
+
+        ImageDecoder decoder = null;
+        try {
+            decoder = nCreate(fd, source);
+        } finally {
+            if (decoder == null) {
+                IoUtils.closeQuietly(stream);
+            } else {
+                decoder.mInputStream = stream;
+                decoder.mOwnsInputStream = true;
+            }
+        }
+        return decoder;
+    }
+
+    @NonNull
+    private static ImageDecoder createFromStream(@NonNull InputStream is,
+            boolean closeInputStream, Source source) throws IOException {
+        // Arbitrary size matches BitmapFactory.
+        byte[] storage = new byte[16 * 1024];
+        ImageDecoder decoder = null;
+        try {
+            decoder = nCreate(is, storage, source);
+        } finally {
+            if (decoder == null) {
+                if (closeInputStream) {
+                    IoUtils.closeQuietly(is);
+                }
+            } else {
+                decoder.mInputStream = is;
+                decoder.mOwnsInputStream = closeInputStream;
+                decoder.mTempStorage = storage;
+            }
+        }
+
+        return decoder;
+    }
+
     /**
      * For backwards compatibility, this does *not* close the InputStream.
+     *
+     * Further, unlike other Sources, this one is not reusable.
      */
     private static class InputStreamSource extends Source {
         InputStreamSource(Resources res, InputStream is, int inputDensity) {
@@ -140,7 +380,15 @@
 
         @Override
         public ImageDecoder createImageDecoder() throws IOException {
-            return new ImageDecoder();
+
+            synchronized (this) {
+                if (mInputStream == null) {
+                    throw new IOException("Cannot reuse InputStreamSource");
+                }
+                InputStream is = mInputStream;
+                mInputStream = null;
+                return createFromStream(is, false, this);
+            }
         }
     }
 
@@ -178,7 +426,14 @@
 
         @Override
         public ImageDecoder createImageDecoder() throws IOException {
-            return new ImageDecoder();
+            synchronized (this) {
+                if (mAssetInputStream == null) {
+                    throw new IOException("Cannot reuse AssetInputStreamSource");
+                }
+                AssetInputStream ais = mAssetInputStream;
+                mAssetInputStream = null;
+                return createFromAsset(ais, this);
+            }
         }
     }
 
@@ -192,16 +447,70 @@
         final Resources mResources;
         final int       mResId;
         int             mResDensity;
+        private Object  mLock = new Object();
 
         @Override
         public Resources getResources() { return mResources; }
 
         @Override
-        public int getDensity() { return mResDensity; }
+        public int getDensity() {
+            synchronized (mLock) {
+                return mResDensity;
+            }
+        }
 
         @Override
         public ImageDecoder createImageDecoder() throws IOException {
-            return new ImageDecoder();
+            TypedValue value = new TypedValue();
+            // This is just used in order to access the underlying Asset and
+            // keep it alive.
+            InputStream is = mResources.openRawResource(mResId, value);
+
+            synchronized (mLock) {
+                if (value.density == TypedValue.DENSITY_DEFAULT) {
+                    mResDensity = DisplayMetrics.DENSITY_DEFAULT;
+                } else if (value.density != TypedValue.DENSITY_NONE) {
+                    mResDensity = value.density;
+                }
+            }
+
+            return createFromAsset((AssetInputStream) is, this);
+        }
+    }
+
+    /**
+     *  ImageDecoder will own the AssetInputStream.
+     */
+    private static ImageDecoder createFromAsset(AssetInputStream ais,
+            Source source) throws IOException {
+        ImageDecoder decoder = null;
+        try {
+            long asset = ais.getNativeAsset();
+            decoder = nCreate(asset, source);
+        } finally {
+            if (decoder == null) {
+                IoUtils.closeQuietly(ais);
+            } else {
+                decoder.mInputStream = ais;
+                decoder.mOwnsInputStream = true;
+            }
+        }
+        return decoder;
+    }
+
+    private static class AssetSource extends Source {
+        AssetSource(@NonNull AssetManager assets, @NonNull String fileName) {
+            mAssets = assets;
+            mFileName = fileName;
+        }
+
+        private final AssetManager mAssets;
+        private final String mFileName;
+
+        @Override
+        public ImageDecoder createImageDecoder() throws IOException {
+            InputStream is = mAssets.open(mFileName);
+            return createFromAsset((AssetInputStream) is, this);
         }
     }
 
@@ -214,17 +523,19 @@
 
         @Override
         public ImageDecoder createImageDecoder() throws IOException {
-            return new ImageDecoder();
+            return createFromFile(mFile, this);
         }
     }
 
     /**
-     *  Contains information about the encoded image.
+     *  Information about an encoded image.
      */
     public static class ImageInfo {
+        private final Size mSize;
         private ImageDecoder mDecoder;
 
         private ImageInfo(@NonNull ImageDecoder decoder) {
+            mSize = new Size(decoder.mWidth, decoder.mHeight);
             mDecoder = decoder;
         }
 
@@ -233,7 +544,7 @@
          */
         @NonNull
         public Size getSize() {
-            return new Size(0, 0);
+            return mSize;
         }
 
         /**
@@ -241,100 +552,271 @@
          */
         @NonNull
         public String getMimeType() {
-            return "";
+            return mDecoder.getMimeType();
         }
 
         /**
          * Whether the image is animated.
          *
-         * <p>Calling {@link #decodeDrawable} will return an
-         * {@link AnimatedImageDrawable}.</p>
+         * <p>If {@code true}, {@link #decodeDrawable decodeDrawable} will
+         * return an {@link AnimatedImageDrawable}.</p>
          */
         public boolean isAnimated() {
             return mDecoder.mAnimated;
         }
+
+        /**
+         * If known, the color space the decoded bitmap will have. Note that the
+         * output color space is not guaranteed to be the color space the bitmap
+         * is encoded with. If not known (when the config is
+         * {@link Bitmap.Config#ALPHA_8} for instance), or there is an error,
+         * it is set to null.
+         */
+        @Nullable
+        public ColorSpace getColorSpace() {
+            return mDecoder.getColorSpace();
+        }
     };
 
-    /**
-     *  Thrown if the provided data is incomplete.
+    /** @removed
+     * @deprecated Subsumed by {@link #DecodeException}.
      */
+    @Deprecated
     public static class IncompleteException extends IOException {};
 
     /**
-     *  Optional listener supplied to {@link #decodeDrawable} or
-     *  {@link #decodeBitmap}.
+     *  Interface for changing the default settings of a decode.
+     *
+     *  <p>Supply an instance to
+     *  {@link #decodeDrawable(Source, OnHeaderDecodedListener) decodeDrawable}
+     *  or {@link #decodeBitmap(Source, OnHeaderDecodedListener) decodeBitmap},
+     *  which will call {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}
+     *  (in the same thread) once the size is known. The implementation of
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded} can then
+     *  change the decode settings as desired.
      */
-    public interface OnHeaderDecodedListener {
+    public static interface OnHeaderDecodedListener {
         /**
-         *  Called when the header is decoded and the size is known.
+         *  Called by {@link ImageDecoder} when the header has been decoded and
+         *  the image size is known.
          *
-         *  @param decoder allows changing the default settings of the decode.
-         *  @param info Information about the encoded image.
-         *  @param source that created the decoder.
+         *  @param decoder the object performing the decode, for changing
+         *      its default settings.
+         *  @param info information about the encoded image.
+         *  @param source object that created {@code decoder}.
          */
-        void onHeaderDecoded(@NonNull ImageDecoder decoder,
+        public void onHeaderDecoded(@NonNull ImageDecoder decoder,
                 @NonNull ImageInfo info, @NonNull Source source);
 
     };
 
-    /**
-     *  An Exception was thrown reading the {@link Source}.
+    /** @removed
+     * @deprecated Replaced by {@link #DecodeException#SOURCE_EXCEPTION}.
      */
+    @Deprecated
     public static final int ERROR_SOURCE_EXCEPTION  = 1;
 
-    /**
-     *  The encoded data was incomplete.
+    /** @removed
+     * @deprecated Replaced by {@link #DecodeException#SOURCE_INCOMPLETE}.
      */
+    @Deprecated
     public static final int ERROR_SOURCE_INCOMPLETE = 2;
 
-    /**
-     *  The encoded data contained an error.
+    /** @removed
+     * @deprecated Replaced by {@link #DecodeException#SOURCE_MALFORMED_DATA}.
      */
+    @Deprecated
     public static final int ERROR_SOURCE_ERROR      = 3;
 
-    @Retention(SOURCE)
-    public @interface Error {}
-
     /**
-     *  Optional listener supplied to the ImageDecoder.
-     *
-     *  Without this listener, errors will throw {@link java.io.IOException}.
+     *  Information about an interrupted decode.
      */
-    public interface OnPartialImageListener {
+    public static final class DecodeException extends IOException {
         /**
-         *  Called when there is only a partial image to display.
-         *
-         *  If decoding is interrupted after having decoded a partial image,
-         *  this listener lets the client know that and allows them to
-         *  optionally finish the rest of the decode/creation process to create
-         *  a partial {@link Drawable}/{@link Bitmap}.
-         *
-         *  @param error indicating what interrupted the decode.
-         *  @param source that had the error.
-         *  @return True to create and return a {@link Drawable}/{@link Bitmap}
-         *      with partial data. False (which is the default) to abort the
-         *      decode and throw {@link java.io.IOException}.
+         *  An Exception was thrown reading the {@link Source}.
          */
-        boolean onPartialImage(@Error int error, @NonNull Source source);
-    }
+        public static final int SOURCE_EXCEPTION  = 1;
 
-    private boolean mAnimated;
-    private Rect mOutPaddingRect;
+        /**
+         *  The encoded data was incomplete.
+         */
+        public static final int SOURCE_INCOMPLETE = 2;
 
-    public ImageDecoder() {
-        mAnimated = true; // This is too avoid throwing an exception in AnimatedImageDrawable
+        /**
+         *  The encoded data contained an error.
+         */
+        public static final int SOURCE_MALFORMED_DATA      = 3;
+
+        /** @hide **/
+        @Retention(SOURCE)
+        @IntDef(value = { SOURCE_EXCEPTION, SOURCE_INCOMPLETE, SOURCE_MALFORMED_DATA },
+                prefix = {"SOURCE_"})
+        public @interface Error {};
+
+        @Error final int mError;
+        @NonNull final Source mSource;
+
+        DecodeException(@Error int error, @Nullable Throwable cause, @NonNull Source source) {
+            super(errorMessage(error, cause), cause);
+            mError = error;
+            mSource = source;
+        }
+
+        /**
+         * Private method called by JNI.
+         */
+        @SuppressWarnings("unused")
+        DecodeException(@Error int error, @Nullable String msg, @Nullable Throwable cause,
+                @NonNull Source source) {
+            super(msg + errorMessage(error, cause), cause);
+            mError = error;
+            mSource = source;
+        }
+
+        /**
+         *  Retrieve the reason that decoding was interrupted.
+         *
+         *  <p>If the error is {@link #SOURCE_EXCEPTION}, the underlying
+         *  {@link java.lang.Throwable} can be retrieved with
+         *  {@link java.lang.Throwable#getCause}.</p>
+         */
+        @Error
+        public int getError() {
+            return mError;
+        }
+
+        /**
+         *  Retrieve the {@link Source Source} that was interrupted.
+         *
+         *  <p>This can be used for equality checking to find the Source which
+         *  failed to completely decode.</p>
+         */
+        @NonNull
+        public Source getSource() {
+            return mSource;
+        }
+
+        private static String errorMessage(@Error int error, @Nullable Throwable cause) {
+            switch (error) {
+                case SOURCE_EXCEPTION:
+                    return "Exception in input: " + cause;
+                case SOURCE_INCOMPLETE:
+                    return "Input was incomplete.";
+                case SOURCE_MALFORMED_DATA:
+                    return "Input contained an error.";
+                default:
+                    return "";
+            }
+        }
     }
 
     /**
-     * Create a new {@link Source} from an asset.
-     * @hide
+     *  Interface for inspecting a {@link DecodeException DecodeException}
+     *  and potentially preventing it from being thrown.
+     *
+     *  <p>If an instance is passed to
+     *  {@link #setOnPartialImageListener setOnPartialImageListener}, a
+     *  {@link DecodeException DecodeException} that would otherwise have been
+     *  thrown can be inspected inside
+     *  {@link OnPartialImageListener#onPartialImage onPartialImage}.
+     *  If {@link OnPartialImageListener#onPartialImage onPartialImage} returns
+     *  {@code true}, a partial image will be created.
+     */
+    public static interface OnPartialImageListener {
+        /**
+         *  Called by {@link ImageDecoder} when there is only a partial image to
+         *  display.
+         *
+         *  <p>If decoding is interrupted after having decoded a partial image,
+         *  this method will be called. The implementation can inspect the
+         *  {@link DecodeException DecodeException} and optionally finish the
+         *  rest of the decode creation process to create a partial {@link Drawable}
+         *  or {@link Bitmap}.
+         *
+         *  @param exception exception containing information about the
+         *      decode interruption.
+         *  @return {@code true} to create and return a {@link Drawable} or
+         *      {@link Bitmap} with partial data. {@code false} (which is the
+         *      default) to abort the decode and throw {@code e}. Any undecoded
+         *      lines in the image will be blank.
+         */
+        boolean onPartialImage(@NonNull DecodeException exception);
+    };
+
+    // Fields
+    private long          mNativePtr;
+    private final int     mWidth;
+    private final int     mHeight;
+    private final boolean mAnimated;
+    private final boolean mIsNinePatch;
+
+    private int        mDesiredWidth;
+    private int        mDesiredHeight;
+    private int        mAllocator = ALLOCATOR_DEFAULT;
+    private boolean    mUnpremultipliedRequired = false;
+    private boolean    mMutable = false;
+    private boolean    mConserveMemory = false;
+    private boolean    mDecodeAsAlphaMask = false;
+    private ColorSpace mDesiredColorSpace = null;
+    private Rect       mCropRect;
+    private Rect       mOutPaddingRect;
+    private Source     mSource;
+
+    private PostProcessor          mPostProcessor;
+    private OnPartialImageListener mOnPartialImageListener;
+
+    // Objects for interacting with the input.
+    private InputStream         mInputStream;
+    private boolean             mOwnsInputStream;
+    private byte[]              mTempStorage;
+    private AssetFileDescriptor mAssetFd;
+    private final AtomicBoolean mClosed = new AtomicBoolean();
+    private final CloseGuard    mCloseGuard = CloseGuard.get();
+
+    /**
+     * Private constructor called by JNI. {@link #close} must be
+     * called after decoding to delete native resources.
+     */
+    @SuppressWarnings("unused")
+    private ImageDecoder(long nativePtr, int width, int height,
+            boolean animated, boolean isNinePatch) {
+        mNativePtr = nativePtr;
+        mWidth = width;
+        mHeight = height;
+        mDesiredWidth = width;
+        mDesiredHeight = height;
+        mAnimated = animated;
+        mIsNinePatch = isNinePatch;
+        mCloseGuard.open("close");
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+
+            // Avoid closing these in finalizer.
+            mInputStream = null;
+            mAssetFd = null;
+
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Create a new {@link Source Source} from a resource.
      *
      * @param res the {@link Resources} object containing the image data.
      * @param resId resource ID of the image data.
-     *      // FIXME: Can be an @DrawableRes?
      * @return a new Source object, which can be passed to
-     *      {@link #decodeDrawable} or {@link #decodeBitmap}.
+     *      {@link #decodeDrawable decodeDrawable} or
+     *      {@link #decodeBitmap decodeBitmap}.
      */
+    @AnyThread
     @NonNull
     public static Source createSource(@NonNull Resources res, int resId)
     {
@@ -342,34 +824,71 @@
     }
 
     /**
-     * Create a new {@link Source} from a {@link android.net.Uri}.
+     * Create a new {@link Source Source} from a {@link android.net.Uri}.
+     *
+     * <h5>Accepts the following URI schemes:</h5>
+     * <ul>
+     * <li>content ({@link ContentResolver#SCHEME_CONTENT})</li>
+     * <li>android.resource ({@link ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+     * <li>file ({@link ContentResolver#SCHEME_FILE})</li>
+     * </ul>
      *
      * @param cr to retrieve from.
      * @param uri of the image file.
      * @return a new Source object, which can be passed to
-     *      {@link #decodeDrawable} or {@link #decodeBitmap}.
+     *      {@link #decodeDrawable decodeDrawable} or
+     *      {@link #decodeBitmap decodeBitmap}.
      */
+    @AnyThread
     @NonNull
     public static Source createSource(@NonNull ContentResolver cr,
             @NonNull Uri uri) {
-        return new ContentResolverSource(cr, uri);
+        return new ContentResolverSource(cr, uri, null);
     }
 
     /**
-     * Create a new {@link Source} from a byte array.
+     * Provide Resources for density scaling.
+     *
+     * @hide
+     */
+    @AnyThread
+    @NonNull
+    public static Source createSource(@NonNull ContentResolver cr,
+            @NonNull Uri uri, @Nullable Resources res) {
+        return new ContentResolverSource(cr, uri, res);
+    }
+
+    /**
+     * Create a new {@link Source Source} from a file in the "assets" directory.
+     */
+    @AnyThread
+    @NonNull
+    public static Source createSource(@NonNull AssetManager assets, @NonNull String fileName) {
+        return new AssetSource(assets, fileName);
+    }
+
+    /**
+     * Create a new {@link Source Source} from a byte array.
      *
      * @param data byte array of compressed image data.
      * @param offset offset into data for where the decoder should begin
      *      parsing.
      * @param length number of bytes, beginning at offset, to parse.
+     * @return a new Source object, which can be passed to
+     *      {@link #decodeDrawable decodeDrawable} or
+     *      {@link #decodeBitmap decodeBitmap}.
      * @throws NullPointerException if data is null.
      * @throws ArrayIndexOutOfBoundsException if offset and length are
      *      not within data.
      * @hide
      */
+    @AnyThread
     @NonNull
     public static Source createSource(@NonNull byte[] data, int offset,
             int length) throws ArrayIndexOutOfBoundsException {
+        if (data == null) {
+            throw new NullPointerException("null byte[] in createSource!");
+        }
         if (offset < 0 || length < 0 || offset >= data.length ||
                 offset + length > data.length) {
             throw new ArrayIndexOutOfBoundsException(
@@ -382,21 +901,29 @@
      * See {@link #createSource(byte[], int, int).
      * @hide
      */
+    @AnyThread
     @NonNull
     public static Source createSource(@NonNull byte[] data) {
         return createSource(data, 0, data.length);
     }
 
     /**
-     * Create a new {@link Source} from a {@link java.nio.ByteBuffer}.
+     * Create a new {@link Source Source} from a {@link java.nio.ByteBuffer}.
      *
-     * <p>The returned {@link Source} effectively takes ownership of the
-     * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after
-     * this call.</p>
+     * <p>Decoding will start from {@link java.nio.ByteBuffer#position() buffer.position()}.
+     * The position of {@code buffer} will not be affected.</p>
      *
-     * Decoding will start from {@link java.nio.ByteBuffer#position()}. The
-     * position after decoding is undefined.
+     * <p>Note: If this {@code Source} is passed to {@link #decodeDrawable decodeDrawable},
+     * and the encoded image is animated, the returned {@link AnimatedImageDrawable}
+     * will continue reading from the {@code buffer}, so its contents must not
+     * be modified, even after the {@code AnimatedImageDrawable} is returned.
+     * {@code buffer}'s contents should never be modified during decode.</p>
+     *
+     * @return a new Source object, which can be passed to
+     *      {@link #decodeDrawable decodeDrawable} or
+     *      {@link #decodeBitmap decodeBitmap}.
      */
+    @AnyThread
     @NonNull
     public static Source createSource(@NonNull ByteBuffer buffer) {
         return new ByteBufferSource(buffer);
@@ -404,23 +931,39 @@
 
     /**
      * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+     *
+     * <p>Unlike other Sources, this one cannot be reused.</p>
+     *
      * @hide
      */
+    @AnyThread
+    @NonNull
     public static Source createSource(Resources res, InputStream is) {
         return new InputStreamSource(res, is, Bitmap.getDefaultDensity());
     }
 
     /**
      * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+     *
+     * <p>Unlike other Sources, this one cannot be reused.</p>
+     *
      * @hide
      */
+    @AnyThread
+    @TestApi
+    @NonNull
     public static Source createSource(Resources res, InputStream is, int density) {
         return new InputStreamSource(res, is, density);
     }
 
     /**
-     * Create a new {@link Source} from a {@link java.io.File}.
+     * Create a new {@link Source Source} from a {@link java.io.File}.
+     *
+     * @return a new Source object, which can be passed to
+     *      {@link #decodeDrawable decodeDrawable} or
+     *      {@link #decodeBitmap decodeBitmap}.
      */
+    @AnyThread
     @NonNull
     public static Source createSource(@NonNull File file) {
         return new FileSource(file);
@@ -431,39 +974,142 @@
      *
      *  <p>This takes an input that functions like
      *  {@link BitmapFactory.Options#inSampleSize}. It returns a width and
-     *  height that can be acheived by sampling the encoded image. Other widths
+     *  height that can be achieved by sampling the encoded image. Other widths
      *  and heights may be supported, but will require an additional (internal)
      *  scaling step. Such internal scaling is *not* supported with
-     *  {@link #setRequireUnpremultiplied} set to {@code true}.</p>
+     *  {@link #setUnpremultipliedRequired} set to {@code true}.</p>
      *
      *  @param sampleSize Sampling rate of the encoded image.
      *  @return {@link android.util.Size} of the width and height after
      *      sampling.
+     *
+     *  @hide
      */
     @NonNull
     public Size getSampledSize(int sampleSize) {
-        return new Size(0, 0);
+        if (sampleSize <= 0) {
+            throw new IllegalArgumentException("sampleSize must be positive! "
+                    + "provided " + sampleSize);
+        }
+        if (mNativePtr == 0) {
+            throw new IllegalStateException("ImageDecoder is closed!");
+        }
+
+        return nGetSampledSize(mNativePtr, sampleSize);
     }
 
     // Modifiers
-    /**
-     *  Resize the output to have the following size.
-     *
-     *  @param width must be greater than 0.
-     *  @param height must be greater than 0.
+    /** @removed
+     * @deprecated Renamed to {@link #setTargetSize}.
      */
-    public void setResize(int width, int height) {
+    @Deprecated
+    public ImageDecoder setResize(int width, int height) {
+        this.setTargetSize(width, height);
+        return this;
     }
 
     /**
-     *  Resize based on a sample size.
+     *  Specify the size of the output {@link Drawable} or {@link Bitmap}.
      *
-     *  <p>This has the same effect as passing the result of
-     *  {@link #getSampledSize} to {@link #setResize(int, int)}.</p>
+     *  <p>By default, the output size will match the size of the encoded
+     *  image, which can be retrieved from the {@link ImageInfo ImageInfo} in
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
      *
-     *  @param sampleSize Sampling rate of the encoded image.
+     *  <p>This will sample or scale the output to an arbitrary size that may
+     *  be smaller or larger than the encoded size.</p>
+     *
+     *  <p>Only the last call to this or {@link #setTargetSampleSize} is
+     *  respected.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     *
+     *  @param width width in pixels of the output, must be greater than 0
+     *  @param height height in pixels of the output, must be greater than 0
      */
-    public void setResize(int sampleSize) {
+    public void setTargetSize(@Px @IntRange(from = 1) int width,
+                              @Px @IntRange(from = 1) int height) {
+        if (width <= 0 || height <= 0) {
+            throw new IllegalArgumentException("Dimensions must be positive! "
+                    + "provided (" + width + ", " + height + ")");
+        }
+
+        mDesiredWidth = width;
+        mDesiredHeight = height;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #setTargetSampleSize}.
+     */
+    @Deprecated
+    public ImageDecoder setResize(int sampleSize) {
+        this.setTargetSampleSize(sampleSize);
+        return this;
+    }
+
+    private int getTargetDimension(int original, int sampleSize, int computed) {
+        // Sampling will never result in a smaller size than 1.
+        if (sampleSize >= original) {
+            return 1;
+        }
+
+        // Use integer divide to find the desired size. If that is what
+        // getSampledSize computed, that is the size to use.
+        int target = original / sampleSize;
+        if (computed == target) {
+            return computed;
+        }
+
+        // If sampleSize does not divide evenly into original, the decoder
+        // may round in either direction. It just needs to get a result that
+        // is close.
+        int reverse = computed * sampleSize;
+        if (Math.abs(reverse - original) < sampleSize) {
+            // This is the size that can be decoded most efficiently.
+            return computed;
+        }
+
+        // The decoder could not get close (e.g. it is a DNG image).
+        return target;
+    }
+
+    /**
+     *  Set the target size with a sampleSize.
+     *
+     *  <p>By default, the output size will match the size of the encoded
+     *  image, which can be retrieved from the {@link ImageInfo ImageInfo} in
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     *
+     *  <p>Requests the decoder to subsample the original image, returning a
+     *  smaller image to save memory. The {@code sampleSize} is the number of pixels
+     *  in either dimension that correspond to a single pixel in the output.
+     *  For example, {@code sampleSize == 4} returns an image that is 1/4 the
+     *  width/height of the original, and 1/16 the number of pixels.</p>
+     *
+     *  <p>Must be greater than or equal to 1.</p>
+     *
+     *  <p>This has the same effect as calling {@link #setTargetSize} with
+     *  dimensions based on the {@code sampleSize}. Unlike dividing the original
+     *  width and height by the {@code sampleSize} manually, calling this method
+     *  allows {@code ImageDecoder} to round in the direction that it can do most
+     *  efficiently.</p>
+     *
+     *  <p>Only the last call to this or {@link #setTargetSize} is respected.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     *
+     *  @param sampleSize sampling rate of the encoded image.
+     */
+    public void setTargetSampleSize(@IntRange(from = 1) int sampleSize) {
+        Size size = this.getSampledSize(sampleSize);
+        int targetWidth = getTargetDimension(mWidth, sampleSize, size.getWidth());
+        int targetHeight = getTargetDimension(mHeight, sampleSize, size.getHeight());
+        this.setTargetSize(targetWidth, targetHeight);
+    }
+
+    private boolean requestedResize() {
+        return mWidth != mDesiredWidth || mHeight != mDesiredHeight;
     }
 
     // These need to stay in sync with ImageDecoder.cpp's Allocator enum.
@@ -473,14 +1119,15 @@
      *  Will typically result in a {@link Bitmap.Config#HARDWARE}
      *  allocation, but may be software for small images. In addition, this will
      *  switch to software when HARDWARE is incompatible, e.g.
-     *  {@link #setMutable}, {@link #setAsAlphaMask}.
+     *  {@link #setMutableRequired setMutableRequired(true)} or
+     *  {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)}.
      */
     public static final int ALLOCATOR_DEFAULT = 0;
 
     /**
      *  Use a software allocation for the pixel memory.
      *
-     *  Useful for drawing to a software {@link Canvas} or for
+     *  <p>Useful for drawing to a software {@link Canvas} or for
      *  accessing the pixels on the final output.
      */
     public static final int ALLOCATOR_SOFTWARE = 1;
@@ -488,92 +1135,177 @@
     /**
      *  Use shared memory for the pixel memory.
      *
-     *  Useful for sharing across processes.
+     *  <p>Useful for sharing across processes.
      */
     public static final int ALLOCATOR_SHARED_MEMORY = 2;
 
     /**
      *  Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}.
      *
-     *  When this is combined with incompatible options, like
-     *  {@link #setMutable} or {@link #setAsAlphaMask}, {@link #decodeDrawable}
-     *  / {@link #decodeBitmap} will throw an
-     *  {@link java.lang.IllegalStateException}.
+     *  <p>When this is combined with incompatible options, like
+     *  {@link #setMutableRequired setMutableRequired(true)} or
+     *  {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)},
+     *  {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap}
+     *  will throw an {@link java.lang.IllegalStateException}.
      */
     public static final int ALLOCATOR_HARDWARE = 3;
 
     /** @hide **/
     @Retention(SOURCE)
+    @IntDef(value = { ALLOCATOR_DEFAULT, ALLOCATOR_SOFTWARE,
+              ALLOCATOR_SHARED_MEMORY, ALLOCATOR_HARDWARE },
+              prefix = {"ALLOCATOR_"})
     public @interface Allocator {};
 
     /**
      *  Choose the backing for the pixel memory.
      *
-     *  This is ignored for animated drawables.
+     *  <p>This is ignored for animated drawables.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
      *
      *  @param allocator Type of allocator to use.
      */
-    public ImageDecoder setAllocator(@Allocator int allocator) {
-        return this;
+    public void setAllocator(@Allocator int allocator) {
+        if (allocator < ALLOCATOR_DEFAULT || allocator > ALLOCATOR_HARDWARE) {
+            throw new IllegalArgumentException("invalid allocator " + allocator);
+        }
+        mAllocator = allocator;
+    }
+
+    /**
+     *  Return the allocator for the pixel memory.
+     */
+    @Allocator
+    public int getAllocator() {
+        return mAllocator;
     }
 
     /**
      *  Specify whether the {@link Bitmap} should have unpremultiplied pixels.
      *
-     *  By default, ImageDecoder will create a {@link Bitmap} with
+     *  <p>By default, ImageDecoder will create a {@link Bitmap} with
      *  premultiplied pixels, which is required for drawing with the
      *  {@link android.view.View} system (i.e. to a {@link Canvas}). Calling
      *  this method with a value of {@code true} will result in
      *  {@link #decodeBitmap} returning a {@link Bitmap} with unpremultiplied
-     *  pixels. See {@link Bitmap#isPremultiplied}. This is incompatible with
-     *  {@link #decodeDrawable}; attempting to decode an unpremultiplied
-     *  {@link Drawable} will throw an {@link java.lang.IllegalStateException}.
+     *  pixels. See {@link Bitmap#isPremultiplied Bitmap.isPremultiplied()}.
+     *  This is incompatible with {@link #decodeDrawable decodeDrawable};
+     *  attempting to decode an unpremultiplied {@link Drawable} will throw an
+     *  {@link java.lang.IllegalStateException}. </p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
      */
-    public ImageDecoder setRequireUnpremultiplied(boolean requireUnpremultiplied) {
+    public void setUnpremultipliedRequired(boolean unpremultipliedRequired) {
+        mUnpremultipliedRequired = unpremultipliedRequired;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #setUnpremultipliedRequired}.
+     */
+    @Deprecated
+    public ImageDecoder setRequireUnpremultiplied(boolean unpremultipliedRequired) {
+        this.setUnpremultipliedRequired(unpremultipliedRequired);
         return this;
     }
 
     /**
+     *  Return whether the {@link Bitmap} will have unpremultiplied pixels.
+     */
+    public boolean isUnpremultipliedRequired() {
+        return mUnpremultipliedRequired;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #isUnpremultipliedRequired}.
+     */
+    @Deprecated
+    public boolean getRequireUnpremultiplied() {
+        return this.isUnpremultipliedRequired();
+    }
+
+    /**
      *  Modify the image after decoding and scaling.
      *
      *  <p>This allows adding effects prior to returning a {@link Drawable} or
      *  {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap},
      *  this is the only way to process the image after decoding.</p>
      *
+     *  <p>If combined with {@link #setTargetSize} and/or {@link #setCrop},
+     *  {@link PostProcessor#onPostProcess} occurs last.</p>
+     *
      *  <p>If set on a nine-patch image, the nine-patch data is ignored.</p>
      *
      *  <p>For an animated image, the drawing commands drawn on the
      *  {@link Canvas} will be recorded immediately and then applied to each
      *  frame.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     *
      */
-    public ImageDecoder setPostProcessor(@Nullable PostProcessor p) {
-        return this;
+    public void setPostProcessor(@Nullable PostProcessor postProcessor) {
+        mPostProcessor = postProcessor;
+    }
+
+    /**
+     *  Return the {@link PostProcessor} currently set.
+     */
+    @Nullable
+    public PostProcessor getPostProcessor() {
+        return mPostProcessor;
     }
 
     /**
      *  Set (replace) the {@link OnPartialImageListener} on this object.
      *
-     *  Will be called if there is an error in the input. Without one, a
-     *  partial {@link Bitmap} will be created.
+     *  <p>Will be called if there is an error in the input. Without one, an
+     *  error will result in an {@code Exception} being thrown.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     *
      */
-    public ImageDecoder setOnPartialImageListener(@Nullable OnPartialImageListener l) {
-        return this;
+    public void setOnPartialImageListener(@Nullable OnPartialImageListener listener) {
+        mOnPartialImageListener = listener;
+    }
+
+    /**
+     *  Return the {@link OnPartialImageListener OnPartialImageListener} currently set.
+     */
+    @Nullable
+    public OnPartialImageListener getOnPartialImageListener() {
+        return mOnPartialImageListener;
     }
 
     /**
      *  Crop the output to {@code subset} of the (possibly) scaled image.
      *
      *  <p>{@code subset} must be contained within the size set by
-     *  {@link #setResize} or the bounds of the image if setResize was not
-     *  called. Otherwise an {@link IllegalStateException} will be thrown by
-     *  {@link #decodeDrawable}/{@link #decodeBitmap}.</p>
+     *  {@link #setTargetSize} or the bounds of the image if setTargetSize was
+     *  not called. Otherwise an {@link IllegalStateException} will be thrown by
+     *  {@link #decodeDrawable decodeDrawable}/{@link #decodeBitmap decodeBitmap}.</p>
      *
      *  <p>NOT intended as a replacement for
-     *  {@link BitmapRegionDecoder#decodeRegion}. This supports all formats,
-     *  but merely crops the output.</p>
+     *  {@link BitmapRegionDecoder#decodeRegion BitmapRegionDecoder.decodeRegion()}.
+     *  This supports all formats, but merely crops the output.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     *
      */
-    public ImageDecoder setCrop(@Nullable Rect subset) {
-        return this;
+    public void setCrop(@Nullable Rect subset) {
+        mCropRect = subset;
+    }
+
+    /**
+     *  Return the cropping rectangle, if set.
+     */
+    @Nullable
+    public Rect getCrop() {
+        return mCropRect;
     }
 
     /**
@@ -582,43 +1314,122 @@
      *  If the image is a nine patch, this Rect will be set to the padding
      *  rectangle during decode. Otherwise it will not be modified.
      *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     *
      *  @hide
      */
-    public ImageDecoder setOutPaddingRect(@NonNull Rect outPadding) {
+    public void setOutPaddingRect(@NonNull Rect outPadding) {
         mOutPaddingRect = outPadding;
-        return this;
     }
 
     /**
      *  Specify whether the {@link Bitmap} should be mutable.
      *
-     *  <p>By default, a {@link Bitmap} created will be immutable, but that can
-     *  be changed with this call.</p>
+     *  <p>By default, a {@link Bitmap} created by {@link #decodeBitmap decodeBitmap}
+     *  will be immutable i.e. {@link Bitmap#isMutable() Bitmap.isMutable()} returns
+     *  {@code false}. This can be changed with {@code setMutableRequired(true)}.
      *
      *  <p>Mutable Bitmaps are incompatible with {@link #ALLOCATOR_HARDWARE},
      *  because {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable.
      *  Attempting to combine them will throw an
      *  {@link java.lang.IllegalStateException}.</p>
      *
-     *  <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable},
+     *  <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable decodeDrawable},
      *  which would require retrieving the Bitmap from the returned Drawable in
      *  order to modify. Attempting to decode a mutable {@link Drawable} will
      *  throw an {@link java.lang.IllegalStateException}.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
      */
+    public void setMutableRequired(boolean mutable) {
+        mMutable = mutable;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #setMutableRequired}.
+     */
+    @Deprecated
     public ImageDecoder setMutable(boolean mutable) {
+        this.setMutableRequired(mutable);
         return this;
     }
 
     /**
-     *  Specify whether to potentially save RAM at the expense of quality.
-     *
-     *  Setting this to {@code true} may result in a {@link Bitmap} with a
-     *  denser {@link Bitmap.Config}, depending on the image. For example, for
-     *  an opaque {@link Bitmap}, this may result in a {@link Bitmap.Config}
-     *  with no alpha information.
+     *  Return whether the decoded {@link Bitmap} will be mutable.
      */
-    public ImageDecoder setPreferRamOverQuality(boolean preferRamOverQuality) {
-        return this;
+    public boolean isMutableRequired() {
+        return mMutable;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #isMutableRequired}.
+     */
+    @Deprecated
+    public boolean getMutable() {
+        return this.isMutableRequired();
+    }
+
+    /**
+     * Save memory if possible by using a denser {@link Bitmap.Config} at the
+     * cost of some image quality.
+     *
+     * <p>For example an opaque 8-bit image may be compressed into an
+     * {@link Bitmap.Config#RGB_565} configuration, sacrificing image
+     * quality to save memory.
+     */
+    public static final int MEMORY_POLICY_LOW_RAM = 0;
+
+    /**
+     * Use the most natural {@link Bitmap.Config} for the internal {@link Bitmap}.
+     *
+     * <p>This is the recommended default for most applications and usages. This
+     * will use the closest {@link Bitmap.Config} for the encoded source. If the
+     * encoded source does not exactly match any {@link Bitmap.Config}, the next
+     * highest quality {@link Bitmap.Config} will be used avoiding any loss in
+     * image quality.
+     */
+    public static final int MEMORY_POLICY_DEFAULT  = 1;
+
+    /** @hide **/
+    @Retention(SOURCE)
+    @IntDef(value = { MEMORY_POLICY_DEFAULT, MEMORY_POLICY_LOW_RAM },
+              prefix = {"MEMORY_POLICY_"})
+    public @interface MemoryPolicy {};
+
+    /**
+     *  Specify the memory policy for the decoded {@link Bitmap}.
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     */
+    public void setMemorySizePolicy(@MemoryPolicy int policy) {
+        mConserveMemory = (policy == MEMORY_POLICY_LOW_RAM);
+    }
+
+    /**
+     *  Retrieve the memory policy for the decoded {@link Bitmap}.
+     */
+    @MemoryPolicy
+    public int getMemorySizePolicy() {
+        return mConserveMemory ? MEMORY_POLICY_LOW_RAM : MEMORY_POLICY_DEFAULT;
+    }
+
+    /** @removed
+     * @deprecated Replaced by {@link #setMemorySizePolicy}.
+     */
+    @Deprecated
+    public void setConserveMemory(boolean conserveMemory) {
+        mConserveMemory = conserveMemory;
+    }
+
+    /** @removed
+     * @deprecated Replaced by {@link #getMemorySizePolicy}.
+     */
+    @Deprecated
+    public boolean getConserveMemory() {
+        return mConserveMemory;
     }
 
     /**
@@ -628,77 +1439,467 @@
      *  with only one channel, treat that channel as alpha. Otherwise this call has
      *  no effect.</p>
      *
-     *  <p>setAsAlphaMask is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
-     *  combine them will result in {@link #decodeDrawable}/
-     *  {@link #decodeBitmap} throwing an
+     *  <p>This is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
+     *  combine them will result in {@link #decodeDrawable decodeDrawable}/
+     *  {@link #decodeBitmap decodeBitmap} throwing an
      *  {@link java.lang.IllegalStateException}.</p>
+     *
+     *  <p>Like all setters on ImageDecoder, this must be called inside
+     *  {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
      */
-    public ImageDecoder setAsAlphaMask(boolean asAlphaMask) {
+    public void setDecodeAsAlphaMaskEnabled(boolean enabled) {
+        mDecodeAsAlphaMask = enabled;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}.
+     */
+    @Deprecated
+    public ImageDecoder setDecodeAsAlphaMask(boolean enabled) {
+        this.setDecodeAsAlphaMaskEnabled(enabled);
         return this;
     }
 
+    /** @removed
+     * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}.
+     */
+    @Deprecated
+    public ImageDecoder setAsAlphaMask(boolean asAlphaMask) {
+        this.setDecodeAsAlphaMask(asAlphaMask);
+        return this;
+    }
+
+    /**
+     *  Return whether to treat single channel input as alpha.
+     *
+     *  <p>This returns whether {@link #setDecodeAsAlphaMaskEnabled} was set to
+     *  {@code true}. It may still return {@code true} even if the image has
+     *  more than one channel and therefore will not be treated as an alpha
+     *  mask.</p>
+     */
+    public boolean isDecodeAsAlphaMaskEnabled() {
+        return mDecodeAsAlphaMask;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}.
+     */
+    @Deprecated
+    public boolean getDecodeAsAlphaMask() {
+        return mDecodeAsAlphaMask;
+    }
+
+    /** @removed
+     * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}.
+     */
+    @Deprecated
+    public boolean getAsAlphaMask() {
+        return this.getDecodeAsAlphaMask();
+    }
+
+    /**
+     * Specify the desired {@link ColorSpace} for the output.
+     *
+     * <p>If non-null, the decoder will try to decode into {@code colorSpace}.
+     * If it is null, which is the default, or the request cannot be met, the
+     * decoder will pick either the color space embedded in the image or the
+     * {@link ColorSpace} best suited for the requested image configuration
+     * (for instance {@link ColorSpace.Named#SRGB sRGB} for the
+     * {@link Bitmap.Config#ARGB_8888} configuration).</p>
+     *
+     * <p>{@link Bitmap.Config#RGBA_F16} always uses the
+     * {@link ColorSpace.Named#LINEAR_EXTENDED_SRGB scRGB} color space.
+     * Bitmaps in other configurations without an embedded color space are
+     * assumed to be in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
+     *
+     * <p class="note">Only {@link ColorSpace.Model#RGB} color spaces are
+     * currently supported. An <code>IllegalArgumentException</code> will
+     * be thrown by {@link #decodeDrawable decodeDrawable}/
+     * {@link #decodeBitmap decodeBitmap} when setting a non-RGB color space
+     * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
+     *
+     * <p class="note">The specified color space's transfer function must be
+     * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
+     * <code>IllegalArgumentException</code> will be thrown by the decode methods
+     * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
+     * specified color space returns null.</p>
+     *
+     * <p>Like all setters on ImageDecoder, this must be called inside
+     * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+     */
+    public void setTargetColorSpace(ColorSpace colorSpace) {
+        mDesiredColorSpace = colorSpace;
+    }
+
+    /**
+     * Closes this resource, relinquishing any underlying resources. This method
+     * is invoked automatically on objects managed by the try-with-resources
+     * statement.
+     *
+     * <p>This is an implementation detail of {@link ImageDecoder}, and should
+     * never be called manually.</p>
+     */
     @Override
     public void close() {
+        mCloseGuard.close();
+        if (!mClosed.compareAndSet(false, true)) {
+            return;
+        }
+        nClose(mNativePtr);
+        mNativePtr = 0;
+
+        if (mOwnsInputStream) {
+            IoUtils.closeQuietly(mInputStream);
+        }
+        IoUtils.closeQuietly(mAssetFd);
+
+        mInputStream = null;
+        mAssetFd = null;
+        mTempStorage = null;
+    }
+
+    private void checkState() {
+        if (mNativePtr == 0) {
+            throw new IllegalStateException("Cannot use closed ImageDecoder!");
+        }
+
+        checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
+
+        if (mAllocator == ALLOCATOR_HARDWARE) {
+            if (mMutable) {
+                throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!");
+            }
+            if (mDecodeAsAlphaMask) {
+                throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!");
+            }
+        }
+
+        if (mPostProcessor != null && mUnpremultipliedRequired) {
+            throw new IllegalStateException("Cannot draw to unpremultiplied pixels!");
+        }
+
+        if (mDesiredColorSpace != null) {
+            if (!(mDesiredColorSpace instanceof ColorSpace.Rgb)) {
+                throw new IllegalArgumentException("The target color space must use the "
+                            + "RGB color model - provided: " + mDesiredColorSpace);
+            }
+            if (((ColorSpace.Rgb) mDesiredColorSpace).getTransferParameters() == null) {
+                throw new IllegalArgumentException("The target color space must use an "
+                            + "ICC parametric transfer function - provided: " + mDesiredColorSpace);
+            }
+        }
+    }
+
+    private static void checkSubset(int width, int height, Rect r) {
+        if (r == null) {
+            return;
+        }
+        if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) {
+            throw new IllegalStateException("Subset " + r + " not contained by "
+                    + "scaled image bounds: (" + width + " x " + height + ")");
+        }
+    }
+
+    @WorkerThread
+    @NonNull
+    private Bitmap decodeBitmapInternal() throws IOException {
+        checkState();
+        return nDecodeBitmap(mNativePtr, this, mPostProcessor != null,
+                mDesiredWidth, mDesiredHeight, mCropRect,
+                mMutable, mAllocator, mUnpremultipliedRequired,
+                mConserveMemory, mDecodeAsAlphaMask, mDesiredColorSpace);
+    }
+
+    private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener,
+            @NonNull Source src) {
+        if (listener != null) {
+            ImageInfo info = new ImageInfo(this);
+            try {
+                listener.onHeaderDecoded(this, info, src);
+            } finally {
+                info.mDecoder = null;
+            }
+        }
     }
 
     /**
      *  Create a {@link Drawable} from a {@code Source}.
      *
      *  @param src representing the encoded image.
-     *  @param listener for learning the {@link ImageInfo} and changing any
-     *      default settings on the {@code ImageDecoder}. If not {@code null},
-     *      this will be called on the same thread as {@code decodeDrawable}
-     *      before that method returns.
+     *  @param listener for learning the {@link ImageInfo ImageInfo} and changing any
+     *      default settings on the {@code ImageDecoder}. This will be called on
+     *      the same thread as {@code decodeDrawable} before that method returns.
+     *      This is required in order to change any of the default settings.
      *  @return Drawable for displaying the image.
      *  @throws IOException if {@code src} is not found, is an unsupported
      *      format, or cannot be decoded for any reason.
      */
+    @WorkerThread
     @NonNull
     public static Drawable decodeDrawable(@NonNull Source src,
+            @NonNull OnHeaderDecodedListener listener) throws IOException {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener cannot be null! "
+                    + "Use decodeDrawable(Source) to not have a listener");
+        }
+        return decodeDrawableImpl(src, listener);
+    }
+
+    @WorkerThread
+    @NonNull
+    private static Drawable decodeDrawableImpl(@NonNull Source src,
             @Nullable OnHeaderDecodedListener listener) throws IOException {
-        Bitmap bitmap = decodeBitmap(src, listener);
-        return new BitmapDrawable(src.getResources(), bitmap);
+        try (ImageDecoder decoder = src.createImageDecoder()) {
+            decoder.mSource = src;
+            decoder.callHeaderDecoded(listener, src);
+
+            if (decoder.mUnpremultipliedRequired) {
+                // Though this could be supported (ignored) for opaque images,
+                // it seems better to always report this error.
+                throw new IllegalStateException("Cannot decode a Drawable " +
+                                                "with unpremultiplied pixels!");
+            }
+
+            if (decoder.mMutable) {
+                throw new IllegalStateException("Cannot decode a mutable " +
+                                                "Drawable!");
+            }
+
+            // this call potentially manipulates the decoder so it must be performed prior to
+            // decoding the bitmap and after decode set the density on the resulting bitmap
+            final int srcDensity = decoder.computeDensity(src);
+            if (decoder.mAnimated) {
+                // AnimatedImageDrawable calls postProcessAndRelease only if
+                // mPostProcessor exists.
+                ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
+                        null : decoder;
+                Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
+                        postProcessPtr, decoder.mDesiredWidth,
+                        decoder.mDesiredHeight, srcDensity,
+                        src.computeDstDensity(), decoder.mCropRect,
+                        decoder.mInputStream, decoder.mAssetFd);
+                // d has taken ownership of these objects.
+                decoder.mInputStream = null;
+                decoder.mAssetFd = null;
+                return d;
+            }
+
+            Bitmap bm = decoder.decodeBitmapInternal();
+            bm.setDensity(srcDensity);
+
+            Resources res = src.getResources();
+            byte[] np = bm.getNinePatchChunk();
+            if (np != null && NinePatch.isNinePatchChunk(np)) {
+                Rect opticalInsets = new Rect();
+                bm.getOpticalInsets(opticalInsets);
+                Rect padding = decoder.mOutPaddingRect;
+                if (padding == null) {
+                    padding = new Rect();
+                }
+                nGetPadding(decoder.mNativePtr, padding);
+                return new NinePatchDrawable(res, bm, np, padding,
+                        opticalInsets, null);
+            }
+
+            return new BitmapDrawable(res, bm);
+        }
     }
 
     /**
-     * See {@link #decodeDrawable(Source, OnHeaderDecodedListener)}.
+     *  Create a {@link Drawable} from a {@code Source}.
+     *
+     *  <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener},
+     *  the default settings will be used. In order to change any settings, call
+     *  {@link #decodeDrawable(Source, OnHeaderDecodedListener)} instead.</p>
+     *
+     *  @param src representing the encoded image.
+     *  @return Drawable for displaying the image.
+     *  @throws IOException if {@code src} is not found, is an unsupported
+     *      format, or cannot be decoded for any reason.
      */
+    @WorkerThread
     @NonNull
     public static Drawable decodeDrawable(@NonNull Source src)
             throws IOException {
-        return decodeDrawable(src, null);
+        return decodeDrawableImpl(src, null);
     }
 
     /**
      *  Create a {@link Bitmap} from a {@code Source}.
      *
      *  @param src representing the encoded image.
-     *  @param listener for learning the {@link ImageInfo} and changing any
-     *      default settings on the {@code ImageDecoder}. If not {@code null},
-     *      this will be called on the same thread as {@code decodeBitmap}
-     *      before that method returns.
+     *  @param listener for learning the {@link ImageInfo ImageInfo} and changing any
+     *      default settings on the {@code ImageDecoder}. This will be called on
+     *      the same thread as {@code decodeBitmap} before that method returns.
+     *      This is required in order to change any of the default settings.
      *  @return Bitmap containing the image.
      *  @throws IOException if {@code src} is not found, is an unsupported
      *      format, or cannot be decoded for any reason.
      */
+    @WorkerThread
     @NonNull
     public static Bitmap decodeBitmap(@NonNull Source src,
-            @Nullable OnHeaderDecodedListener listener) throws IOException {
-        TypedValue value = new TypedValue();
-        value.density = src.getDensity();
-        ImageDecoder decoder = src.createImageDecoder();
-        if (listener != null) {
-            listener.onHeaderDecoded(decoder, new ImageInfo(decoder), src);
+            @NonNull OnHeaderDecodedListener listener) throws IOException {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener cannot be null! "
+                    + "Use decodeBitmap(Source) to not have a listener");
         }
-        return BitmapFactory.decodeResourceStream(src.getResources(), value,
-                ((InputStreamSource) src).mInputStream, decoder.mOutPaddingRect, null);
+        return decodeBitmapImpl(src, listener);
+    }
+
+    @WorkerThread
+    @NonNull
+    private static Bitmap decodeBitmapImpl(@NonNull Source src,
+            @Nullable OnHeaderDecodedListener listener) throws IOException {
+        try (ImageDecoder decoder = src.createImageDecoder()) {
+            decoder.mSource = src;
+            decoder.callHeaderDecoded(listener, src);
+
+            // this call potentially manipulates the decoder so it must be performed prior to
+            // decoding the bitmap
+            final int srcDensity = decoder.computeDensity(src);
+            Bitmap bm = decoder.decodeBitmapInternal();
+            bm.setDensity(srcDensity);
+
+            Rect padding = decoder.mOutPaddingRect;
+            if (padding != null) {
+                byte[] np = bm.getNinePatchChunk();
+                if (np != null && NinePatch.isNinePatchChunk(np)) {
+                    nGetPadding(decoder.mNativePtr, padding);
+                }
+            }
+
+            return bm;
+        }
+    }
+
+    // This method may modify the decoder so it must be called prior to performing the decode
+    private int computeDensity(@NonNull Source src) {
+        // if the caller changed the size then we treat the density as unknown
+        if (this.requestedResize()) {
+            return Bitmap.DENSITY_NONE;
+        }
+
+        final int srcDensity = src.getDensity();
+        if (srcDensity == Bitmap.DENSITY_NONE) {
+            return srcDensity;
+        }
+
+        // Scaling up nine-patch divs is imprecise and is better handled
+        // at draw time. An app won't be relying on the internal Bitmap's
+        // size, so it is safe to let NinePatchDrawable handle scaling.
+        // mPostProcessor disables nine-patching, so behave normally if
+        // it is present.
+        if (mIsNinePatch && mPostProcessor == null) {
+            return srcDensity;
+        }
+
+        // Special stuff for compatibility mode: if the target density is not
+        // the same as the display density, but the resource -is- the same as
+        // the display density, then don't scale it down to the target density.
+        // This allows us to load the system's density-correct resources into
+        // an application in compatibility mode, without scaling those down
+        // to the compatibility density only to have them scaled back up when
+        // drawn to the screen.
+        Resources res = src.getResources();
+        if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) {
+            return srcDensity;
+        }
+
+        final int dstDensity = src.computeDstDensity();
+        if (srcDensity == dstDensity) {
+            return srcDensity;
+        }
+
+        // For P and above, only resize if it would be a downscale. Scale up prior
+        // to P in case the app relies on the Bitmap's size without considering density.
+        if (srcDensity < dstDensity && sApiLevel >= Build.VERSION_CODES.P) {
+            return srcDensity;
+        }
+
+        float scale = (float) dstDensity / srcDensity;
+        int scaledWidth = (int) (mWidth * scale + 0.5f);
+        int scaledHeight = (int) (mHeight * scale + 0.5f);
+        this.setTargetSize(scaledWidth, scaledHeight);
+        return dstDensity;
+    }
+
+    @NonNull
+    private String getMimeType() {
+        return nGetMimeType(mNativePtr);
+    }
+
+    @Nullable
+    private ColorSpace getColorSpace() {
+        return nGetColorSpace(mNativePtr);
     }
 
     /**
-     *  See {@link #decodeBitmap(Source, OnHeaderDecodedListener)}.
+     *  Create a {@link Bitmap} from a {@code Source}.
+     *
+     *  <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener},
+     *  the default settings will be used. In order to change any settings, call
+     *  {@link #decodeBitmap(Source, OnHeaderDecodedListener)} instead.</p>
+     *
+     *  @param src representing the encoded image.
+     *  @return Bitmap containing the image.
+     *  @throws IOException if {@code src} is not found, is an unsupported
+     *      format, or cannot be decoded for any reason.
      */
+    @WorkerThread
     @NonNull
     public static Bitmap decodeBitmap(@NonNull Source src) throws IOException {
-        return decodeBitmap(src, null);
+        return decodeBitmapImpl(src, null);
     }
+
+    /**
+     * Private method called by JNI.
+     */
+    @SuppressWarnings("unused")
+    private int postProcessAndRelease(@NonNull Canvas canvas) {
+        try {
+            return mPostProcessor.onPostProcess(canvas);
+        } finally {
+            canvas.release();
+        }
+    }
+
+    /**
+     * Private method called by JNI.
+     */
+    @SuppressWarnings("unused")
+    private void onPartialImage(@DecodeException.Error int error, @Nullable Throwable cause)
+            throws DecodeException {
+        DecodeException exception = new DecodeException(error, cause, mSource);
+        if (mOnPartialImageListener == null
+                || !mOnPartialImageListener.onPartialImage(exception)) {
+            throw exception;
+        }
+    }
+
+    private static native ImageDecoder nCreate(long asset, Source src) throws IOException;
+    private static native ImageDecoder nCreate(ByteBuffer buffer, int position,
+                                               int limit, Source src) throws IOException;
+    private static native ImageDecoder nCreate(byte[] data, int offset, int length,
+                                               Source src) throws IOException;
+    private static native ImageDecoder nCreate(InputStream is, byte[] storage,
+                                               Source src) throws IOException;
+    // The fd must be seekable.
+    private static native ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException;
+    @NonNull
+    private static native Bitmap nDecodeBitmap(long nativePtr,
+            @NonNull ImageDecoder decoder,
+            boolean doPostProcess,
+            int width, int height,
+            @Nullable Rect cropRect, boolean mutable,
+            int allocator, boolean unpremulRequired,
+            boolean conserveMemory, boolean decodeAsAlphaMask,
+            @Nullable ColorSpace desiredColorSpace)
+        throws IOException;
+    private static native Size nGetSampledSize(long nativePtr,
+                                               int sampleSize);
+    private static native void nGetPadding(long nativePtr, @NonNull Rect outRect);
+    private static native void nClose(long nativePtr);
+    private static native String nGetMimeType(long nativePtr);
+    private static native ColorSpace nGetColorSpace(long nativePtr);
 }
diff --git a/android/graphics/PostProcessor.java b/android/graphics/PostProcessor.java
index b1712e9..6fed39b 100644
--- a/android/graphics/PostProcessor.java
+++ b/android/graphics/PostProcessor.java
@@ -16,25 +16,26 @@
 
 package android.graphics;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.graphics.drawable.AnimatedImageDrawable;
 import android.graphics.drawable.Drawable;
 
 /**
  *  Helper interface for adding custom processing to an image.
  *
- *  <p>The image being processed may be a {@link Drawable}, {@link Bitmap} or frame
- *  of an animated image produced by {@link ImageDecoder}. This is called before
- *  the requested object is returned.</p>
+ *  <p>The image being processed may be a {@link Drawable}, a {@link Bitmap}, or
+ *  a frame of an {@link AnimatedImageDrawable} produced by {@link ImageDecoder}.
+ *  This is called before the requested object is returned.</p>
  *
- *  <p>This custom processing also applies to image types that are otherwise
- *  immutable, such as {@link Bitmap.Config#HARDWARE}.</p>
+ *  <p>This custom processing can even be applied to images that will be returned
+ *  as immutable objects, such as a {@link Bitmap} with {@code Config}
+ *  {@link Bitmap.Config#HARDWARE} returned by {@link ImageDecoder}.</p>
  *
- *  <p>On an animated image, the callback will only be called once, but the drawing
- *  commands will be applied to each frame, as if the {@code Canvas} had been
- *  returned by {@link Picture#beginRecording}.<p>
+ *  <p>On an {@link AnimatedImageDrawable}, the callback will only be called once,
+ *  but the drawing commands will be applied to each frame, as if the {@link Canvas}
+ *  had been returned by {@link Picture#beginRecording Picture.beginRecording}.<p>
  *
- *  <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor}.</p>
+ *  <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor setPostProcessor}.</p>
  */
 public interface PostProcessor {
     /**
@@ -43,43 +44,44 @@
      *  <p>Drawing to the {@link Canvas} will behave as if the initial processing
      *  (e.g. decoding) already exists in the Canvas. An implementation can draw
      *  effects on top of this, or it can even draw behind it using
-     *  {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency
-     *  to the corners to achieve rounded corners. That can be done with the
-     *  following code:</p>
+     *  {@link PorterDuff.Mode#DST_OVER PorterDuff.Mode.DST_OVER}. A common
+     *  effect is to add transparency to the corners to achieve rounded corners.
+     *  That can be done with the following code:</p>
      *
-     *  <code>
-     *      Path path = new Path();
-     *      path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
-     *      int width = canvas.getWidth();
-     *      int height = canvas.getHeight();
-     *      path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
-     *      Paint paint = new Paint();
-     *      paint.setAntiAlias(true);
-     *      paint.setColor(Color.TRANSPARENT);
-     *      paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
-     *      canvas.drawPath(path, paint);
-     *      return PixelFormat.TRANSLUCENT;
-     *  </code>
+     *  <pre class="prettyprint">
+     *  Path path = new Path();
+     *  path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+     *  int width = canvas.getWidth();
+     *  int height = canvas.getHeight();
+     *  path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
+     *  Paint paint = new Paint();
+     *  paint.setAntiAlias(true);
+     *  paint.setColor(Color.TRANSPARENT);
+     *  paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+     *  canvas.drawPath(path, paint);
+     *  return PixelFormat.TRANSLUCENT;
+     *  </pre>
      *
      *
      *  @param canvas The {@link Canvas} to draw to.
      *  @return Opacity of the result after drawing.
-     *      {@link PixelFormat#UNKNOWN} means that the implementation did not
-     *      change whether the image has alpha. Return this unless you added
-     *      transparency (e.g. with the code above, in which case you should
-     *      return {@code PixelFormat.TRANSLUCENT}) or you forced the image to
-     *      be opaque (e.g. by drawing everywhere with an opaque color and
-     *      {@code PorterDuff.Mode.DST_OVER}, in which case you should return
-     *      {@code PixelFormat.OPAQUE}).
-     *      {@link PixelFormat#TRANSLUCENT} means that the implementation added
-     *      transparency. This is safe to return even if the image already had
-     *      transparency. This is also safe to return if the result is opaque,
-     *      though it may draw more slowly.
-     *      {@link PixelFormat#OPAQUE} means that the implementation forced the
-     *      image to be opaque. This is safe to return even if the image was
-     *      already opaque.
-     *      {@link PixelFormat#TRANSPARENT} (or any other integer) is not
-     *      allowed, and will result in throwing an
+     *      {@link PixelFormat#UNKNOWN PixelFormat.UNKNOWN} means that the
+     *      implementation did not change whether the image has alpha. Return
+     *      this unless you added transparency (e.g. with the code above, in
+     *      which case you should return
+     *      {@link PixelFormat#TRANSLUCENT PixelFormat.TRANSLUCENT}) or you
+     *      forced the image to be opaque (e.g. by drawing everywhere with an
+     *      opaque color and {@link PorterDuff.Mode#DST_OVER PorterDuff.Mode.DST_OVER},
+     *      in which case you should return {@link PixelFormat#OPAQUE PixelFormat.OPAQUE}).
+     *      {@link PixelFormat#TRANSLUCENT PixelFormat.TRANSLUCENT} means that
+     *      the implementation added transparency. This is safe to return even
+     *      if the image already had transparency. This is also safe to return
+     *      if the result is opaque, though it may draw more slowly.
+     *      {@link PixelFormat#OPAQUE PixelFormat.OPAQUE} means that the
+     *      implementation forced the image to be opaque. This is safe to return
+     *      even if the image was already opaque.
+     *      {@link PixelFormat#TRANSPARENT PixelFormat.TRANSPARENT} (or any other
+     *      integer) is not allowed, and will result in throwing an
      *      {@link java.lang.IllegalArgumentException}.
      */
     @PixelFormat.Opacity
diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java
index 20c22b7..18dd97f 100644
--- a/android/graphics/Typeface.java
+++ b/android/graphics/Typeface.java
@@ -738,6 +738,23 @@
     /**
      * Creates a typeface object that best matches the specified existing typeface and the specified
      * weight and italic style
+     * <p>Below are numerical values and corresponding common weight names.</p>
+     * <table>
+     * <thead>
+     * <tr><th>Value</th><th>Common weight name</th></tr>
+     * </thead>
+     * <tbody>
+     * <tr><td>100</td><td>Thin</td></tr>
+     * <tr><td>200</td><td>Extra Light</td></tr>
+     * <tr><td>300</td><td>Light</td></tr>
+     * <tr><td>400</td><td>Normal</td></tr>
+     * <tr><td>500</td><td>Medium</td></tr>
+     * <tr><td>600</td><td>Semi Bold</td></tr>
+     * <tr><td>700</td><td>Bold</td></tr>
+     * <tr><td>800</td><td>Extra Bold</td></tr>
+     * <tr><td>900</td><td>Black</td></tr>
+     * </tbody>
+     * </table>
      *
      * <p>
      * This method is thread safe.
@@ -749,6 +766,9 @@
      * @param italic {@code true} if italic style is desired to be drawn. Otherwise, {@code false}
      * @return A {@link Typeface} object for drawing specified weight and italic style. Never
      *         returns {@code null}
+     *
+     * @see #getWeight()
+     * @see #isItalic()
      */
     public static @NonNull Typeface create(@Nullable Typeface family,
             @IntRange(from = 1, to = 1000) int weight, boolean italic) {
diff --git a/android/hardware/biometrics/BiometricDialog.java b/android/hardware/biometrics/BiometricPrompt.java
similarity index 93%
rename from android/hardware/biometrics/BiometricDialog.java
rename to android/hardware/biometrics/BiometricPrompt.java
index dd848a3..1c9de45 100644
--- a/android/hardware/biometrics/BiometricDialog.java
+++ b/android/hardware/biometrics/BiometricPrompt.java
@@ -38,7 +38,7 @@
 /**
  * A class that manages a system-provided biometric dialog.
  */
-public class BiometricDialog implements BiometricAuthenticator, BiometricConstants {
+public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants {
 
     /**
      * @hide
@@ -190,11 +190,11 @@
         }
 
         /**
-         * Creates a {@link BiometricDialog}.
-         * @return a {@link BiometricDialog}
+         * Creates a {@link BiometricPrompt}.
+         * @return a {@link BiometricPrompt}
          * @throws IllegalArgumentException if any of the required fields are not set.
          */
-        public BiometricDialog build() {
+        public BiometricPrompt build() {
             final CharSequence title = mBundle.getCharSequence(KEY_TITLE);
             final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT);
 
@@ -203,7 +203,7 @@
             } else if (TextUtils.isEmpty(negative)) {
                 throw new IllegalArgumentException("Negative text must be set and non-empty");
             }
-            return new BiometricDialog(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo);
+            return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo);
         }
     }
 
@@ -213,7 +213,7 @@
     private ButtonInfo mPositiveButtonInfo;
     private ButtonInfo mNegativeButtonInfo;
 
-    IBiometricDialogReceiver mDialogReceiver = new IBiometricDialogReceiver.Stub() {
+    IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
         @Override
         public void onDialogDismissed(int reason) {
             // Check the reason and invoke OnClickListener(s) if necessary
@@ -229,7 +229,7 @@
         }
     };
 
-    private BiometricDialog(Context context, Bundle bundle,
+    private BiometricPrompt(Context context, Bundle bundle,
             ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) {
         mBundle = bundle;
         mPositiveButtonInfo = positiveButtonInfo;
@@ -239,7 +239,7 @@
     }
 
     /**
-     * A wrapper class for the crypto objects supported by BiometricDialog. Currently the framework
+     * A wrapper class for the crypto objects supported by BiometricPrompt. Currently the framework
      * supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
      */
     public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
@@ -308,8 +308,8 @@
     }
 
     /**
-     * Callback structure provided to {@link BiometricDialog#authenticate(CancellationSignal,
-     * Executor, AuthenticationCallback)} or {@link BiometricDialog#authenticate(CryptoObject,
+     * Callback structure provided to {@link BiometricPrompt#authenticate(CancellationSignal,
+     * Executor, AuthenticationCallback)} or {@link BiometricPrompt#authenticate(CryptoObject,
      * CancellationSignal, Executor, AuthenticationCallback)}. Users must provide an implementation
      * of this for listening to authentication events.
      */
@@ -378,7 +378,7 @@
             @NonNull CancellationSignal cancel,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
-        if (!(callback instanceof BiometricDialog.AuthenticationCallback)) {
+        if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
             throw new IllegalArgumentException("Callback cannot be casted");
         }
         authenticate(crypto, cancel, executor, (AuthenticationCallback) callback);
@@ -395,7 +395,7 @@
     public void authenticate(@NonNull CancellationSignal cancel,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
-        if (!(callback instanceof BiometricDialog.AuthenticationCallback)) {
+        if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
             throw new IllegalArgumentException("Callback cannot be casted");
         }
         authenticate(cancel, executor, (AuthenticationCallback) callback);
@@ -410,8 +410,8 @@
      * operation can be canceled by using the provided cancel object. The application will receive
      * authentication errors through {@link AuthenticationCallback}, and button events through the
      * corresponding callback set in {@link Builder#setNegativeButton(CharSequence, Executor,
-     * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricDialog} object,
-     * and calling {@link BiometricDialog#authenticate( CancellationSignal, Executor,
+     * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object,
+     * and calling {@link BiometricPrompt#authenticate( CancellationSignal, Executor,
      * AuthenticationCallback)} while an existing authentication attempt is occurring will stop the
      * previous client and start a new authentication. The interrupted client will receive a
      * cancelled notification through {@link AuthenticationCallback#onAuthenticationError(int,
@@ -445,8 +445,8 @@
      * provided cancel object. The application will receive authentication errors through {@link
      * AuthenticationCallback}, and button events through the corresponding callback set in {@link
      * Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.  It is
-     * safe to reuse the {@link BiometricDialog} object, and calling {@link
-     * BiometricDialog#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while
+     * safe to reuse the {@link BiometricPrompt} object, and calling {@link
+     * BiometricPrompt#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while
      * an existing authentication attempt is occurring will stop the previous client and start a new
      * authentication. The interrupted client will receive a cancelled notification through {@link
      * AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
@@ -470,15 +470,15 @@
     private boolean handlePreAuthenticationErrors(AuthenticationCallback callback,
             Executor executor) {
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
-            sendError(BiometricDialog.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback,
+            sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback,
                       executor);
             return true;
         } else if (!mFingerprintManager.isHardwareDetected()) {
-            sendError(BiometricDialog.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback,
+            sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback,
                       executor);
             return true;
         } else if (!mFingerprintManager.hasEnrolledFingerprints()) {
-            sendError(BiometricDialog.BIOMETRIC_ERROR_NO_BIOMETRICS, callback,
+            sendError(BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS, callback,
                       executor);
             return true;
         }
diff --git a/android/hardware/camera2/CameraManager.java b/android/hardware/camera2/CameraManager.java
index 4124536..7ebe0f9 100644
--- a/android/hardware/camera2/CameraManager.java
+++ b/android/hardware/camera2/CameraManager.java
@@ -43,6 +43,9 @@
 import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
@@ -924,6 +927,37 @@
                     idCount++;
                 }
             }
+
+            // The sort logic must match the logic in
+            // libcameraservice/common/CameraProviderManager.cpp::getAPI1CompatibleCameraDeviceIds
+            Arrays.sort(cameraIds, new Comparator<String>() {
+                    @Override
+                    public int compare(String s1, String s2) {
+                        int s1Int = 0, s2Int = 0;
+                        try {
+                            s1Int = Integer.parseInt(s1);
+                        } catch (NumberFormatException e) {
+                            s1Int = -1;
+                        }
+
+                        try {
+                            s2Int = Integer.parseInt(s2);
+                        } catch (NumberFormatException e) {
+                            s2Int = -1;
+                        }
+
+                        // Uint device IDs first
+                        if (s1Int >= 0 && s2Int >= 0) {
+                            return s1Int - s2Int;
+                        } else if (s1Int >= 0) {
+                            return -1;
+                        } else if (s2Int >= 0) {
+                            return 1;
+                        } else {
+                            // Simple string compare if both id are not uint
+                            return s1.compareTo(s2);
+                        }
+                    }});
             return cameraIds;
         }
 
diff --git a/android/hardware/camera2/CaptureRequest.java b/android/hardware/camera2/CaptureRequest.java
index 2252571..411a97e 100644
--- a/android/hardware/camera2/CaptureRequest.java
+++ b/android/hardware/camera2/CaptureRequest.java
@@ -2105,8 +2105,8 @@
      * the thumbnail data will also be rotated.</p>
      * <p>Note that this orientation is relative to the orientation of the camera sensor, given
      * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
-     * <p>To translate from the device orientation given by the Android sensor APIs, the following
-     * sample code may be used:</p>
+     * <p>To translate from the device orientation given by the Android sensor APIs for camera
+     * sensors which are not EXTERNAL, the following sample code may be used:</p>
      * <pre><code>private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
      *     if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0;
      *     int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);
@@ -2125,6 +2125,8 @@
      *     return jpegOrientation;
      * }
      * </code></pre>
+     * <p>For EXTERNAL cameras the sensor orientation will always be set to 0 and the facing will
+     * also be set to EXTERNAL. The above code is not relevant in such case.</p>
      * <p><b>Units</b>: Degrees in multiples of 90</p>
      * <p><b>Range of valid values:</b><br>
      * 0, 90, 180, 270</p>
diff --git a/android/hardware/camera2/CaptureResult.java b/android/hardware/camera2/CaptureResult.java
index 8df5447..c156616 100644
--- a/android/hardware/camera2/CaptureResult.java
+++ b/android/hardware/camera2/CaptureResult.java
@@ -2422,8 +2422,8 @@
      * the thumbnail data will also be rotated.</p>
      * <p>Note that this orientation is relative to the orientation of the camera sensor, given
      * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
-     * <p>To translate from the device orientation given by the Android sensor APIs, the following
-     * sample code may be used:</p>
+     * <p>To translate from the device orientation given by the Android sensor APIs for camera
+     * sensors which are not EXTERNAL, the following sample code may be used:</p>
      * <pre><code>private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
      *     if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0;
      *     int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);
@@ -2442,6 +2442,8 @@
      *     return jpegOrientation;
      * }
      * </code></pre>
+     * <p>For EXTERNAL cameras the sensor orientation will always be set to 0 and the facing will
+     * also be set to EXTERNAL. The above code is not relevant in such case.</p>
      * <p><b>Units</b>: Degrees in multiples of 90</p>
      * <p><b>Range of valid values:</b><br>
      * 0, 90, 180, 270</p>
diff --git a/android/hardware/display/BrightnessConfiguration.java b/android/hardware/display/BrightnessConfiguration.java
index 67e97bf..6d9ba77 100644
--- a/android/hardware/display/BrightnessConfiguration.java
+++ b/android/hardware/display/BrightnessConfiguration.java
@@ -86,7 +86,9 @@
             sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")");
         }
         sb.append("], '");
-        sb.append(mDescription);
+        if (mDescription != null) {
+            sb.append(mDescription);
+        }
         sb.append("'}");
         return sb.toString();
     }
@@ -96,7 +98,9 @@
         int result = 1;
         result = result * 31 + Arrays.hashCode(mLux);
         result = result * 31 + Arrays.hashCode(mNits);
-        result = result * 31 + mDescription.hashCode();
+        if (mDescription != null) {
+            result = result * 31 + mDescription.hashCode();
+        }
         return result;
     }
 
diff --git a/android/hardware/display/Curve.java b/android/hardware/display/Curve.java
new file mode 100644
index 0000000..ac28fdd
--- /dev/null
+++ b/android/hardware/display/Curve.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 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 android.hardware.display;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/** @hide */
+public final class Curve implements Parcelable {
+    private final float[] mX;
+    private final float[] mY;
+
+    public Curve(float[] x, float[] y) {
+        mX = x;
+        mY = y;
+    }
+
+    public float[] getX() {
+        return mX;
+    }
+
+    public float[] getY() {
+        return mY;
+    }
+
+    public static final Creator<Curve> CREATOR = new Creator<Curve>() {
+        public Curve createFromParcel(Parcel in) {
+            float[] x = in.createFloatArray();
+            float[] y = in.createFloatArray();
+            return new Curve(x, y);
+        }
+
+        public Curve[] newArray(int size) {
+            return new Curve[size];
+        }
+    };
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeFloatArray(mX);
+        out.writeFloatArray(mY);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java
index efb9517..b182fa2 100644
--- a/android/hardware/display/DisplayManager.java
+++ b/android/hardware/display/DisplayManager.java
@@ -28,6 +28,7 @@
 import android.graphics.Point;
 import android.media.projection.MediaProjection;
 import android.os.Handler;
+import android.util.Pair;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.Surface;
@@ -748,6 +749,22 @@
     }
 
     /**
+     * Returns the minimum brightness curve, which guarantess that any brightness curve that dips
+     * below it is rejected by the system.
+     * This prevent auto-brightness from setting the screen so dark as to prevent the user from
+     * resetting or disabling it, and maps lux to the absolute minimum nits that are still readable
+     * in that ambient brightness.
+     *
+     * @return The minimum brightness curve (as lux values and their corresponding nits values).
+     *
+     * @hide
+     */
+    @SystemApi
+    public Pair<float[], float[]> getMinimumBrightnessCurve() {
+        return mGlobal.getMinimumBrightnessCurve();
+    }
+
+    /**
      * Listens for changes in available display devices.
      */
     public interface DisplayListener {
diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java
index 2d0ef2f..d968a3e 100644
--- a/android/hardware/display/DisplayManagerGlobal.java
+++ b/android/hardware/display/DisplayManagerGlobal.java
@@ -31,6 +31,7 @@
 import android.os.ServiceManager;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.DisplayAdjustments;
@@ -563,6 +564,24 @@
     }
 
     /**
+     * Returns the minimum brightness curve, which guarantess that any brightness curve that dips
+     * below it is rejected by the system.
+     * This prevent auto-brightness from setting the screen so dark as to prevent the user from
+     * resetting or disabling it, and maps lux to the absolute minimum nits that are still readable
+     * in that ambient brightness.
+     *
+     * @return The minimum brightness curve (as lux values and their corresponding nits values).
+     */
+    public Pair<float[], float[]> getMinimumBrightnessCurve() {
+        try {
+            Curve curve = mDm.getMinimumBrightnessCurve();
+            return Pair.create(curve.getX(), curve.getY());
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Retrieves ambient brightness stats.
      */
     public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
diff --git a/android/hardware/fingerprint/FingerprintManager.java b/android/hardware/fingerprint/FingerprintManager.java
index a6c8c67..40d31bf 100644
--- a/android/hardware/fingerprint/FingerprintManager.java
+++ b/android/hardware/fingerprint/FingerprintManager.java
@@ -31,9 +31,9 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.hardware.biometrics.BiometricAuthenticator;
-import android.hardware.biometrics.BiometricDialog;
 import android.hardware.biometrics.BiometricFingerprintConstants;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricPromptReceiver;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -57,7 +57,7 @@
 
 /**
  * A class that coordinates access to the fingerprint hardware.
- * @deprecated See {@link BiometricDialog} which shows a system-provided dialog upon starting
+ * @deprecated See {@link BiometricPrompt} which shows a system-provided dialog upon starting
  * authentication. In a world where devices may have different types of biometric authentication,
  * it's much more realistic to have a system-provided authentication dialog since the method may
  * vary by vendor/device.
@@ -111,7 +111,7 @@
     /**
      * A wrapper class for the crypto objects supported by FingerprintManager. Currently the
      * framework supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
-     * @deprecated See {@link android.hardware.biometrics.BiometricDialog.CryptoObject}
+     * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
      */
     @Deprecated
     public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
@@ -155,7 +155,7 @@
     /**
      * Container for callback data from {@link FingerprintManager#authenticate(CryptoObject,
      *     CancellationSignal, int, AuthenticationCallback, Handler)}.
-     * @deprecated See {@link android.hardware.biometrics.BiometricDialog.AuthenticationResult}
+     * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
      */
     @Deprecated
     public static class AuthenticationResult {
@@ -204,7 +204,7 @@
      * FingerprintManager#authenticate(CryptoObject, CancellationSignal,
      * int, AuthenticationCallback, Handler) } must provide an implementation of this for listening to
      * fingerprint events.
-     * @deprecated See {@link android.hardware.biometrics.BiometricDialog.AuthenticationCallback}
+     * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
      */
     @Deprecated
     public static abstract class AuthenticationCallback
@@ -378,10 +378,10 @@
      *         by <a href="{@docRoot}training/articles/keystore.html">Android Keystore
      *         facility</a>.
      * @throws IllegalStateException if the crypto primitive is not initialized.
-     * @deprecated See {@link BiometricDialog#authenticate(CancellationSignal, Executor,
-     * BiometricDialog.AuthenticationCallback)} and {@link BiometricDialog#authenticate(
-     * BiometricDialog.CryptoObject, CancellationSignal, Executor,
-     * BiometricDialog.AuthenticationCallback)}
+     * @deprecated See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
+     * BiometricPrompt.AuthenticationCallback)} and {@link BiometricPrompt#authenticate(
+     * BiometricPrompt.CryptoObject, CancellationSignal, Executor,
+     * BiometricPrompt.AuthenticationCallback)}
      */
     @Deprecated
     @RequiresPermission(anyOf = {USE_BIOMETRIC, USE_FINGERPRINT})
@@ -444,7 +444,7 @@
 
     /**
      * Per-user version, see {@link FingerprintManager#authenticate(CryptoObject,
-     * CancellationSignal, Bundle, Executor, IBiometricDialogReceiver, AuthenticationCallback)}
+     * CancellationSignal, Bundle, Executor, IBiometricPromptReceiver, AuthenticationCallback)}
      * @param userId the user ID that the fingerprint hardware will authenticate for.
      */
     private void authenticate(int userId,
@@ -452,7 +452,7 @@
             @NonNull CancellationSignal cancel,
             @NonNull Bundle bundle,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull IBiometricDialogReceiver receiver,
+            @NonNull IBiometricPromptReceiver receiver,
             @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
         mCryptoObject = crypto;
         if (cancel.isCanceled()) {
@@ -480,8 +480,8 @@
     }
 
     /**
-     * Private method, see {@link BiometricDialog#authenticate(CancellationSignal, Executor,
-     * BiometricDialog.AuthenticationCallback)}
+     * Private method, see {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
+     * BiometricPrompt.AuthenticationCallback)}
      * @param cancel
      * @param executor
      * @param callback
@@ -491,7 +491,7 @@
             @NonNull CancellationSignal cancel,
             @NonNull Bundle bundle,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull IBiometricDialogReceiver receiver,
+            @NonNull IBiometricPromptReceiver receiver,
             @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
         if (cancel == null) {
             throw new IllegalArgumentException("Must supply a cancellation signal");
@@ -512,8 +512,8 @@
     }
 
     /**
-     * Private method, see {@link BiometricDialog#authenticate(BiometricDialog.CryptoObject,
-     * CancellationSignal, Executor, BiometricDialog.AuthenticationCallback)}
+     * Private method, see {@link BiometricPrompt#authenticate(BiometricPrompt.CryptoObject,
+     * CancellationSignal, Executor, BiometricPrompt.AuthenticationCallback)}
      * @param crypto
      * @param cancel
      * @param executor
@@ -524,7 +524,7 @@
             @NonNull CancellationSignal cancel,
             @NonNull Bundle bundle,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull IBiometricDialogReceiver receiver,
+            @NonNull IBiometricPromptReceiver receiver,
             @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
         if (crypto == null) {
             throw new IllegalArgumentException("Must supply a crypto object");
@@ -743,7 +743,7 @@
      * Determine if there is at least one fingerprint enrolled.
      *
      * @return true if at least one fingerprint is enrolled, false otherwise
-     * @deprecated See {@link BiometricDialog} and
+     * @deprecated See {@link BiometricPrompt} and
      * {@link FingerprintManager#FINGERPRINT_ERROR_NO_FINGERPRINTS}
      */
     @Deprecated
@@ -777,7 +777,7 @@
      * Determine if fingerprint hardware is present and functional.
      *
      * @return true if hardware is present and functional, false otherwise.
-     * @deprecated See {@link BiometricDialog} and
+     * @deprecated See {@link BiometricPrompt} and
      * {@link FingerprintManager#FINGERPRINT_ERROR_HW_UNAVAILABLE}
      */
     @Deprecated
@@ -1158,7 +1158,7 @@
         @Override // binder call
         public void onError(long deviceId, int error, int vendorCode) {
             if (mExecutor != null) {
-                // BiometricDialog case
+                // BiometricPrompt case
                 if (error == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED) {
                     // User tapped somewhere to cancel, the biometric dialog is already dismissed.
                     mExecutor.execute(() -> {
@@ -1172,7 +1172,7 @@
                         mExecutor.execute(() -> {
                             sendErrorResult(deviceId, error, vendorCode);
                         });
-                    }, BiometricDialog.HIDE_DIALOG_DELAY);
+                    }, BiometricPrompt.HIDE_DIALOG_DELAY);
                 }
             } else {
                 mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget();
diff --git a/android/location/LocationManager.java b/android/location/LocationManager.java
index a523958..6eb3d8d 100644
--- a/android/location/LocationManager.java
+++ b/android/location/LocationManager.java
@@ -29,6 +29,7 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
@@ -392,6 +393,18 @@
     }
 
     /**
+     * @hide
+     */
+    @TestApi
+    public String[] getBackgroundThrottlingWhitelist() {
+        try {
+            return mService.getBackgroundThrottlingWhitelist();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @hide - hide this constructor because it has a parameter
      * of type ILocationManager, which is a system private class. The
      * right way to create an instance of this class is using the
@@ -1249,40 +1262,11 @@
     @SystemApi
     @RequiresPermission(WRITE_SECURE_SETTINGS)
     public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
-        final List<String> allProvidersList = getAllProviders();
-        // Update all providers on device plus gps and network provider when disabling location.
-        Set<String> allProvidersSet = new ArraySet<>(allProvidersList.size() + 2);
-        allProvidersSet.addAll(allProvidersList);
-        // When disabling location, disable gps and network provider that could have been enabled by
-        // location mode api.
-        if (enabled == false) {
-            allProvidersSet.add(GPS_PROVIDER);
-            allProvidersSet.add(NETWORK_PROVIDER);
+        try {
+            mService.setLocationEnabledForUser(enabled, userHandle.getIdentifier());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
-        if (allProvidersSet.isEmpty()) {
-            return;
-        }
-        // to ensure thread safety, we write the provider name with a '+' or '-'
-        // and let the SettingsProvider handle it rather than reading and modifying
-        // the list of enabled providers.
-        final String prefix = enabled ? "+" : "-";
-        StringBuilder locationProvidersAllowed = new StringBuilder();
-        for (String provider : allProvidersSet) {
-            checkProvider(provider);
-            if (provider.equals(PASSIVE_PROVIDER)) {
-                continue;
-            }
-            locationProvidersAllowed.append(prefix);
-            locationProvidersAllowed.append(provider);
-            locationProvidersAllowed.append(",");
-        }
-        // Remove the trailing comma
-        locationProvidersAllowed.setLength(locationProvidersAllowed.length() - 1);
-        Settings.Secure.putStringForUser(
-                mContext.getContentResolver(),
-                Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
-                locationProvidersAllowed.toString(),
-                userHandle.getIdentifier());
     }
 
     /**
@@ -1295,22 +1279,11 @@
      */
     @SystemApi
     public boolean isLocationEnabledForUser(UserHandle userHandle) {
-        final String allowedProviders = Settings.Secure.getStringForUser(
-                mContext.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
-                userHandle.getIdentifier());
-        if (allowedProviders == null) {
-            return false;
+        try {
+            return mService.isLocationEnabledForUser(userHandle.getIdentifier());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
-        final List<String> providerList = Arrays.asList(allowedProviders.split(","));
-        for(String provider : getAllProviders()) {
-            if (provider.equals(PASSIVE_PROVIDER)) {
-                continue;
-            }
-            if (providerList.contains(provider)) {
-                return true;
-            }
-        }
-        return false;
     }
 
     /**
@@ -1362,9 +1335,12 @@
     @SystemApi
     public boolean isProviderEnabledForUser(String provider, UserHandle userHandle) {
         checkProvider(provider);
-        String allowedProviders = Settings.Secure.getStringForUser(mContext.getContentResolver(),
-                Settings.Secure.LOCATION_PROVIDERS_ALLOWED, userHandle.getIdentifier());
-        return TextUtils.delimitedStringContains(allowedProviders, ',', provider);
+
+        try {
+            return mService.isProviderEnabledForUser(provider, userHandle.getIdentifier());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -1383,16 +1359,13 @@
     public boolean setProviderEnabledForUser(
             String provider, boolean enabled, UserHandle userHandle) {
         checkProvider(provider);
-        // to ensure thread safety, we write the provider name with a '+' or '-'
-        // and let the SettingsProvider handle it rather than reading and modifying
-        // the list of enabled providers.
-        if (enabled) {
-            provider = "+" + provider;
-        } else {
-            provider = "-" + provider;
+
+        try {
+            return mService.setProviderEnabledForUser(
+                    provider, enabled, userHandle.getIdentifier());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
-        return Settings.Secure.putStringForUser(mContext.getContentResolver(),
-                Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider, userHandle.getIdentifier());
     }
 
     /**
diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java
index d432658..9152ff2 100644
--- a/android/media/AudioAttributes.java
+++ b/android/media/AudioAttributes.java
@@ -180,6 +180,7 @@
      * IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES
      *            if applicable, as well as audioattributes.proto.
      *            Also consider adding them to <aaudio/AAudio.h> for the NDK.
+     *            Also consider adding them to UsageTypeConverter for service dump and etc.
      */
 
     /**
@@ -249,9 +250,10 @@
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,    SUPPRESSIBLE_MEDIA);
         SUPPRESSIBLE_USAGES.put(USAGE_GAME,                              SUPPRESSIBLE_MEDIA);
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT,                         SUPPRESSIBLE_MEDIA);
+        /** default volume assignment is STREAM_MUSIC, handle unknown usage as media */
+        SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN,                           SUPPRESSIBLE_MEDIA);
         SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING,    SUPPRESSIBLE_SYSTEM);
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION,           SUPPRESSIBLE_SYSTEM);
-        SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN,                           SUPPRESSIBLE_SYSTEM);
     }
 
     /**
@@ -1056,8 +1058,7 @@
             case USAGE_ASSISTANCE_ACCESSIBILITY:
                 return AudioSystem.STREAM_ACCESSIBILITY;
             case USAGE_UNKNOWN:
-                return fromGetVolumeControlStream ?
-                        AudioManager.USE_DEFAULT_STREAM_TYPE : AudioSystem.STREAM_MUSIC;
+                return AudioSystem.STREAM_MUSIC;
             default:
                 if (fromGetVolumeControlStream) {
                     throw new IllegalArgumentException("Unknown usage value " + aa.getUsage() +
diff --git a/android/media/AudioFocusRequest.java b/android/media/AudioFocusRequest.java
index 7104dad..fe89b89 100644
--- a/android/media/AudioFocusRequest.java
+++ b/android/media/AudioFocusRequest.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.media.AudioManager.OnAudioFocusChangeListener;
 import android.os.Bundle;
 import android.os.Handler;
@@ -262,6 +263,7 @@
      * Returns the focus change listener set for this {@code AudioFocusRequest}.
      * @return null if no {@link AudioManager.OnAudioFocusChangeListener} was set.
      */
+    @TestApi
     public @Nullable OnAudioFocusChangeListener getOnAudioFocusChangeListener() {
         return mFocusListener;
     }
diff --git a/android/media/AudioFormat.java b/android/media/AudioFormat.java
index f98480b..d0a2c98 100644
--- a/android/media/AudioFormat.java
+++ b/android/media/AudioFormat.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -437,6 +438,7 @@
      * @param mask a combination of the CHANNEL_IN_* definitions, even CHANNEL_IN_DEFAULT
      * @return number of channels for the mask
      */
+    @TestApi
     public static int channelCountFromInChannelMask(int mask) {
         return Integer.bitCount(mask);
     }
@@ -446,6 +448,7 @@
      * @param mask a combination of the CHANNEL_OUT_* definitions, but not CHANNEL_OUT_DEFAULT
      * @return number of channels for the mask
      */
+    @TestApi
     public static int channelCountFromOutChannelMask(int mask) {
         return Integer.bitCount(mask);
     }
@@ -492,6 +495,7 @@
     // CHANNEL_IN_ALL is not yet defined; if added then it should match AUDIO_CHANNEL_IN_ALL
 
     /** @hide */
+    @TestApi
     public static int getBytesPerSample(int audioFormat)
     {
         switch (audioFormat) {
@@ -562,6 +566,7 @@
     }
 
     /** @hide */
+    @TestApi
     public static boolean isEncodingLinearPcm(int audioFormat)
     {
         switch (audioFormat) {
diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java
index aeef215..fdb7499 100644
--- a/android/media/AudioManager.java
+++ b/android/media/AudioManager.java
@@ -63,6 +63,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
@@ -4786,6 +4787,21 @@
     }
 
     /**
+     * Add {@link MicrophoneInfo} by device information while filtering certain types.
+     */
+    private void addMicrophonesFromAudioDeviceInfo(ArrayList<MicrophoneInfo> microphones,
+                    HashSet<Integer> filterTypes) {
+        AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS);
+        for (AudioDeviceInfo device : devices) {
+            if (filterTypes.contains(device.getType())) {
+                continue;
+            }
+            MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device);
+            microphones.add(microphone);
+        }
+    }
+
+    /**
      * Returns a list of {@link MicrophoneInfo} that corresponds to the characteristics
      * of all available microphones. The list is empty when no microphones are available
      * on the device. An error during the query will result in an IOException being thrown.
@@ -4796,21 +4812,17 @@
     public List<MicrophoneInfo> getMicrophones() throws IOException {
         ArrayList<MicrophoneInfo> microphones = new ArrayList<MicrophoneInfo>();
         int status = AudioSystem.getMicrophones(microphones);
+        HashSet<Integer> filterTypes = new HashSet<>();
+        filterTypes.add(AudioDeviceInfo.TYPE_TELEPHONY);
         if (status != AudioManager.SUCCESS) {
-            // fail and bail!
+            // fail and populate microphones with unknown characteristics by device information.
             Log.e(TAG, "getMicrophones failed:" + status);
-            return new ArrayList<MicrophoneInfo>(); // Always return a list.
+            addMicrophonesFromAudioDeviceInfo(microphones, filterTypes);
+            return microphones;
         }
         setPortIdForMicrophones(microphones);
-        AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS);
-        for (AudioDeviceInfo device : devices) {
-            if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC ||
-                    device.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
-                continue;
-            }
-            MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device);
-            microphones.add(microphone);
-        }
+        filterTypes.add(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+        addMicrophonesFromAudioDeviceInfo(microphones, filterTypes);
         return microphones;
     }
 
diff --git a/android/media/AudioPlaybackConfiguration.java b/android/media/AudioPlaybackConfiguration.java
index 8a36f91..7dfdb20 100644
--- a/android/media/AudioPlaybackConfiguration.java
+++ b/android/media/AudioPlaybackConfiguration.java
@@ -43,6 +43,8 @@
     /** @hide */
     public static final int PLAYER_PIID_INVALID = -1;
     /** @hide */
+    public static final int PLAYER_PIID_UNASSIGNED = 0;
+    /** @hide */
     public static final int PLAYER_UPID_INVALID = -1;
 
     // information about the implementation
diff --git a/android/media/AudioPresentation.java b/android/media/AudioPresentation.java
index e39cb7d..ce71436 100644
--- a/android/media/AudioPresentation.java
+++ b/android/media/AudioPresentation.java
@@ -18,8 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-
-import com.android.internal.annotations.VisibleForTesting;
+import android.annotation.TestApi;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -94,7 +93,7 @@
     /**
      * @hide
      */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @TestApi
     public AudioPresentation(int presentationId,
                         int programId,
                         @NonNull Map<String, String> labels,
@@ -119,7 +118,7 @@
      * decoder. Presentation id is typically sequential, but does not have to be.
      * @hide
      */
-    @VisibleForTesting
+    @TestApi
     public int getPresentationId() {
         return mPresentationId;
     }
@@ -129,7 +128,7 @@
      * Program id can be used to further uniquely identify the presentation to a decoder.
      * @hide
      */
-    @VisibleForTesting
+    @TestApi
     public int getProgramId() {
         return mProgramId;
     }
diff --git a/android/media/AudioRecord.java b/android/media/AudioRecord.java
index 4f0dccb..6b35dd4 100644
--- a/android/media/AudioRecord.java
+++ b/android/media/AudioRecord.java
@@ -1628,7 +1628,6 @@
         int status = native_get_active_microphones(activeMicrophones);
         if (status != AudioManager.SUCCESS) {
             Log.e(TAG, "getActiveMicrophones failed:" + status);
-            return new ArrayList<MicrophoneInfo>();
         }
         AudioManager.setPortIdForMicrophones(activeMicrophones);
 
diff --git a/android/media/BufferingParams.java b/android/media/BufferingParams.java
index 521e897..aaae5e7 100644
--- a/android/media/BufferingParams.java
+++ b/android/media/BufferingParams.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import android.annotation.IntDef;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -63,6 +64,7 @@
  * <p>Users should use {@link Builder} to change {@link BufferingParams}.
  * @hide
  */
+@TestApi
 public final class BufferingParams implements Parcelable {
     private static final int BUFFERING_NO_MARK = -1;
 
diff --git a/android/media/ExifInterface.java b/android/media/ExifInterface.java
index bc0e43b..7888436 100644
--- a/android/media/ExifInterface.java
+++ b/android/media/ExifInterface.java
@@ -66,7 +66,7 @@
 /**
  * This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
  * <p>
- * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW and RAF.
+ * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF.
  * <p>
  * Attribute mutation is supported for JPEG image files.
  */
@@ -2524,46 +2524,46 @@
     private void getHeifAttributes(ByteOrderedDataInputStream in) throws IOException {
         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
         try {
-            if (mSeekableFileDescriptor != null) {
-                retriever.setDataSource(mSeekableFileDescriptor);
-            } else {
-                retriever.setDataSource(new MediaDataSource() {
-                    long mPosition;
+            retriever.setDataSource(new MediaDataSource() {
+                long mPosition;
 
-                    @Override
-                    public void close() throws IOException {}
+                @Override
+                public void close() throws IOException {}
 
-                    @Override
-                    public int readAt(long position, byte[] buffer, int offset, int size)
-                            throws IOException {
-                        if (size == 0) {
-                            return 0;
-                        }
-                        if (position < 0) {
-                            return -1;
-                        }
-                        if (mPosition != position) {
-                            in.seek(position);
-                            mPosition = position;
-                        }
-
-                        int bytesRead = in.read(buffer, offset, size);
-                        if (bytesRead < 0) {
-                            mPosition = -1; // need to seek on next read
-                            return -1;
-                        }
-
-                        mPosition += bytesRead;
-                        return bytesRead;
+                @Override
+                public int readAt(long position, byte[] buffer, int offset, int size)
+                        throws IOException {
+                    if (size == 0) {
+                        return 0;
                     }
-
-                    @Override
-                    public long getSize() throws IOException {
+                    if (position < 0) {
                         return -1;
                     }
-                });
-            }
+                    if (mPosition != position) {
+                        in.seek(position);
+                        mPosition = position;
+                    }
 
+                    int bytesRead = in.read(buffer, offset, size);
+                    if (bytesRead < 0) {
+                        mPosition = -1; // need to seek on next read
+                        return -1;
+                    }
+
+                    mPosition += bytesRead;
+                    return bytesRead;
+                }
+
+                @Override
+                public long getSize() throws IOException {
+                    return -1;
+                }
+            });
+
+            String exifOffsetStr = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET);
+            String exifLengthStr = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH);
             String hasImage = retriever.extractMetadata(
                     MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
             String hasVideo = retriever.extractMetadata(
@@ -2622,6 +2622,30 @@
                         ExifAttribute.createUShort(orientation, mExifByteOrder));
             }
 
+            if (exifOffsetStr != null && exifLengthStr != null) {
+                int offset = Integer.parseInt(exifOffsetStr);
+                int length = Integer.parseInt(exifLengthStr);
+                if (length <= 6) {
+                    throw new IOException("Invalid exif length");
+                }
+                in.seek(offset);
+                byte[] identifier = new byte[6];
+                if (in.read(identifier) != 6) {
+                    throw new IOException("Can't read identifier");
+                }
+                offset += 6;
+                length -= 6;
+                if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
+                    throw new IOException("Invalid identifier");
+                }
+
+                byte[] bytes = new byte[length];
+                if (in.read(bytes) != length) {
+                    throw new IOException("Can't read exif");
+                }
+                readExifSegment(bytes, IFD_TYPE_PRIMARY);
+            }
+
             if (DEBUG) {
                 Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation);
             }
diff --git a/android/media/Image.java b/android/media/Image.java
index 37c5785..9828275 100644
--- a/android/media/Image.java
+++ b/android/media/Image.java
@@ -193,6 +193,13 @@
     public abstract int getTransform();
 
     /**
+     * Get the scaling mode associated with this frame.
+     * @return The scaling mode that needs to be applied for this frame.
+     * @hide
+     */
+    public abstract int getScalingMode();
+
+    /**
      * Get the {@link android.hardware.HardwareBuffer HardwareBuffer} handle of the input image
      * intended for GPU and/or hardware access.
      * <p>
diff --git a/android/media/ImageReader.java b/android/media/ImageReader.java
index 72d52d3..8ec0e35 100644
--- a/android/media/ImageReader.java
+++ b/android/media/ImageReader.java
@@ -871,6 +871,12 @@
         }
 
         @Override
+        public int getScalingMode() {
+            throwISEIfImageIsInvalid();
+            return mScalingMode;
+        }
+
+        @Override
         public HardwareBuffer getHardwareBuffer() {
             throwISEIfImageIsInvalid();
             return nativeGetHardwareBuffer();
@@ -1004,14 +1010,11 @@
         private long mNativeBuffer;
 
         /**
-         * This field is set by native code during nativeImageSetup().
+         * These fields are set by native code during nativeImageSetup().
          */
         private long mTimestamp;
-
-        /**
-         * This field is set by native code during nativeImageSetup().
-         */
         private int mTransform;
+        private int mScalingMode;
 
         private SurfacePlane[] mPlanes;
         private int mFormat = ImageFormat.UNKNOWN;
diff --git a/android/media/ImageWriter.java b/android/media/ImageWriter.java
index 8ee27ae..397768a 100644
--- a/android/media/ImageWriter.java
+++ b/android/media/ImageWriter.java
@@ -371,7 +371,7 @@
 
         Rect crop = image.getCropRect();
         nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), crop.left, crop.top,
-                crop.right, crop.bottom, image.getTransform());
+                crop.right, crop.bottom, image.getTransform(), image.getScalingMode());
 
         /**
          * Only remove and cleanup the Images that are owned by this
@@ -558,7 +558,7 @@
         Rect crop = image.getCropRect();
         nativeAttachAndQueueImage(mNativeContext, image.getNativeContext(), image.getFormat(),
                 image.getTimestamp(), crop.left, crop.top, crop.right, crop.bottom,
-                image.getTransform());
+                image.getTransform(), image.getScalingMode());
     }
 
     /**
@@ -676,6 +676,7 @@
         private long mTimestamp = DEFAULT_TIMESTAMP;
 
         private int mTransform = 0; //Default no transform
+        private int mScalingMode = 0; //Default frozen scaling mode
 
         public WriterSurfaceImage(ImageWriter writer) {
             mOwner = writer;
@@ -721,6 +722,13 @@
         }
 
         @Override
+        public int getScalingMode() {
+            throwISEIfImageIsInvalid();
+
+            return mScalingMode;
+        }
+
+        @Override
         public long getTimestamp() {
             throwISEIfImageIsInvalid();
 
@@ -866,11 +874,12 @@
     private synchronized native void nativeDequeueInputImage(long nativeCtx, Image wi);
 
     private synchronized native void nativeQueueInputImage(long nativeCtx, Image image,
-            long timestampNs, int left, int top, int right, int bottom, int transform);
+            long timestampNs, int left, int top, int right, int bottom, int transform,
+            int scalingMode);
 
     private synchronized native int nativeAttachAndQueueImage(long nativeCtx,
             long imageNativeBuffer, int imageFormat, long timestampNs, int left,
-            int top, int right, int bottom, int transform);
+            int top, int right, int bottom, int transform, int scalingMode);
 
     private synchronized native void cancelImage(long nativeCtx, Image image);
 
diff --git a/android/media/MediaCodec.java b/android/media/MediaCodec.java
index e3fba0c..1f00c78 100644
--- a/android/media/MediaCodec.java
+++ b/android/media/MediaCodec.java
@@ -3574,6 +3574,7 @@
         private final static int TYPE_YUV = 1;
 
         private final int mTransform = 0; //Default no transform
+        private final int mScalingMode = 0; //Default frozen scaling mode
 
         @Override
         public int getFormat() {
@@ -3600,6 +3601,12 @@
         }
 
         @Override
+        public int getScalingMode() {
+            throwISEIfImageIsInvalid();
+            return mScalingMode;
+        }
+
+        @Override
         public long getTimestamp() {
             throwISEIfImageIsInvalid();
             return mTimestamp;
diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java
index 2a601f9..c29300d 100644
--- a/android/media/MediaCodecInfo.java
+++ b/android/media/MediaCodecInfo.java
@@ -2971,6 +2971,8 @@
         public static final int AACObjectLD         = 23;
         public static final int AACObjectHE_PS      = 29;
         public static final int AACObjectELD        = 39;
+        /** xHE-AAC (includes USAC) */
+        public static final int AACObjectXHE        = 42;
 
         // from OMX_VIDEO_VP8LEVELTYPE
         public static final int VP8Level_Version0 = 0x01;
diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java
index 0955dd6..8ab5ec4 100644
--- a/android/media/MediaMetadataRetriever.java
+++ b/android/media/MediaMetadataRetriever.java
@@ -890,5 +890,14 @@
      */
     public static final int METADATA_KEY_VIDEO_FRAME_COUNT = 32;
 
+    /**
+     * @hide
+     */
+    public static final int METADATA_KEY_EXIF_OFFSET = 33;
+
+    /**
+     * @hide
+     */
+    public static final int METADATA_KEY_EXIF_LENGTH = 34;
     // Add more here...
 }
diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java
index aef31b1..392a1eb 100644
--- a/android/media/MediaPlayer.java
+++ b/android/media/MediaPlayer.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.ActivityThread;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
@@ -1680,6 +1681,7 @@
      * @hide
      */
     @NonNull
+    @TestApi
     public native BufferingParams getBufferingParams();
 
     /**
@@ -1696,6 +1698,7 @@
      * @throws IllegalArgumentException if params is invalid or not supported.
      * @hide
      */
+    @TestApi
     public native void setBufferingParams(@NonNull BufferingParams params);
 
     /**
@@ -2128,7 +2131,13 @@
             mTimeProvider.close();
             mTimeProvider = null;
         }
-        mOnSubtitleDataListener = null;
+        synchronized(this) {
+            mSubtitleDataListenerDisabled = false;
+            mExtSubtitleDataListener = null;
+            mExtSubtitleDataHandler = null;
+            mOnMediaTimeDiscontinuityListener = null;
+            mOnMediaTimeDiscontinuityHandler = null;
+        }
 
         // Modular DRM clean up
         mOnDrmConfigHelper = null;
@@ -2699,7 +2708,7 @@
     private int mSelectedSubtitleTrackIndex = -1;
     private Vector<InputStream> mOpenSubtitleSources;
 
-    private OnSubtitleDataListener mSubtitleDataListener = new OnSubtitleDataListener() {
+    private final OnSubtitleDataListener mIntSubtitleDataListener = new OnSubtitleDataListener() {
         @Override
         public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
             int index = data.getTrackIndex();
@@ -2725,7 +2734,9 @@
             }
             mSelectedSubtitleTrackIndex = -1;
         }
-        setOnSubtitleDataListener(null);
+        synchronized (this) {
+            mSubtitleDataListenerDisabled = true;
+        }
         if (track == null) {
             return;
         }
@@ -2745,7 +2756,9 @@
                 selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true);
             } catch (IllegalStateException e) {
             }
-            setOnSubtitleDataListener(mSubtitleDataListener);
+            synchronized (this) {
+                mSubtitleDataListenerDisabled = false;
+            }
         }
         // no need to select out-of-band tracks
     }
@@ -3304,6 +3317,7 @@
     private static final int MEDIA_SUBTITLE_DATA = 201;
     private static final int MEDIA_META_DATA = 202;
     private static final int MEDIA_DRM_INFO = 210;
+    private static final int MEDIA_TIME_DISCONTINUITY = 211;
     private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000;
 
     private TimeProvider mTimeProvider;
@@ -3514,15 +3528,34 @@
                 return;
 
             case MEDIA_SUBTITLE_DATA:
-                OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener;
-                if (onSubtitleDataListener == null) {
-                    return;
+                final OnSubtitleDataListener extSubtitleListener;
+                final Handler extSubtitleHandler;
+                synchronized(this) {
+                    if (mSubtitleDataListenerDisabled) {
+                        return;
+                    }
+                    extSubtitleListener = mExtSubtitleDataListener;
+                    extSubtitleHandler = mExtSubtitleDataHandler;
                 }
                 if (msg.obj instanceof Parcel) {
                     Parcel parcel = (Parcel) msg.obj;
-                    SubtitleData data = new SubtitleData(parcel);
+                    final SubtitleData data = new SubtitleData(parcel);
                     parcel.recycle();
-                    onSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
+
+                    mIntSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
+
+                    if (extSubtitleListener != null) {
+                        if (extSubtitleHandler == null) {
+                            extSubtitleListener.onSubtitleData(mMediaPlayer, data);
+                        } else {
+                            extSubtitleHandler.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    extSubtitleListener.onSubtitleData(mMediaPlayer, data);
+                                }
+                            });
+                        }
+                    }
                 }
                 return;
 
@@ -3553,6 +3586,43 @@
                 }
                 return;
 
+            case MEDIA_TIME_DISCONTINUITY:
+                final OnMediaTimeDiscontinuityListener mediaTimeListener;
+                final Handler mediaTimeHandler;
+                synchronized(this) {
+                    mediaTimeListener = mOnMediaTimeDiscontinuityListener;
+                    mediaTimeHandler = mOnMediaTimeDiscontinuityHandler;
+                }
+                if (mediaTimeListener == null) {
+                    return;
+                }
+                if (msg.obj instanceof Parcel) {
+                    Parcel parcel = (Parcel) msg.obj;
+                    parcel.setDataPosition(0);
+                    long anchorMediaUs = parcel.readLong();
+                    long anchorRealUs = parcel.readLong();
+                    float playbackRate = parcel.readFloat();
+                    parcel.recycle();
+                    final MediaTimestamp timestamp;
+                    if (anchorMediaUs != -1 && anchorRealUs != -1) {
+                        timestamp = new MediaTimestamp(
+                                anchorMediaUs /*Us*/, anchorRealUs * 1000 /*Ns*/, playbackRate);
+                    } else {
+                        timestamp = MediaTimestamp.TIMESTAMP_UNKNOWN;
+                    }
+                    if (mediaTimeHandler == null) {
+                        mediaTimeListener.onMediaTimeDiscontinuity(mMediaPlayer, timestamp);
+                    } else {
+                        mediaTimeHandler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                mediaTimeListener.onMediaTimeDiscontinuity(mMediaPlayer, timestamp);
+                            }
+                        });
+                    }
+                }
+                return;
+
             default:
                 Log.e(TAG, "Unknown message type " + msg.what);
                 return;
@@ -3877,13 +3947,15 @@
     private void setOnSubtitleDataListenerInt(
             @Nullable OnSubtitleDataListener listener, @Nullable Handler handler) {
         synchronized (this) {
-            mOnSubtitleDataListener = listener;
-            mOnSubtitleDataHandler = handler;
+            mExtSubtitleDataListener = listener;
+            mExtSubtitleDataHandler = handler;
         }
     }
 
-    private OnSubtitleDataListener mOnSubtitleDataListener;
-    private Handler mOnSubtitleDataHandler;
+    private boolean mSubtitleDataListenerDisabled;
+    /** External OnSubtitleDataListener, the one set by {@link #setOnSubtitleDataListener}. */
+    private OnSubtitleDataListener mExtSubtitleDataListener;
+    private Handler mExtSubtitleDataHandler;
 
     /**
      * Interface definition of a callback to be invoked when discontinuity in the normal progression
diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java
index 90b6bff..82d64f3 100644
--- a/android/media/MediaRecorder.java
+++ b/android/media/MediaRecorder.java
@@ -1434,7 +1434,6 @@
         int status = native_getActiveMicrophones(activeMicrophones);
         if (status != AudioManager.SUCCESS) {
             Log.e(TAG, "getActiveMicrophones failed:" + status);
-            return new ArrayList<MicrophoneInfo>();
         }
         AudioManager.setPortIdForMicrophones(activeMicrophones);
 
diff --git a/android/media/MediaTimestamp.java b/android/media/MediaTimestamp.java
index 938dd14..dd43b4e 100644
--- a/android/media/MediaTimestamp.java
+++ b/android/media/MediaTimestamp.java
@@ -98,4 +98,13 @@
                 && (this.nanoTime == that.nanoTime)
                 && (this.clockRate == that.clockRate);
     }
+
+    @Override
+    public String toString() {
+        return getClass().getName()
+                + "{AnchorMediaTimeUs=" + mediaTimeUs
+                + " AnchorSystemNanoTime=" + nanoTime
+                + " clockRate=" + clockRate
+                + "}";
+    }
 }
diff --git a/android/media/PlaybackParams.java b/android/media/PlaybackParams.java
index 938a953..b85e4d0 100644
--- a/android/media/PlaybackParams.java
+++ b/android/media/PlaybackParams.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import android.annotation.IntDef;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -151,6 +152,7 @@
      * @param audioStretchMode
      * @return this <code>PlaybackParams</code> instance.
      */
+    @TestApi
     public PlaybackParams setAudioStretchMode(@AudioStretchMode int audioStretchMode) {
         mAudioStretchMode = audioStretchMode;
         mSet |= SET_AUDIO_STRETCH_MODE;
@@ -163,6 +165,7 @@
      * @return audio stretch mode
      * @throws IllegalStateException if the audio stretch mode is not set.
      */
+    @TestApi
     public @AudioStretchMode int getAudioStretchMode() {
         if ((mSet & SET_AUDIO_STRETCH_MODE) == 0) {
             throw new IllegalStateException("audio stretch mode not set");
diff --git a/android/media/PlayerBase.java b/android/media/PlayerBase.java
index 80049ba..7c6367e 100644
--- a/android/media/PlayerBase.java
+++ b/android/media/PlayerBase.java
@@ -31,6 +31,7 @@
 import android.os.ServiceManager;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IAppOpsCallback;
 import com.android.internal.app.IAppOpsService;
 
@@ -58,20 +59,29 @@
     protected float mRightVolume = 1.0f;
     protected float mAuxEffectSendLevel = 0.0f;
 
-    // for AppOps
-    private IAppOpsService mAppOps; // may be null
-    private IAppOpsCallback mAppOpsCallback;
-    private boolean mHasAppOpsPlayAudio = true; // sync'd on mLock
+    // NEVER call into AudioService (see getService()) with mLock held: PlayerBase can run in
+    // the same process as AudioService, which can synchronously call back into this class,
+    // causing deadlocks between the two
     private final Object mLock = new Object();
 
+    // for AppOps
+    private @Nullable IAppOpsService mAppOps;
+    private IAppOpsCallback mAppOpsCallback;
+    @GuardedBy("mLock")
+    private boolean mHasAppOpsPlayAudio = true;
+
     private final int mImplType;
     // uniquely identifies the Player Interface throughout the system (P I Id)
-    private int mPlayerIId;
+    private int mPlayerIId = AudioPlaybackConfiguration.PLAYER_PIID_UNASSIGNED;
 
-    private int mState; // sync'd on mLock
-    private int mStartDelayMs = 0; // sync'd on mLock
-    private float mPanMultiplierL = 1.0f; // sync'd on mLock
-    private float mPanMultiplierR = 1.0f; // sync'd on mLock
+    @GuardedBy("mLock")
+    private int mState;
+    @GuardedBy("mLock")
+    private int mStartDelayMs = 0;
+    @GuardedBy("mLock")
+    private float mPanMultiplierL = 1.0f;
+    @GuardedBy("mLock")
+    private float mPanMultiplierR = 1.0f;
 
     /**
      * Constructor. Must be given audio attributes, as they are required for AppOps.
@@ -134,16 +144,24 @@
         }
     }
 
+    private void updateState(int state) {
+        final int piid;
+        synchronized (mLock) {
+            mState = state;
+            piid = mPlayerIId;
+        }
+        try {
+            getService().playerEvent(piid, state);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error talking to audio service, "
+                    + AudioPlaybackConfiguration.toLogFriendlyPlayerState(state)
+                    + " state will not be tracked for piid=" + piid, e);
+        }
+    }
+
     void baseStart() {
         if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); }
-        try {
-            synchronized (mLock) {
-                mState = AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
-                getService().playerEvent(mPlayerIId, mState);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e);
-        }
+        updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
         synchronized (mLock) {
             if (isRestricted_sync()) {
                 playerSetVolume(true/*muting*/,0, 0);
@@ -165,26 +183,12 @@
 
     void basePause() {
         if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); }
-        try {
-            synchronized (mLock) {
-                mState = AudioPlaybackConfiguration.PLAYER_STATE_PAUSED;
-                getService().playerEvent(mPlayerIId, mState);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Error talking to audio service, PAUSED state will not be tracked", e);
-        }
+        updateState(AudioPlaybackConfiguration.PLAYER_STATE_PAUSED);
     }
 
     void baseStop() {
         if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); }
-        try {
-            synchronized (mLock) {
-                mState = AudioPlaybackConfiguration.PLAYER_STATE_STOPPED;
-                getService().playerEvent(mPlayerIId, mState);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Error talking to audio service, STOPPED state will not be tracked", e);
-        }
+        updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED);
     }
 
     void baseSetPan(float pan) {
@@ -228,12 +232,16 @@
      */
     void baseRelease() {
         if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId + " state=" + mState); }
+        boolean releasePlayer = false;
+        synchronized (mLock) {
+            if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
+                releasePlayer = true;
+                mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED;
+            }
+        }
         try {
-            synchronized (mLock) {
-                if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
-                    getService().releasePlayer(mPlayerIId);
-                    mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED;
-                }
+            if (releasePlayer) {
+                getService().releasePlayer(mPlayerIId);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Error talking to audio service, the player will still be tracked", e);
diff --git a/android/media/VolumeShaper.java b/android/media/VolumeShaper.java
index 3068706..b654214 100644
--- a/android/media/VolumeShaper.java
+++ b/android/media/VolumeShaper.java
@@ -18,6 +18,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -843,6 +844,7 @@
              * @return the same {@code Builder} instance.
              * @throws IllegalArgumentException if flag is not recognized.
              */
+            @TestApi
             public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) {
                 if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) {
                     throw new IllegalArgumentException("invalid bits in flag: " + optionFlags);
diff --git a/android/media/audiofx/AudioEffect.java b/android/media/audiofx/AudioEffect.java
index 21d6873..24c595f 100644
--- a/android/media/audiofx/AudioEffect.java
+++ b/android/media/audiofx/AudioEffect.java
@@ -18,6 +18,7 @@
 
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.TestApi;
 import android.app.ActivityThread;
 import android.os.Handler;
 import android.os.Looper;
@@ -133,9 +134,10 @@
               .fromString("7261676f-6d75-7369-6364-28e2fd3ac39e");
 
     /**
-     * Null effect UUID. Used when the UUID for effect type of
+     * Null effect UUID. See {@link AudioEffect(UUID, UUID, int, int)} for use.
      * @hide
      */
+    @TestApi
     public static final UUID EFFECT_TYPE_NULL = UUID
             .fromString("ec7178ec-e5e1-4432-a3f4-4657e6795210");
 
@@ -492,6 +494,7 @@
      * @return true if the device implements the specified effect type, false otherwise.
      * @hide
      */
+    @TestApi
     public static boolean isEffectTypeAvailable(UUID type) {
         AudioEffect.Descriptor[] desc = AudioEffect.queryEffects();
         if (desc == null) {
@@ -544,6 +547,7 @@
      * @throws IllegalStateException
      * @hide
      */
+    @TestApi
     public int setParameter(byte[] param, byte[] value)
             throws IllegalStateException {
         checkState("setParameter()");
@@ -556,6 +560,7 @@
      * @see #setParameter(byte[], byte[])
      * @hide
      */
+    @TestApi
     public int setParameter(int param, int value) throws IllegalStateException {
         byte[] p = intToByteArray(param);
         byte[] v = intToByteArray(value);
@@ -569,6 +574,7 @@
      * @see #setParameter(byte[], byte[])
      * @hide
      */
+    @TestApi
     public int setParameter(int param, short value)
             throws IllegalStateException {
         byte[] p = intToByteArray(param);
@@ -583,6 +589,7 @@
      * @see #setParameter(byte[], byte[])
      * @hide
      */
+    @TestApi
     public int setParameter(int param, byte[] value)
             throws IllegalStateException {
         byte[] p = intToByteArray(param);
@@ -596,6 +603,7 @@
      * @see #setParameter(byte[], byte[])
      * @hide
      */
+    @TestApi
     public int setParameter(int[] param, int[] value)
             throws IllegalStateException {
         if (param.length > 2 || value.length > 2) {
@@ -647,6 +655,7 @@
      * @see #setParameter(byte[], byte[])
      * @hide
      */
+    @TestApi
     public int setParameter(int[] param, byte[] value)
             throws IllegalStateException {
         if (param.length > 2) {
@@ -675,6 +684,7 @@
      * @throws IllegalStateException
      * @hide
      */
+    @TestApi
     public int getParameter(byte[] param, byte[] value)
             throws IllegalStateException {
         checkState("getParameter()");
@@ -688,6 +698,7 @@
      * @see #getParameter(byte[], byte[])
      * @hide
      */
+    @TestApi
     public int getParameter(int param, byte[] value)
             throws IllegalStateException {
         byte[] p = intToByteArray(param);
@@ -703,6 +714,7 @@
      * In case of success, returns the number of meaningful integers in value array.
      * @hide
      */
+    @TestApi
     public int getParameter(int param, int[] value)
             throws IllegalStateException {
         if (value.length > 2) {
@@ -734,6 +746,7 @@
      * In case of success, returns the number of meaningful short integers in value array.
      * @hide
      */
+    @TestApi
     public int getParameter(int param, short[] value)
             throws IllegalStateException {
         if (value.length > 2) {
@@ -799,6 +812,7 @@
      * In case of success, returns the number of meaningful short integers in value array.
      * @hide
      */
+    @TestApi
     public int getParameter(int[] param, short[] value)
             throws IllegalStateException {
         if (param.length > 2 || value.length > 2) {
@@ -938,6 +952,7 @@
      * @param listener
      * @hide
      */
+    @TestApi
     public void setParameterListener(OnParameterChangeListener listener) {
         synchronized (mListenerLock) {
             mParameterChangeListener = listener;
@@ -999,6 +1014,7 @@
      * when a parameter is changed in the effect engine by the controlling application.
      * @hide
      */
+    @TestApi
     public interface OnParameterChangeListener {
         /**
          * Called on the listener to notify it that a parameter value has changed.
@@ -1291,6 +1307,7 @@
     /**
      * @hide
      */
+    @TestApi
     public static boolean isError(int status) {
         return (status < 0);
     }
@@ -1298,6 +1315,7 @@
     /**
      * @hide
      */
+    @TestApi
     public static int byteArrayToInt(byte[] valueBuf) {
         return byteArrayToInt(valueBuf, 0);
 
@@ -1316,6 +1334,7 @@
     /**
      * @hide
      */
+    @TestApi
     public static byte[] intToByteArray(int value) {
         ByteBuffer converter = ByteBuffer.allocate(4);
         converter.order(ByteOrder.nativeOrder());
@@ -1326,6 +1345,7 @@
     /**
      * @hide
      */
+    @TestApi
     public static short byteArrayToShort(byte[] valueBuf) {
         return byteArrayToShort(valueBuf, 0);
     }
@@ -1343,6 +1363,7 @@
     /**
      * @hide
      */
+    @TestApi
     public static byte[] shortToByteArray(short value) {
         ByteBuffer converter = ByteBuffer.allocate(2);
         converter.order(ByteOrder.nativeOrder());
diff --git a/android/media/session/MediaController.java b/android/media/session/MediaController.java
index f16804c..84f85e7 100644
--- a/android/media/session/MediaController.java
+++ b/android/media/session/MediaController.java
@@ -531,7 +531,7 @@
          *
          * @param state The new playback state of the session
          */
-        public void onPlaybackStateChanged(@NonNull PlaybackState state) {
+        public void onPlaybackStateChanged(@Nullable PlaybackState state) {
         }
 
         /**
diff --git a/android/media/session/MediaSessionManager.java b/android/media/session/MediaSessionManager.java
index 519af1b..fbc1438 100644
--- a/android/media/session/MediaSessionManager.java
+++ b/android/media/session/MediaSessionManager.java
@@ -47,6 +47,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 /**
@@ -342,12 +343,16 @@
     }
 
     /**
-     * Returns whether the app is trusted.
+     * Checks whether the remote user is a trusted app.
      * <p>
      * An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL
      * permission or has an enabled notification listener.
      *
-     * @param userInfo The remote user info
+     * @param userInfo The remote user info from either
+     *            {@link MediaSession#getCurrentControllerInfo()} or
+     *            {@link MediaBrowserService#getCurrentBrowserInfo()}.
+     * @return {@code true} if the remote user is trusted and its package name matches with the UID.
+     *            {@code false} otherwise.
      */
     public boolean isTrustedForMediaControl(RemoteUserInfo userInfo) {
         if (userInfo.getPackageName() == null) {
@@ -814,6 +819,11 @@
                     && mPid == otherUserInfo.mPid
                     && mUid == otherUserInfo.mUid;
         }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPackageName, mPid, mUid);
+        }
     }
 
     private static final class SessionsChangedWrapper {
diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java
index 1525508..a61ea50 100644
--- a/android/net/IpSecManager.java
+++ b/android/net/IpSecManager.java
@@ -20,7 +20,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.content.Context;
@@ -140,6 +139,7 @@
         }
     }
 
+    private final Context mContext;
     private final IIpSecService mService;
 
     /**
@@ -336,6 +336,9 @@
      */
     public void applyTransportModeTransform(@NonNull Socket socket,
             @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        // Ensure creation of FD. See b/77548890 for more details.
+        socket.getSoLinger();
+
         applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
     }
 
@@ -440,6 +443,9 @@
      * @throws IOException indicating that the transform could not be removed from the socket
      */
     public void removeTransportModeTransforms(@NonNull Socket socket) throws IOException {
+        // Ensure creation of FD. See b/77548890 for more details.
+        socket.getSoLinger();
+
         removeTransportModeTransforms(socket.getFileDescriptor$());
     }
 
@@ -659,8 +665,8 @@
      * to create Network objects which are accessible to the Android system.
      * @hide
      */
-    @SystemApi
     public static final class IpSecTunnelInterface implements AutoCloseable {
+        private final String mOpPackageName;
         private final IIpSecService mService;
         private final InetAddress mRemoteAddress;
         private final InetAddress mLocalAddress;
@@ -682,13 +688,14 @@
          * tunneled traffic.
          *
          * @param address the local address for traffic inside the tunnel
+         * @param prefixLen length of the InetAddress prefix
          * @hide
          */
-        @SystemApi
         @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
-        public void addAddress(@NonNull LinkAddress address) throws IOException {
+        public void addAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
             try {
-                mService.addAddressToTunnelInterface(mResourceId, address);
+                mService.addAddressToTunnelInterface(
+                        mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -700,22 +707,24 @@
          * <p>Remove an address which was previously added to the IpSecTunnelInterface
          *
          * @param address to be removed
+         * @param prefixLen length of the InetAddress prefix
          * @hide
          */
-        @SystemApi
         @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
-        public void removeAddress(@NonNull LinkAddress address) throws IOException {
+        public void removeAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
             try {
-                mService.removeAddressFromTunnelInterface(mResourceId, address);
+                mService.removeAddressFromTunnelInterface(
+                        mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
         }
 
-        private IpSecTunnelInterface(@NonNull IIpSecService service,
+        private IpSecTunnelInterface(@NonNull Context ctx, @NonNull IIpSecService service,
                 @NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress,
                 @NonNull Network underlyingNetwork)
                 throws ResourceUnavailableException, IOException {
+            mOpPackageName = ctx.getOpPackageName();
             mService = service;
             mLocalAddress = localAddress;
             mRemoteAddress = remoteAddress;
@@ -727,7 +736,8 @@
                                 localAddress.getHostAddress(),
                                 remoteAddress.getHostAddress(),
                                 underlyingNetwork,
-                                new Binder());
+                                new Binder(),
+                                mOpPackageName);
                 switch (result.status) {
                     case Status.OK:
                         break;
@@ -756,7 +766,7 @@
         @Override
         public void close() {
             try {
-                mService.deleteTunnelInterface(mResourceId);
+                mService.deleteTunnelInterface(mResourceId, mOpPackageName);
                 mResourceId = INVALID_RESOURCE_ID;
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
@@ -795,13 +805,13 @@
      * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
      * @hide
      */
-    @SystemApi
     @NonNull
     @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
     public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress,
             @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork)
             throws ResourceUnavailableException, IOException {
-        return new IpSecTunnelInterface(mService, localAddress, remoteAddress, underlyingNetwork);
+        return new IpSecTunnelInterface(
+                mContext, mService, localAddress, remoteAddress, underlyingNetwork);
     }
 
     /**
@@ -821,13 +831,13 @@
      *         layer failure.
      * @hide
      */
-    @SystemApi
     @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
     public void applyTunnelModeTransform(@NonNull IpSecTunnelInterface tunnel,
             @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
         try {
             mService.applyTunnelModeTransform(
-                    tunnel.getResourceId(), direction, transform.getResourceId());
+                    tunnel.getResourceId(), direction,
+                    transform.getResourceId(), mContext.getOpPackageName());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -839,7 +849,8 @@
      * @param context the application context for this manager
      * @hide
      */
-    public IpSecManager(IIpSecService service) {
+    public IpSecManager(Context ctx, IIpSecService service) {
+        mContext = ctx;
         mService = checkNotNull(service, "missing service");
     }
 }
diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java
index 099fe02..62f7996 100644
--- a/android/net/IpSecTransform.java
+++ b/android/net/IpSecTransform.java
@@ -22,7 +22,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
 import android.content.Context;
 import android.os.Binder;
 import android.os.Handler;
@@ -130,7 +129,8 @@
         synchronized (this) {
             try {
                 IIpSecService svc = getIpSecService();
-                IpSecTransformResponse result = svc.createTransform(mConfig, new Binder());
+                IpSecTransformResponse result = svc.createTransform(
+                        mConfig, new Binder(), mContext.getOpPackageName());
                 int status = result.status;
                 checkResultStatus(status);
                 mResourceId = result.resourceId;
@@ -249,7 +249,6 @@
      *
      * @hide
      */
-    @SystemApi
     public static class NattKeepaliveCallback {
         /** The specified {@code Network} is not connected. */
         public static final int ERROR_INVALID_NETWORK = 1;
@@ -280,7 +279,6 @@
      *
      * @hide
      */
-    @SystemApi
     @RequiresPermission(anyOf = {
             android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
             android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
@@ -323,7 +321,6 @@
      *
      * @hide
      */
-    @SystemApi
     @RequiresPermission(anyOf = {
             android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
             android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
@@ -476,7 +473,6 @@
          * @throws IOException indicating other errors
          * @hide
          */
-        @SystemApi
         @NonNull
         @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
         public IpSecTransform buildTunnelModeTransform(
diff --git a/android/net/NetworkCapabilities.java b/android/net/NetworkCapabilities.java
index 374b3ab..a8e8179 100644
--- a/android/net/NetworkCapabilities.java
+++ b/android/net/NetworkCapabilities.java
@@ -254,9 +254,8 @@
     /**
      * Indicates that this network is not congested.
      * <p>
-     * When a network is congested, the device should defer network traffic that
-     * can be done at a later time without breaking developer contracts.
-     * @hide
+     * When a network is congested, applications should defer network traffic
+     * that can be done at a later time, such as uploading analytics.
      */
     public static final int NET_CAPABILITY_NOT_CONGESTED = 20;
 
@@ -318,7 +317,7 @@
 
     /**
      * Capabilities that suggest that a network is restricted.
-     * {@see #maybeMarkCapabilitiesRestricted}.
+     * {@see #maybeMarkCapabilitiesRestricted}, {@see #FORCE_RESTRICTED_CAPABILITIES}
      */
     @VisibleForTesting
     /* package */ static final long RESTRICTED_CAPABILITIES =
@@ -329,7 +328,13 @@
             (1 << NET_CAPABILITY_IA) |
             (1 << NET_CAPABILITY_IMS) |
             (1 << NET_CAPABILITY_RCS) |
-            (1 << NET_CAPABILITY_XCAP) |
+            (1 << NET_CAPABILITY_XCAP);
+
+    /**
+     * Capabilities that force network to be restricted.
+     * {@see #maybeMarkCapabilitiesRestricted}.
+     */
+    private static final long FORCE_RESTRICTED_CAPABILITIES =
             (1 << NET_CAPABILITY_OEM_PAID);
 
     /**
@@ -533,16 +538,21 @@
      * @hide
      */
     public void maybeMarkCapabilitiesRestricted() {
+        // Check if we have any capability that forces the network to be restricted.
+        final boolean forceRestrictedCapability =
+                (mNetworkCapabilities & FORCE_RESTRICTED_CAPABILITIES) != 0;
+
         // Verify there aren't any unrestricted capabilities.  If there are we say
-        // the whole thing is unrestricted.
+        // the whole thing is unrestricted unless it is forced to be restricted.
         final boolean hasUnrestrictedCapabilities =
-                ((mNetworkCapabilities & UNRESTRICTED_CAPABILITIES) != 0);
+                (mNetworkCapabilities & UNRESTRICTED_CAPABILITIES) != 0;
 
         // Must have at least some restricted capabilities.
         final boolean hasRestrictedCapabilities =
-                ((mNetworkCapabilities & RESTRICTED_CAPABILITIES) != 0);
+                (mNetworkCapabilities & RESTRICTED_CAPABILITIES) != 0;
 
-        if (hasRestrictedCapabilities && !hasUnrestrictedCapabilities) {
+        if (forceRestrictedCapability
+                || (hasRestrictedCapabilities && !hasUnrestrictedCapabilities)) {
             removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
         }
     }
diff --git a/android/net/NetworkPolicy.java b/android/net/NetworkPolicy.java
index 1a28732..e84c85e 100644
--- a/android/net/NetworkPolicy.java
+++ b/android/net/NetworkPolicy.java
@@ -19,7 +19,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.BackupUtils;
-import android.util.Pair;
+import android.util.Range;
 import android.util.RecurrenceRule;
 
 import com.android.internal.util.Preconditions;
@@ -136,7 +136,7 @@
         return 0;
     }
 
-    public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+    public Iterator<Range<ZonedDateTime>> cycleIterator() {
         return cycleRule.cycleIterator();
     }
 
diff --git a/android/net/NetworkPolicyManager.java b/android/net/NetworkPolicyManager.java
index bf6b7e0..6546c39 100644
--- a/android/net/NetworkPolicyManager.java
+++ b/android/net/NetworkPolicyManager.java
@@ -31,6 +31,7 @@
 import android.os.UserHandle;
 import android.util.DebugUtils;
 import android.util.Pair;
+import android.util.Range;
 
 import com.google.android.collect.Sets;
 
@@ -258,8 +259,21 @@
     }
 
     /** {@hide} */
+    @Deprecated
     public static Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator(NetworkPolicy policy) {
-        return policy.cycleIterator();
+        final Iterator<Range<ZonedDateTime>> it = policy.cycleIterator();
+        return new Iterator<Pair<ZonedDateTime, ZonedDateTime>>() {
+            @Override
+            public boolean hasNext() {
+                return it.hasNext();
+            }
+
+            @Override
+            public Pair<ZonedDateTime, ZonedDateTime> next() {
+                final Range<ZonedDateTime> r = it.next();
+                return Pair.create(r.getLower(), r.getUpper());
+            }
+        };
     }
 
     /**
diff --git a/android/net/NetworkRequest.java b/android/net/NetworkRequest.java
index 3d9d6e2..bd4a27c 100644
--- a/android/net/NetworkRequest.java
+++ b/android/net/NetworkRequest.java
@@ -168,9 +168,6 @@
          * the requested network's required capabilities.  Note that when searching
          * for a network to satisfy a request, all capabilities requested must be
          * satisfied.
-         * <p>
-         * If the given capability was previously added to the list of unwanted capabilities
-         * then the capability will also be removed from the list of unwanted capabilities.
          *
          * @param capability The capability to add.
          * @return The builder to facilitate chaining
@@ -182,8 +179,7 @@
         }
 
         /**
-         * Removes (if found) the given capability from this builder instance from both required
-         * and unwanted capabilities lists.
+         * Removes (if found) the given capability from this builder instance.
          *
          * @param capability The capability to remove.
          * @return The builder to facilitate chaining.
@@ -231,6 +227,8 @@
          *
          * @param capability The capability to add to unwanted capability list.
          * @return The builder to facilitate chaining.
+         *
+         * @removed
          */
         public Builder addUnwantedCapability(@NetworkCapabilities.NetCapability int capability) {
             mNetworkCapabilities.addUnwantedCapability(capability);
@@ -436,6 +434,15 @@
     }
 
     /**
+     * @see Builder#addUnwantedCapability(int)
+     *
+     * @removed
+     */
+    public boolean hasUnwantedCapability(@NetCapability int capability) {
+        return networkCapabilities.hasUnwantedCapability(capability);
+    }
+
+    /**
      * @see Builder#addTransportType(int)
      */
     public boolean hasTransport(@Transport int transportType) {
diff --git a/android/net/NetworkState.java b/android/net/NetworkState.java
index b00cb48..321f971 100644
--- a/android/net/NetworkState.java
+++ b/android/net/NetworkState.java
@@ -26,6 +26,8 @@
  * @hide
  */
 public class NetworkState implements Parcelable {
+    private static final boolean SANITY_CHECK_ROAMING = false;
+
     public static final NetworkState EMPTY = new NetworkState(null, null, null, null, null, null);
 
     public final NetworkInfo networkInfo;
@@ -47,7 +49,7 @@
 
         // This object is an atomic view of a network, so the various components
         // should always agree on roaming state.
-        if (networkInfo != null && networkCapabilities != null) {
+        if (SANITY_CHECK_ROAMING && networkInfo != null && networkCapabilities != null) {
             if (networkInfo.isRoaming() == networkCapabilities
                     .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)) {
                 Slog.wtf("NetworkState", "Roaming state disagreement between " + networkInfo
diff --git a/android/net/apf/ApfFilter.java b/android/net/apf/ApfFilter.java
index d190432..d5ff2dd 100644
--- a/android/net/apf/ApfFilter.java
+++ b/android/net/apf/ApfFilter.java
@@ -16,21 +16,21 @@
 
 package android.net.apf;
 
+import static android.net.util.NetworkConstants.*;
 import static android.system.OsConstants.*;
-
 import static com.android.internal.util.BitUtils.bytesToBEInt;
 import static com.android.internal.util.BitUtils.getUint16;
 import static com.android.internal.util.BitUtils.getUint32;
 import static com.android.internal.util.BitUtils.getUint8;
-import static com.android.internal.util.BitUtils.uint16;
 import static com.android.internal.util.BitUtils.uint32;
-import static com.android.internal.util.BitUtils.uint8;
 
-import android.os.SystemClock;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.NetworkUtils;
-import android.net.apf.ApfGenerator;
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.apf.ApfGenerator.Register;
 import android.net.ip.IpClient;
@@ -39,31 +39,29 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.RaEvent;
 import android.net.util.InterfaceParams;
+import android.os.PowerManager;
+import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.PacketSocketAddress;
 import android.text.format.DateUtils;
 import android.util.Log;
 import android.util.Pair;
-
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
-
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.lang.Thread;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.SocketException;
 import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
 import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
-
 import libcore.io.IoBridge;
 
 /**
@@ -215,10 +213,6 @@
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
 
     private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
-    private static final int ICMP6_ROUTER_SOLICITATION = 133;
-    private static final int ICMP6_ROUTER_ADVERTISEMENT = 134;
-    private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
-    private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
 
     // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
     private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
@@ -258,9 +252,26 @@
     private long mUniqueCounter;
     @GuardedBy("this")
     private boolean mMulticastFilter;
+    @GuardedBy("this")
+    private boolean mInDozeMode;
     private final boolean mDrop802_3Frames;
     private final int[] mEthTypeBlackList;
 
+    // Detects doze mode state transitions.
+    private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) {
+                PowerManager powerManager =
+                        (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+                final boolean deviceIdle = powerManager.isDeviceIdleMode();
+                setDozeMode(deviceIdle);
+            }
+        }
+    };
+    private final Context mContext;
+
     // Our IPv4 address, if we have just one, otherwise null.
     @GuardedBy("this")
     private byte[] mIPv4Address;
@@ -269,13 +280,14 @@
     private int mIPv4PrefixLength;
 
     @VisibleForTesting
-    ApfFilter(ApfConfiguration config, InterfaceParams ifParams,
+    ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
             IpClient.Callback ipClientCallback, IpConnectivityLog log) {
         mApfCapabilities = config.apfCapabilities;
         mIpClientCallback = ipClientCallback;
         mInterfaceParams = ifParams;
         mMulticastFilter = config.multicastFilter;
         mDrop802_3Frames = config.ieee802_3Filter;
+        mContext = context;
 
         // Now fill the black list from the passed array
         mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
@@ -284,6 +296,10 @@
 
         // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
         maybeStartFilter();
+
+        // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
+        mContext.registerReceiver(mDeviceIdleReceiver,
+                new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
     }
 
     private void log(String s) {
@@ -522,7 +538,7 @@
             // to our packet socket. b/29586253
             if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 ||
                     getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 ||
-                    getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMP6_ROUTER_ADVERTISEMENT) {
+                    getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) {
                 throw new InvalidRaException("Not an ICMP6 router advertisement");
             }
 
@@ -889,10 +905,11 @@
     private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
         // Here's a basic summary of what the IPv6 filter program does:
         //
-        // if it's not ICMPv6:
-        //   if it's multicast and we're dropping multicast:
-        //     drop
-        //   pass
+        // if we're dropping multicast
+        //   if it's not IPCMv6 or it's ICMPv6 but we're in doze mode:
+        //     if it's multicast:
+        //       drop
+        //     pass
         // if it's ICMPv6 RS to any:
         //   drop
         // if it's ICMPv6 NA to ff02::1:
@@ -902,28 +919,44 @@
 
         // Drop multicast if the multicast filter is enabled.
         if (mMulticastFilter) {
-            // Don't touch ICMPv6 multicast here, we deal with it in more detail later.
-            String skipIpv6MulticastFilterLabel = "skipIPv6MulticastFilter";
-            gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIpv6MulticastFilterLabel);
+            final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter";
+            final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast";
 
-            // Drop all other packets sent to ff00::/8.
+            // While in doze mode, drop ICMPv6 multicast pings, let the others pass.
+            // While awake, let all ICMPv6 multicasts through.
+            if (mInDozeMode) {
+                // Not ICMPv6? -> Proceed to multicast filtering
+                gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel);
+
+                // ICMPv6 but not ECHO? -> Skip the multicast filter.
+                // (ICMPv6 ECHO requests will go through the multicast filter below).
+                gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+                gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel);
+            } else {
+                gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel);
+            }
+
+            // Drop all other packets sent to ff00::/8 (multicast prefix).
+            gen.defineLabel(dropAllIPv6MulticastsLabel);
             gen.addLoad8(Register.R0, IPV6_DEST_ADDR_OFFSET);
             gen.addJumpIfR0Equals(0xff, gen.DROP_LABEL);
-            // Not multicast and not ICMPv6. Pass.
+            // Not multicast. Pass.
             gen.addJump(gen.PASS_LABEL);
-            gen.defineLabel(skipIpv6MulticastFilterLabel);
+            gen.defineLabel(skipIPv6MulticastFilterLabel);
         } else {
             // If not ICMPv6, pass.
             gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, gen.PASS_LABEL);
         }
 
+        // If we got this far, the packet is ICMPv6.  Drop some specific types.
+
         // Add unsolicited multicast neighbor announcements filter
         String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
         gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
         // Drop all router solicitations (b/32833400)
-        gen.addJumpIfR0Equals(ICMP6_ROUTER_SOLICITATION, gen.DROP_LABEL);
+        gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, gen.DROP_LABEL);
         // If not neighbor announcements, skip filter.
-        gen.addJumpIfR0NotEquals(ICMP6_NEIGHBOR_ANNOUNCEMENT, skipUnsolicitedMulticastNALabel);
+        gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
         // If to ff02::1, drop.
         // TODO: Drop only if they don't contain the address of on-link neighbours.
         gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
@@ -1168,9 +1201,9 @@
      * Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet
      * filtering using APF programs.
      */
-    public static ApfFilter maybeCreate(ApfConfiguration config,
+    public static ApfFilter maybeCreate(Context context, ApfConfiguration config,
             InterfaceParams ifParams, IpClient.Callback ipClientCallback) {
-        if (config == null || ifParams == null) return null;
+        if (context == null || config == null || ifParams == null) return null;
         ApfCapabilities apfCapabilities =  config.apfCapabilities;
         if (apfCapabilities == null) return null;
         if (apfCapabilities.apfVersionSupported == 0) return null;
@@ -1187,7 +1220,8 @@
             Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
             return null;
         }
-        return new ApfFilter(config, ifParams, ipClientCallback, new IpConnectivityLog());
+
+        return new ApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog());
     }
 
     public synchronized void shutdown() {
@@ -1197,12 +1231,11 @@
             mReceiveThread = null;
         }
         mRas.clear();
+        mContext.unregisterReceiver(mDeviceIdleReceiver);
     }
 
     public synchronized void setMulticastFilter(boolean isEnabled) {
-        if (mMulticastFilter == isEnabled) {
-            return;
-        }
+        if (mMulticastFilter == isEnabled) return;
         mMulticastFilter = isEnabled;
         if (!isEnabled) {
             mNumProgramUpdatesAllowingMulticast++;
@@ -1210,6 +1243,13 @@
         installNewProgramLocked();
     }
 
+    @VisibleForTesting
+    public synchronized void setDozeMode(boolean isEnabled) {
+        if (mInDozeMode == isEnabled) return;
+        mInDozeMode = isEnabled;
+        installNewProgramLocked();
+    }
+
     /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */
     private static LinkAddress findIPv4LinkAddress(LinkProperties lp) {
         LinkAddress ipv4Address = null;
diff --git a/android/net/dns/ResolvUtil.java b/android/net/dns/ResolvUtil.java
new file mode 100644
index 0000000..97d20f4
--- /dev/null
+++ b/android/net/dns/ResolvUtil.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 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 android.net.dns;
+
+import android.net.Network;
+import android.net.NetworkUtils;
+import android.system.GaiException;
+import android.system.OsConstants;
+import android.system.StructAddrinfo;
+
+import libcore.io.Libcore;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+
+/**
+ * DNS resolution utility class.
+ *
+ * @hide
+ */
+public class ResolvUtil {
+    // Non-portable DNS resolution flag.
+    private static final long NETID_USE_LOCAL_NAMESERVERS = 0x80000000L;
+
+    private ResolvUtil() {}
+
+    public static InetAddress[] blockingResolveAllLocally(Network network, String name)
+            throws UnknownHostException {
+        final StructAddrinfo hints = new StructAddrinfo();
+        // Unnecessary, but expressly no AI_ADDRCONFIG.
+        hints.ai_flags = 0;
+        // Fetch all IP addresses at once to minimize re-resolution.
+        hints.ai_family = OsConstants.AF_UNSPEC;
+        hints.ai_socktype = OsConstants.SOCK_DGRAM;
+
+        final Network networkForResolv = getNetworkWithUseLocalNameserversFlag(network);
+
+        try {
+            return Libcore.os.android_getaddrinfo(name, hints, (int) networkForResolv.netId);
+        } catch (GaiException gai) {
+            gai.rethrowAsUnknownHostException(name + ": TLS-bypass resolution failed");
+            return null;  // keep compiler quiet
+        }
+    }
+
+    public static Network getNetworkWithUseLocalNameserversFlag(Network network) {
+        final long netidForResolv = NETID_USE_LOCAL_NAMESERVERS | (long) network.netId;
+        return new Network((int) netidForResolv);
+    }
+}
diff --git a/android/net/http/X509TrustManagerExtensions.java b/android/net/http/X509TrustManagerExtensions.java
index e0fa63a..f9b6dfc 100644
--- a/android/net/http/X509TrustManagerExtensions.java
+++ b/android/net/http/X509TrustManagerExtensions.java
@@ -21,7 +21,6 @@
 
 import com.android.org.conscrypt.TrustManagerImpl;
 
-import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.security.cert.CertificateException;
@@ -133,8 +132,6 @@
     /**
      * Returns {@code true} if the TrustManager uses the same trust configuration for the provided
      * hostnames.
-     *
-     * @hide
      */
     @SystemApi
     public boolean isSameTrustConfiguration(String hostname1, String hostname2) {
diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java
index 9863370..87249df 100644
--- a/android/net/ip/IpClient.java
+++ b/android/net/ip/IpClient.java
@@ -40,6 +40,7 @@
 import android.net.util.NetdService;
 import android.net.util.NetworkConstants;
 import android.net.util.SharedLog;
+import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
 import android.os.Message;
 import android.os.RemoteException;
@@ -150,6 +151,28 @@
         public void setNeighborDiscoveryOffload(boolean enable) {}
     }
 
+    public static class WaitForProvisioningCallback extends Callback {
+        private final ConditionVariable mCV = new ConditionVariable();
+        private LinkProperties mCallbackLinkProperties;
+
+        public LinkProperties waitForProvisioning() {
+            mCV.block();
+            return mCallbackLinkProperties;
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties newLp) {
+            mCallbackLinkProperties = newLp;
+            mCV.open();
+        }
+
+        @Override
+        public void onProvisioningFailure(LinkProperties newLp) {
+            mCallbackLinkProperties = null;
+            mCV.open();
+        }
+    }
+
     // Use a wrapper class to log in order to ensure complete and detailed
     // logging. This method is lighter weight than annotations/reflection
     // and has the following benefits:
@@ -281,6 +304,11 @@
                 return this;
             }
 
+            public Builder withoutMultinetworkPolicyTracker() {
+                mConfig.mUsingMultinetworkPolicyTracker = false;
+                return this;
+            }
+
             public Builder withoutIpReachabilityMonitor() {
                 mConfig.mUsingIpReachabilityMonitor = false;
                 return this;
@@ -343,6 +371,7 @@
 
         /* package */ boolean mEnableIPv4 = true;
         /* package */ boolean mEnableIPv6 = true;
+        /* package */ boolean mUsingMultinetworkPolicyTracker = true;
         /* package */ boolean mUsingIpReachabilityMonitor = true;
         /* package */ int mRequestedPreDhcpActionMs;
         /* package */ InitialConfiguration mInitialConfig;
@@ -374,6 +403,7 @@
             return new StringJoiner(", ", getClass().getSimpleName() + "{", "}")
                     .add("mEnableIPv4: " + mEnableIPv4)
                     .add("mEnableIPv6: " + mEnableIPv6)
+                    .add("mUsingMultinetworkPolicyTracker: " + mUsingMultinetworkPolicyTracker)
                     .add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor)
                     .add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs)
                     .add("mInitialConfig: " + mInitialConfig)
@@ -559,7 +589,6 @@
     private final NetlinkTracker mNetlinkTracker;
     private final WakeupMessage mProvisioningTimeoutAlarm;
     private final WakeupMessage mDhcpActionTimeoutAlarm;
-    private final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
     private final SharedLog mLog;
     private final LocalLog mConnectivityPacketLog;
     private final MessageHandlingLogger mMsgStateLogger;
@@ -573,6 +602,7 @@
      */
     private LinkProperties mLinkProperties;
     private ProvisioningConfiguration mConfiguration;
+    private MultinetworkPolicyTracker mMultinetworkPolicyTracker;
     private IpReachabilityMonitor mIpReachabilityMonitor;
     private DhcpClient mDhcpClient;
     private DhcpResults mDhcpResults;
@@ -685,9 +715,6 @@
         mLinkProperties = new LinkProperties();
         mLinkProperties.setInterfaceName(mInterfaceName);
 
-        mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(mContext, getHandler(),
-                () -> { mLog.log("OBSERVED AvoidBadWifi changed"); });
-
         mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                 mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
         mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
@@ -719,8 +746,6 @@
         } catch (RemoteException e) {
             logError("Couldn't register NetlinkTracker: %s", e);
         }
-
-        mMultinetworkPolicyTracker.start();
     }
 
     private void stopStateMachineUpdaters() {
@@ -729,8 +754,6 @@
         } catch (RemoteException e) {
             logError("Couldn't unregister NetlinkTracker: %s", e);
         }
-
-        mMultinetworkPolicyTracker.shutdown();
     }
 
     @Override
@@ -1028,7 +1051,8 @@
         // Note that we can still be disconnected by IpReachabilityMonitor
         // if the IPv6 default gateway (but not the IPv6 DNS servers; see
         // accompanying code in IpReachabilityMonitor) is unreachable.
-        final boolean ignoreIPv6ProvisioningLoss = !mMultinetworkPolicyTracker.getAvoidBadWifi();
+        final boolean ignoreIPv6ProvisioningLoss = (mMultinetworkPolicyTracker != null)
+                && !mMultinetworkPolicyTracker.getAvoidBadWifi();
 
         // Additionally:
         //
@@ -1490,7 +1514,7 @@
                     mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
             apfConfig.ethTypeBlackList =
                     mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
-            mApfFilter = ApfFilter.maybeCreate(apfConfig, mInterfaceParams, mCallback);
+            mApfFilter = ApfFilter.maybeCreate(mContext, apfConfig, mInterfaceParams, mCallback);
             // TODO: investigate the effects of any multicast filtering racing/interfering with the
             // rest of this IP configuration startup.
             if (mApfFilter == null) {
@@ -1520,6 +1544,13 @@
                 return;
             }
 
+            if (mConfiguration.mUsingMultinetworkPolicyTracker) {
+                mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(
+                        mContext, getHandler(),
+                        () -> { mLog.log("OBSERVED AvoidBadWifi changed"); });
+                mMultinetworkPolicyTracker.start();
+            }
+
             if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
                 doImmediateProvisioningFailure(
                         IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
@@ -1537,6 +1568,11 @@
                 mIpReachabilityMonitor = null;
             }
 
+            if (mMultinetworkPolicyTracker != null) {
+                mMultinetworkPolicyTracker.shutdown();
+                mMultinetworkPolicyTracker = null;
+            }
+
             if (mDhcpClient != null) {
                 mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP);
                 mDhcpClient.doQuit();
diff --git a/android/net/ip/IpManager.java b/android/net/ip/IpManager.java
index 508a43d..2eb36a2 100644
--- a/android/net/ip/IpManager.java
+++ b/android/net/ip/IpManager.java
@@ -114,35 +114,6 @@
     public static class Callback extends IpClient.Callback {
     }
 
-    public static class WaitForProvisioningCallback extends Callback {
-        private LinkProperties mCallbackLinkProperties;
-
-        public LinkProperties waitForProvisioning() {
-            synchronized (this) {
-                try {
-                    wait();
-                } catch (InterruptedException e) {}
-                return mCallbackLinkProperties;
-            }
-        }
-
-        @Override
-        public void onProvisioningSuccess(LinkProperties newLp) {
-            synchronized (this) {
-                mCallbackLinkProperties = newLp;
-                notify();
-            }
-        }
-
-        @Override
-        public void onProvisioningFailure(LinkProperties newLp) {
-            synchronized (this) {
-                mCallbackLinkProperties = null;
-                notify();
-            }
-        }
-    }
-
     public IpManager(Context context, String ifName, Callback callback) {
         super(context, ifName, callback);
     }
diff --git a/android/net/metrics/ApfStats.java b/android/net/metrics/ApfStats.java
index 3b0dc7e..76a781d 100644
--- a/android/net/metrics/ApfStats.java
+++ b/android/net/metrics/ApfStats.java
@@ -20,7 +20,7 @@
 import android.os.Parcelable;
 
 /**
- * An event logged for an interface with APF capabilities when its IpManager state machine exits.
+ * An event logged for an interface with APF capabilities when its IpClient state machine exits.
  * {@hide}
  */
 public final class ApfStats implements Parcelable {
diff --git a/android/net/util/NetworkConstants.java b/android/net/util/NetworkConstants.java
index 984c9f8..53fd01f 100644
--- a/android/net/util/NetworkConstants.java
+++ b/android/net/util/NetworkConstants.java
@@ -136,6 +136,8 @@
      *     - https://tools.ietf.org/html/rfc4861
      */
     public static final int ICMPV6_HEADER_MIN_LEN = 4;
+    public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+    public static final int ICMPV6_ECHO_REPLY_TYPE = 129;
     public static final int ICMPV6_ROUTER_SOLICITATION    = 133;
     public static final int ICMPV6_ROUTER_ADVERTISEMENT   = 134;
     public static final int ICMPV6_NEIGHBOR_SOLICITATION  = 135;
@@ -147,7 +149,6 @@
     public static final int ICMPV6_ND_OPTION_TLLA = 2;
     public static final int ICMPV6_ND_OPTION_MTU  = 5;
 
-    public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
 
     /**
      * UDP constants.
diff --git a/android/net/wifi/WifiConfiguration.java b/android/net/wifi/WifiConfiguration.java
index b77b1ad..f6c67c9 100644
--- a/android/net/wifi/WifiConfiguration.java
+++ b/android/net/wifi/WifiConfiguration.java
@@ -28,10 +28,12 @@
 import android.net.wifi.WifiInfo;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.BackupUtils;
 import android.util.Log;
+import android.util.TimeUtils;
 
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
@@ -611,37 +613,6 @@
 
     /**
      * @hide
-     * Last time the system tried to connect and failed.
-     */
-    public long lastConnectionFailure;
-
-    /**
-     * @hide
-     * Last time the system tried to roam and failed because of authentication failure or DHCP
-     * RENEW failure.
-     */
-    public long lastRoamingFailure;
-
-    /** @hide */
-    public static int ROAMING_FAILURE_IP_CONFIG = 1;
-    /** @hide */
-    public static int ROAMING_FAILURE_AUTH_FAILURE = 2;
-
-    /**
-     * @hide
-     * Initial amount of time this Wifi configuration gets blacklisted for network switching
-     * because of roaming failure
-     */
-    public long roamingFailureBlackListTimeMilli = 1000;
-
-    /**
-     * @hide
-     * Last roaming failure reason code
-     */
-    public int lastRoamingFailureReason;
-
-    /**
-     * @hide
      * Last time the system was disconnected to this configuration.
      */
     public long lastDisconnected;
@@ -1620,8 +1591,9 @@
         }
         if (mNetworkSelectionStatus.getConnectChoice() != null) {
             sbuf.append(" connect choice: ").append(mNetworkSelectionStatus.getConnectChoice());
-            sbuf.append(" connect choice set time: ").append(mNetworkSelectionStatus
-                    .getConnectChoiceTimestamp());
+            sbuf.append(" connect choice set time: ")
+                    .append(TimeUtils.logTimeOfDay(
+                            mNetworkSelectionStatus.getConnectChoiceTimestamp()));
         }
         sbuf.append(" hasEverConnected: ")
                 .append(mNetworkSelectionStatus.getHasEverConnected()).append("\n");
@@ -1724,7 +1696,7 @@
             sbuf.append(" networkSelectionBSSID="
                     + mNetworkSelectionStatus.getNetworkSelectionBSSID());
         }
-        long now_ms = System.currentTimeMillis();
+        long now_ms = SystemClock.elapsedRealtime();
         if (mNetworkSelectionStatus.getDisableTime() != NetworkSelectionStatus
                 .INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP) {
             sbuf.append('\n');
@@ -1746,35 +1718,9 @@
 
         if (this.lastConnected != 0) {
             sbuf.append('\n');
-            long diff = now_ms - this.lastConnected;
-            if (diff <= 0) {
-                sbuf.append("lastConnected since <incorrect>");
-            } else {
-                sbuf.append("lastConnected: ").append(Long.toString(diff / 1000)).append("sec ");
-            }
+            sbuf.append("lastConnected: ").append(TimeUtils.logTimeOfDay(this.lastConnected));
+            sbuf.append(" ");
         }
-        if (this.lastConnectionFailure != 0) {
-            sbuf.append('\n');
-            long diff = now_ms - this.lastConnectionFailure;
-            if (diff <= 0) {
-                sbuf.append("lastConnectionFailure since <incorrect> ");
-            } else {
-                sbuf.append("lastConnectionFailure: ").append(Long.toString(diff / 1000));
-                sbuf.append("sec ");
-            }
-        }
-        if (this.lastRoamingFailure != 0) {
-            sbuf.append('\n');
-            long diff = now_ms - this.lastRoamingFailure;
-            if (diff <= 0) {
-                sbuf.append("lastRoamingFailure since <incorrect> ");
-            } else {
-                sbuf.append("lastRoamingFailure: ").append(Long.toString(diff / 1000));
-                sbuf.append("sec ");
-            }
-        }
-        sbuf.append("roamingFailureBlackListTimeMilli: ").
-                append(Long.toString(this.roamingFailureBlackListTimeMilli));
         sbuf.append('\n');
         if (this.linkedConfigurations != null) {
             for (String key : this.linkedConfigurations.keySet()) {
@@ -2119,10 +2065,6 @@
 
             lastConnected = source.lastConnected;
             lastDisconnected = source.lastDisconnected;
-            lastConnectionFailure = source.lastConnectionFailure;
-            lastRoamingFailure = source.lastRoamingFailure;
-            lastRoamingFailureReason = source.lastRoamingFailureReason;
-            roamingFailureBlackListTimeMilli = source.roamingFailureBlackListTimeMilli;
             numScorerOverride = source.numScorerOverride;
             numScorerOverrideAndSwitchedNetwork = source.numScorerOverrideAndSwitchedNetwork;
             numAssociation = source.numAssociation;
@@ -2188,10 +2130,6 @@
         dest.writeInt(lastUpdateUid);
         dest.writeString(creatorName);
         dest.writeString(lastUpdateName);
-        dest.writeLong(lastConnectionFailure);
-        dest.writeLong(lastRoamingFailure);
-        dest.writeInt(lastRoamingFailureReason);
-        dest.writeLong(roamingFailureBlackListTimeMilli);
         dest.writeInt(numScorerOverride);
         dest.writeInt(numScorerOverrideAndSwitchedNetwork);
         dest.writeInt(numAssociation);
@@ -2257,10 +2195,6 @@
                 config.lastUpdateUid = in.readInt();
                 config.creatorName = in.readString();
                 config.lastUpdateName = in.readString();
-                config.lastConnectionFailure = in.readLong();
-                config.lastRoamingFailure = in.readLong();
-                config.lastRoamingFailureReason = in.readInt();
-                config.roamingFailureBlackListTimeMilli = in.readLong();
                 config.numScorerOverride = in.readInt();
                 config.numScorerOverrideAndSwitchedNetwork = in.readInt();
                 config.numAssociation = in.readInt();
diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java
index 433285b..9c6c8a9 100644
--- a/android/net/wifi/WifiManager.java
+++ b/android/net/wifi/WifiManager.java
@@ -2141,7 +2141,8 @@
     }
 
     /**
-     * Sets the Wi-Fi AP Configuration.
+     * Sets the Wi-Fi AP Configuration.  The AP configuration must either be open or
+     * WPA2 PSK networks.
      * @return {@code true} if the operation succeeded, {@code false} otherwise
      *
      * @hide
@@ -2150,8 +2151,7 @@
     @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE)
     public boolean setWifiApConfiguration(WifiConfiguration wifiConfig) {
         try {
-            mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName());
-            return true;
+            return mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/android/os/BatteryManager.java b/android/os/BatteryManager.java
index 6363161..954071a 100644
--- a/android/os/BatteryManager.java
+++ b/android/os/BatteryManager.java
@@ -353,4 +353,20 @@
     public static boolean isPlugWired(int plugType) {
         return plugType == BATTERY_PLUGGED_USB || plugType == BATTERY_PLUGGED_AC;
     }
+
+    /**
+     * Compute an approximation for how much time (in milliseconds) remains until the battery is
+     * fully charged. Returns -1 if no time can be computed: either there is not enough current
+     * data to make a decision or the battery is currently discharging.
+     *
+     * @return how much time is left, in milliseconds, until the battery is fully charged or -1 if
+     *         the computation fails
+     */
+    public long computeChargeTimeRemaining() {
+        try {
+            return mBatteryStats.computeChargeTimeRemaining();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java
index 6ebb102..1d232bf 100644
--- a/android/os/BatteryStats.java
+++ b/android/os/BatteryStats.java
@@ -3814,6 +3814,9 @@
                 final BatterySipper bs = sippers.get(i);
                 String label;
                 switch (bs.drainType) {
+                    case AMBIENT_DISPLAY:
+                        label = "ambi";
+                        break;
                     case IDLE:
                         label="idle";
                         break;
@@ -4975,6 +4978,9 @@
                 final BatterySipper bs = sippers.get(i);
                 pw.print(prefix);
                 switch (bs.drainType) {
+                    case AMBIENT_DISPLAY:
+                        pw.print("    Ambient display: ");
+                        break;
                     case IDLE:
                         pw.print("    Idle: ");
                         break;
@@ -7777,6 +7783,9 @@
                 int n = SystemProto.PowerUseItem.UNKNOWN_SIPPER;
                 int uid = 0;
                 switch (bs.drainType) {
+                    case AMBIENT_DISPLAY:
+                        n = SystemProto.PowerUseItem.AMBIENT_DISPLAY;
+                        break;
                     case IDLE:
                         n = SystemProto.PowerUseItem.IDLE;
                         break;
diff --git a/android/os/Build.java b/android/os/Build.java
index 8378a82..7162b8a 100644
--- a/android/os/Build.java
+++ b/android/os/Build.java
@@ -907,6 +907,8 @@
          * <li>{@link android.app.Service#startForeground Service.startForeground} requires
          * that apps hold the permission
          * {@link android.Manifest.permission#FOREGROUND_SERVICE}.</li>
+         * <li>{@link android.widget.LinearLayout} will always remeasure weighted children,
+         * even if there is no excess space.</li>
          * </ul>
          */
         public static final int P = CUR_DEVELOPMENT; // STOPSHIP Replace with the real version.
diff --git a/android/os/DeviceIdleManager.java b/android/os/DeviceIdleManager.java
new file mode 100644
index 0000000..9039f92
--- /dev/null
+++ b/android/os/DeviceIdleManager.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 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 android.os;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+
+/**
+ * Access to the service that keeps track of device idleness and drives low power mode based on
+ * that.
+ *
+ * @hide
+ */
+@TestApi
+@SystemService(Context.DEVICE_IDLE_CONTROLLER)
+public class DeviceIdleManager {
+    private final Context mContext;
+    private final IDeviceIdleController mService;
+
+    /**
+     * @hide
+     */
+    public DeviceIdleManager(@NonNull Context context, @NonNull IDeviceIdleController service) {
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * @return package names the system has white-listed to opt out of power save restrictions,
+     * except for device idle mode.
+     */
+    public @NonNull String[] getSystemPowerWhitelistExceptIdle() {
+        try {
+            return mService.getSystemPowerWhitelistExceptIdle();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return new String[0];
+        }
+    }
+
+    /**
+     * @return package names the system has white-listed to opt out of power save restrictions for
+     * all modes.
+     */
+    public @NonNull String[] getSystemPowerWhitelist() {
+        try {
+            return mService.getSystemPowerWhitelist();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return new String[0];
+        }
+    }
+}
diff --git a/android/os/Environment.java b/android/os/Environment.java
index 03203d0..213260f 100644
--- a/android/os/Environment.java
+++ b/android/os/Environment.java
@@ -16,6 +16,7 @@
 
 package android.os;
 
+import android.annotation.TestApi;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.os.storage.StorageManager;
@@ -1033,6 +1034,7 @@
      *
      * @hide
      */
+    @TestApi
     public static File buildPath(File base, String... segments) {
         File cur = base;
         for (String segment : segments) {
diff --git a/android/os/MessageQueue.java b/android/os/MessageQueue.java
index 96e7a59..b1c33c2 100644
--- a/android/os/MessageQueue.java
+++ b/android/os/MessageQueue.java
@@ -254,6 +254,7 @@
         } else if (record != null) {
             record.mEvents = 0;
             mFileDescriptorRecords.removeAt(index);
+            nativeSetFileDescriptorEvents(mPtr, fdNum, 0);
         }
     }
 
diff --git a/android/os/Parcel.java b/android/os/Parcel.java
index e3c4870..5142928 100644
--- a/android/os/Parcel.java
+++ b/android/os/Parcel.java
@@ -1857,26 +1857,7 @@
         int code = readExceptionCode();
         if (code != 0) {
             String msg = readString();
-            String remoteStackTrace = null;
-            final int remoteStackPayloadSize = readInt();
-            if (remoteStackPayloadSize > 0) {
-                remoteStackTrace = readString();
-            }
-            Exception e = createException(code, msg);
-            // Attach remote stack trace if availalble
-            if (remoteStackTrace != null) {
-                RemoteException cause = new RemoteException(
-                        "Remote stack trace:\n" + remoteStackTrace, null, false, false);
-                try {
-                    Throwable rootCause = ExceptionUtils.getRootCause(e);
-                    if (rootCause != null) {
-                        rootCause.initCause(cause);
-                    }
-                } catch (RuntimeException ex) {
-                    Log.e(TAG, "Cannot set cause " + cause + " for " + e, ex);
-                }
-            }
-            SneakyThrow.sneakyThrow(e);
+            readException(code, msg);
         }
     }
 
@@ -1921,7 +1902,26 @@
      * @param msg The exception message.
      */
     public final void readException(int code, String msg) {
-        SneakyThrow.sneakyThrow(createException(code, msg));
+        String remoteStackTrace = null;
+        final int remoteStackPayloadSize = readInt();
+        if (remoteStackPayloadSize > 0) {
+            remoteStackTrace = readString();
+        }
+        Exception e = createException(code, msg);
+        // Attach remote stack trace if availalble
+        if (remoteStackTrace != null) {
+            RemoteException cause = new RemoteException(
+                    "Remote stack trace:\n" + remoteStackTrace, null, false, false);
+            try {
+                Throwable rootCause = ExceptionUtils.getRootCause(e);
+                if (rootCause != null) {
+                    rootCause.initCause(cause);
+                }
+            } catch (RuntimeException ex) {
+                Log.e(TAG, "Cannot set cause " + cause + " for " + e, ex);
+            }
+        }
+        SneakyThrow.sneakyThrow(e);
     }
 
     /**
diff --git a/android/os/ServiceManager.java b/android/os/ServiceManager.java
index 34c7845..165276d 100644
--- a/android/os/ServiceManager.java
+++ b/android/os/ServiceManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2007 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.
@@ -16,9 +16,98 @@
 
 package android.os;
 
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BinderInternal;
+import com.android.internal.util.StatLogger;
+
+import java.util.HashMap;
 import java.util.Map;
 
+/** @hide */
 public final class ServiceManager {
+    private static final String TAG = "ServiceManager";
+    private static final Object sLock = new Object();
+
+    private static IServiceManager sServiceManager;
+
+    /**
+     * Cache for the "well known" services, such as WM and AM.
+     */
+    private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();
+
+    /**
+     * We do the "slow log" at most once every this interval.
+     */
+    private static final int SLOW_LOG_INTERVAL_MS = 5000;
+
+    /**
+     * We do the "stats log" at most once every this interval.
+     */
+    private static final int STATS_LOG_INTERVAL_MS = 5000;
+
+    /**
+     * Threshold in uS for a "slow" call, used on core UIDs. We use a more relax value to
+     * avoid logspam.
+     */
+    private static final long GET_SERVICE_SLOW_THRESHOLD_US_CORE =
+            SystemProperties.getInt("debug.servicemanager.slow_call_core_ms", 10) * 1000;
+
+    /**
+     * Threshold in uS for a "slow" call, used on non-core UIDs. We use a more relax value to
+     * avoid logspam.
+     */
+    private static final long GET_SERVICE_SLOW_THRESHOLD_US_NON_CORE =
+            SystemProperties.getInt("debug.servicemanager.slow_call_ms", 50) * 1000;
+
+    /**
+     * We log stats logging ever this many getService() calls.
+     */
+    private static final int GET_SERVICE_LOG_EVERY_CALLS_CORE =
+            SystemProperties.getInt("debug.servicemanager.log_calls_core", 100);
+
+    /**
+     * We log stats logging ever this many getService() calls.
+     */
+    private static final int GET_SERVICE_LOG_EVERY_CALLS_NON_CORE =
+            SystemProperties.getInt("debug.servicemanager.log_calls", 200);
+
+    @GuardedBy("sLock")
+    private static int sGetServiceAccumulatedUs;
+
+    @GuardedBy("sLock")
+    private static int sGetServiceAccumulatedCallCount;
+
+    @GuardedBy("sLock")
+    private static long sLastStatsLogUptime;
+
+    @GuardedBy("sLock")
+    private static long sLastSlowLogUptime;
+
+    @GuardedBy("sLock")
+    private static long sLastSlowLogActualTime;
+
+    interface Stats {
+        int GET_SERVICE = 0;
+
+        int COUNT = GET_SERVICE + 1;
+    }
+
+    public static final StatLogger sStatLogger = new StatLogger(new String[] {
+            "getService()",
+    });
+
+    private static IServiceManager getIServiceManager() {
+        if (sServiceManager != null) {
+            return sServiceManager;
+        }
+
+        // Find the service manager
+        sServiceManager = ServiceManagerNative
+                .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
+        return sServiceManager;
+    }
 
     /**
      * Returns a reference to a service with the given name.
@@ -27,14 +116,32 @@
      * @return a reference to the service, or <code>null</code> if the service doesn't exist
      */
     public static IBinder getService(String name) {
+        try {
+            IBinder service = sCache.get(name);
+            if (service != null) {
+                return service;
+            } else {
+                return Binder.allowBlocking(rawGetService(name));
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "error in getService", e);
+        }
         return null;
     }
 
     /**
-     * Is not supposed to return null, but that is fine for layoutlib.
+     * Returns a reference to a service with the given name, or throws
+     * {@link NullPointerException} if none is found.
+     *
+     * @hide
      */
     public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException {
-        throw new ServiceNotFoundException(name);
+        final IBinder binder = getService(name);
+        if (binder != null) {
+            return binder;
+        } else {
+            throw new ServiceNotFoundException(name);
+        }
     }
 
     /**
@@ -45,7 +152,39 @@
      * @param service the service object
      */
     public static void addService(String name, IBinder service) {
-        // pass
+        addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
+    }
+
+    /**
+     * Place a new @a service called @a name into the service
+     * manager.
+     *
+     * @param name the name of the new service
+     * @param service the service object
+     * @param allowIsolated set to true to allow isolated sandboxed processes
+     * to access this service
+     */
+    public static void addService(String name, IBinder service, boolean allowIsolated) {
+        addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
+    }
+
+    /**
+     * Place a new @a service called @a name into the service
+     * manager.
+     *
+     * @param name the name of the new service
+     * @param service the service object
+     * @param allowIsolated set to true to allow isolated sandboxed processes
+     * @param dumpPriority supported dump priority levels as a bitmask
+     * to access this service
+     */
+    public static void addService(String name, IBinder service, boolean allowIsolated,
+            int dumpPriority) {
+        try {
+            getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
+        } catch (RemoteException e) {
+            Log.e(TAG, "error in addService", e);
+        }
     }
 
     /**
@@ -53,7 +192,17 @@
      * service manager.  Non-blocking.
      */
     public static IBinder checkService(String name) {
-        return null;
+        try {
+            IBinder service = sCache.get(name);
+            if (service != null) {
+                return service;
+            } else {
+                return Binder.allowBlocking(getIServiceManager().checkService(name));
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "error in checkService", e);
+            return null;
+        }
     }
 
     /**
@@ -62,9 +211,12 @@
      * case of an exception
      */
     public static String[] listServices() {
-        // actual implementation returns null sometimes, so it's ok
-        // to return null instead of an empty list.
-        return null;
+        try {
+            return getIServiceManager().listServices(IServiceManager.DUMP_FLAG_PRIORITY_ALL);
+        } catch (RemoteException e) {
+            Log.e(TAG, "error in listServices", e);
+            return null;
+        }
     }
 
     /**
@@ -76,7 +228,10 @@
      * @hide
      */
     public static void initServiceCache(Map<String, IBinder> cache) {
-        // pass
+        if (sCache.size() != 0) {
+            throw new IllegalStateException("setServiceCache may only be called once");
+        }
+        sCache.putAll(cache);
     }
 
     /**
@@ -87,9 +242,63 @@
      * @hide
      */
     public static class ServiceNotFoundException extends Exception {
-        // identical to the original implementation
         public ServiceNotFoundException(String name) {
             super("No service published for: " + name);
         }
     }
+
+    private static IBinder rawGetService(String name) throws RemoteException {
+        final long start = sStatLogger.getTime();
+
+        final IBinder binder = getIServiceManager().getService(name);
+
+        final int time = (int) sStatLogger.logDurationStat(Stats.GET_SERVICE, start);
+
+        final int myUid = Process.myUid();
+        final boolean isCore = UserHandle.isCore(myUid);
+
+        final long slowThreshold = isCore
+                ? GET_SERVICE_SLOW_THRESHOLD_US_CORE
+                : GET_SERVICE_SLOW_THRESHOLD_US_NON_CORE;
+
+        synchronized (sLock) {
+            sGetServiceAccumulatedUs += time;
+            sGetServiceAccumulatedCallCount++;
+
+            final long nowUptime = SystemClock.uptimeMillis();
+
+            // Was a slow call?
+            if (time >= slowThreshold) {
+                // We do a slow log:
+                // - At most once in every SLOW_LOG_INTERVAL_MS
+                // - OR it was slower than the previously logged slow call.
+                if ((nowUptime > (sLastSlowLogUptime + SLOW_LOG_INTERVAL_MS))
+                        || (sLastSlowLogActualTime < time)) {
+                    EventLogTags.writeServiceManagerSlow(time / 1000, name);
+
+                    sLastSlowLogUptime = nowUptime;
+                    sLastSlowLogActualTime = time;
+                }
+            }
+
+            // Every GET_SERVICE_LOG_EVERY_CALLS calls, log the total time spent in getService().
+
+            final int logInterval = isCore
+                    ? GET_SERVICE_LOG_EVERY_CALLS_CORE
+                    : GET_SERVICE_LOG_EVERY_CALLS_NON_CORE;
+
+            if ((sGetServiceAccumulatedCallCount >= logInterval)
+                    && (nowUptime >= (sLastStatsLogUptime + STATS_LOG_INTERVAL_MS))) {
+
+                EventLogTags.writeServiceManagerStats(
+                        sGetServiceAccumulatedCallCount, // Total # of getService() calls.
+                        sGetServiceAccumulatedUs / 1000, // Total time spent in getService() calls.
+                        (int) (nowUptime - sLastStatsLogUptime)); // Uptime duration since last log.
+                sGetServiceAccumulatedCallCount = 0;
+                sGetServiceAccumulatedUs = 0;
+                sLastStatsLogUptime = nowUptime;
+            }
+        }
+        return binder;
+    }
 }
diff --git a/android/os/StrictMode.java b/android/os/StrictMode.java
index a93e25a..59380fd 100644
--- a/android/os/StrictMode.java
+++ b/android/os/StrictMode.java
@@ -39,6 +39,7 @@
 import android.os.strictmode.IntentReceiverLeakedViolation;
 import android.os.strictmode.LeakedClosableViolation;
 import android.os.strictmode.NetworkViolation;
+import android.os.strictmode.NonSdkApiUsedViolation;
 import android.os.strictmode.ResourceMismatchViolation;
 import android.os.strictmode.ServiceConnectionLeakedViolation;
 import android.os.strictmode.SqliteObjectLeakedViolation;
@@ -76,6 +77,7 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
 
 /**
  * StrictMode is a developer tool which detects things you might be doing by accident and brings
@@ -262,6 +264,9 @@
     /** @hide */
     @TestApi public static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy
 
+    /** @hide */
+    @TestApi public static final int DETECT_VM_NON_SDK_API_USAGE = 0x40 << 24; // for VmPolicy
+
     private static final int ALL_VM_DETECT_BITS =
             DETECT_VM_CURSOR_LEAKS
                     | DETECT_VM_CLOSABLE_LEAKS
@@ -271,7 +276,9 @@
                     | DETECT_VM_FILE_URI_EXPOSURE
                     | DETECT_VM_CLEARTEXT_NETWORK
                     | DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION
-                    | DETECT_VM_UNTAGGED_SOCKET;
+                    | DETECT_VM_UNTAGGED_SOCKET
+                    | DETECT_VM_NON_SDK_API_USAGE;
+
 
     // Byte 3: Penalty
 
@@ -413,6 +420,13 @@
      */
     private static final AtomicInteger sDropboxCallsInFlight = new AtomicInteger(0);
 
+    /**
+     * Callback supplied to dalvik / libcore to get informed of usages of java API that are not
+     * a part of the public SDK.
+     */
+    private static final Consumer<String> sNonSdkApiUsageConsumer =
+            message -> onVmPolicyViolation(new NonSdkApiUsedViolation(message));
+
     private StrictMode() {}
 
     /**
@@ -796,6 +810,23 @@
             }
 
             /**
+             * Detect reflective usage of APIs that are not part of the public Android SDK.
+             */
+            public Builder detectNonSdkApiUsage() {
+                return enable(DETECT_VM_NON_SDK_API_USAGE);
+            }
+
+            /**
+             * Permit reflective usage of APIs that are not part of the public Android SDK. Note
+             * that this <b>only</b> affects {@code StrictMode}, the underlying runtime may
+             * continue to restrict or warn on access to methods that are not part of the
+             * public SDK.
+             */
+            public Builder permitNonSdkApiUsage() {
+                return disable(DETECT_VM_NON_SDK_API_USAGE);
+            }
+
+            /**
              * Detect everything that's potentially suspect.
              *
              * <p>In the Honeycomb release this includes leaks of SQLite cursors, Activities, and
@@ -826,6 +857,8 @@
                     detectContentUriWithoutPermission();
                     detectUntaggedSockets();
                 }
+
+                // TODO: Decide whether to detect non SDK API usage beyond a certain API level.
                 return this;
             }
 
@@ -1848,6 +1881,13 @@
             } else if (networkPolicy != NETWORK_POLICY_ACCEPT) {
                 Log.w(TAG, "Dropping requested network policy due to missing service!");
             }
+
+
+            if ((sVmPolicy.mask & DETECT_VM_NON_SDK_API_USAGE) != 0) {
+                VMRuntime.setNonSdkApiUsageConsumer(sNonSdkApiUsageConsumer);
+            } else {
+                VMRuntime.setNonSdkApiUsageConsumer(null);
+            }
         }
     }
 
@@ -2576,6 +2616,8 @@
                 return DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION;
             } else if (mViolation instanceof UntaggedSocketViolation) {
                 return DETECT_VM_UNTAGGED_SOCKET;
+            } else if (mViolation instanceof NonSdkApiUsedViolation) {
+                return DETECT_VM_NON_SDK_API_USAGE;
             }
             throw new IllegalStateException("missing violation bit");
         }
diff --git a/android/os/SystemProperties.java b/android/os/SystemProperties.java
index 8eb39c0..7d3ba6a 100644
--- a/android/os/SystemProperties.java
+++ b/android/os/SystemProperties.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.util.Log;
 import android.util.MutableInt;
 
@@ -35,6 +36,7 @@
  * {@hide}
  */
 @SystemApi
+@TestApi
 public class SystemProperties {
     private static final String TAG = "SystemProperties";
     private static final boolean TRACK_KEY_ACCESS = false;
@@ -110,6 +112,7 @@
      */
     @NonNull
     @SystemApi
+    @TestApi
     public static String get(@NonNull String key, @Nullable String def) {
         if (TRACK_KEY_ACCESS) onKeyAccess(key);
         return native_get(key, def);
diff --git a/android/os/UserHandle.java b/android/os/UserHandle.java
index 094f004..4d4f31d 100644
--- a/android/os/UserHandle.java
+++ b/android/os/UserHandle.java
@@ -82,6 +82,7 @@
     public static final int USER_SERIAL_SYSTEM = 0;
 
     /** @hide A user handle to indicate the "system" user of the device */
+    @TestApi
     public static final UserHandle SYSTEM = new UserHandle(USER_SYSTEM);
 
     /**
diff --git a/android/os/UserManager.java b/android/os/UserManager.java
index a9eb360..9b20ed2 100644
--- a/android/os/UserManager.java
+++ b/android/os/UserManager.java
@@ -1149,6 +1149,7 @@
      * primary user are two separate users. Previously system user and primary user are combined as
      * a single owner user.  see @link {android.os.UserHandle#USER_OWNER}
      */
+    @TestApi
     public static boolean isSplitSystemUser() {
         return RoSystemProperties.FW_SYSTEM_USER_SPLIT;
     }
diff --git a/android/os/WorkSource.java b/android/os/WorkSource.java
index 17d83db..3270719 100644
--- a/android/os/WorkSource.java
+++ b/android/os/WorkSource.java
@@ -924,13 +924,17 @@
         /** @hide */
         @VisibleForTesting
         public int[] getUids() {
-            return mUids;
+            int[] uids = new int[mSize];
+            System.arraycopy(mUids, 0, uids, 0, mSize);
+            return uids;
         }
 
         /** @hide */
         @VisibleForTesting
         public String[] getTags() {
-            return mTags;
+            String[] tags = new String[mSize];
+            System.arraycopy(mTags, 0, tags, 0, mSize);
+            return tags;
         }
 
         /** @hide */
diff --git a/android/os/ZygoteProcess.java b/android/os/ZygoteProcess.java
index b9dd376..673da50 100644
--- a/android/os/ZygoteProcess.java
+++ b/android/os/ZygoteProcess.java
@@ -166,6 +166,11 @@
     private List<String> mApiBlacklistExemptions = Collections.emptyList();
 
     /**
+     * Proportion of hidden API accesses that should be logged to the event log; 0 - 0x10000.
+     */
+    private int mHiddenApiAccessLogSampleRate;
+
+    /**
      * The state of the connection to the primary zygote.
      */
     private ZygoteState primaryZygoteState;
@@ -467,7 +472,8 @@
      * <p>The list of exemptions will take affect for all new processes forked from the zygote after
      * this call.
      *
-     * @param exemptions List of hidden API exemption prefixes.
+     * @param exemptions List of hidden API exemption prefixes. Any matching members are treated as
+     *        whitelisted/public APIs (i.e. allowed, no logging of usage).
      */
     public void setApiBlacklistExemptions(List<String> exemptions) {
         synchronized (mLock) {
@@ -477,6 +483,21 @@
         }
     }
 
+    /**
+     * Set the precentage of detected hidden API accesses that are logged to the event log.
+     *
+     * <p>This rate will take affect for all new processes forked from the zygote after this call.
+     *
+     * @param rate An integer between 0 and 0x10000 inclusive. 0 means no event logging.
+     */
+    public void setHiddenApiAccessLogSampleRate(int rate) {
+        synchronized (mLock) {
+            mHiddenApiAccessLogSampleRate = rate;
+            maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
+            maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState);
+        }
+    }
+
     @GuardedBy("mLock")
     private void maybeSetApiBlacklistExemptions(ZygoteState state, boolean sendIfEmpty) {
         if (state == null || state.isClosed()) {
@@ -504,6 +525,29 @@
         }
     }
 
+    private void maybeSetHiddenApiAccessLogSampleRate(ZygoteState state) {
+        if (state == null || state.isClosed()) {
+            return;
+        }
+        if (mHiddenApiAccessLogSampleRate == -1) {
+            return;
+        }
+        try {
+            state.writer.write(Integer.toString(1));
+            state.writer.newLine();
+            state.writer.write("--hidden-api-log-sampling-rate="
+                    + Integer.toString(mHiddenApiAccessLogSampleRate));
+            state.writer.newLine();
+            state.writer.flush();
+            int status = state.inputStream.readInt();
+            if (status != 0) {
+                Slog.e(LOG_TAG, "Failed to set hidden API log sampling rate; status " + status);
+            }
+        } catch (IOException ioe) {
+            Slog.e(LOG_TAG, "Failed to set hidden API log sampling rate", ioe);
+        }
+    }
+
     /**
      * Tries to open socket to Zygote process if not already open. If
      * already open, does nothing.  May block and retry.  Requires that mLock be held.
@@ -519,6 +563,7 @@
                 throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
             }
             maybeSetApiBlacklistExemptions(primaryZygoteState, false);
+            maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
         }
         if (primaryZygoteState.matches(abi)) {
             return primaryZygoteState;
@@ -532,6 +577,7 @@
                 throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
             }
             maybeSetApiBlacklistExemptions(secondaryZygoteState, false);
+            maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState);
         }
 
         if (secondaryZygoteState.matches(abi)) {
diff --git a/android/os/storage/DiskInfo.java b/android/os/storage/DiskInfo.java
index 9114107..d493cce 100644
--- a/android/os/storage/DiskInfo.java
+++ b/android/os/storage/DiskInfo.java
@@ -17,6 +17,7 @@
 package android.os.storage;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.res.Resources;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -93,7 +94,7 @@
         return true;
     }
 
-    public String getDescription() {
+    public @Nullable String getDescription() {
         final Resources res = Resources.getSystem();
         if ((flags & FLAG_SD) != 0) {
             if (isInteresting(label)) {
@@ -112,6 +113,17 @@
         }
     }
 
+    public @Nullable String getShortDescription() {
+        final Resources res = Resources.getSystem();
+        if (isSd()) {
+            return res.getString(com.android.internal.R.string.storage_sd_card);
+        } else if (isUsb()) {
+            return res.getString(com.android.internal.R.string.storage_usb_drive);
+        } else {
+            return null;
+        }
+    }
+
     public boolean isAdoptable() {
         return (flags & FLAG_ADOPTABLE) != 0;
     }
diff --git a/android/security/keystore/SessionExpiredException.java b/android/os/strictmode/NonSdkApiUsedViolation.java
similarity index 69%
rename from android/security/keystore/SessionExpiredException.java
rename to android/os/strictmode/NonSdkApiUsedViolation.java
index 7c8d5e4..2f0cb50 100644
--- a/android/security/keystore/SessionExpiredException.java
+++ b/android/os/strictmode/NonSdkApiUsedViolation.java
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-package android.security.keystore;
+package android.os.strictmode;
 
 /**
- * @deprecated Use {@link android.security.keystore.recovery.SessionExpiredException}.
- * @hide
+ * Subclass of {@code Violation} that is used when a process accesses
+ * a non SDK API.
  */
-public class SessionExpiredException extends RecoveryControllerException {
-    public SessionExpiredException(String msg) {
-        super(msg);
+public final class NonSdkApiUsedViolation extends Violation {
+    /** @hide */
+    public NonSdkApiUsedViolation(String message) {
+        super(message);
     }
 }
diff --git a/android/provider/Settings.java b/android/provider/Settings.java
index 68fc6c1..5b7adf0 100644
--- a/android/provider/Settings.java
+++ b/android/provider/Settings.java
@@ -1179,6 +1179,23 @@
     public static final String ACTION_ZEN_MODE_SETTINGS = "android.settings.ZEN_MODE_SETTINGS";
 
     /**
+     * Activity Action: Show Zen Mode visual effects configuration settings.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ZEN_MODE_BLOCKED_EFFECTS_SETTINGS =
+            "android.settings.ZEN_MODE_BLOCKED_EFFECTS_SETTINGS";
+
+    /**
+     * Activity Action: Show Zen Mode onboarding activity.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ZEN_MODE_ONBOARDING = "android.settings.ZEN_MODE_ONBOARDING";
+
+    /**
      * Activity Action: Show Zen Mode (aka Do Not Disturb) priority configuration settings.
      */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
@@ -3113,6 +3130,9 @@
          */
         public static final String DISPLAY_COLOR_MODE = "display_color_mode";
 
+        private static final Validator DISPLAY_COLOR_MODE_VALIDATOR =
+                new SettingsValidators.InclusiveIntegerRangeValidator(0, 2);
+
         /**
          * The amount of time in milliseconds before the device goes to sleep or begins
          * to dream after a period of inactivity.  This value is also known as the
@@ -3133,9 +3153,6 @@
          */
         public static final String SCREEN_BRIGHTNESS = "screen_brightness";
 
-        private static final Validator SCREEN_BRIGHTNESS_VALIDATOR =
-                new SettingsValidators.InclusiveIntegerRangeValidator(0, 255);
-
         /**
          * The screen backlight brightness between 0 and 255.
          * @hide
@@ -3753,17 +3770,6 @@
                 new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
 
         /**
-         * User-selected RTT mode. When on, outgoing and incoming calls will be answered as RTT
-         * calls when supported by the device and carrier. Boolean value.
-         * 0 = OFF
-         * 1 = ON
-         */
-        public static final String RTT_CALLING_MODE = "rtt_calling_mode";
-
-        /** @hide */
-        public static final Validator RTT_CALLING_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
-
-        /**
          * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
          * boolean (1 or 0).
          */
@@ -4071,7 +4077,6 @@
             FONT_SCALE,
             DIM_SCREEN,
             SCREEN_OFF_TIMEOUT,
-            SCREEN_BRIGHTNESS,
             SCREEN_BRIGHTNESS_MODE,
             SCREEN_AUTO_BRIGHTNESS_ADJ,
             SCREEN_BRIGHTNESS_FOR_VR,
@@ -4088,7 +4093,6 @@
             DTMF_TONE_WHEN_DIALING,
             DTMF_TONE_TYPE_WHEN_DIALING,
             HEARING_AID,
-            RTT_CALLING_MODE,
             TTY_MODE,
             MASTER_MONO,
             SOUND_EFFECTS_ENABLED,
@@ -4108,6 +4112,7 @@
             SHOW_BATTERY_PERCENT,
             NOTIFICATION_VIBRATION_INTENSITY,
             HAPTIC_FEEDBACK_INTENSITY,
+            DISPLAY_COLOR_MODE
         };
 
         /**
@@ -4220,6 +4225,7 @@
             PRIVATE_SETTINGS.add(LOCK_TO_APP_ENABLED);
             PRIVATE_SETTINGS.add(EGG_MODE);
             PRIVATE_SETTINGS.add(SHOW_BATTERY_PERCENT);
+            PRIVATE_SETTINGS.add(DISPLAY_COLOR_MODE);
         }
 
         /**
@@ -4241,8 +4247,8 @@
             VALIDATORS.put(NEXT_ALARM_FORMATTED, NEXT_ALARM_FORMATTED_VALIDATOR);
             VALIDATORS.put(FONT_SCALE, FONT_SCALE_VALIDATOR);
             VALIDATORS.put(DIM_SCREEN, DIM_SCREEN_VALIDATOR);
+            VALIDATORS.put(DISPLAY_COLOR_MODE, DISPLAY_COLOR_MODE_VALIDATOR);
             VALIDATORS.put(SCREEN_OFF_TIMEOUT, SCREEN_OFF_TIMEOUT_VALIDATOR);
-            VALIDATORS.put(SCREEN_BRIGHTNESS, SCREEN_BRIGHTNESS_VALIDATOR);
             VALIDATORS.put(SCREEN_BRIGHTNESS_FOR_VR, SCREEN_BRIGHTNESS_FOR_VR_VALIDATOR);
             VALIDATORS.put(SCREEN_BRIGHTNESS_MODE, SCREEN_BRIGHTNESS_MODE_VALIDATOR);
             VALIDATORS.put(MODE_RINGER_STREAMS_AFFECTED, MODE_RINGER_STREAMS_AFFECTED_VALIDATOR);
@@ -4287,7 +4293,6 @@
             VALIDATORS.put(DTMF_TONE_TYPE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR);
             VALIDATORS.put(HEARING_AID, HEARING_AID_VALIDATOR);
             VALIDATORS.put(TTY_MODE, TTY_MODE_VALIDATOR);
-            VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR);
             VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, NOTIFICATION_LIGHT_PULSE_VALIDATOR);
             VALIDATORS.put(POINTER_LOCATION, POINTER_LOCATION_VALIDATOR);
             VALIDATORS.put(SHOW_TOUCHES, SHOW_TOUCHES_VALIDATOR);
@@ -6660,6 +6665,17 @@
         private static final Validator TTY_MODE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
 
         /**
+         * User-selected RTT mode. When on, outgoing and incoming calls will be answered as RTT
+         * calls when supported by the device and carrier. Boolean value.
+         * 0 = OFF
+         * 1 = ON
+         */
+        public static final String RTT_CALLING_MODE = "rtt_calling_mode";
+
+        private static final Validator RTT_CALLING_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
+
+        /**
+        /**
          * Controls whether settings backup is enabled.
          * Type: int ( 0 = disabled, 1 = enabled )
          * @hide
@@ -7383,6 +7399,17 @@
                 BOOLEAN_VALIDATOR;
 
         /**
+         * Whether the swipe up gesture to switch apps should be enabled.
+         *
+         * @hide
+         */
+        public static final String SWIPE_UP_TO_SWITCH_APPS_ENABLED =
+                "swipe_up_to_switch_apps_enabled";
+
+        private static final Validator SWIPE_UP_TO_SWITCH_APPS_ENABLED_VALIDATOR =
+                BOOLEAN_VALIDATOR;
+
+        /**
          * Whether or not the smart camera lift trigger that launches the camera when the user moves
          * the phone into a position for taking photos should be enabled.
          *
@@ -7885,6 +7912,7 @@
             PREFERRED_TTY_MODE,
             ENHANCED_VOICE_PRIVACY_ENABLED,
             TTY_MODE_ENABLED,
+            RTT_CALLING_MODE,
             INCALL_POWER_BUTTON_BEHAVIOR,
             NIGHT_DISPLAY_CUSTOM_START_TIME,
             NIGHT_DISPLAY_CUSTOM_END_TIME,
@@ -7892,6 +7920,7 @@
             NIGHT_DISPLAY_AUTO_MODE,
             SYNC_PARENT_SOUNDS,
             CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED,
+            SWIPE_UP_TO_SWITCH_APPS_ENABLED,
             CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED,
             SYSTEM_NAVIGATION_KEYS_ENABLED,
             QS_TILES,
@@ -8014,6 +8043,7 @@
             VALIDATORS.put(ENHANCED_VOICE_PRIVACY_ENABLED,
                     ENHANCED_VOICE_PRIVACY_ENABLED_VALIDATOR);
             VALIDATORS.put(TTY_MODE_ENABLED, TTY_MODE_ENABLED_VALIDATOR);
+            VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR);
             VALIDATORS.put(INCALL_POWER_BUTTON_BEHAVIOR, INCALL_POWER_BUTTON_BEHAVIOR_VALIDATOR);
             VALIDATORS.put(NIGHT_DISPLAY_CUSTOM_START_TIME,
                     NIGHT_DISPLAY_CUSTOM_START_TIME_VALIDATOR);
@@ -8024,6 +8054,8 @@
             VALIDATORS.put(SYNC_PARENT_SOUNDS, SYNC_PARENT_SOUNDS_VALIDATOR);
             VALIDATORS.put(CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED,
                     CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED_VALIDATOR);
+            VALIDATORS.put(SWIPE_UP_TO_SWITCH_APPS_ENABLED,
+                    SWIPE_UP_TO_SWITCH_APPS_ENABLED_VALIDATOR);
             VALIDATORS.put(CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED,
                     CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED_VALIDATOR);
             VALIDATORS.put(SYSTEM_NAVIGATION_KEYS_ENABLED,
@@ -8935,6 +8967,20 @@
        /** {@hide} */
        public static final String NETSTATS_UID_TAG_DELETE_AGE = "netstats_uid_tag_delete_age";
 
+       /** {@hide} */
+       public static final String NETPOLICY_QUOTA_ENABLED = "netpolicy_quota_enabled";
+       /** {@hide} */
+       public static final String NETPOLICY_QUOTA_UNLIMITED = "netpolicy_quota_unlimited";
+       /** {@hide} */
+       public static final String NETPOLICY_QUOTA_LIMITED = "netpolicy_quota_limited";
+       /** {@hide} */
+       public static final String NETPOLICY_QUOTA_FRAC_JOBS = "netpolicy_quota_frac_jobs";
+       /** {@hide} */
+       public static final String NETPOLICY_QUOTA_FRAC_MULTIPATH = "netpolicy_quota_frac_multipath";
+
+       /** {@hide} */
+       public static final String NETPOLICY_OVERRIDE_ENABLED = "netpolicy_override_enabled";
+
        /**
         * User preference for which network(s) should be used. Only the
         * connectivity service should touch this.
@@ -9309,6 +9355,15 @@
                "network_metered_multipath_preference";
 
         /**
+         * Default daily multipath budget used by ConnectivityManager.getMultipathPreference()
+         * on metered networks. This default quota is only used if quota could not be determined
+         * from data plan or data limit/warning set by the user.
+         * @hide
+         */
+        public static final String NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES =
+                "network_default_daily_multipath_quota_bytes";
+
+        /**
          * Network watchlist last report time.
          * @hide
          */
@@ -10797,6 +10852,15 @@
                 = "time_only_mode_constants";
 
         /**
+         * Whether of not to send keycode sleep for ungaze when Home is the foreground activity on
+         * watch type devices.
+         * Type: int (0 for false, 1 for true)
+         * Default: 0
+         * @hide
+         */
+        public static final String UNGAZE_SLEEP_ENABLED = "ungaze_sleep_enabled";
+
+        /**
          * Whether or not Network Watchlist feature is enabled.
          * Type: int (0 for false, 1 for true)
          * Default: 0
@@ -11076,6 +11140,14 @@
         public static final String ALWAYS_FINISH_ACTIVITIES = "always_finish_activities";
 
         /**
+         * If nonzero, all system error dialogs will be hidden.  For example, the
+         * crash and ANR dialogs will not be shown, and the system will just proceed
+         * as if they had been accepted by the user.
+         * @hide
+         */
+        public static final String HIDE_ERROR_DIALOGS = "hide_error_dialogs";
+
+        /**
          * Use Dock audio output for media:
          *      0 = disabled
          *      1 = enabled
@@ -11694,6 +11766,38 @@
                 "hidden_api_blacklist_exemptions";
 
         /**
+         * Sampling rate for hidden API access event logs, as an integer in the range 0 to 0x10000
+         * inclusive.
+         *
+         * @hide
+         */
+        public static final String HIDDEN_API_ACCESS_LOG_SAMPLING_RATE =
+                "hidden_api_access_log_sampling_rate";
+
+        /**
+         * Hidden API enforcement policy for apps targeting SDK versions prior to the latest
+         * version.
+         *
+         * Values correspond to @{@link
+         * android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy}
+         *
+         * @hide
+         */
+        public static final String HIDDEN_API_POLICY_PRE_P_APPS =
+                "hidden_api_policy_pre_p_apps";
+
+        /**
+         * Hidden API enforcement policy for apps targeting the current SDK version.
+         *
+         * Values correspond to @{@link
+         * android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy}
+         *
+         * @hide
+         */
+        public static final String HIDDEN_API_POLICY_P_APPS =
+                "hidden_api_policy_p_apps";
+
+        /**
          * Timeout for a single {@link android.media.soundtrigger.SoundTriggerDetectionService}
          * operation (in ms).
          *
@@ -12532,6 +12636,19 @@
          */
          public static final String SWAP_ENABLED = "swap_enabled";
 
+        /**
+         * Blacklist of GNSS satellites.
+         *
+         * This is a list of integers separated by commas to represent pairs of (constellation,
+         * svid). Thus, the number of integers should be even.
+         *
+         * E.g.: "3,0,5,24" denotes (constellation=3, svid=0) and (constellation=5, svid=24) are
+         * blacklisted. Note that svid=0 denotes all svids in the
+         * constellation are blacklisted.
+         *
+         * @hide
+         */
+        public static final String GNSS_SATELLITE_BLACKLIST = "gnss_satellite_blacklist";
     }
 
     /**
diff --git a/android/se/omapi/Channel.java b/android/se/omapi/Channel.java
index c8efede..5db3c1a 100644
--- a/android/se/omapi/Channel.java
+++ b/android/se/omapi/Channel.java
@@ -39,7 +39,7 @@
  *
  * @see <a href="http://globalplatform.org">GlobalPlatform Open Mobile API</a>
  */
-public class Channel {
+public final class Channel implements java.nio.channels.Channel {
 
     private static final String TAG = "OMAPI.Channel";
     private Session mSession;
@@ -64,7 +64,7 @@
      * before closing the channel.
      */
     public void close() {
-        if (!isClosed()) {
+        if (isOpen()) {
             synchronized (mLock) {
                 try {
                     mChannel.close();
@@ -76,21 +76,21 @@
     }
 
     /**
-     * Tells if this channel is closed.
+     * Tells if this channel is open.
      *
-     * @return <code>true</code> if the channel is closed or in case of an error.
-     *         <code>false</code> otherwise.
+     * @return <code>false</code> if the channel is closed or in case of an error.
+     *         <code>true</code> otherwise.
      */
-    public boolean isClosed() {
+    public boolean isOpen() {
         if (!mService.isConnected()) {
             Log.e(TAG, "service not connected to system");
-            return true;
+            return false;
         }
         try {
-            return mChannel.isClosed();
+            return !mChannel.isClosed();
         } catch (RemoteException e) {
             Log.e(TAG, "Exception in isClosed()");
-            return true;
+            return false;
         }
     }
 
diff --git a/android/se/omapi/Reader.java b/android/se/omapi/Reader.java
index 9be3da6..80262f7 100644
--- a/android/se/omapi/Reader.java
+++ b/android/se/omapi/Reader.java
@@ -37,7 +37,7 @@
  *
  * @see <a href="http://globalplatform.org">GlobalPlatform Open Mobile API</a>
  */
-public class Reader {
+public final class Reader {
 
     private static final String TAG = "OMAPI.Reader";
     private final String mName;
diff --git a/android/se/omapi/SEService.java b/android/se/omapi/SEService.java
index 311dc4c..14727f0 100644
--- a/android/se/omapi/SEService.java
+++ b/android/se/omapi/SEService.java
@@ -32,6 +32,7 @@
 import android.util.Log;
 
 import java.util.HashMap;
+import java.util.concurrent.Executor;
 
 /**
  * The SEService realises the communication to available Secure Elements on the
@@ -40,7 +41,7 @@
  *
  * @see <a href="http://simalliance.org">SIMalliance Open Mobile API  v3.0</a>
  */
-public class SEService {
+public final class SEService {
 
     /**
      * Error code used with ServiceSpecificException.
@@ -62,11 +63,11 @@
     /**
      * Interface to send call-backs to the application when the service is connected.
      */
-    public interface SecureElementListener {
+    public interface OnConnectedListener {
         /**
          * Called by the framework when the service is connected.
          */
-        void onServiceConnected();
+        void onConnected();
     }
 
     /**
@@ -74,16 +75,22 @@
      * SEService could be bound to the backend.
      */
     private class SEListener extends ISecureElementListener.Stub {
-        public SecureElementListener mListener = null;
+        public OnConnectedListener mListener = null;
+        public Executor mExecutor = null;
 
         @Override
         public IBinder asBinder() {
             return this;
         }
 
-        public void onServiceConnected() {
-            if (mListener != null) {
-                mListener.onServiceConnected();
+        public void onConnected() {
+            if (mListener != null && mExecutor != null) {
+                mExecutor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        mListener.onConnected();
+                    }
+                });
             }
         }
     }
@@ -116,22 +123,26 @@
      * the specified listener is called or if isConnected() returns
      * <code>true</code>. <br>
      * The call-back object passed as a parameter will have its
-     * onServiceConnected() method called when the connection actually happen.
+     * onConnected() method called when the connection actually happen.
      *
      * @param context
      *            the context of the calling application. Cannot be
      *            <code>null</code>.
      * @param listener
-     *            a SecureElementListener object.
+     *            a OnConnectedListener object.
+     * @param executor
+     *            an Executor which will be used when invoking the callback.
      */
-    public SEService(@NonNull Context context, @NonNull SecureElementListener listener) {
+    public SEService(@NonNull Context context, @NonNull Executor executor,
+            @NonNull OnConnectedListener listener) {
 
-        if (context == null) {
-            throw new NullPointerException("context must not be null");
+        if (context == null || listener == null || executor == null) {
+            throw new NullPointerException("Arguments must not be null");
         }
 
         mContext = context;
         mSEListener.mListener = listener;
+        mSEListener.mExecutor = executor;
 
         mConnection = new ServiceConnection() {
 
@@ -140,7 +151,7 @@
 
                 mSecureElementService = ISecureElementService.Stub.asInterface(service);
                 if (mSEListener != null) {
-                    mSEListener.onServiceConnected();
+                    mSEListener.onConnected();
                 }
                 Log.i(TAG, "Service onServiceConnected");
             }
@@ -171,12 +182,12 @@
     }
 
     /**
-     * Returns the list of available Secure Element readers.
+     * Returns an array of available Secure Element readers.
      * There must be no duplicated objects in the returned list.
      * All available readers shall be listed even if no card is inserted.
      *
-     * @return The readers list, as an array of Readers. If there are no
-     * readers the returned array is of length 0.
+     * @return An array of Readers. If there are no readers the returned array
+     * is of length 0.
      */
     public @NonNull Reader[] getReaders() {
         if (mSecureElementService == null) {
@@ -212,7 +223,8 @@
      * (including any binding to an underlying service).
      * As a result isConnected() will return false after shutdown() was called.
      * After this method call, the SEService object is not connected.
-     * It is recommended to call this method in the termination method of the calling application
+     * This method should be called when connection to the Secure Element is not needed
+     * or in the termination method of the calling application
      * (or part of this application) which is bound to this SEService.
      */
     public void shutdown() {
diff --git a/android/se/omapi/Session.java b/android/se/omapi/Session.java
index adfeddd..d5f8c82 100644
--- a/android/se/omapi/Session.java
+++ b/android/se/omapi/Session.java
@@ -39,7 +39,7 @@
  *
  * @see <a href="http://simalliance.org">SIMalliance Open Mobile API  v3.0</a>
  */
-public class Session {
+public final class Session {
 
     private final Object mLock = new Object();
     private final SEService mService;
@@ -225,6 +225,32 @@
     }
 
     /**
+     * This method is provided to ease the development of mobile application and for compliancy
+     * with existing applications.
+     * This method is equivalent to openBasicChannel(aid, P2=0x00)
+     *
+     * @param aid the AID of the Applet to be selected on this channel, as a
+     *            byte array, or null if no Applet is to be selected.
+     * @throws IOException if there is a communication problem to the reader or
+     *             the Secure Element.
+     * @throws IllegalStateException if the Secure Element session is used after
+     *             being closed.
+     * @throws IllegalArgumentException if the aid's length is not within 5 to
+     *             16 (inclusive).
+     * @throws SecurityException if the calling application cannot be granted
+     *             access to this AID or the default Applet on this
+     *             session.
+     * @throws NoSuchElementException if the AID on the Secure Element is not available or cannot be
+     *             selected.
+     * @throws UnsupportedOperationException if the given P2 parameter is not
+     *             supported by the device
+     * @return an instance of Channel if available or null.
+     */
+    public @Nullable Channel openBasicChannel(@Nullable byte[] aid) throws IOException {
+        return openBasicChannel(aid, (byte) 0x00);
+    }
+
+    /**
      * Open a logical channel with the Secure Element, selecting the Applet represented by
      * the given AID. If the AID is null, which means no Applet is to be selected on this
      * channel, the default Applet is used. It's up to the Secure Element to choose which
@@ -304,4 +330,32 @@
             }
         }
     }
+
+    /**
+     * This method is provided to ease the development of mobile application and for compliancy
+     * with existing applications.
+     * This method is equivalent to openLogicalChannel(aid, P2=0x00)
+     *
+     * @param aid the AID of the Applet to be selected on this channel, as a
+     *            byte array.
+     * @throws IOException if there is a communication problem to the reader or
+     *             the Secure Element.
+     * @throws IllegalStateException if the Secure Element is used after being
+     *             closed.
+     * @throws IllegalArgumentException if the aid's length is not within 5 to
+     *             16 (inclusive).
+     * @throws SecurityException if the calling application cannot be granted
+     *             access to this AID or the default Applet on this
+     *             session.
+     * @throws NoSuchElementException if the AID on the Secure Element is not
+     *             available or cannot be selected or a logical channel is already
+     *             open to a non-multiselectable Applet.
+     * @throws UnsupportedOperationException if the given P2 parameter is not
+     *             supported by the device.
+     * @return an instance of Channel. Null if the Secure Element is unable to
+     *         provide a new logical channel.
+     */
+    public @Nullable Channel openLogicalChannel(@Nullable byte[] aid) throws IOException {
+        return openLogicalChannel(aid, (byte) 0x00);
+    }
 }
diff --git a/android/security/ConfirmationCallback.java b/android/security/ConfirmationCallback.java
index 4670bce..fd027f0 100644
--- a/android/security/ConfirmationCallback.java
+++ b/android/security/ConfirmationCallback.java
@@ -33,22 +33,22 @@
      *
      * @param dataThatWasConfirmed the data that was confirmed, see above for the format.
      */
-    public void onConfirmedByUser(@NonNull byte[] dataThatWasConfirmed) {}
+    public void onConfirmed(@NonNull byte[] dataThatWasConfirmed) {}
 
     /**
      * Called when the requested prompt was dismissed (not accepted) by the user.
      */
-    public void onDismissedByUser() {}
+    public void onDismissed() {}
 
     /**
      * Called when the requested prompt was dismissed by the application.
      */
-    public void onDismissedByApplication() {}
+    public void onCanceled() {}
 
     /**
      * Called when the requested prompt was dismissed because of a low-level error.
      *
-     * @param e an exception representing the error.
+     * @param e a throwable representing the error.
      */
-    public void onError(Exception e) {}
+    public void onError(Throwable e) {}
 }
diff --git a/android/security/ConfirmationDialog.java b/android/security/ConfirmationPrompt.java
similarity index 88%
rename from android/security/ConfirmationDialog.java
rename to android/security/ConfirmationPrompt.java
index 1697106..5330cff 100644
--- a/android/security/ConfirmationDialog.java
+++ b/android/security/ConfirmationPrompt.java
@@ -68,7 +68,7 @@
  * {@link #presentPrompt presentPrompt()} method. The <i>Relying Party</i> stores the nonce locally
  * since it'll use it in a later step.
  * <li> If the user approves the prompt a <i>Confirmation Response</i> is returned in the
- * {@link ConfirmationCallback#onConfirmedByUser onConfirmedByUser(byte[])} callback as the
+ * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the
  * <code>dataThatWasConfirmed</code> parameter. This blob contains the text that was shown to the
  * user, the <code>extraData</code> parameter, and possibly other data.
  * <li> The application signs the <i>Confirmation Response</i> with the previously created key and
@@ -82,8 +82,8 @@
  * last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it
  * along the nonce in the <code>extraData</code> blob.
  */
-public class ConfirmationDialog {
-    private static final String TAG = "ConfirmationDialog";
+public class ConfirmationPrompt {
+    private static final String TAG = "ConfirmationPrompt";
 
     private CharSequence mPromptText;
     private byte[] mExtraData;
@@ -97,15 +97,15 @@
             ConfirmationCallback callback) {
         switch (responseCode) {
             case KeyStore.CONFIRMATIONUI_OK:
-                callback.onConfirmedByUser(dataThatWasConfirmed);
+                callback.onConfirmed(dataThatWasConfirmed);
                 break;
 
             case KeyStore.CONFIRMATIONUI_CANCELED:
-                callback.onDismissedByUser();
+                callback.onDismissed();
                 break;
 
             case KeyStore.CONFIRMATIONUI_ABORTED:
-                callback.onDismissedByApplication();
+                callback.onCanceled();
                 break;
 
             case KeyStore.CONFIRMATIONUI_SYSTEM_ERROR:
@@ -145,21 +145,25 @@
             };
 
     /**
-     * A builder that collects arguments, to be shown on the system-provided confirmation dialog.
+     * A builder that collects arguments, to be shown on the system-provided confirmation prompt.
      */
-    public static class Builder {
+    public static final class Builder {
 
+        private Context mContext;
         private CharSequence mPromptText;
         private byte[] mExtraData;
 
         /**
-         * Creates a builder for the confirmation dialog.
+         * Creates a builder for the confirmation prompt.
+         *
+         * @param context the application context
          */
-        public Builder() {
+        public Builder(Context context) {
+            mContext = context;
         }
 
         /**
-         * Sets the prompt text for the dialog.
+         * Sets the prompt text for the prompt.
          *
          * @param promptText the text to present in the prompt.
          * @return the builder.
@@ -170,7 +174,7 @@
         }
 
         /**
-         * Sets the extra data for the dialog.
+         * Sets the extra data for the prompt.
          *
          * @param extraData data to include in the response data.
          * @return the builder.
@@ -181,24 +185,23 @@
         }
 
         /**
-         * Creates a {@link ConfirmationDialog} with the arguments supplied to this builder.
+         * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder.
          *
-         * @param context the application context
-         * @return a {@link ConfirmationDialog}
+         * @return a {@link ConfirmationPrompt}
          * @throws IllegalArgumentException if any of the required fields are not set.
          */
-        public ConfirmationDialog build(Context context) {
+        public ConfirmationPrompt build() {
             if (TextUtils.isEmpty(mPromptText)) {
                 throw new IllegalArgumentException("prompt text must be set and non-empty");
             }
             if (mExtraData == null) {
                 throw new IllegalArgumentException("extraData must be set");
             }
-            return new ConfirmationDialog(context, mPromptText, mExtraData);
+            return new ConfirmationPrompt(mContext, mPromptText, mExtraData);
         }
     }
 
-    private ConfirmationDialog(Context context, CharSequence promptText, byte[] extraData) {
+    private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) {
         mContext = context;
         mPromptText = promptText;
         mExtraData = extraData;
@@ -227,10 +230,10 @@
         return uiOptionsAsFlags;
     }
 
-    private boolean isAccessibilityServiceRunning() {
+    private static boolean isAccessibilityServiceRunning(Context context) {
         boolean serviceRunning = false;
         try {
-            ContentResolver contentResolver = mContext.getContentResolver();
+            ContentResolver contentResolver = context.getContentResolver();
             int a11yEnabled = Settings.Secure.getInt(contentResolver,
                     Settings.Secure.ACCESSIBILITY_ENABLED);
             if (a11yEnabled == 1) {
@@ -249,12 +252,12 @@
      * When the prompt is no longer being presented, one of the methods in
      * {@link ConfirmationCallback} is called on the supplied callback object.
      *
-     * Confirmation dialogs may not be available when accessibility services are running so this
+     * Confirmation prompts may not be available when accessibility services are running so this
      * may fail with a {@link ConfirmationNotAvailableException} exception even if
      * {@link #isSupported} returns {@code true}.
      *
      * @param executor the executor identifying the thread that will receive the callback.
-     * @param callback the callback to use when the dialog is done showing.
+     * @param callback the callback to use when the prompt is done showing.
      * @throws IllegalArgumentException if the prompt text is too long or malfomed.
      * @throws ConfirmationAlreadyPresentingException if another prompt is being presented.
      * @throws ConfirmationNotAvailableException if confirmation prompts are not supported.
@@ -265,7 +268,7 @@
         if (mCallback != null) {
             throw new ConfirmationAlreadyPresentingException();
         }
-        if (isAccessibilityServiceRunning()) {
+        if (isAccessibilityServiceRunning(mContext)) {
             throw new ConfirmationNotAvailableException();
         }
         mCallback = callback;
@@ -301,7 +304,7 @@
      * Cancels a prompt currently being displayed.
      *
      * On success, the
-     * {@link ConfirmationCallback#onDismissedByApplication onDismissedByApplication()} method on
+     * {@link ConfirmationCallback#onCanceled onCanceled()} method on
      * the supplied callback object will be called asynchronously.
      *
      * @throws IllegalStateException if no prompt is currently being presented.
@@ -324,9 +327,13 @@
     /**
      * Checks if the device supports confirmation prompts.
      *
+     * @param context the application context.
      * @return true if confirmation prompts are supported by the device.
      */
-    public static boolean isSupported() {
+    public static boolean isSupported(Context context) {
+        if (isAccessibilityServiceRunning(context)) {
+            return false;
+        }
         return KeyStore.getInstance().isConfirmationPromptSupported();
     }
 }
diff --git a/android/security/KeyStoreException.java b/android/security/KeyStoreException.java
index 88e768c..30389a2 100644
--- a/android/security/KeyStoreException.java
+++ b/android/security/KeyStoreException.java
@@ -16,12 +16,15 @@
 
 package android.security;
 
+import android.annotation.TestApi;
+
 /**
  * KeyStore/keymaster exception with positive error codes coming from the KeyStore and negative
  * ones from keymaster.
  *
  * @hide
  */
+@TestApi
 public class KeyStoreException extends Exception {
 
     private final int mErrorCode;
diff --git a/android/security/keystore/BackwardsCompat.java b/android/security/keystore/BackwardsCompat.java
deleted file mode 100644
index cf5fe1f..0000000
--- a/android/security/keystore/BackwardsCompat.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Function;
-
-/**
- * Helpers for converting classes between old and new API, so we can preserve backwards
- * compatibility while teamfooding. This will be removed soon.
- *
- * @hide
- */
-class BackwardsCompat {
-
-
-    static KeychainProtectionParams toLegacyKeychainProtectionParams(
-            android.security.keystore.recovery.KeyChainProtectionParams keychainProtectionParams
-    ) {
-        return new KeychainProtectionParams.Builder()
-                .setUserSecretType(keychainProtectionParams.getUserSecretType())
-                .setSecret(keychainProtectionParams.getSecret())
-                .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
-                .setKeyDerivationParams(
-                        toLegacyKeyDerivationParams(
-                                keychainProtectionParams.getKeyDerivationParams()))
-                .build();
-    }
-
-    static KeyDerivationParams toLegacyKeyDerivationParams(
-            android.security.keystore.recovery.KeyDerivationParams keyDerivationParams
-    ) {
-        return new KeyDerivationParams(
-                keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
-    }
-
-    static WrappedApplicationKey toLegacyWrappedApplicationKey(
-            android.security.keystore.recovery.WrappedApplicationKey wrappedApplicationKey
-    ) {
-        return new WrappedApplicationKey.Builder()
-                .setAlias(wrappedApplicationKey.getAlias())
-                .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
-                .build();
-    }
-
-    static android.security.keystore.recovery.KeyDerivationParams fromLegacyKeyDerivationParams(
-            KeyDerivationParams keyDerivationParams
-    ) {
-        return android.security.keystore.recovery.KeyDerivationParams.createSha256Params(
-                keyDerivationParams.getSalt());
-    }
-
-    static android.security.keystore.recovery.WrappedApplicationKey fromLegacyWrappedApplicationKey(
-            WrappedApplicationKey wrappedApplicationKey
-    ) {
-        return new android.security.keystore.recovery.WrappedApplicationKey.Builder()
-                .setAlias(wrappedApplicationKey.getAlias())
-                .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
-                .build();
-    }
-
-    static List<android.security.keystore.recovery.WrappedApplicationKey>
-            fromLegacyWrappedApplicationKeys(List<WrappedApplicationKey> wrappedApplicationKeys
-    ) {
-        return map(wrappedApplicationKeys, BackwardsCompat::fromLegacyWrappedApplicationKey);
-    }
-
-    static List<android.security.keystore.recovery.KeyChainProtectionParams>
-            fromLegacyKeychainProtectionParams(
-                    List<KeychainProtectionParams> keychainProtectionParams) {
-        return map(keychainProtectionParams, BackwardsCompat::fromLegacyKeychainProtectionParam);
-    }
-
-    static android.security.keystore.recovery.KeyChainProtectionParams
-            fromLegacyKeychainProtectionParam(KeychainProtectionParams keychainProtectionParams) {
-        return new android.security.keystore.recovery.KeyChainProtectionParams.Builder()
-                .setUserSecretType(keychainProtectionParams.getUserSecretType())
-                .setSecret(keychainProtectionParams.getSecret())
-                .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
-                .setKeyDerivationParams(
-                        fromLegacyKeyDerivationParams(
-                                keychainProtectionParams.getKeyDerivationParams()))
-                .build();
-    }
-
-    static KeychainSnapshot toLegacyKeychainSnapshot(
-            android.security.keystore.recovery.KeyChainSnapshot keychainSnapshot
-    ) {
-        return new KeychainSnapshot.Builder()
-                .setCounterId(keychainSnapshot.getCounterId())
-                .setEncryptedRecoveryKeyBlob(keychainSnapshot.getEncryptedRecoveryKeyBlob())
-                .setTrustedHardwarePublicKey(keychainSnapshot.getTrustedHardwarePublicKey())
-                .setSnapshotVersion(keychainSnapshot.getSnapshotVersion())
-                .setMaxAttempts(keychainSnapshot.getMaxAttempts())
-                .setServerParams(keychainSnapshot.getServerParams())
-                .setKeychainProtectionParams(
-                        map(keychainSnapshot.getKeyChainProtectionParams(),
-                                BackwardsCompat::toLegacyKeychainProtectionParams))
-                .setWrappedApplicationKeys(
-                        map(keychainSnapshot.getWrappedApplicationKeys(),
-                                BackwardsCompat::toLegacyWrappedApplicationKey))
-                .build();
-    }
-
-    static <A, B> List<B> map(List<A> as, Function<A, B> f) {
-        ArrayList<B> bs = new ArrayList<>(as.size());
-        for (A a : as) {
-            bs.add(f.apply(a));
-        }
-        return bs;
-    }
-}
diff --git a/android/security/keystore/BadCertificateFormatException.java b/android/security/keystore/BadCertificateFormatException.java
deleted file mode 100644
index c51b773..0000000
--- a/android/security/keystore/BadCertificateFormatException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.BadCertificateFormatException}.
- * @hide
- */
-public class BadCertificateFormatException extends RecoveryControllerException {
-    public BadCertificateFormatException(String msg) {
-        super(msg);
-    }
-}
diff --git a/android/security/keystore/DecryptionFailedException.java b/android/security/keystore/DecryptionFailedException.java
deleted file mode 100644
index c0b52f7..0000000
--- a/android/security/keystore/DecryptionFailedException.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.DecryptionFailedException}.
- * @hide
- */
-public class DecryptionFailedException extends RecoveryControllerException {
-
-    public DecryptionFailedException(String msg) {
-        super(msg);
-    }
-}
diff --git a/android/security/keystore/InternalRecoveryServiceException.java b/android/security/keystore/InternalRecoveryServiceException.java
deleted file mode 100644
index 40076f7..0000000
--- a/android/security/keystore/InternalRecoveryServiceException.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.InternalRecoveryServiceException}.
- * @hide
- */
-public class InternalRecoveryServiceException extends RecoveryControllerException {
-    public InternalRecoveryServiceException(String msg) {
-        super(msg);
-    }
-
-    public InternalRecoveryServiceException(String message, Throwable cause) {
-        super(message, cause);
-    }
-}
diff --git a/android/security/keystore/KeyDerivationParams.java b/android/security/keystore/KeyDerivationParams.java
deleted file mode 100644
index e475dc3..0000000
--- a/android/security/keystore/KeyDerivationParams.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.security.keystore;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.KeyDerivationParams}.
- * @hide
- */
-public final class KeyDerivationParams implements Parcelable {
-    private final int mAlgorithm;
-    private byte[] mSalt;
-
-    /** @hide */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
-    public @interface KeyDerivationAlgorithm {
-    }
-
-    /**
-     * Salted SHA256
-     */
-    public static final int ALGORITHM_SHA256 = 1;
-
-    /**
-     * Argon2ID
-     * @hide
-     */
-    // TODO: add Argon2ID support.
-    public static final int ALGORITHM_ARGON2ID = 2;
-
-    /**
-     * Creates instance of the class to to derive key using salted SHA256 hash.
-     */
-    public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) {
-        return new KeyDerivationParams(ALGORITHM_SHA256, salt);
-    }
-
-    KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
-        mAlgorithm = algorithm;
-        mSalt = Preconditions.checkNotNull(salt);
-    }
-
-    /**
-     * Gets algorithm.
-     */
-    public @KeyDerivationAlgorithm int getAlgorithm() {
-        return mAlgorithm;
-    }
-
-    /**
-     * Gets salt.
-     */
-    public @NonNull byte[] getSalt() {
-        return mSalt;
-    }
-
-    public static final Parcelable.Creator<KeyDerivationParams> CREATOR =
-            new Parcelable.Creator<KeyDerivationParams>() {
-        public KeyDerivationParams createFromParcel(Parcel in) {
-                return new KeyDerivationParams(in);
-        }
-
-        public KeyDerivationParams[] newArray(int length) {
-            return new KeyDerivationParams[length];
-        }
-    };
-
-    /**
-     * @hide
-     */
-    @Override
-    public void writeToParcel(Parcel out, int flags) {
-        out.writeInt(mAlgorithm);
-        out.writeByteArray(mSalt);
-    }
-
-    /**
-     * @hide
-     */
-    protected KeyDerivationParams(Parcel in) {
-        mAlgorithm = in.readInt();
-        mSalt = in.createByteArray();
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-}
diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java
index c0d0fb0..b2e0f67 100644
--- a/android/security/keystore/KeyGenParameterSpec.java
+++ b/android/security/keystore/KeyGenParameterSpec.java
@@ -19,6 +19,7 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.KeyguardManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.security.GateKeeper;
@@ -594,6 +595,14 @@
     /**
      * Returns {@code true} if the key is authorized to be used only if a test of user presence has
      * been performed between the {@code Signature.initSign()} and {@code Signature.sign()} calls.
+     * It requires that the KeyStore implementation have a direct way to validate the user presence
+     * for example a KeyStore hardware backed strongbox can use a button press that is observable
+     * in hardware. A test for user presence is tangential to authentication. The test can be part
+     * of an authentication step as long as this step can be validated by the hardware protecting
+     * the key and cannot be spoofed. For example, a physical button press can be used as a test of
+     * user presence if the other pins connected to the button are not able to simulate a button
+     * press. There must be no way for the primary processor to fake a button press, or that
+     * button must not be used as a test of user presence.
      */
     public boolean isUserPresenceRequired() {
         return mUserPresenceRequired;
@@ -673,8 +682,8 @@
     }
 
     /**
-     * Returns {@code true} if the screen must be unlocked for this key to be used for encryption or
-     * signing. Decryption and signature verification will still be available when the screen is
+     * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
+     * signing. Encryption and signature verification will still be available when the screen is
      * locked.
      *
      * @see Builder#setUnlockedDeviceRequired(boolean)
@@ -1180,6 +1189,14 @@
         /**
          * Sets whether a test of user presence is required to be performed between the
          * {@code Signature.initSign()} and {@code Signature.sign()} method calls.
+         * It requires that the KeyStore implementation have a direct way to validate the user
+         * presence for example a KeyStore hardware backed strongbox can use a button press that
+         * is observable in hardware. A test for user presence is tangential to authentication. The
+         * test can be part of an authentication step as long as this step can be validated by the
+         * hardware protecting the key and cannot be spoofed. For example, a physical button press
+         * can be used as a test of user presence if the other pins connected to the button are not
+         * able to simulate a button press.There must be no way for the primary processor to fake a
+         * button press, or that button must not be used as a test of user presence.
          */
         @NonNull
         public Builder setUserPresenceRequired(boolean required) {
@@ -1227,6 +1244,7 @@
          *
          * Sets whether to include a temporary unique ID field in the attestation certificate.
          */
+        @TestApi
         @NonNull
         public Builder setUniqueIdIncluded(boolean uniqueIdIncluded) {
             mUniqueIdIncluded = uniqueIdIncluded;
diff --git a/android/security/keystore/KeyProtection.java b/android/security/keystore/KeyProtection.java
index 4daf30c..fdcad85 100644
--- a/android/security/keystore/KeyProtection.java
+++ b/android/security/keystore/KeyProtection.java
@@ -19,6 +19,7 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.KeyguardManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.security.GateKeeper;
@@ -445,6 +446,14 @@
     /**
      * Returns {@code true} if the key is authorized to be used only if a test of user presence has
      * been performed between the {@code Signature.initSign()} and {@code Signature.sign()} calls.
+     * It requires that the KeyStore implementation have a direct way to validate the user presence
+     * for example a KeyStore hardware backed strongbox can use a button press that is observable
+     * in hardware. A test for user presence is tangential to authentication. The test can be part
+     * of an authentication step as long as this step can be validated by the hardware protecting
+     * the key and cannot be spoofed. For example, a physical button press can be used as a test of
+     * user presence if the other pins connected to the button are not able to simulate a button
+     * press. There must be no way for the primary processor to fake a button press, or that
+     * button must not be used as a test of user presence.
      */
     public boolean isUserPresenceRequired() {
         return mUserPresenceRequred;
@@ -493,6 +502,7 @@
      * @see KeymasterUtils#addUserAuthArgs
      * @hide
      */
+    @TestApi
     public long getBoundToSpecificSecureUserId() {
         return mBoundToSecureUserId;
     }
@@ -508,8 +518,8 @@
     }
 
     /**
-     * Returns {@code true} if the screen must be unlocked for this key to be used for encryption or
-     * signing. Decryption and signature verification will still be available when the screen is
+     * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
+     * signing. Encryption and signature verification will still be available when the screen is
      * locked.
      *
      * @see Builder#setUnlockedDeviceRequired(boolean)
@@ -840,7 +850,15 @@
 
         /**
          * Sets whether a test of user presence is required to be performed between the
-         * {@code Signature.initSign()} and {@code Signature.sign()} method calls.
+         * {@code Signature.initSign()} and {@code Signature.sign()} method calls. It requires that
+         * the KeyStore implementation have a direct way to validate the user presence for example
+         * a KeyStore hardware backed strongbox can use a button press that is observable in
+         * hardware. A test for user presence is tangential to authentication. The test can be part
+         * of an authentication step as long as this step can be validated by the hardware
+         * protecting the key and cannot be spoofed. For example, a physical button press can be
+         * used as a test of user presence if the other pins connected to the button are not able
+         * to simulate a button press. There must be no way for the primary processor to fake a
+         * button press, or that button must not be used as a test of user presence.
          */
         @NonNull
         public Builder setUserPresenceRequired(boolean required) {
@@ -910,6 +928,7 @@
          * @see KeyProtection#getBoundToSpecificSecureUserId()
          * @hide
          */
+        @TestApi
         public Builder setBoundToSpecificSecureUserId(long secureUserId) {
             mBoundToSecureUserId = secureUserId;
             return this;
diff --git a/android/security/keystore/KeychainProtectionParams.java b/android/security/keystore/KeychainProtectionParams.java
deleted file mode 100644
index 19a087d..0000000
--- a/android/security/keystore/KeychainProtectionParams.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.security.keystore;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.KeyChainProtectionParams}.
- * @hide
- */
-public final class KeychainProtectionParams implements Parcelable {
-    /** @hide */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
-    public @interface UserSecretType {
-    }
-
-    /**
-     * Lockscreen secret is required to recover KeyStore.
-     */
-    public static final int TYPE_LOCKSCREEN = 100;
-
-    /**
-     * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
-     */
-    public static final int TYPE_CUSTOM_PASSWORD = 101;
-
-    /** @hide */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN})
-    public @interface LockScreenUiFormat {
-    }
-
-    /**
-     * Pin with digits only.
-     */
-    public static final int TYPE_PIN = 1;
-
-    /**
-     * Password. String with latin-1 characters only.
-     */
-    public static final int TYPE_PASSWORD = 2;
-
-    /**
-     * Pattern with 3 by 3 grid.
-     */
-    public static final int TYPE_PATTERN = 3;
-
-    @UserSecretType
-    private Integer mUserSecretType;
-
-    @LockScreenUiFormat
-    private Integer mLockScreenUiFormat;
-
-    /**
-     * Parameters of the key derivation function, including algorithm, difficulty, salt.
-     */
-    private KeyDerivationParams mKeyDerivationParams;
-    private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
-
-    /**
-     * @param secret Constructor creates a reference to the secret. Caller must use
-     * @link {#clearSecret} to overwrite its value in memory.
-     * @hide
-     */
-    public KeychainProtectionParams(@UserSecretType int userSecretType,
-            @LockScreenUiFormat int lockScreenUiFormat,
-            @NonNull KeyDerivationParams keyDerivationParams,
-            @NonNull byte[] secret) {
-        mUserSecretType = userSecretType;
-        mLockScreenUiFormat = lockScreenUiFormat;
-        mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams);
-        mSecret = Preconditions.checkNotNull(secret);
-    }
-
-    private KeychainProtectionParams() {
-
-    }
-
-    /**
-     * @see TYPE_LOCKSCREEN
-     * @see TYPE_CUSTOM_PASSWORD
-     */
-    public @UserSecretType int getUserSecretType() {
-        return mUserSecretType;
-    }
-
-    /**
-     * Specifies UX shown to user during recovery.
-     * Default value is {@code TYPE_LOCKSCREEN}
-     *
-     * @see TYPE_PIN
-     * @see TYPE_PASSWORD
-     * @see TYPE_PATTERN
-     */
-    public @LockScreenUiFormat int getLockScreenUiFormat() {
-        return mLockScreenUiFormat;
-    }
-
-    /**
-     * Specifies function used to derive symmetric key from user input
-     * Format is defined in separate util class.
-     */
-    public @NonNull KeyDerivationParams getKeyDerivationParams() {
-        return mKeyDerivationParams;
-    }
-
-    /**
-     * Secret derived from user input.
-     * Default value is empty array
-     *
-     * @return secret or empty array
-     */
-    public @NonNull byte[] getSecret() {
-        return mSecret;
-    }
-
-    /**
-     * Builder for creating {@link KeychainProtectionParams}.
-     */
-    public static class Builder {
-        private KeychainProtectionParams mInstance = new KeychainProtectionParams();
-
-        /**
-         * Sets user secret type.
-         *
-         * @see TYPE_LOCKSCREEN
-         * @see TYPE_CUSTOM_PASSWORD
-         * @param userSecretType The secret type
-         * @return This builder.
-         */
-        public Builder setUserSecretType(@UserSecretType int userSecretType) {
-            mInstance.mUserSecretType = userSecretType;
-            return this;
-        }
-
-        /**
-         * Sets UI format.
-         *
-         * @see TYPE_PIN
-         * @see TYPE_PASSWORD
-         * @see TYPE_PATTERN
-         * @param lockScreenUiFormat The UI format
-         * @return This builder.
-         */
-        public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) {
-            mInstance.mLockScreenUiFormat = lockScreenUiFormat;
-            return this;
-        }
-
-        /**
-         * Sets parameters of the key derivation function.
-         *
-         * @param keyDerivationParams Key derivation Params
-         * @return This builder.
-         */
-        public Builder setKeyDerivationParams(@NonNull KeyDerivationParams
-                keyDerivationParams) {
-            mInstance.mKeyDerivationParams = keyDerivationParams;
-            return this;
-        }
-
-        /**
-         * Secret derived from user input, or empty array.
-         *
-         * @param secret The secret.
-         * @return This builder.
-         */
-        public Builder setSecret(@NonNull byte[] secret) {
-            mInstance.mSecret = secret;
-            return this;
-        }
-
-
-        /**
-         * Creates a new {@link KeychainProtectionParams} instance.
-         * The instance will include default values, if {@link setSecret}
-         * or {@link setUserSecretType} were not called.
-         *
-         * @return new instance
-         * @throws NullPointerException if some required fields were not set.
-         */
-        @NonNull public KeychainProtectionParams build() {
-            if (mInstance.mUserSecretType == null) {
-                mInstance.mUserSecretType = TYPE_LOCKSCREEN;
-            }
-            Preconditions.checkNotNull(mInstance.mLockScreenUiFormat);
-            Preconditions.checkNotNull(mInstance.mKeyDerivationParams);
-            if (mInstance.mSecret == null) {
-                mInstance.mSecret = new byte[]{};
-            }
-            return mInstance;
-        }
-    }
-
-    /**
-     * Removes secret from memory than object is no longer used.
-     * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
-     */
-    @Override
-    protected void finalize() throws Throwable {
-        clearSecret();
-        super.finalize();
-    }
-
-    /**
-     * Fills mSecret with zeroes.
-     */
-    public void clearSecret() {
-        Arrays.fill(mSecret, (byte) 0);
-    }
-
-    public static final Parcelable.Creator<KeychainProtectionParams> CREATOR =
-            new Parcelable.Creator<KeychainProtectionParams>() {
-        public KeychainProtectionParams createFromParcel(Parcel in) {
-            return new KeychainProtectionParams(in);
-        }
-
-        public KeychainProtectionParams[] newArray(int length) {
-            return new KeychainProtectionParams[length];
-        }
-    };
-
-    @Override
-    public void writeToParcel(Parcel out, int flags) {
-        out.writeInt(mUserSecretType);
-        out.writeInt(mLockScreenUiFormat);
-        out.writeTypedObject(mKeyDerivationParams, flags);
-        out.writeByteArray(mSecret);
-    }
-
-    /**
-     * @hide
-     */
-    protected KeychainProtectionParams(Parcel in) {
-        mUserSecretType = in.readInt();
-        mLockScreenUiFormat = in.readInt();
-        mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR);
-        mSecret = in.createByteArray();
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-}
diff --git a/android/security/keystore/KeychainSnapshot.java b/android/security/keystore/KeychainSnapshot.java
deleted file mode 100644
index cf18fd1..0000000
--- a/android/security/keystore/KeychainSnapshot.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.security.keystore;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.util.List;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.KeyChainSnapshot}.
- * @hide
- */
-public final class KeychainSnapshot implements Parcelable {
-    private static final int DEFAULT_MAX_ATTEMPTS = 10;
-    private static final long DEFAULT_COUNTER_ID = 1L;
-
-    private int mSnapshotVersion;
-    private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
-    private long mCounterId = DEFAULT_COUNTER_ID;
-    private byte[] mServerParams;
-    private byte[] mPublicKey;
-    private List<KeychainProtectionParams> mKeychainProtectionParams;
-    private List<WrappedApplicationKey> mEntryRecoveryData;
-    private byte[] mEncryptedRecoveryKeyBlob;
-
-    /**
-     * @hide
-     * Deprecated, consider using builder.
-     */
-    public KeychainSnapshot(
-            int snapshotVersion,
-            @NonNull List<KeychainProtectionParams> keychainProtectionParams,
-            @NonNull List<WrappedApplicationKey> wrappedApplicationKeys,
-            @NonNull byte[] encryptedRecoveryKeyBlob) {
-        mSnapshotVersion = snapshotVersion;
-        mKeychainProtectionParams =
-                Preconditions.checkCollectionElementsNotNull(keychainProtectionParams,
-                        "keychainProtectionParams");
-        mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys,
-                "wrappedApplicationKeys");
-        mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
-    }
-
-    private KeychainSnapshot() {
-
-    }
-
-    /**
-     * Snapshot version for given account. It is incremented when user secret or list of application
-     * keys changes.
-     */
-    public int getSnapshotVersion() {
-        return mSnapshotVersion;
-    }
-
-    /**
-     * Number of user secret guesses allowed during Keychain recovery.
-     */
-    public int getMaxAttempts() {
-        return mMaxAttempts;
-    }
-
-    /**
-     * CounterId which is rotated together with user secret.
-     */
-    public long getCounterId() {
-        return mCounterId;
-    }
-
-    /**
-     * Server parameters.
-     */
-    public @NonNull byte[] getServerParams() {
-        return mServerParams;
-    }
-
-    /**
-     * Public key used to encrypt {@code encryptedRecoveryKeyBlob}.
-     *
-     * See implementation for binary key format
-     */
-    // TODO: document key format.
-    public @NonNull byte[] getTrustedHardwarePublicKey() {
-        return mPublicKey;
-    }
-
-    /**
-     * UI and key derivation parameters. Note that combination of secrets may be used.
-     */
-    public @NonNull List<KeychainProtectionParams> getKeychainProtectionParams() {
-        return mKeychainProtectionParams;
-    }
-
-    /**
-     * List of application keys, with key material encrypted by
-     * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
-     */
-    public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
-        return mEntryRecoveryData;
-    }
-
-    /**
-     * Recovery key blob, encrypted by user secret and recovery service public key.
-     */
-    public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
-        return mEncryptedRecoveryKeyBlob;
-    }
-
-    public static final Parcelable.Creator<KeychainSnapshot> CREATOR =
-            new Parcelable.Creator<KeychainSnapshot>() {
-        public KeychainSnapshot createFromParcel(Parcel in) {
-            return new KeychainSnapshot(in);
-        }
-
-        public KeychainSnapshot[] newArray(int length) {
-            return new KeychainSnapshot[length];
-        }
-    };
-
-    /**
-     * Builder for creating {@link KeychainSnapshot}.
-     *
-     * @hide
-     */
-    public static class Builder {
-        private KeychainSnapshot mInstance = new KeychainSnapshot();
-
-        /**
-         * Snapshot version for given account.
-         *
-         * @param snapshotVersion The snapshot version
-         * @return This builder.
-         */
-        public Builder setSnapshotVersion(int snapshotVersion) {
-            mInstance.mSnapshotVersion = snapshotVersion;
-            return this;
-        }
-
-        /**
-         * Sets the number of user secret guesses allowed during Keychain recovery.
-         *
-         * @param maxAttempts The maximum number of guesses.
-         * @return This builder.
-         */
-        public Builder setMaxAttempts(int maxAttempts) {
-            mInstance.mMaxAttempts = maxAttempts;
-            return this;
-        }
-
-        /**
-         * Sets counter id.
-         *
-         * @param counterId The counter id.
-         * @return This builder.
-         */
-        public Builder setCounterId(long counterId) {
-            mInstance.mCounterId = counterId;
-            return this;
-        }
-
-        /**
-         * Sets server parameters.
-         *
-         * @param serverParams The server parameters
-         * @return This builder.
-         */
-        public Builder setServerParams(byte[] serverParams) {
-            mInstance.mServerParams = serverParams;
-            return this;
-        }
-
-        /**
-         * Sets public key used to encrypt recovery blob.
-         *
-         * @param publicKey The public key
-         * @return This builder.
-         */
-        public Builder setTrustedHardwarePublicKey(byte[] publicKey) {
-            mInstance.mPublicKey = publicKey;
-            return this;
-        }
-
-        /**
-         * Sets UI and key derivation parameters
-         *
-         * @param recoveryMetadata The UI and key derivation parameters
-         * @return This builder.
-         */
-        public Builder setKeychainProtectionParams(
-                @NonNull List<KeychainProtectionParams> recoveryMetadata) {
-            mInstance.mKeychainProtectionParams = recoveryMetadata;
-            return this;
-        }
-
-        /**
-         * List of application keys.
-         *
-         * @param entryRecoveryData List of application keys
-         * @return This builder.
-         */
-        public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) {
-            mInstance.mEntryRecoveryData = entryRecoveryData;
-            return this;
-        }
-
-        /**
-         * Sets recovery key blob
-         *
-         * @param encryptedRecoveryKeyBlob The recovery key blob.
-         * @return This builder.
-         */
-        public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) {
-            mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
-            return this;
-        }
-
-
-        /**
-         * Creates a new {@link KeychainSnapshot} instance.
-         *
-         * @return new instance
-         * @throws NullPointerException if some required fields were not set.
-         */
-        @NonNull public KeychainSnapshot build() {
-            Preconditions.checkCollectionElementsNotNull(mInstance.mKeychainProtectionParams,
-                    "recoveryMetadata");
-            Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
-                    "entryRecoveryData");
-            Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob);
-            Preconditions.checkNotNull(mInstance.mServerParams);
-            Preconditions.checkNotNull(mInstance.mPublicKey);
-            return mInstance;
-        }
-    }
-
-    @Override
-    public void writeToParcel(Parcel out, int flags) {
-        out.writeInt(mSnapshotVersion);
-        out.writeTypedList(mKeychainProtectionParams);
-        out.writeByteArray(mEncryptedRecoveryKeyBlob);
-        out.writeTypedList(mEntryRecoveryData);
-    }
-
-    /**
-     * @hide
-     */
-    protected KeychainSnapshot(Parcel in) {
-        mSnapshotVersion = in.readInt();
-        mKeychainProtectionParams = in.createTypedArrayList(KeychainProtectionParams.CREATOR);
-        mEncryptedRecoveryKeyBlob = in.createByteArray();
-        mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-}
diff --git a/android/security/keystore/LockScreenRequiredException.java b/android/security/keystore/LockScreenRequiredException.java
deleted file mode 100644
index 0970284..0000000
--- a/android/security/keystore/LockScreenRequiredException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.LockScreenRequiredException}.
- * @hide
- */
-public class LockScreenRequiredException extends RecoveryControllerException {
-    public LockScreenRequiredException(String msg) {
-        super(msg);
-    }
-}
diff --git a/android/security/keystore/RecoveryClaim.java b/android/security/keystore/RecoveryClaim.java
deleted file mode 100644
index 12be607..0000000
--- a/android/security/keystore/RecoveryClaim.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoverySession}.
- * @hide
- */
-public class RecoveryClaim {
-
-    private final RecoverySession mRecoverySession;
-    private final byte[] mClaimBytes;
-
-    RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) {
-        mRecoverySession = recoverySession;
-        mClaimBytes = claimBytes;
-    }
-
-    /**
-     * Returns the session associated with the recovery attempt. This is used to match the symmetric
-     * key, which remains internal to the framework, for decrypting the claim response.
-     *
-     * @return The session data.
-     */
-    public RecoverySession getRecoverySession() {
-        return mRecoverySession;
-    }
-
-    /**
-     * Returns the encrypted claim's bytes.
-     *
-     * <p>This should be sent by the recovery agent to the remote secure hardware, which will use
-     * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key
-     * to the device.
-     */
-    public byte[] getClaimBytes() {
-        return mClaimBytes;
-    }
-}
diff --git a/android/security/keystore/RecoveryController.java b/android/security/keystore/RecoveryController.java
deleted file mode 100644
index ca67e35..0000000
--- a/android/security/keystore/RecoveryController.java
+++ /dev/null
@@ -1,467 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.security.keystore;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.PendingIntent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-import android.util.Log;
-
-import com.android.internal.widget.ILockSettings;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoveryController}.
- * @hide
- */
-public class RecoveryController {
-    private static final String TAG = "RecoveryController";
-
-    /** Key has been successfully synced. */
-    public static final int RECOVERY_STATUS_SYNCED = 0;
-    /** Waiting for recovery agent to sync the key. */
-    public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
-    /** Recovery account is not available. */
-    public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
-    /** Key cannot be synced. */
-    public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
-
-    /**
-     * Failed because no snapshot is yet pending to be synced for the user.
-     *
-     * @hide
-     */
-    public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
-
-    /**
-     * Failed due to an error internal to the recovery service. This is unexpected and indicates
-     * either a problem with the logic in the service, or a problem with a dependency of the
-     * service (such as AndroidKeyStore).
-     *
-     * @hide
-     */
-    public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
-
-    /**
-     * Failed because the user does not have a lock screen set.
-     *
-     * @hide
-     */
-    public static final int ERROR_INSECURE_USER = 23;
-
-    /**
-     * Error thrown when attempting to use a recovery session that has since been closed.
-     *
-     * @hide
-     */
-    public static final int ERROR_SESSION_EXPIRED = 24;
-
-    /**
-     * Failed because the provided certificate was not a valid X509 certificate.
-     *
-     * @hide
-     */
-    public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
-
-    /**
-     * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
-     * the data has become corrupted, the data has been tampered with, etc.
-     *
-     * @hide
-     */
-    public static final int ERROR_DECRYPTION_FAILED = 26;
-
-
-    private final ILockSettings mBinder;
-
-    private RecoveryController(ILockSettings binder) {
-        mBinder = binder;
-    }
-
-    /**
-     * Deprecated.
-     * Gets a new instance of the class.
-     */
-    public static RecoveryController getInstance() {
-        throw new UnsupportedOperationException("using Deprecated RecoveryController version");
-    }
-
-    /**
-     * Initializes key recovery service for the calling application. RecoveryController
-     * randomly chooses one of the keys from the list and keeps it to use for future key export
-     * operations. Collection of all keys in the list must be signed by the provided {@code
-     * rootCertificateAlias}, which must also be present in the list of root certificates
-     * preinstalled on the device. The random selection allows RecoveryController to select
-     * which of a set of remote recovery service devices will be used.
-     *
-     * <p>In addition, RecoveryController enforces a delay of three months between
-     * consecutive initialization attempts, to limit the ability of an attacker to often switch
-     * remote recovery devices and significantly increase number of recovery attempts.
-     *
-     * @param rootCertificateAlias alias of a root certificate preinstalled on the device
-     * @param signedPublicKeyList binary blob a list of X509 certificates and signature
-     * @throws BadCertificateFormatException if the {@code signedPublicKeyList} is in a bad format.
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public void initRecoveryService(
-            @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
-            throws BadCertificateFormatException, InternalRecoveryServiceException {
-        throw new UnsupportedOperationException("Deprecated initRecoveryService method called");
-
-    }
-
-    /**
-     * Returns data necessary to store all recoverable keys for given account. Key material is
-     * encrypted with user secret and recovery public key.
-     *
-     * @param account specific to Recovery agent.
-     * @return Data necessary to recover keystore.
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public @NonNull KeychainSnapshot getRecoveryData(@NonNull byte[] account)
-            throws InternalRecoveryServiceException {
-        try {
-            return BackwardsCompat.toLegacyKeychainSnapshot(mBinder.getKeyChainSnapshot());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
-                return null;
-            }
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
-     * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
-     * most one registered listener at any time.
-     *
-     * @param intent triggered when new snapshot is available. Unregisters listener if the value is
-     *     {@code null}.
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
-            throws InternalRecoveryServiceException {
-        try {
-            mBinder.setSnapshotCreatedPendingIntent(intent);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot
-     * version. Version zero is used, if no snapshots were created for the account.
-     *
-     * @return Map from recovery agent accounts to snapshot versions.
-     * @see KeychainSnapshot#getSnapshotVersion
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
-            throws InternalRecoveryServiceException {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Server parameters used to generate new recovery key blobs. This value will be included in
-     * {@code KeychainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
-     * in vaultParams {@link #startRecoverySession}
-     *
-     * @param serverParams included in recovery key blob.
-     * @see #getRecoveryData
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
-        try {
-            mBinder.setServerParams(serverParams);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Updates recovery status for given keys. It is used to notify keystore that key was
-     * successfully stored on the server or there were an error. Application can check this value
-     * using {@code getRecoveyStatus}.
-     *
-     * @param packageName Application whose recoverable keys' statuses are to be updated.
-     * @param aliases List of application-specific key aliases. If the array is empty, updates the
-     *     status for all existing recoverable keys.
-     * @param status Status specific to recovery agent.
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public void setRecoveryStatus(
-            @NonNull String packageName, @Nullable String[] aliases, int status)
-            throws NameNotFoundException, InternalRecoveryServiceException {
-        try {
-            for (String alias : aliases) {
-                mBinder.setRecoveryStatus(alias, status);
-            }
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status.
-     * Negative status values are reserved for recovery agent specific codes. List of common codes:
-     *
-     * <ul>
-     *   <li>{@link #RECOVERY_STATUS_SYNCED}
-     *   <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
-     *   <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
-     *   <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
-     * </ul>
-     *
-     * @return {@code Map} from KeyStore alias to recovery status.
-     * @see #setRecoveryStatus
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public Map<String, Integer> getRecoveryStatus() throws InternalRecoveryServiceException {
-        try {
-            // IPC doesn't support generic Maps.
-            @SuppressWarnings("unchecked")
-            Map<String, Integer> result =
-                    (Map<String, Integer>) mBinder.getRecoveryStatus();
-            return result;
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
-     * is necessary to recover data.
-     *
-     * @param secretTypes {@link KeychainProtectionParams#TYPE_LOCKSCREEN} or {@link
-     *     KeychainProtectionParams#TYPE_CUSTOM_PASSWORD}
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public void setRecoverySecretTypes(
-            @NonNull @KeychainProtectionParams.UserSecretType int[] secretTypes)
-            throws InternalRecoveryServiceException {
-        try {
-            mBinder.setRecoverySecretTypes(secretTypes);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
-     * necessary to generate KeychainSnapshot.
-     *
-     * @return list of recovery secret types
-     * @see KeychainSnapshot
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public @NonNull @KeychainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
-            throws InternalRecoveryServiceException {
-        try {
-            return mBinder.getRecoverySecretTypes();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
-     * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
-     * called.
-     *
-     * @return list of recovery secret types
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    @NonNull
-    public @KeychainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
-            throws InternalRecoveryServiceException {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Initializes recovery session and returns a blob with proof of recovery secrets possession.
-     * The method generates symmetric key for a session, which trusted remote device can use to
-     * return recovery key.
-     *
-     * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
-     * used to create the recovery blob on the source device.
-     * Keystore will verify the certificate using root of trust.
-     * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
-     *     Used to limit number of guesses.
-     * @param vaultChallenge Data passed from server for this recovery session and used to prevent
-     *     replay attacks
-     * @param secrets Secrets provided by user, the method only uses type and secret fields.
-     * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
-     *     encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
-     *     key and parameters necessary to identify the counter with the number of failed recovery
-     *     attempts.
-     * @throws BadCertificateFormatException if the {@code verifierPublicKey} is in an incorrect
-     *     format.
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    @NonNull public RecoveryClaim startRecoverySession(
-            @NonNull byte[] verifierPublicKey,
-            @NonNull byte[] vaultParams,
-            @NonNull byte[] vaultChallenge,
-            @NonNull List<KeychainProtectionParams> secrets)
-            throws BadCertificateFormatException, InternalRecoveryServiceException {
-        try {
-            RecoverySession recoverySession = RecoverySession.newInstance(this);
-            byte[] recoveryClaim =
-                    mBinder.startRecoverySession(
-                            recoverySession.getSessionId(),
-                            verifierPublicKey,
-                            vaultParams,
-                            vaultChallenge,
-                            BackwardsCompat.fromLegacyKeychainProtectionParams(secrets));
-            return new RecoveryClaim(recoverySession, recoveryClaim);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
-                throw new BadCertificateFormatException(e.getMessage());
-            }
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Imports keys.
-     *
-     * @param session Related recovery session, as originally created by invoking
-     *        {@link #startRecoverySession(byte[], byte[], byte[], List)}.
-     * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
-     * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
-     *     and session. KeyStore only uses package names from the application info in {@link
-     *     WrappedApplicationKey}. Caller is responsibility to perform certificates check.
-     * @return Map from alias to raw key material.
-     * @throws SessionExpiredException if {@code session} has since been closed.
-     * @throws DecryptionFailedException if unable to decrypt the snapshot.
-     * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
-     */
-    public Map<String, byte[]> recoverKeys(
-            @NonNull RecoverySession session,
-            @NonNull byte[] recoveryKeyBlob,
-            @NonNull List<WrappedApplicationKey> applicationKeys)
-            throws SessionExpiredException, DecryptionFailedException,
-            InternalRecoveryServiceException {
-        try {
-            return (Map<String, byte[]>) mBinder.recoverKeys(
-                    session.getSessionId(),
-                    recoveryKeyBlob,
-                    BackwardsCompat.fromLegacyWrappedApplicationKeys(applicationKeys));
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            if (e.errorCode == ERROR_DECRYPTION_FAILED) {
-                throw new DecryptionFailedException(e.getMessage());
-            }
-            if (e.errorCode == ERROR_SESSION_EXPIRED) {
-                throw new SessionExpiredException(e.getMessage());
-            }
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    /**
-     * Deletes all data associated with {@code session}. Should not be invoked directly but via
-     * {@link RecoverySession#close()}.
-     *
-     * @hide
-     */
-    void closeSession(RecoverySession session) {
-        try {
-            mBinder.closeSession(session.getSessionId());
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Unexpected error trying to close session", e);
-        }
-    }
-
-    /**
-     * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the
-     * raw material of the key.
-     *
-     * @param alias The key alias.
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
-     *     to generate recoverable keys, as the snapshots are encrypted using a key derived from the
-     *     lock screen.
-     */
-    public byte[] generateAndStoreKey(@NonNull String alias)
-            throws InternalRecoveryServiceException, LockScreenRequiredException {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Removes a key called {@code alias} from the recoverable key store.
-     *
-     * @param alias The key alias.
-     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
-     *     service.
-     */
-    public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
-        try {
-            mBinder.removeKey(alias);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        } catch (ServiceSpecificException e) {
-            throw wrapUnexpectedServiceSpecificException(e);
-        }
-    }
-
-    private InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
-            ServiceSpecificException e) {
-        if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
-            return new InternalRecoveryServiceException(e.getMessage());
-        }
-
-        // Should never happen. If it does, it's a bug, and we need to update how the method that
-        // called this throws its exceptions.
-        return new InternalRecoveryServiceException("Unexpected error code for method: "
-                + e.errorCode, e);
-    }
-}
diff --git a/android/security/keystore/RecoveryControllerException.java b/android/security/keystore/RecoveryControllerException.java
deleted file mode 100644
index f990c23..0000000
--- a/android/security/keystore/RecoveryControllerException.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-import java.security.GeneralSecurityException;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoveryController}.
- * @hide
- */
-public abstract class RecoveryControllerException extends GeneralSecurityException {
-    RecoveryControllerException() { }
-
-    RecoveryControllerException(String msg) {
-        super(msg);
-    }
-
-    public RecoveryControllerException(String message, Throwable cause) {
-        super(message, cause);
-    }
-}
diff --git a/android/security/keystore/RecoverySession.java b/android/security/keystore/RecoverySession.java
deleted file mode 100644
index 8a3e06b..0000000
--- a/android/security/keystore/RecoverySession.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.security.keystore;
-
-import java.security.SecureRandom;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoverySession}.
- * @hide
- */
-public class RecoverySession implements AutoCloseable {
-
-    private static final int SESSION_ID_LENGTH_BYTES = 16;
-
-    private final String mSessionId;
-    private final RecoveryController mRecoveryController;
-
-    private RecoverySession(RecoveryController recoveryController, String sessionId) {
-        mRecoveryController = recoveryController;
-        mSessionId = sessionId;
-    }
-
-    /**
-     * A new session, started by {@code recoveryManager}.
-     */
-    static RecoverySession newInstance(RecoveryController recoveryController) {
-        return new RecoverySession(recoveryController, newSessionId());
-    }
-
-    /**
-     * Returns a new random session ID.
-     */
-    private static String newSessionId() {
-        SecureRandom secureRandom = new SecureRandom();
-        byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
-        secureRandom.nextBytes(sessionId);
-        StringBuilder sb = new StringBuilder();
-        for (byte b : sessionId) {
-            sb.append(Byte.toHexString(b, /*upperCase=*/ false));
-        }
-        return sb.toString();
-    }
-
-    /**
-     * An internal session ID, used by the framework to match recovery claims to snapshot responses.
-     */
-    String getSessionId() {
-        return mSessionId;
-    }
-
-    @Override
-    public void close() {
-        mRecoveryController.closeSession(this);
-    }
-}
diff --git a/android/security/keystore/UserPresenceUnavailableException.java b/android/security/keystore/UserPresenceUnavailableException.java
index cf4099e..1b053a5 100644
--- a/android/security/keystore/UserPresenceUnavailableException.java
+++ b/android/security/keystore/UserPresenceUnavailableException.java
@@ -16,13 +16,13 @@
 
 package android.security.keystore;
 
-import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
 
 /**
  * Indicates the condition that a proof of user-presence was
  * requested but this proof was not presented.
  */
-public class UserPresenceUnavailableException extends InvalidAlgorithmParameterException {
+public class UserPresenceUnavailableException extends InvalidKeyException {
     /**
      * Constructs a {@code UserPresenceUnavailableException} without a detail message or cause.
      */
diff --git a/android/security/keystore/WrappedApplicationKey.java b/android/security/keystore/WrappedApplicationKey.java
deleted file mode 100644
index 2ce8c7d..0000000
--- a/android/security/keystore/WrappedApplicationKey.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.security.keystore;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.WrappedApplicationKey}.
- * @hide
- */
-public final class WrappedApplicationKey implements Parcelable {
-    private String mAlias;
-    // The only supported format is AES-256 symmetric key.
-    private byte[] mEncryptedKeyMaterial;
-
-    /**
-     * Builder for creating {@link WrappedApplicationKey}.
-     */
-    public static class Builder {
-        private WrappedApplicationKey mInstance = new WrappedApplicationKey();
-
-        /**
-         * Sets Application-specific alias of the key.
-         *
-         * @param alias The alias.
-         * @return This builder.
-         */
-        public Builder setAlias(@NonNull String alias) {
-            mInstance.mAlias = alias;
-            return this;
-        }
-
-        /**
-         * Sets key material encrypted by recovery key.
-         *
-         * @param encryptedKeyMaterial The key material
-         * @return This builder
-         */
-
-        public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
-            mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
-            return this;
-        }
-
-        /**
-         * Creates a new {@link WrappedApplicationKey} instance.
-         *
-         * @return new instance
-         * @throws NullPointerException if some required fields were not set.
-         */
-        @NonNull public WrappedApplicationKey build() {
-            Preconditions.checkNotNull(mInstance.mAlias);
-            Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial);
-            return mInstance;
-        }
-    }
-
-    private WrappedApplicationKey() {
-
-    }
-
-    /**
-     * Deprecated - consider using Builder.
-     * @hide
-     */
-    public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
-        mAlias = Preconditions.checkNotNull(alias);
-        mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
-    }
-
-    /**
-     * Application-specific alias of the key.
-     *
-     * @see java.security.KeyStore.aliases
-     */
-    public @NonNull String getAlias() {
-        return mAlias;
-    }
-
-    /** Key material encrypted by recovery key. */
-    public @NonNull byte[] getEncryptedKeyMaterial() {
-        return mEncryptedKeyMaterial;
-    }
-
-    public static final Parcelable.Creator<WrappedApplicationKey> CREATOR =
-            new Parcelable.Creator<WrappedApplicationKey>() {
-                public WrappedApplicationKey createFromParcel(Parcel in) {
-                    return new WrappedApplicationKey(in);
-                }
-
-                public WrappedApplicationKey[] newArray(int length) {
-                    return new WrappedApplicationKey[length];
-                }
-            };
-
-    /**
-     * @hide
-     */
-    @Override
-    public void writeToParcel(Parcel out, int flags) {
-        out.writeString(mAlias);
-        out.writeByteArray(mEncryptedKeyMaterial);
-    }
-
-    /**
-     * @hide
-     */
-    protected WrappedApplicationKey(Parcel in) {
-        mAlias = in.readString();
-        mEncryptedKeyMaterial = in.createByteArray();
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-}
diff --git a/android/security/keystore/recovery/RecoveryController.java b/android/security/keystore/recovery/RecoveryController.java
index 281822a..b84843b 100644
--- a/android/security/keystore/recovery/RecoveryController.java
+++ b/android/security/keystore/recovery/RecoveryController.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.app.KeyguardManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -250,6 +251,16 @@
      */
     public static final int ERROR_INVALID_CERTIFICATE = 28;
 
+
+    /**
+     * Failed because the provided certificate contained serial version which is lower that the
+     * version device is already initialized with. It is not possible to downgrade serial version of
+     * the provided certificate.
+     *
+     * @hide
+     */
+    public static final int ERROR_DOWNGRADE_CERTIFICATE = 29;
+
     private final ILockSettings mBinder;
     private final KeyStore mKeyStore;
 
@@ -278,6 +289,18 @@
     }
 
     /**
+     * Checks whether the recoverable key store is currently available.
+     *
+     * <p>If it returns true, the device must currently be using a screen lock that is supported for
+     * use with the recoverable key store, i.e. AOSP PIN, pattern or password.
+     */
+    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+    public static boolean isRecoverableKeyStoreEnabled(@NonNull Context context) {
+        KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+        return keyguardManager != null && keyguardManager.isDeviceSecure();
+    }
+
+    /**
      * @deprecated Use {@link #initRecoveryService(String, byte[], byte[])} instead.
      */
     @Deprecated
@@ -340,6 +363,10 @@
                     || e.errorCode == ERROR_INVALID_CERTIFICATE) {
                 throw new CertificateException("Invalid certificate for recovery service", e);
             }
+            if (e.errorCode == ERROR_DOWNGRADE_CERTIFICATE) {
+                throw new CertificateException(
+                        "Downgrading certificate serial version isn't supported.", e);
+            }
             throw wrapUnexpectedServiceSpecificException(e);
         }
     }
diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java
index 3726e66..32737c5 100644
--- a/android/service/notification/NotificationListenerService.java
+++ b/android/service/notification/NotificationListenerService.java
@@ -28,6 +28,7 @@
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
 import android.app.NotificationManager;
+import android.app.Person;
 import android.app.Service;
 import android.companion.CompanionDeviceManager;
 import android.content.ComponentName;
@@ -1195,13 +1196,13 @@
      */
     private void maybePopulatePeople(Notification notification) {
         if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) {
-            ArrayList<Notification.Person> people = notification.extras.getParcelableArrayList(
+            ArrayList<Person> people = notification.extras.getParcelableArrayList(
                     Notification.EXTRA_PEOPLE_LIST);
             if (people != null && people.isEmpty()) {
                 int size = people.size();
                 String[] peopleArray = new String[size];
                 for (int i = 0; i < size; i++) {
-                    Notification.Person person = people.get(i);
+                    Person person = people.get(i);
                     peopleArray[i] = person.resolveToLegacyUri();
                 }
                 notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray);
diff --git a/android/service/notification/ZenModeConfig.java b/android/service/notification/ZenModeConfig.java
index 3830b7a..7b01f7a 100644
--- a/android/service/notification/ZenModeConfig.java
+++ b/android/service/notification/ZenModeConfig.java
@@ -83,7 +83,8 @@
     private static final int DAY_MINUTES = 24 * 60;
     private static final int ZERO_VALUE_MS = 10 * SECONDS_MS;
 
-    // Default allow categories set in readXml() from default_zen_mode_config.xml, fallback values:
+    // Default allow categories set in readXml() from default_zen_mode_config.xml,
+    // fallback/upgrade values:
     private static final boolean DEFAULT_ALLOW_ALARMS = true;
     private static final boolean DEFAULT_ALLOW_MEDIA = true;
     private static final boolean DEFAULT_ALLOW_SYSTEM = false;
@@ -97,7 +98,7 @@
     private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS =
             Policy.getAllSuppressedVisualEffects();
 
-    public static final int XML_VERSION = 6;
+    public static final int XML_VERSION = 7;
     public static final String ZEN_TAG = "zen";
     private static final String ZEN_ATT_VERSION = "version";
     private static final String ZEN_ATT_USER = "user";
@@ -1487,14 +1488,18 @@
     /**
      * Returns a description of the current do not disturb settings from config.
      * - If turned on manually and end time is known, returns end time.
+     * - If turned on manually and end time is on forever until turned off, return null if
+     * describeForeverCondition is false, else return String describing indefinite behavior
      * - If turned on by an automatic rule, returns the automatic rule name.
      * - If on due to an app, returns the app name.
      * - If there's a combination of rules/apps that trigger, then shows the one that will
      *  last the longest if applicable.
-     * @return null if do not disturb is off.
+     * @return null if DND is off or describeForeverCondition is false and
+     * DND is on forever (until turned off)
      */
-    public static String getDescription(Context context, boolean zenOn, ZenModeConfig config) {
-        if (!zenOn) {
+    public static String getDescription(Context context, boolean zenOn, ZenModeConfig config,
+            boolean describeForeverCondition) {
+        if (!zenOn || config == null) {
             return null;
         }
 
@@ -1513,8 +1518,11 @@
             } else {
                 if (id == null) {
                     // Do not disturb manually triggered to remain on forever until turned off
-                    // No subtext
-                    return null;
+                    if (describeForeverCondition) {
+                        return context.getString(R.string.zen_mode_forever);
+                    } else {
+                        return null;
+                    }
                 } else {
                     latestEndTime = tryParseCountdownConditionId(id);
                     if (latestEndTime > 0) {
diff --git a/android/service/textclassifier/TextClassifierService.java b/android/service/textclassifier/TextClassifierService.java
index f1bb72c..b461c0d 100644
--- a/android/service/textclassifier/TextClassifierService.java
+++ b/android/service/textclassifier/TextClassifierService.java
@@ -17,6 +17,7 @@
 package android.service.textclassifier;
 
 import android.Manifest;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -97,7 +98,8 @@
             Preconditions.checkNotNull(request);
             Preconditions.checkNotNull(callback);
             TextClassifierService.this.onSuggestSelection(
-                    sessionId, request, mCancellationSignal,
+                    request.getText(), request.getStartIndex(), request.getEndIndex(),
+                    TextSelection.Options.from(sessionId, request), mCancellationSignal,
                     new Callback<TextSelection>() {
                         @Override
                         public void onSuccess(TextSelection result) {
@@ -130,7 +132,8 @@
             Preconditions.checkNotNull(request);
             Preconditions.checkNotNull(callback);
             TextClassifierService.this.onClassifyText(
-                    sessionId, request, mCancellationSignal,
+                    request.getText(), request.getStartIndex(), request.getEndIndex(),
+                    TextClassification.Options.from(sessionId, request), mCancellationSignal,
                     new Callback<TextClassification>() {
                         @Override
                         public void onSuccess(TextClassification result) {
@@ -161,7 +164,8 @@
             Preconditions.checkNotNull(request);
             Preconditions.checkNotNull(callback);
             TextClassifierService.this.onGenerateLinks(
-                    sessionId, request, mCancellationSignal,
+                    request.getText(), TextLinks.Options.from(sessionId, request),
+                    mCancellationSignal,
                     new Callback<TextLinks>() {
                         @Override
                         public void onSuccess(TextLinks result) {
@@ -234,6 +238,25 @@
             @NonNull CancellationSignal cancellationSignal,
             @NonNull Callback<TextSelection> callback);
 
+    // TODO: Remove once apps can build against the latest sdk.
+    /** @hide */
+    public void onSuggestSelection(
+            @NonNull CharSequence text,
+            @IntRange(from = 0) int selectionStartIndex,
+            @IntRange(from = 0) int selectionEndIndex,
+            @Nullable TextSelection.Options options,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull Callback<TextSelection> callback) {
+        final TextClassificationSessionId sessionId = options.getSessionId();
+        final TextSelection.Request request = options.getRequest() != null
+                ? options.getRequest()
+                : new TextSelection.Request.Builder(
+                        text, selectionStartIndex, selectionEndIndex)
+                        .setDefaultLocales(options.getDefaultLocales())
+                        .build();
+        onSuggestSelection(sessionId, request, cancellationSignal, callback);
+    }
+
     /**
      * Classifies the specified text and returns a {@link TextClassification} object that can be
      * used to generate a widget for handling the classified text.
@@ -249,6 +272,26 @@
             @NonNull CancellationSignal cancellationSignal,
             @NonNull Callback<TextClassification> callback);
 
+    // TODO: Remove once apps can build against the latest sdk.
+    /** @hide */
+    public void onClassifyText(
+            @NonNull CharSequence text,
+            @IntRange(from = 0) int startIndex,
+            @IntRange(from = 0) int endIndex,
+            @Nullable TextClassification.Options options,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull Callback<TextClassification> callback) {
+        final TextClassificationSessionId sessionId = options.getSessionId();
+        final TextClassification.Request request = options.getRequest() != null
+                ? options.getRequest()
+                : new TextClassification.Request.Builder(
+                        text, startIndex, endIndex)
+                        .setDefaultLocales(options.getDefaultLocales())
+                        .setReferenceTime(options.getReferenceTime())
+                        .build();
+        onClassifyText(sessionId, request, cancellationSignal, callback);
+    }
+
     /**
      * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
      * links information.
@@ -264,6 +307,23 @@
             @NonNull CancellationSignal cancellationSignal,
             @NonNull Callback<TextLinks> callback);
 
+    // TODO: Remove once apps can build against the latest sdk.
+    /** @hide */
+    public void onGenerateLinks(
+            @NonNull CharSequence text,
+            @Nullable TextLinks.Options options,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull Callback<TextLinks> callback) {
+        final TextClassificationSessionId sessionId = options.getSessionId();
+        final TextLinks.Request request = options.getRequest() != null
+                ? options.getRequest()
+                : new TextLinks.Request.Builder(text)
+                        .setDefaultLocales(options.getDefaultLocales())
+                        .setEntityConfig(options.getEntityConfig())
+                        .build();
+        onGenerateLinks(sessionId, request, cancellationSignal, callback);
+    }
+
     /**
      * Writes the selection event.
      * This is called when a selection event occurs. e.g. user changed selection; or smart selection
@@ -283,17 +343,17 @@
      * @param context the text classification context
      * @param sessionId the session's Id
      */
-    public abstract void onCreateTextClassificationSession(
+    public void onCreateTextClassificationSession(
             @NonNull TextClassificationContext context,
-            @NonNull TextClassificationSessionId sessionId);
+            @NonNull TextClassificationSessionId sessionId) {}
 
     /**
      * Destroys the text classification session identified by the specified sessionId.
      *
      * @param sessionId the id of the session to destroy
      */
-    public abstract void onDestroyTextClassificationSession(
-            @NonNull TextClassificationSessionId sessionId);
+    public void onDestroyTextClassificationSession(
+            @NonNull TextClassificationSessionId sessionId) {}
 
     /**
      * Returns a TextClassifier that runs in this service's process.
diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java
index a132730..7f75f0a 100644
--- a/android/service/wallpaper/WallpaperService.java
+++ b/android/service/wallpaper/WallpaperService.java
@@ -813,7 +813,7 @@
                     }
                     final int relayoutResult = mSession.relayout(
                         mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
-                            View.VISIBLE, 0, mWinFrame, mOverscanInsets, mContentInsets,
+                            View.VISIBLE, 0, -1, mWinFrame, mOverscanInsets, mContentInsets,
                             mVisibleInsets, mStableInsets, mOutsets, mBackdropFrame,
                             mDisplayCutout, mMergedConfiguration, mSurfaceHolder.mSurface);
 
diff --git a/android/support/v4/media/session/MediaControllerCompat.java b/android/support/v4/media/session/MediaControllerCompat.java
index 5e6f4ea..4a4ad32 100644
--- a/android/support/v4/media/session/MediaControllerCompat.java
+++ b/android/support/v4/media/session/MediaControllerCompat.java
@@ -667,13 +667,13 @@
     public static abstract class Callback implements IBinder.DeathRecipient {
         private final Object mCallbackObj;
         MessageHandler mHandler;
-        boolean mHasExtraCallback;
+        IMediaControllerCallback mIControllerCallback;
 
         public Callback() {
             if (android.os.Build.VERSION.SDK_INT >= 21) {
                 mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21(this));
             } else {
-                mCallbackObj = new StubCompat(this);
+                mCallbackObj = mIControllerCallback = new StubCompat(this);
             }
         }
 
@@ -789,6 +789,14 @@
         public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
         }
 
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY)
+        public IMediaControllerCallback getIControllerCallback() {
+            return mIControllerCallback;
+        }
+
         @Override
         public void binderDied() {
             onSessionDestroyed();
@@ -837,7 +845,8 @@
             public void onSessionEvent(String event, Bundle extras) {
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
-                    if (callback.mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 23) {
+                    if (callback.mIControllerCallback != null
+                            && android.os.Build.VERSION.SDK_INT < 23) {
                         // Ignore. ExtraCallback will handle this.
                     } else {
                         callback.onSessionEvent(event, extras);
@@ -849,7 +858,7 @@
             public void onPlaybackStateChanged(Object stateObj) {
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
-                    if (callback.mHasExtraCallback) {
+                    if (callback.mIControllerCallback != null) {
                         // Ignore. ExtraCallback will handle this.
                     } else {
                         callback.onPlaybackStateChanged(
@@ -1944,7 +1953,7 @@
             if (mExtraBinder != null) {
                 ExtraCallback extraCallback = new ExtraCallback(callback);
                 mCallbackMap.put(callback, extraCallback);
-                callback.mHasExtraCallback = true;
+                callback.mIControllerCallback = extraCallback;
                 try {
                     mExtraBinder.registerCallbackListener(extraCallback);
                 } catch (RemoteException e) {
@@ -1952,7 +1961,7 @@
                 }
             } else {
                 synchronized (mPendingCallbacks) {
-                    callback.mHasExtraCallback = false;
+                    callback.mIControllerCallback = null;
                     mPendingCallbacks.add(callback);
                 }
             }
@@ -1965,7 +1974,7 @@
                 try {
                     ExtraCallback extraCallback = mCallbackMap.remove(callback);
                     if (extraCallback != null) {
-                        callback.mHasExtraCallback = false;
+                        callback.mIControllerCallback = null;
                         mExtraBinder.unregisterCallbackListener(extraCallback);
                     }
                 } catch (RemoteException e) {
@@ -2173,7 +2182,7 @@
                 for (Callback callback : mPendingCallbacks) {
                     ExtraCallback extraCallback = new ExtraCallback(callback);
                     mCallbackMap.put(callback, extraCallback);
-                    callback.mHasExtraCallback = true;
+                    callback.mIControllerCallback = extraCallback;
                     try {
                         mExtraBinder.registerCallbackListener(extraCallback);
                     } catch (RemoteException e) {
diff --git a/android/support/v4/media/session/PlaybackStateCompat.java b/android/support/v4/media/session/PlaybackStateCompat.java
index e6420ea..b9c51ca 100644
--- a/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/android/support/v4/media/session/PlaybackStateCompat.java
@@ -15,7 +15,6 @@
  */
 package android.support.v4.media.session;
 
-
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.os.Build;
diff --git a/android/system/Os.java b/android/system/Os.java
index 404b822..6301ff9 100644
--- a/android/system/Os.java
+++ b/android/system/Os.java
@@ -186,17 +186,6 @@
     public static int getgid() { return Libcore.os.getgid(); }
 
     /**
-     * See <a href="http://man7.org/linux/man-pages/man2/getgroups.2.html">getgroups(2)</a>.
-     *
-     * <p>Should the number of groups change during the execution of this call, the call may
-     *    return an arbitrary subset. This may be worth reconsidering should this be exposed
-     *    as public API.
-     *
-     * @hide
-     */
-    public static int[] getgroups() throws ErrnoException { return Libcore.os.getgroups(); }
-
-    /**
      * See <a href="http://man7.org/linux/man-pages/man3/getenv.3.html">getenv(3)</a>.
      */
     public static String getenv(String name) { return Libcore.os.getenv(name); }
@@ -507,13 +496,6 @@
     public static void setgid(int gid) throws ErrnoException { Libcore.os.setgid(gid); }
 
     /**
-     * See <a href="http://man7.org/linux/man-pages/man2/setgroups.2.html">setgroups(2)</a>.
-     *
-     * @hide
-     */
-    public static void setgroups(int[] gids) throws ErrnoException { Libcore.os.setgroups(gids); }
-
-    /**
      * See <a href="http://man7.org/linux/man-pages/man2/setpgid.2.html">setpgid(2)</a>.
      */
     /** @hide */ public static void setpgid(int pid, int pgid) throws ErrnoException { Libcore.os.setpgid(pid, pgid); }
diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java
index 36333e4..3bf951d 100644
--- a/android/telecom/Connection.java
+++ b/android/telecom/Connection.java
@@ -2600,7 +2600,6 @@
     }
 
     /**
-     *
      * Request audio routing to a specific bluetooth device. Calling this method may result in
      * the device routing audio to a different bluetooth device than the one specified if the
      * bluetooth stack is unable to route audio to the requested device.
@@ -2611,13 +2610,13 @@
      * Used by self-managed {@link ConnectionService}s which wish to use bluetooth audio for a
      * self-managed {@link Connection} (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.)
      * <p>
-     * See also {@link InCallService#requestBluetoothAudio(String)}
-     * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by
-     *                         {@link BluetoothDevice#getAddress()}.
+     * See also {@link InCallService#requestBluetoothAudio(BluetoothDevice)}
+     * @param bluetoothDevice The bluetooth device to connect to.
      */
-    public void requestBluetoothAudio(@NonNull String bluetoothAddress) {
+    public void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) {
         for (Listener l : mListeners) {
-            l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH, bluetoothAddress);
+            l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH,
+                    bluetoothDevice.getAddress());
         }
     }
 
diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java
index af65c65..bd25ab2 100644
--- a/android/telecom/InCallService.java
+++ b/android/telecom/InCallService.java
@@ -428,12 +428,11 @@
      * A list of available devices can be obtained via
      * {@link CallAudioState#getSupportedBluetoothDevices()}
      *
-     * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by
-     *                         {@link BluetoothDevice#getAddress()}.
+     * @param bluetoothDevice The bluetooth device to connect to.
      */
-    public final void requestBluetoothAudio(@NonNull String bluetoothAddress) {
+    public final void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) {
         if (mPhone != null) {
-            mPhone.requestBluetoothAudio(bluetoothAddress);
+            mPhone.requestBluetoothAudio(bluetoothDevice.getAddress());
         }
     }
 
diff --git a/android/telecom/PhoneAccount.java b/android/telecom/PhoneAccount.java
index 95eb14a..b3a3bf2 100644
--- a/android/telecom/PhoneAccount.java
+++ b/android/telecom/PhoneAccount.java
@@ -129,6 +129,9 @@
      * <p>
      * By default, Self-Managed {@link PhoneAccount}s do not log their calls to the call log.
      * Setting this extra to {@code true} provides a means for them to log their calls.
+     * <p>
+     * Note: Only calls where the {@link Call.Details#getHandle()} {@link Uri#getScheme()} is
+     * {@link #SCHEME_SIP} or {@link #SCHEME_TEL} will be logged at the current time.
      */
     public static final String EXTRA_LOG_SELF_MANAGED_CALLS =
             "android.telecom.extra.LOG_SELF_MANAGED_CALLS";
diff --git a/android/telephony/AccessNetworkConstants.java b/android/telephony/AccessNetworkConstants.java
index cac9f2b..3b773b3 100644
--- a/android/telephony/AccessNetworkConstants.java
+++ b/android/telephony/AccessNetworkConstants.java
@@ -16,8 +16,6 @@
 
 package android.telephony;
 
-import android.annotation.SystemApi;
-
 /**
  * Contains access network related constants.
  */
@@ -39,7 +37,6 @@
      * Wireless transportation type
      * @hide
      */
-    @SystemApi
     public static final class TransportType {
         /** Wireless Wide Area Networks (i.e. Cellular) */
         public static final int WWAN = 1;
diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java
index 4683161..e244131 100644
--- a/android/telephony/CarrierConfigManager.java
+++ b/android/telephony/CarrierConfigManager.java
@@ -1684,6 +1684,14 @@
             "data_warning_threshold_bytes_long";
 
     /**
+     * Controls if the device should automatically notify the user as they reach
+     * their cellular data warning. When set to {@code false} the carrier is
+     * expected to have implemented their own notification mechanism.
+     */
+    public static final String KEY_DATA_WARNING_NOTIFICATION_BOOL =
+            "data_warning_notification_bool";
+
+    /**
      * Controls the cellular data limit.
      * <p>
      * If the user uses more than this amount of data in their billing cycle, as defined by
@@ -1698,6 +1706,22 @@
             "data_limit_threshold_bytes_long";
 
     /**
+     * Controls if the device should automatically notify the user as they reach
+     * their cellular data limit. When set to {@code false} the carrier is
+     * expected to have implemented their own notification mechanism.
+     */
+    public static final String KEY_DATA_LIMIT_NOTIFICATION_BOOL =
+            "data_limit_notification_bool";
+
+    /**
+     * Controls if the device should automatically notify the user when rapid
+     * cellular data usage is observed. When set to {@code false} the carrier is
+     * expected to have implemented their own notification mechanism.
+     */
+    public static final String KEY_DATA_RAPID_NOTIFICATION_BOOL =
+            "data_rapid_notification_bool";
+
+    /**
      * Offset to be reduced from rsrp threshold while calculating signal strength level.
      * @hide
      */
@@ -1954,7 +1978,7 @@
         sDefaults.putBoolean(KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false);
         sDefaults.putBoolean(KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, false);
         sDefaults.putBoolean(KEY_CARRIER_VOLTE_OVERRIDE_WFC_PROVISIONING_BOOL, false);
-        sDefaults.putBoolean(KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL, false);
+        sDefaults.putBoolean(KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL, true);
         sDefaults.putBoolean(KEY_CARRIER_ALLOW_TURNOFF_IMS_BOOL, true);
         sDefaults.putBoolean(KEY_CARRIER_IMS_GBA_REQUIRED_BOOL, false);
         sDefaults.putBoolean(KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL, false);
@@ -2165,7 +2189,10 @@
 
         sDefaults.putInt(KEY_MONTHLY_DATA_CYCLE_DAY_INT, DATA_CYCLE_USE_PLATFORM_DEFAULT);
         sDefaults.putLong(KEY_DATA_WARNING_THRESHOLD_BYTES_LONG, DATA_CYCLE_USE_PLATFORM_DEFAULT);
+        sDefaults.putBoolean(KEY_DATA_WARNING_NOTIFICATION_BOOL, true);
         sDefaults.putLong(KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG, DATA_CYCLE_USE_PLATFORM_DEFAULT);
+        sDefaults.putBoolean(KEY_DATA_LIMIT_NOTIFICATION_BOOL, true);
+        sDefaults.putBoolean(KEY_DATA_RAPID_NOTIFICATION_BOOL, true);
 
         // Rat families: {GPRS, EDGE}, {EVDO, EVDO_A, EVDO_B}, {UMTS, HSPA, HSDPA, HSUPA, HSPAP},
         // {LTE, LTE_CA}
@@ -2187,7 +2214,7 @@
         sDefaults.putStringArray(KEY_FILTERED_CNAP_NAMES_STRING_ARRAY, null);
         sDefaults.putBoolean(KEY_EDITABLE_WFC_ROAMING_MODE_BOOL, false);
         sDefaults.putBoolean(KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL, false);
-        sDefaults.putBoolean(KEY_PERSIST_LPP_MODE_BOOL, false);
+        sDefaults.putBoolean(KEY_PERSIST_LPP_MODE_BOOL, true);
         sDefaults.putStringArray(KEY_CARRIER_WIFI_STRING_ARRAY, null);
         sDefaults.putInt(KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT, -1);
         sDefaults.putInt(KEY_EMERGENCY_NOTIFICATION_DELAY_INT, -1);
diff --git a/android/telephony/NetworkRegistrationState.java b/android/telephony/NetworkRegistrationState.java
index bba779d..0e2e0ce 100644
--- a/android/telephony/NetworkRegistrationState.java
+++ b/android/telephony/NetworkRegistrationState.java
@@ -18,7 +18,6 @@
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -31,7 +30,6 @@
  * Description of a mobile network registration state
  * @hide
  */
-@SystemApi
 public class NetworkRegistrationState implements Parcelable {
     /**
      * Network domain
diff --git a/android/telephony/NetworkService.java b/android/telephony/NetworkService.java
index 35682a7..b431590 100644
--- a/android/telephony/NetworkService.java
+++ b/android/telephony/NetworkService.java
@@ -17,7 +17,6 @@
 package android.telephony;
 
 import android.annotation.CallSuper;
-import android.annotation.SystemApi;
 import android.app.Service;
 import android.content.Intent;
 import android.os.Handler;
@@ -47,7 +46,6 @@
  * </service>
  * @hide
  */
-@SystemApi
 public abstract class NetworkService extends Service {
 
     private final String TAG = NetworkService.class.getSimpleName();
@@ -206,8 +204,10 @@
         }
     }
 
-    /** @hide */
-    protected NetworkService() {
+    /**
+     * Default constructor.
+     */
+    public NetworkService() {
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
 
diff --git a/android/telephony/NetworkServiceCallback.java b/android/telephony/NetworkServiceCallback.java
index dbad02f..ad3b00f 100644
--- a/android/telephony/NetworkServiceCallback.java
+++ b/android/telephony/NetworkServiceCallback.java
@@ -17,7 +17,6 @@
 package android.telephony;
 
 import android.annotation.IntDef;
-import android.annotation.SystemApi;
 import android.os.RemoteException;
 import android.telephony.NetworkService.NetworkServiceProvider;
 
@@ -33,7 +32,6 @@
  *
  * @hide
  */
-@SystemApi
 public class NetworkServiceCallback {
 
     private static final String mTag = NetworkServiceCallback.class.getSimpleName();
diff --git a/android/telephony/ServiceState.java b/android/telephony/ServiceState.java
index e971d08..8ffdb21 100644
--- a/android/telephony/ServiceState.java
+++ b/android/telephony/ServiceState.java
@@ -17,7 +17,6 @@
 package android.telephony;
 
 import android.annotation.IntDef;
-import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -977,11 +976,13 @@
     }
 
     /** @hide */
+    @TestApi
     public void setCellBandwidths(int[] bandwidths) {
         mCellBandwidths = bandwidths;
     }
 
     /** @hide */
+    @TestApi
     public void setChannelNumber(int channelNumber) {
         mChannelNumber = channelNumber;
     }
@@ -1172,6 +1173,7 @@
     }
 
     /** @hide */
+    @TestApi
     public void setRilVoiceRadioTechnology(int rt) {
         if (rt == RIL_RADIO_TECHNOLOGY_LTE_CA) {
             rt = RIL_RADIO_TECHNOLOGY_LTE;
@@ -1181,6 +1183,7 @@
     }
 
     /** @hide */
+    @TestApi
     public void setRilDataRadioTechnology(int rt) {
         if (rt == RIL_RADIO_TECHNOLOGY_LTE_CA) {
             rt = RIL_RADIO_TECHNOLOGY_LTE;
@@ -1530,7 +1533,6 @@
      * @return List of registration states
      * @hide
      */
-    @SystemApi
     public List<NetworkRegistrationState> getNetworkRegistrationStates() {
         synchronized (mNetworkRegistrationStates) {
             return new ArrayList<>(mNetworkRegistrationStates);
@@ -1544,7 +1546,6 @@
      * @return List of registration states.
      * @hide
      */
-    @SystemApi
     public List<NetworkRegistrationState> getNetworkRegistrationStates(int transportType) {
         List<NetworkRegistrationState> list = new ArrayList<>();
 
@@ -1567,7 +1568,6 @@
      * @return The matching NetworkRegistrationState.
      * @hide
      */
-    @SystemApi
     public NetworkRegistrationState getNetworkRegistrationStates(int transportType, int domain) {
         synchronized (mNetworkRegistrationStates) {
             for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) {
diff --git a/android/telephony/SubscriptionManager.java b/android/telephony/SubscriptionManager.java
index 754fe68..a9389be 100644
--- a/android/telephony/SubscriptionManager.java
+++ b/android/telephony/SubscriptionManager.java
@@ -477,6 +477,9 @@
      * <p>
      * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
      * the user is interested in.
+     * <p>
+     * Receivers should protect themselves by checking that the sender holds the
+     * {@code android.permission.MANAGE_SUBSCRIPTION_PLANS} permission.
      */
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
     @SystemApi
@@ -1719,6 +1722,8 @@
      * </ul>
      *
      * @param subId the subscriber this relationship applies to
+     * @throws SecurityException if the caller doesn't meet the requirements
+     *             outlined above.
      */
     @SystemApi
     public @NonNull List<SubscriptionPlan> getSubscriptionPlans(int subId) {
@@ -1744,10 +1749,13 @@
      * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
      * </ul>
      *
-     * @param subId the subscriber this relationship applies to
+     * @param subId the subscriber this relationship applies to. An empty list
+     *            may be sent to clear any existing plans.
      * @param plans the list of plans. The first plan is always the primary and
      *            most important plan. Any additional plans are secondary and
      *            may not be displayed or used by decision making logic.
+     * @throws SecurityException if the caller doesn't meet the requirements
+     *             outlined above.
      */
     @SystemApi
     public void setSubscriptionPlans(int subId, @NonNull List<SubscriptionPlan> plans) {
@@ -1788,6 +1796,8 @@
      *            be automatically cleared, or {@code 0} to leave in the
      *            requested state until explicitly cleared, or the next reboot,
      *            whichever happens first.
+     * @throws SecurityException if the caller doesn't meet the requirements
+     *             outlined above.
      */
     @SystemApi
     public void setSubscriptionOverrideUnmetered(int subId, boolean overrideUnmetered,
@@ -1822,6 +1832,8 @@
      *            be automatically cleared, or {@code 0} to leave in the
      *            requested state until explicitly cleared, or the next reboot,
      *            whichever happens first.
+     * @throws SecurityException if the caller doesn't meet the requirements
+     *             outlined above.
      */
     @SystemApi
     public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested,
diff --git a/android/telephony/SubscriptionPlan.java b/android/telephony/SubscriptionPlan.java
index 4ffb70b..e8bbe42 100644
--- a/android/telephony/SubscriptionPlan.java
+++ b/android/telephony/SubscriptionPlan.java
@@ -24,7 +24,7 @@
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.util.Pair;
+import android.util.Range;
 import android.util.RecurrenceRule;
 
 import com.android.internal.util.Preconditions;
@@ -209,7 +209,7 @@
      * any recurrence rules. The iterator starts from the currently active cycle
      * and walks backwards through time.
      */
-    public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+    public Iterator<Range<ZonedDateTime>> cycleIterator() {
         return cycleRule.cycleIterator();
     }
 
@@ -227,6 +227,9 @@
         /**
          * Start defining a {@link SubscriptionPlan} that covers a very specific
          * window of time, and never automatically recurs.
+         *
+         * @param start The exact time at which the plan starts.
+         * @param end The exact time at which the plan ends.
          */
         public static Builder createNonrecurring(ZonedDateTime start, ZonedDateTime end) {
             if (!end.isAfter(start)) {
@@ -237,28 +240,43 @@
         }
 
         /**
-         * Start defining a {@link SubscriptionPlan} that will recur
-         * automatically every month. It will always recur on the same day of a
-         * particular month. When a particular month ends before the defined
-         * recurrence day, the plan will recur on the last instant of that
-         * month.
+         * Start defining a {@link SubscriptionPlan} that starts at a specific
+         * time, and automatically recurs after each specific period of time,
+         * repeating indefinitely.
+         * <p>
+         * When the given period is set to exactly one month, the plan will
+         * always recur on the day of the month defined by
+         * {@link ZonedDateTime#getDayOfMonth()}. When a particular month ends
+         * before this day, the plan will recur on the last possible instant of
+         * that month.
+         *
+         * @param start The exact time at which the plan starts.
+         * @param period The period after which the plan automatically recurs.
          */
+        public static Builder createRecurring(ZonedDateTime start, Period period) {
+            if (period.isZero() || period.isNegative()) {
+                throw new IllegalArgumentException("Period " + period + " must be positive");
+            }
+            return new Builder(start, null, period);
+        }
+
+        /** {@hide} */
+        @SystemApi
+        @Deprecated
         public static Builder createRecurringMonthly(ZonedDateTime start) {
             return new Builder(start, null, Period.ofMonths(1));
         }
 
-        /**
-         * Start defining a {@link SubscriptionPlan} that will recur
-         * automatically every week.
-         */
+        /** {@hide} */
+        @SystemApi
+        @Deprecated
         public static Builder createRecurringWeekly(ZonedDateTime start) {
             return new Builder(start, null, Period.ofDays(7));
         }
 
-        /**
-         * Start defining a {@link SubscriptionPlan} that will recur
-         * automatically every day.
-         */
+        /** {@hide} */
+        @SystemApi
+        @Deprecated
         public static Builder createRecurringDaily(ZonedDateTime start) {
             return new Builder(start, null, Period.ofDays(1));
         }
diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java
index e15d35b..956a5b1 100644
--- a/android/telephony/TelephonyManager.java
+++ b/android/telephony/TelephonyManager.java
@@ -2968,7 +2968,7 @@
             IPhoneSubInfo info = getSubscriberInfo();
             if (info == null)
                 return null;
-            return info.getGroupIdLevel1(mContext.getOpPackageName());
+            return info.getGroupIdLevel1ForSubscriber(getSubId(), mContext.getOpPackageName());
         } catch (RemoteException ex) {
             return null;
         } catch (NullPointerException ex) {
@@ -5156,7 +5156,12 @@
      * {@link #AUTHTYPE_EAP_SIM}
      * @param data authentication challenge data, base64 encoded.
      * See 3GPP TS 31.102 7.1.2 for more details.
-     * @return the response of authentication, or null if not available
+     * @return the response of authentication. This value will be null in the following cases:
+     *   Authentication error, incorrect MAC
+     *   Authentication error, security context not supported
+     *   Key freshness failure
+     *   Authentication error, no memory space available
+     *   Authentication error, no memory space available in EFMUK
      */
     // TODO(b/73660190): This should probably require MODIFY_PHONE_STATE, not
     // READ_PRIVILEGED_PHONE_STATE. It certainly shouldn't reference the permission in Javadoc since
@@ -5177,7 +5182,13 @@
      * {@link #AUTHTYPE_EAP_SIM}
      * @param data authentication challenge data, base64 encoded.
      * See 3GPP TS 31.102 7.1.2 for more details.
-     * @return the response of authentication, or null if not available
+     * @return the response of authentication. This value will be null in the following cases only
+     * (see 3GPP TS 31.102 7.3.1):
+     *   Authentication error, incorrect MAC
+     *   Authentication error, security context not supported
+     *   Key freshness failure
+     *   Authentication error, no memory space available
+     *   Authentication error, no memory space available in EFMUK
      * @hide
      */
     public String getIccAuthentication(int subId, int appType, int authType, String data) {
diff --git a/android/telephony/data/DataCallResponse.java b/android/telephony/data/DataCallResponse.java
index 25f5133..acc2470 100644
--- a/android/telephony/data/DataCallResponse.java
+++ b/android/telephony/data/DataCallResponse.java
@@ -19,7 +19,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.net.LinkAddress;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -34,7 +33,6 @@
  *
  * @hide
  */
-@SystemApi
 public final class DataCallResponse implements Parcelable {
     private final int mStatus;
     private final int mSuggestedRetryTime;
diff --git a/android/telephony/data/DataProfile.java b/android/telephony/data/DataProfile.java
index e8597b2..dd274c5 100644
--- a/android/telephony/data/DataProfile.java
+++ b/android/telephony/data/DataProfile.java
@@ -16,7 +16,6 @@
 
 package android.telephony.data;
 
-import android.annotation.SystemApi;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -30,7 +29,6 @@
  *
  * @hide
  */
-@SystemApi
 public final class DataProfile implements Parcelable {
 
     // The types indicating the data profile is used on GSM (3GPP) or CDMA (3GPP2) network.
diff --git a/android/telephony/data/DataService.java b/android/telephony/data/DataService.java
index e8c1cb1..0835f7d 100644
--- a/android/telephony/data/DataService.java
+++ b/android/telephony/data/DataService.java
@@ -20,7 +20,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.app.Service;
 import android.content.Intent;
 import android.net.LinkProperties;
@@ -55,7 +54,6 @@
  * </service>
  * @hide
  */
-@SystemApi
 public abstract class DataService extends Service {
     private static final String TAG = DataService.class.getSimpleName();
 
@@ -429,8 +427,10 @@
         }
     }
 
-    /** @hide */
-    protected DataService() {
+    /**
+     * Default constructor.
+     */
+    public DataService() {
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
 
diff --git a/android/telephony/data/DataServiceCallback.java b/android/telephony/data/DataServiceCallback.java
index 4af31b5..bff8260 100644
--- a/android/telephony/data/DataServiceCallback.java
+++ b/android/telephony/data/DataServiceCallback.java
@@ -17,7 +17,6 @@
 package android.telephony.data;
 
 import android.annotation.IntDef;
-import android.annotation.SystemApi;
 import android.net.LinkProperties;
 import android.os.RemoteException;
 import android.telephony.Rlog;
@@ -35,7 +34,6 @@
  *
  * @hide
  */
-@SystemApi
 public class DataServiceCallback {
 
     private static final String TAG = DataServiceCallback.class.getSimpleName();
@@ -125,7 +123,6 @@
      *
      * @param result The result code. Must be one of the {@link ResultCode}.
      */
-    @SystemApi
     public void onSetDataProfileComplete(@ResultCode int result) {
         IDataServiceCallback callback = mCallback.get();
         if (callback != null) {
diff --git a/android/telephony/ims/stub/ImsFeatureConfiguration.java b/android/telephony/ims/stub/ImsFeatureConfiguration.java
index 2f52c0a..dfb6e2c 100644
--- a/android/telephony/ims/stub/ImsFeatureConfiguration.java
+++ b/android/telephony/ims/stub/ImsFeatureConfiguration.java
@@ -77,6 +77,11 @@
             result = 31 * result + featureType;
             return result;
         }
+
+        @Override
+        public String toString() {
+            return "{s=" + slotId + ", f=" + featureType + "}";
+        }
     }
 
     /**
diff --git a/android/telephony/mbms/DownloadRequest.java b/android/telephony/mbms/DownloadRequest.java
index 602c796..9e3302b 100644
--- a/android/telephony/mbms/DownloadRequest.java
+++ b/android/telephony/mbms/DownloadRequest.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Parcel;
@@ -184,6 +185,7 @@
          * @hide
          */
         @SystemApi
+        @TestApi
         public Builder setServiceId(String serviceId) {
             fileServiceId = serviceId;
             return this;
diff --git a/android/text/MeasuredParagraph.java b/android/text/MeasuredParagraph.java
index 96edfa3..c2c3182 100644
--- a/android/text/MeasuredParagraph.java
+++ b/android/text/MeasuredParagraph.java
@@ -303,10 +303,9 @@
      *
      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
      */
-    public void getBounds(@NonNull Paint paint, @IntRange(from = 0) int start,
-            @IntRange(from = 0) int end, @NonNull Rect bounds) {
-        nGetBounds(mNativePtr, mCopiedBuffer, paint.getNativeInstance(), start, end,
-                paint.getBidiFlags(), bounds);
+    public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
+            @NonNull Rect bounds) {
+        nGetBounds(mNativePtr, mCopiedBuffer, start, end, bounds);
     }
 
     /**
@@ -743,6 +742,6 @@
     @CriticalNative
     private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
 
-    private static native void nGetBounds(long nativePtr, char[] buf, long paintPtr, int start,
-            int end, int bidiFlag, Rect rect);
+    private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
+            Rect rect);
 }
diff --git a/android/text/PrecomputedText.java b/android/text/PrecomputedText.java
index 413df05..369f357 100644
--- a/android/text/PrecomputedText.java
+++ b/android/text/PrecomputedText.java
@@ -16,6 +16,7 @@
 
 package android.text;
 
+import android.annotation.FloatRange;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -44,13 +45,17 @@
  * <pre>
  * An example usage is:
  * <code>
- *  void asyncSetText(final TextView textView, final String longString, Handler bgThreadHandler) {
+ *  static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) {
  *      // construct precompute related parameters using the TextView that we will set the text on.
- *      final PrecomputedText.Params params = textView.getTextParams();
- *      bgThreadHandler.post(() -> {
- *          final PrecomputedText precomputedText =
- *                  PrecomputedText.create(expensiveLongString, params);
+ *      final PrecomputedText.Params params = textView.getTextMetricsParams();
+ *      final Reference textViewRef = new WeakReference<>(textView);
+ *      bgExecutor.submit(() -> {
+ *          TextView textView = textViewRef.get();
+ *          if (textView == null) return;
+ *          final PrecomputedText precomputedText = PrecomputedText.create(longString, params);
  *          textView.post(() -> {
+ *              TextView textView = textViewRef.get();
+ *              if (textView == null) return;
  *              textView.setText(precomputedText);
  *          });
  *      });
@@ -363,6 +368,7 @@
 
     /**
      * Return the underlying text.
+     * @hide
      */
     public @NonNull CharSequence getText() {
         return mText;
@@ -451,32 +457,65 @@
             + ", gave " + pos);
     }
 
-    /** @hide */
-    public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
+    /**
+     * Returns text width for the given range.
+     * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
+     * IllegalArgumentException will be thrown.
+     *
+     * @param start the inclusive start offset in the text
+     * @param end the exclusive end offset in the text
+     * @return the text width
+     * @throws IllegalArgumentException if start and end offset are in the different paragraph.
+     */
+    public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start,
+            @IntRange(from = 0) int end) {
+        Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
+        Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
+        Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
+
+        if (start == end) {
+            return 0;
+        }
         final int paraIndex = findParaIndex(start);
         final int paraStart = getParagraphStart(paraIndex);
         final int paraEnd = getParagraphEnd(paraIndex);
         if (start < paraStart || paraEnd < end) {
-            throw new RuntimeException("Cannot measured across the paragraph:"
+            throw new IllegalArgumentException("Cannot measured across the paragraph:"
                 + "para: (" + paraStart + ", " + paraEnd + "), "
                 + "request: (" + start + ", " + end + ")");
         }
         return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
     }
 
-    /** @hide */
+    /**
+     * Retrieves the text bounding box for the given range.
+     * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
+     * IllegalArgumentException will be thrown.
+     *
+     * @param start the inclusive start offset in the text
+     * @param end the exclusive end offset in the text
+     * @param bounds the output rectangle
+     * @throws IllegalArgumentException if start and end offset are in the different paragraph.
+     */
     public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
             @NonNull Rect bounds) {
+        Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
+        Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
+        Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
+        Preconditions.checkNotNull(bounds);
+        if (start == end) {
+            bounds.set(0, 0, 0, 0);
+            return;
+        }
         final int paraIndex = findParaIndex(start);
         final int paraStart = getParagraphStart(paraIndex);
         final int paraEnd = getParagraphEnd(paraIndex);
         if (start < paraStart || paraEnd < end) {
-            throw new RuntimeException("Cannot measured across the paragraph:"
+            throw new IllegalArgumentException("Cannot measured across the paragraph:"
                 + "para: (" + paraStart + ", " + paraEnd + "), "
                 + "request: (" + start + ", " + end + ")");
         }
-        getMeasuredParagraph(paraIndex).getBounds(mParams.mPaint,
-                start - paraStart, end - paraStart, bounds);
+        getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds);
     }
 
     /**
diff --git a/android/text/Selection.java b/android/text/Selection.java
index 3445658..5256e47 100644
--- a/android/text/Selection.java
+++ b/android/text/Selection.java
@@ -180,7 +180,7 @@
      * Remove the selection or cursor, if any, from the text.
      */
     public static final void removeSelection(Spannable text) {
-        text.removeSpan(SELECTION_START);
+        text.removeSpan(SELECTION_START, Spanned.SPAN_INTERMEDIATE);
         text.removeSpan(SELECTION_END);
         removeMemory(text);
     }
diff --git a/android/text/Spannable.java b/android/text/Spannable.java
index 39b78eb..8315b2a 100644
--- a/android/text/Spannable.java
+++ b/android/text/Spannable.java
@@ -46,6 +46,19 @@
     public void removeSpan(Object what);
 
     /**
+     * Remove the specified object from the range of text to which it
+     * was attached, if any.  It is OK to remove an object that was never
+     * attached in the first place.
+     *
+     * See {@link Spanned} for an explanation of what the flags mean.
+     *
+     * @hide
+     */
+    default void removeSpan(Object what, int flags) {
+        removeSpan(what);
+    }
+
+    /**
      * Factory used by TextView to create new {@link Spannable Spannables}. You can subclass
      * it to provide something other than {@link SpannableString}.
      *
diff --git a/android/text/SpannableStringBuilder.java b/android/text/SpannableStringBuilder.java
index d41dfdc..41a9c45 100644
--- a/android/text/SpannableStringBuilder.java
+++ b/android/text/SpannableStringBuilder.java
@@ -312,7 +312,7 @@
                     // The following condition indicates that the span would become empty
                     (textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) {
                 mIndexOfSpan.remove(mSpans[i]);
-                removeSpan(i);
+                removeSpan(i, 0 /* flags */);
                 return true;
             }
             return resolveGap(mSpanStarts[i]) <= end && (i & 1) != 0 &&
@@ -472,7 +472,7 @@
     }
 
     // Note: caller is responsible for removing the mIndexOfSpan entry.
-    private void removeSpan(int i) {
+    private void removeSpan(int i, int flags) {
         Object object = mSpans[i];
 
         int start = mSpanStarts[i];
@@ -496,7 +496,9 @@
         // Invariants must be restored before sending span removed notifications.
         restoreInvariants();
 
-        sendSpanRemoved(object, start, end);
+        if ((flags & Spanned.SPAN_INTERMEDIATE) == 0) {
+            sendSpanRemoved(object, start, end);
+        }
     }
 
     // Documentation from interface
@@ -782,10 +784,19 @@
      * Remove the specified markup object from the buffer.
      */
     public void removeSpan(Object what) {
+        removeSpan(what, 0 /* flags */);
+    }
+
+    /**
+     * Remove the specified markup object from the buffer.
+     *
+     * @hide
+     */
+    public void removeSpan(Object what, int flags) {
         if (mIndexOfSpan == null) return;
         Integer i = mIndexOfSpan.remove(what);
         if (i != null) {
-            removeSpan(i.intValue());
+            removeSpan(i.intValue(), flags);
         }
     }
 
diff --git a/android/text/SpannableStringInternal.java b/android/text/SpannableStringInternal.java
index 5dd1a52..bcc2fda 100644
--- a/android/text/SpannableStringInternal.java
+++ b/android/text/SpannableStringInternal.java
@@ -249,6 +249,13 @@
     }
 
     /* package */ void removeSpan(Object what) {
+        removeSpan(what, 0 /* flags */);
+    }
+
+    /**
+     * @hide
+     */
+    public void removeSpan(Object what, int flags) {
         int count = mSpanCount;
         Object[] spans = mSpans;
         int[] data = mSpanData;
@@ -262,11 +269,13 @@
 
                 System.arraycopy(spans, i + 1, spans, i, c);
                 System.arraycopy(data, (i + 1) * COLUMNS,
-                                 data, i * COLUMNS, c * COLUMNS);
+                        data, i * COLUMNS, c * COLUMNS);
 
                 mSpanCount--;
 
-                sendSpanRemoved(what, ostart, oend);
+                if ((flags & Spanned.SPAN_INTERMEDIATE) == 0) {
+                    sendSpanRemoved(what, ostart, oend);
+                }
                 return;
             }
         }
diff --git a/android/text/format/Formatter.java b/android/text/format/Formatter.java
index ad3b4b6..de86a66 100644
--- a/android/text/format/Formatter.java
+++ b/android/text/format/Formatter.java
@@ -40,6 +40,10 @@
     public static final int FLAG_SHORTER = 1 << 0;
     /** {@hide} */
     public static final int FLAG_CALCULATE_ROUNDED = 1 << 1;
+    /** {@hide} */
+    public static final int FLAG_SI_UNITS = 1 << 2;
+    /** {@hide} */
+    public static final int FLAG_IEC_UNITS = 1 << 3;
 
     /** {@hide} */
     public static class BytesResult {
@@ -90,7 +94,7 @@
         if (context == null) {
             return "";
         }
-        final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0);
+        final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SI_UNITS);
         return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
                 res.value, res.units));
     }
@@ -103,41 +107,43 @@
         if (context == null) {
             return "";
         }
-        final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER);
+        final BytesResult res = formatBytes(context.getResources(), sizeBytes,
+                FLAG_SI_UNITS | FLAG_SHORTER);
         return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
                 res.value, res.units));
     }
 
     /** {@hide} */
     public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
+        final int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000;
         final boolean isNegative = (sizeBytes < 0);
         float result = isNegative ? -sizeBytes : sizeBytes;
         int suffix = com.android.internal.R.string.byteShort;
         long mult = 1;
         if (result > 900) {
             suffix = com.android.internal.R.string.kilobyteShort;
-            mult = 1000;
-            result = result / 1000;
+            mult = unit;
+            result = result / unit;
         }
         if (result > 900) {
             suffix = com.android.internal.R.string.megabyteShort;
-            mult *= 1000;
-            result = result / 1000;
+            mult *= unit;
+            result = result / unit;
         }
         if (result > 900) {
             suffix = com.android.internal.R.string.gigabyteShort;
-            mult *= 1000;
-            result = result / 1000;
+            mult *= unit;
+            result = result / unit;
         }
         if (result > 900) {
             suffix = com.android.internal.R.string.terabyteShort;
-            mult *= 1000;
-            result = result / 1000;
+            mult *= unit;
+            result = result / unit;
         }
         if (result > 900) {
             suffix = com.android.internal.R.string.petabyteShort;
-            mult *= 1000;
-            result = result / 1000;
+            mult *= unit;
+            result = result / unit;
         }
         // Note we calculate the rounded long by ourselves, but still let String.format()
         // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
diff --git a/android/text/method/LinkMovementMethod.java b/android/text/method/LinkMovementMethod.java
index f332358..e60377b 100644
--- a/android/text/method/LinkMovementMethod.java
+++ b/android/text/method/LinkMovementMethod.java
@@ -219,7 +219,7 @@
                     links[0].onClick(widget);
                 } else if (action == MotionEvent.ACTION_DOWN) {
                     if (widget.getContext().getApplicationInfo().targetSdkVersion
-                            > Build.VERSION_CODES.O_MR1) {
+                            >= Build.VERSION_CODES.P) {
                         // Selection change will reposition the toolbar. Hide it for a few ms for a
                         // smoother transition.
                         widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
diff --git a/android/text/util/Linkify.java b/android/text/util/Linkify.java
index 9c6a3f5..c905f49 100644
--- a/android/text/util/Linkify.java
+++ b/android/text/util/Linkify.java
@@ -100,13 +100,20 @@
      *  take an options mask. Note that this uses the
      *  {@link android.webkit.WebView#findAddress(String) findAddress()} method in
      *  {@link android.webkit.WebView} for finding addresses, which has various
-     *  limitations.
+     *  limitations and has been deprecated.
+     *  @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks(
+     *  TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative
+     *  is available.
      */
+    @Deprecated
     public static final int MAP_ADDRESSES = 0x08;
 
     /**
      *  Bit mask indicating that all available patterns should be matched in
      *  methods that take an options mask
+     *  <p><strong>Note:</strong></p> {@link #MAP_ADDRESSES} is deprecated.
+     *  Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)}
+     *  instead and avoid it even when targeting API levels where no alternative is available.
      */
     public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
 
diff --git a/android/util/LruCache.java b/android/util/LruCache.java
index 5208606..4015488 100644
--- a/android/util/LruCache.java
+++ b/android/util/LruCache.java
@@ -20,10 +20,6 @@
 import java.util.Map;
 
 /**
- * BEGIN LAYOUTLIB CHANGE
- * This is a custom version that doesn't use the non standard LinkedHashMap#eldest.
- * END LAYOUTLIB CHANGE
- *
  * A cache that holds strong references to a limited number of values. Each time
  * a value is accessed, it is moved to the head of a queue. When a value is
  * added to a full cache, the value at the end of that queue is evicted and may
@@ -91,9 +87,8 @@
 
     /**
      * Sets the size of the cache.
-     * @param maxSize The new maximum size.
      *
-     * @hide
+     * @param maxSize The new maximum size.
      */
     public void resize(int maxSize) {
         if (maxSize <= 0) {
@@ -190,10 +185,13 @@
     }
 
     /**
+     * Remove the eldest entries until the total of remaining entries is at or
+     * below the requested size.
+     *
      * @param maxSize the maximum size of the cache before returning. May be -1
-     *     to evict even 0-sized elements.
+     *            to evict even 0-sized elements.
      */
-    private void trimToSize(int maxSize) {
+    public void trimToSize(int maxSize) {
         while (true) {
             K key;
             V value;
@@ -207,16 +205,7 @@
                     break;
                 }
 
-                // BEGIN LAYOUTLIB CHANGE
-                // get the last item in the linked list.
-                // This is not efficient, the goal here is to minimize the changes
-                // compared to the platform version.
-                Map.Entry<K, V> toEvict = null;
-                for (Map.Entry<K, V> entry : map.entrySet()) {
-                    toEvict = entry;
-                }
-                // END LAYOUTLIB CHANGE
-
+                Map.Entry<K, V> toEvict = map.eldest();
                 if (toEvict == null) {
                     break;
                 }
diff --git a/android/util/RecurrenceRule.java b/android/util/RecurrenceRule.java
index 9f115eb..9c89876 100644
--- a/android/util/RecurrenceRule.java
+++ b/android/util/RecurrenceRule.java
@@ -149,6 +149,10 @@
         }
     };
 
+    public boolean isRecurring() {
+        return period != null;
+    }
+
     @Deprecated
     public boolean isMonthly() {
         return start != null
@@ -158,7 +162,7 @@
                 && period.getDays() == 0;
     }
 
-    public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+    public Iterator<Range<ZonedDateTime>> cycleIterator() {
         if (period != null) {
             return new RecurringIterator();
         } else {
@@ -166,7 +170,7 @@
         }
     }
 
-    private class NonrecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> {
+    private class NonrecurringIterator implements Iterator<Range<ZonedDateTime>> {
         boolean hasNext;
 
         public NonrecurringIterator() {
@@ -179,13 +183,13 @@
         }
 
         @Override
-        public Pair<ZonedDateTime, ZonedDateTime> next() {
+        public Range<ZonedDateTime> next() {
             hasNext = false;
-            return new Pair<>(start, end);
+            return new Range<>(start, end);
         }
     }
 
-    private class RecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> {
+    private class RecurringIterator implements Iterator<Range<ZonedDateTime>> {
         int i;
         ZonedDateTime cycleStart;
         ZonedDateTime cycleEnd;
@@ -231,12 +235,12 @@
         }
 
         @Override
-        public Pair<ZonedDateTime, ZonedDateTime> next() {
+        public Range<ZonedDateTime> next() {
             if (LOGD) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd);
-            Pair<ZonedDateTime, ZonedDateTime> p = new Pair<>(cycleStart, cycleEnd);
+            Range<ZonedDateTime> r = new Range<>(cycleStart, cycleEnd);
             i--;
             updateCycle();
-            return p;
+            return r;
         }
     }
 
diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java
index 66a9c6c..f59c0b5 100644
--- a/android/view/DisplayCutout.java
+++ b/android/view/DisplayCutout.java
@@ -31,6 +31,7 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.util.PathParser;
 import android.util.proto.ProtoOutputStream;
 
@@ -75,15 +76,19 @@
             false /* copyArguments */);
 
 
+    private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null);
     private static final Object CACHE_LOCK = new Object();
+
     @GuardedBy("CACHE_LOCK")
     private static String sCachedSpec;
     @GuardedBy("CACHE_LOCK")
     private static int sCachedDisplayWidth;
     @GuardedBy("CACHE_LOCK")
+    private static int sCachedDisplayHeight;
+    @GuardedBy("CACHE_LOCK")
     private static float sCachedDensity;
     @GuardedBy("CACHE_LOCK")
-    private static DisplayCutout sCachedCutout;
+    private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR;
 
     private final Rect mSafeInsets;
     private final Region mBounds;
@@ -347,7 +352,7 @@
     }
 
     /**
-     * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
+     * Creates the bounding path according to @android:string/config_mainBuiltInDisplayCutout.
      *
      * @hide
      */
@@ -357,6 +362,16 @@
     }
 
     /**
+     * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
+     *
+     * @hide
+     */
+    public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) {
+        return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
+                displayWidth, displayHeight, res.getDisplayMetrics().density).first;
+    }
+
+    /**
      * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec.
      *
      * @hide
@@ -364,11 +379,17 @@
     @VisibleForTesting(visibility = PRIVATE)
     public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight,
             float density) {
+        return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second;
+    }
+
+    private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec,
+            int displayWidth, int displayHeight, float density) {
         if (TextUtils.isEmpty(spec)) {
-            return null;
+            return NULL_PAIR;
         }
         synchronized (CACHE_LOCK) {
             if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth
+                    && sCachedDisplayHeight == displayHeight
                     && sCachedDensity == density) {
                 return sCachedCutout;
             }
@@ -398,7 +419,7 @@
             p = PathParser.createPathFromPathData(spec);
         } catch (Throwable e) {
             Log.wtf(TAG, "Could not inflate cutout: ", e);
-            return null;
+            return NULL_PAIR;
         }
 
         final Matrix m = new Matrix();
@@ -414,7 +435,7 @@
                 bottomPath = PathParser.createPathFromPathData(bottomSpec);
             } catch (Throwable e) {
                 Log.wtf(TAG, "Could not inflate bottom cutout: ", e);
-                return null;
+                return NULL_PAIR;
             }
             // Keep top transform
             m.postTranslate(0, displayHeight);
@@ -422,10 +443,11 @@
             p.addPath(bottomPath);
         }
 
-        final DisplayCutout result = fromBounds(p);
+        final Pair<Path, DisplayCutout> result = new Pair<>(p, fromBounds(p));
         synchronized (CACHE_LOCK) {
             sCachedSpec = spec;
             sCachedDisplayWidth = displayWidth;
+            sCachedDisplayHeight = displayHeight;
             sCachedDensity = density;
             sCachedCutout = result;
         }
diff --git a/android/view/HapticFeedbackConstants.java b/android/view/HapticFeedbackConstants.java
index b147928..db01cea 100644
--- a/android/view/HapticFeedbackConstants.java
+++ b/android/view/HapticFeedbackConstants.java
@@ -77,6 +77,55 @@
     public static final int TEXT_HANDLE_MOVE = 9;
 
     /**
+     * The user unlocked the device
+     * @hide
+     */
+    public static final int ENTRY_BUMP = 10;
+
+    /**
+     * The user has moved the dragged object within a droppable area.
+     * @hide
+     */
+    public static final int DRAG_CROSSING = 11;
+
+    /**
+     * The user has started a gesture (e.g. on the soft keyboard).
+     * @hide
+     */
+    public static final int GESTURE_START = 12;
+
+    /**
+     * The user has finished a gesture (e.g. on the soft keyboard).
+     * @hide
+     */
+    public static final int GESTURE_END = 13;
+
+    /**
+     * The user's squeeze crossed the gesture's initiation threshold.
+     * @hide
+     */
+    public static final int EDGE_SQUEEZE = 14;
+
+    /**
+     * The user's squeeze crossed the gesture's release threshold.
+     * @hide
+     */
+    public static final int EDGE_RELEASE = 15;
+
+    /**
+     * A haptic effect to signal the confirmation or successful completion of a user
+     * interaction.
+     * @hide
+     */
+    public static final int CONFIRM = 16;
+
+    /**
+     * A haptic effect to signal the rejection or failure of a user interaction.
+     * @hide
+     */
+    public static final int REJECT = 17;
+
+    /**
      * The phone has booted with safe mode enabled.
      * This is a private constant.  Feel free to renumber as desired.
      * @hide
diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java
index d4610a5..5deee11 100644
--- a/android/view/SurfaceControl.java
+++ b/android/view/SurfaceControl.java
@@ -87,6 +87,7 @@
     private static native void nativeMergeTransaction(long transactionObj,
             long otherTransactionObj);
     private static native void nativeSetAnimationTransaction(long transactionObj);
+    private static native void nativeSetEarlyWakeup(long transactionObj);
 
     private static native void nativeSetLayer(long transactionObj, long nativeObject, int zorder);
     private static native void nativeSetRelativeLayer(long transactionObj, long nativeObject,
@@ -1642,6 +1643,19 @@
         }
 
         /**
+         * Indicate that SurfaceFlinger should wake up earlier than usual as a result of this
+         * transaction. This should be used when the caller thinks that the scene is complex enough
+         * that it's likely to hit GL composition, and thus, SurfaceFlinger needs to more time in
+         * order not to miss frame deadlines.
+         * <p>
+         * Corresponds to setting ISurfaceComposer::eEarlyWakeup
+         */
+        public Transaction setEarlyWakeup() {
+            nativeSetEarlyWakeup(mNativeObject);
+            return this;
+        }
+
+        /**
          * Merge the other transaction into this transaction, clearing the
          * other transaction as if it had been applied.
          */
diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java
index ebb2af4..7e54647 100644
--- a/android/view/SurfaceView.java
+++ b/android/view/SurfaceView.java
@@ -16,115 +16,1237 @@
 
 package android.view;
 
-import com.android.layoutlib.bridge.MockView;
+import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
+import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER;
+import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER;
 
 import android.content.Context;
+import android.content.res.CompatibilityInfo.Translator;
+import android.content.res.Configuration;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.SystemClock;
 import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.view.SurfaceCallbackHelper;
+
+import java.util.ArrayList;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
- * Mock version of the SurfaceView.
- * Only non override public methods from the real SurfaceView have been added in there.
- * Methods that take an unknown class as parameter or as return object, have been removed for now.
+ * Provides a dedicated drawing surface embedded inside of a view hierarchy.
+ * You can control the format of this surface and, if you like, its size; the
+ * SurfaceView takes care of placing the surface at the correct location on the
+ * screen
  *
- * TODO: generate automatically.
+ * <p>The surface is Z ordered so that it is behind the window holding its
+ * SurfaceView; the SurfaceView punches a hole in its window to allow its
+ * surface to be displayed. The view hierarchy will take care of correctly
+ * compositing with the Surface any siblings of the SurfaceView that would
+ * normally appear on top of it. This can be used to place overlays such as
+ * buttons on top of the Surface, though note however that it can have an
+ * impact on performance since a full alpha-blended composite will be performed
+ * each time the Surface changes.
  *
+ * <p> The transparent region that makes the surface visible is based on the
+ * layout positions in the view hierarchy. If the post-layout transform
+ * properties are used to draw a sibling view on top of the SurfaceView, the
+ * view may not be properly composited with the surface.
+ *
+ * <p>Access to the underlying surface is provided via the SurfaceHolder interface,
+ * which can be retrieved by calling {@link #getHolder}.
+ *
+ * <p>The Surface will be created for you while the SurfaceView's window is
+ * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated}
+ * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the
+ * Surface is created and destroyed as the window is shown and hidden.
+ *
+ * <p>One of the purposes of this class is to provide a surface in which a
+ * secondary thread can render into the screen. If you are going to use it
+ * this way, you need to be aware of some threading semantics:
+ *
+ * <ul>
+ * <li> All SurfaceView and
+ * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called
+ * from the thread running the SurfaceView's window (typically the main thread
+ * of the application). They thus need to correctly synchronize with any
+ * state that is also touched by the drawing thread.
+ * <li> You must ensure that the drawing thread only touches the underlying
+ * Surface while it is valid -- between
+ * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()}
+ * and
+ * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}.
+ * </ul>
+ *
+ * <p class="note"><strong>Note:</strong> Starting in platform version
+ * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is
+ * updated synchronously with other View rendering. This means that translating
+ * and scaling a SurfaceView on screen will not cause rendering artifacts. Such
+ * artifacts may occur on previous versions of the platform when its window is
+ * positioned asynchronously.</p>
  */
-public class SurfaceView extends MockView {
+public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback {
+    private static final String TAG = "SurfaceView";
+    private static final boolean DEBUG = false;
+
+    final ArrayList<SurfaceHolder.Callback> mCallbacks
+            = new ArrayList<SurfaceHolder.Callback>();
+
+    final int[] mLocation = new int[2];
+
+    final ReentrantLock mSurfaceLock = new ReentrantLock();
+    final Surface mSurface = new Surface();       // Current surface in use
+    boolean mDrawingStopped = true;
+    // We use this to track if the application has produced a frame
+    // in to the Surface. Up until that point, we should be careful not to punch
+    // holes.
+    boolean mDrawFinished = false;
+
+    final Rect mScreenRect = new Rect();
+    SurfaceSession mSurfaceSession;
+
+    SurfaceControlWithBackground mSurfaceControl;
+    // In the case of format changes we switch out the surface in-place
+    // we need to preserve the old one until the new one has drawn.
+    SurfaceControl mDeferredDestroySurfaceControl;
+    final Rect mTmpRect = new Rect();
+    final Configuration mConfiguration = new Configuration();
+
+    int mSubLayer = APPLICATION_MEDIA_SUBLAYER;
+
+    boolean mIsCreating = false;
+    private volatile boolean mRtHandlingPositionUpdates = false;
+
+    private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener
+            = new ViewTreeObserver.OnScrollChangedListener() {
+                    @Override
+                    public void onScrollChanged() {
+                        updateSurface();
+                    }
+            };
+
+    private final ViewTreeObserver.OnPreDrawListener mDrawListener =
+            new ViewTreeObserver.OnPreDrawListener() {
+                @Override
+                public boolean onPreDraw() {
+                    // reposition ourselves where the surface is
+                    mHaveFrame = getWidth() > 0 && getHeight() > 0;
+                    updateSurface();
+                    return true;
+                }
+            };
+
+    boolean mRequestedVisible = false;
+    boolean mWindowVisibility = false;
+    boolean mLastWindowVisibility = false;
+    boolean mViewVisibility = false;
+    boolean mWindowStopped = false;
+
+    int mRequestedWidth = -1;
+    int mRequestedHeight = -1;
+    /* Set SurfaceView's format to 565 by default to maintain backward
+     * compatibility with applications assuming this format.
+     */
+    int mRequestedFormat = PixelFormat.RGB_565;
+
+    boolean mHaveFrame = false;
+    boolean mSurfaceCreated = false;
+    long mLastLockTime = 0;
+
+    boolean mVisible = false;
+    int mWindowSpaceLeft = -1;
+    int mWindowSpaceTop = -1;
+    int mSurfaceWidth = -1;
+    int mSurfaceHeight = -1;
+    int mFormat = -1;
+    final Rect mSurfaceFrame = new Rect();
+    int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1;
+    private Translator mTranslator;
+
+    private boolean mGlobalListenersAdded;
+    private boolean mAttachedToWindow;
+
+    private int mSurfaceFlags = SurfaceControl.HIDDEN;
+
+    private int mPendingReportDraws;
+
+    private SurfaceControl.Transaction mRtTransaction = new SurfaceControl.Transaction();
 
     public SurfaceView(Context context) {
         this(context, null);
     }
 
     public SurfaceView(Context context, AttributeSet attrs) {
-        this(context, attrs , 0);
+        this(context, attrs, 0);
     }
 
-    public SurfaceView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
+    public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
     }
 
     public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+        mRenderNode.requestPositionUpdates(this);
+
+        setWillNotDraw(true);
     }
 
-    public boolean gatherTransparentRegion(Region region) {
-      return false;
-    }
-
-    public void setZOrderMediaOverlay(boolean isMediaOverlay) {
-    }
-
-    public void setZOrderOnTop(boolean onTop) {
-    }
-
-    public void setSecure(boolean isSecure) {
-    }
-
+    /**
+     * Return the SurfaceHolder providing access and control over this
+     * SurfaceView's underlying surface.
+     *
+     * @return SurfaceHolder The holder of the surface.
+     */
     public SurfaceHolder getHolder() {
         return mSurfaceHolder;
     }
 
-    private SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
+    private void updateRequestedVisibility() {
+        mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped;
+    }
+
+    /** @hide */
+    @Override
+    public void windowStopped(boolean stopped) {
+        mWindowStopped = stopped;
+        updateRequestedVisibility();
+        updateSurface();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        getViewRootImpl().addWindowStoppedCallback(this);
+        mWindowStopped = false;
+
+        mViewVisibility = getVisibility() == VISIBLE;
+        updateRequestedVisibility();
+
+        mAttachedToWindow = true;
+        mParent.requestTransparentRegion(SurfaceView.this);
+        if (!mGlobalListenersAdded) {
+            ViewTreeObserver observer = getViewTreeObserver();
+            observer.addOnScrollChangedListener(mScrollChangedListener);
+            observer.addOnPreDrawListener(mDrawListener);
+            mGlobalListenersAdded = true;
+        }
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        mWindowVisibility = visibility == VISIBLE;
+        updateRequestedVisibility();
+        updateSurface();
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        mViewVisibility = visibility == VISIBLE;
+        boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped;
+        if (newRequestedVisible != mRequestedVisible) {
+            // our base class (View) invalidates the layout only when
+            // we go from/to the GONE state. However, SurfaceView needs
+            // to request a re-layout when the visibility changes at all.
+            // This is needed because the transparent region is computed
+            // as part of the layout phase, and it changes (obviously) when
+            // the visibility changes.
+            requestLayout();
+        }
+        mRequestedVisible = newRequestedVisible;
+        updateSurface();
+    }
+
+    private void performDrawFinished() {
+        if (mPendingReportDraws > 0) {
+            mDrawFinished = true;
+            if (mAttachedToWindow) {
+                notifyDrawFinished();
+                invalidate();
+            }
+        } else {
+            Log.e(TAG, System.identityHashCode(this) + "finished drawing"
+                    + " but no pending report draw (extra call"
+                    + " to draw completion runnable?)");
+        }
+    }
+
+    void notifyDrawFinished() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot != null) {
+            viewRoot.pendingDrawFinished();
+        }
+        mPendingReportDraws--;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        // It's possible to create a SurfaceView using the default constructor and never
+        // attach it to a view hierarchy, this is a common use case when dealing with
+        // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage
+        // the lifecycle. Instead of attaching it to a view, he/she can just pass
+        // the SurfaceHolder forward, most live wallpapers do it.
+        if (viewRoot != null) {
+            viewRoot.removeWindowStoppedCallback(this);
+        }
+
+        mAttachedToWindow = false;
+        if (mGlobalListenersAdded) {
+            ViewTreeObserver observer = getViewTreeObserver();
+            observer.removeOnScrollChangedListener(mScrollChangedListener);
+            observer.removeOnPreDrawListener(mDrawListener);
+            mGlobalListenersAdded = false;
+        }
+
+        while (mPendingReportDraws > 0) {
+            notifyDrawFinished();
+        }
+
+        mRequestedVisible = false;
+
+        updateSurface();
+        if (mSurfaceControl != null) {
+            mSurfaceControl.destroy();
+        }
+        mSurfaceControl = null;
+
+        mHaveFrame = false;
+
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = mRequestedWidth >= 0
+                ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0)
+                : getDefaultSize(0, widthMeasureSpec);
+        int height = mRequestedHeight >= 0
+                ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0)
+                : getDefaultSize(0, heightMeasureSpec);
+        setMeasuredDimension(width, height);
+    }
+
+    /** @hide */
+    @Override
+    protected boolean setFrame(int left, int top, int right, int bottom) {
+        boolean result = super.setFrame(left, top, right, bottom);
+        updateSurface();
+        return result;
+    }
+
+    @Override
+    public boolean gatherTransparentRegion(Region region) {
+        if (isAboveParent() || !mDrawFinished) {
+            return super.gatherTransparentRegion(region);
+        }
+
+        boolean opaque = true;
+        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
+            // this view draws, remove it from the transparent region
+            opaque = super.gatherTransparentRegion(region);
+        } else if (region != null) {
+            int w = getWidth();
+            int h = getHeight();
+            if (w>0 && h>0) {
+                getLocationInWindow(mLocation);
+                // otherwise, punch a hole in the whole hierarchy
+                int l = mLocation[0];
+                int t = mLocation[1];
+                region.op(l, t, l+w, t+h, Region.Op.UNION);
+            }
+        }
+        if (PixelFormat.formatHasAlpha(mRequestedFormat)) {
+            opaque = false;
+        }
+        return opaque;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (mDrawFinished && !isAboveParent()) {
+            // draw() is not called when SKIP_DRAW is set
+            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
+                // punch a whole in the view-hierarchy below us
+                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+            }
+        }
+        super.draw(canvas);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mDrawFinished && !isAboveParent()) {
+            // draw() is not called when SKIP_DRAW is set
+            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+                // punch a whole in the view-hierarchy below us
+                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+            }
+        }
+        super.dispatchDraw(canvas);
+    }
+
+    /**
+     * Control whether the surface view's surface is placed on top of another
+     * regular surface view in the window (but still behind the window itself).
+     * This is typically used to place overlays on top of an underlying media
+     * surface view.
+     *
+     * <p>Note that this must be set before the surface view's containing
+     * window is attached to the window manager.
+     *
+     * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
+     */
+    public void setZOrderMediaOverlay(boolean isMediaOverlay) {
+        mSubLayer = isMediaOverlay
+            ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER;
+    }
+
+    /**
+     * Control whether the surface view's surface is placed on top of its
+     * window.  Normally it is placed behind the window, to allow it to
+     * (for the most part) appear to composite with the views in the
+     * hierarchy.  By setting this, you cause it to be placed above the
+     * window.  This means that none of the contents of the window this
+     * SurfaceView is in will be visible on top of its surface.
+     *
+     * <p>Note that this must be set before the surface view's containing
+     * window is attached to the window manager.
+     *
+     * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
+     */
+    public void setZOrderOnTop(boolean onTop) {
+        if (onTop) {
+            mSubLayer = APPLICATION_PANEL_SUBLAYER;
+        } else {
+            mSubLayer = APPLICATION_MEDIA_SUBLAYER;
+        }
+    }
+
+    /**
+     * Control whether the surface view's content should be treated as secure,
+     * preventing it from appearing in screenshots or from being viewed on
+     * non-secure displays.
+     *
+     * <p>Note that this must be set before the surface view's containing
+     * window is attached to the window manager.
+     *
+     * <p>See {@link android.view.Display#FLAG_SECURE} for details.
+     *
+     * @param isSecure True if the surface view is secure.
+     */
+    public void setSecure(boolean isSecure) {
+        if (isSecure) {
+            mSurfaceFlags |= SurfaceControl.SECURE;
+        } else {
+            mSurfaceFlags &= ~SurfaceControl.SECURE;
+        }
+    }
+
+    private void updateOpaqueFlag() {
+        if (!PixelFormat.formatHasAlpha(mRequestedFormat)) {
+            mSurfaceFlags |= SurfaceControl.OPAQUE;
+        } else {
+            mSurfaceFlags &= ~SurfaceControl.OPAQUE;
+        }
+    }
+
+    private Rect getParentSurfaceInsets() {
+        final ViewRootImpl root = getViewRootImpl();
+        if (root == null) {
+            return null;
+        } else {
+            return root.mWindowAttributes.surfaceInsets;
+        }
+    }
+
+    /** @hide */
+    protected void updateSurface() {
+        if (!mHaveFrame) {
+            return;
+        }
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) {
+            return;
+        }
+
+        mTranslator = viewRoot.mTranslator;
+        if (mTranslator != null) {
+            mSurface.setCompatibilityTranslator(mTranslator);
+        }
+
+        int myWidth = mRequestedWidth;
+        if (myWidth <= 0) myWidth = getWidth();
+        int myHeight = mRequestedHeight;
+        if (myHeight <= 0) myHeight = getHeight();
+
+        final boolean formatChanged = mFormat != mRequestedFormat;
+        final boolean visibleChanged = mVisible != mRequestedVisible;
+        final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged)
+                && mRequestedVisible;
+        final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight;
+        final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility;
+        boolean redrawNeeded = false;
+
+        if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) {
+            getLocationInWindow(mLocation);
+
+            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+                    + "Changes: creating=" + creating
+                    + " format=" + formatChanged + " size=" + sizeChanged
+                    + " visible=" + visibleChanged
+                    + " left=" + (mWindowSpaceLeft != mLocation[0])
+                    + " top=" + (mWindowSpaceTop != mLocation[1]));
+
+            try {
+                final boolean visible = mVisible = mRequestedVisible;
+                mWindowSpaceLeft = mLocation[0];
+                mWindowSpaceTop = mLocation[1];
+                mSurfaceWidth = myWidth;
+                mSurfaceHeight = myHeight;
+                mFormat = mRequestedFormat;
+                mLastWindowVisibility = mWindowVisibility;
+
+                mScreenRect.left = mWindowSpaceLeft;
+                mScreenRect.top = mWindowSpaceTop;
+                mScreenRect.right = mWindowSpaceLeft + getWidth();
+                mScreenRect.bottom = mWindowSpaceTop + getHeight();
+                if (mTranslator != null) {
+                    mTranslator.translateRectInAppWindowToScreen(mScreenRect);
+                }
+
+                final Rect surfaceInsets = getParentSurfaceInsets();
+                mScreenRect.offset(surfaceInsets.left, surfaceInsets.top);
+
+                if (creating) {
+                    mSurfaceSession = new SurfaceSession(viewRoot.mSurface);
+                    mDeferredDestroySurfaceControl = mSurfaceControl;
+
+                    updateOpaqueFlag();
+                    final String name = "SurfaceView - " + viewRoot.getTitle().toString();
+
+                    mSurfaceControl = new SurfaceControlWithBackground(
+                            name,
+                            (mSurfaceFlags & SurfaceControl.OPAQUE) != 0,
+                            new SurfaceControl.Builder(mSurfaceSession)
+                                    .setSize(mSurfaceWidth, mSurfaceHeight)
+                                    .setFormat(mFormat)
+                                    .setFlags(mSurfaceFlags));
+                } else if (mSurfaceControl == null) {
+                    return;
+                }
+
+                boolean realSizeChanged = false;
+
+                mSurfaceLock.lock();
+                try {
+                    mDrawingStopped = !visible;
+
+                    if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+                            + "Cur surface: " + mSurface);
+
+                    SurfaceControl.openTransaction();
+                    try {
+                        mSurfaceControl.setLayer(mSubLayer);
+                        if (mViewVisibility) {
+                            mSurfaceControl.show();
+                        } else {
+                            mSurfaceControl.hide();
+                        }
+
+                        // While creating the surface, we will set it's initial
+                        // geometry. Outside of that though, we should generally
+                        // leave it to the RenderThread.
+                        //
+                        // There is one more case when the buffer size changes we aren't yet
+                        // prepared to sync (as even following the transaction applying
+                        // we still need to latch a buffer).
+                        // b/28866173
+                        if (sizeChanged || creating || !mRtHandlingPositionUpdates) {
+                            mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top);
+                            mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth,
+                                    0.0f, 0.0f,
+                                    mScreenRect.height() / (float) mSurfaceHeight);
+                        }
+                        if (sizeChanged) {
+                            mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight);
+                        }
+                    } finally {
+                        SurfaceControl.closeTransaction();
+                    }
+
+                    if (sizeChanged || creating) {
+                        redrawNeeded = true;
+                    }
+
+                    mSurfaceFrame.left = 0;
+                    mSurfaceFrame.top = 0;
+                    if (mTranslator == null) {
+                        mSurfaceFrame.right = mSurfaceWidth;
+                        mSurfaceFrame.bottom = mSurfaceHeight;
+                    } else {
+                        float appInvertedScale = mTranslator.applicationInvertedScale;
+                        mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f);
+                        mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f);
+                    }
+
+                    final int surfaceWidth = mSurfaceFrame.right;
+                    final int surfaceHeight = mSurfaceFrame.bottom;
+                    realSizeChanged = mLastSurfaceWidth != surfaceWidth
+                            || mLastSurfaceHeight != surfaceHeight;
+                    mLastSurfaceWidth = surfaceWidth;
+                    mLastSurfaceHeight = surfaceHeight;
+                } finally {
+                    mSurfaceLock.unlock();
+                }
+
+                try {
+                    redrawNeeded |= visible && !mDrawFinished;
+
+                    SurfaceHolder.Callback callbacks[] = null;
+
+                    final boolean surfaceChanged = creating;
+                    if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) {
+                        mSurfaceCreated = false;
+                        if (mSurface.isValid()) {
+                            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+                                    + "visibleChanged -- surfaceDestroyed");
+                            callbacks = getSurfaceCallbacks();
+                            for (SurfaceHolder.Callback c : callbacks) {
+                                c.surfaceDestroyed(mSurfaceHolder);
+                            }
+                            // Since Android N the same surface may be reused and given to us
+                            // again by the system server at a later point. However
+                            // as we didn't do this in previous releases, clients weren't
+                            // necessarily required to clean up properly in
+                            // surfaceDestroyed. This leads to problems for example when
+                            // clients don't destroy their EGL context, and try
+                            // and create a new one on the same surface following reuse.
+                            // Since there is no valid use of the surface in-between
+                            // surfaceDestroyed and surfaceCreated, we force a disconnect,
+                            // so the next connect will always work if we end up reusing
+                            // the surface.
+                            if (mSurface.isValid()) {
+                                mSurface.forceScopedDisconnect();
+                            }
+                        }
+                    }
+
+                    if (creating) {
+                        mSurface.copyFrom(mSurfaceControl);
+                    }
+
+                    if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion
+                            < Build.VERSION_CODES.O) {
+                        // Some legacy applications use the underlying native {@link Surface} object
+                        // as a key to whether anything has changed. In these cases, updates to the
+                        // existing {@link Surface} will be ignored when the size changes.
+                        // Therefore, we must explicitly recreate the {@link Surface} in these
+                        // cases.
+                        mSurface.createFrom(mSurfaceControl);
+                    }
+
+                    if (visible && mSurface.isValid()) {
+                        if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) {
+                            mSurfaceCreated = true;
+                            mIsCreating = true;
+                            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+                                    + "visibleChanged -- surfaceCreated");
+                            if (callbacks == null) {
+                                callbacks = getSurfaceCallbacks();
+                            }
+                            for (SurfaceHolder.Callback c : callbacks) {
+                                c.surfaceCreated(mSurfaceHolder);
+                            }
+                        }
+                        if (creating || formatChanged || sizeChanged
+                                || visibleChanged || realSizeChanged) {
+                            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+                                    + "surfaceChanged -- format=" + mFormat
+                                    + " w=" + myWidth + " h=" + myHeight);
+                            if (callbacks == null) {
+                                callbacks = getSurfaceCallbacks();
+                            }
+                            for (SurfaceHolder.Callback c : callbacks) {
+                                c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight);
+                            }
+                        }
+                        if (redrawNeeded) {
+                            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+                                    + "surfaceRedrawNeeded");
+                            if (callbacks == null) {
+                                callbacks = getSurfaceCallbacks();
+                            }
+
+                            mPendingReportDraws++;
+                            viewRoot.drawPending();
+                            SurfaceCallbackHelper sch =
+                                    new SurfaceCallbackHelper(this::onDrawFinished);
+                            sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
+                        }
+                    }
+                } finally {
+                    mIsCreating = false;
+                    if (mSurfaceControl != null && !mSurfaceCreated) {
+                        mSurface.release();
+                        // If we are not in the stopped state, then the destruction of the Surface
+                        // represents a visual change we need to display, and we should go ahead
+                        // and destroy the SurfaceControl. However if we are in the stopped state,
+                        // we can just leave the Surface around so it can be a part of animations,
+                        // and we let the life-time be tied to the parent surface.
+                        if (!mWindowStopped) {
+                            mSurfaceControl.destroy();
+                            mSurfaceControl = null;
+                        }
+                    }
+                }
+            } catch (Exception ex) {
+                Log.e(TAG, "Exception configuring surface", ex);
+            }
+            if (DEBUG) Log.v(
+                TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top
+                + " w=" + mScreenRect.width() + " h=" + mScreenRect.height()
+                + ", frame=" + mSurfaceFrame);
+        } else {
+            // Calculate the window position in case RT loses the window
+            // and we need to fallback to a UI-thread driven position update
+            getLocationInSurface(mLocation);
+            final boolean positionChanged = mWindowSpaceLeft != mLocation[0]
+                    || mWindowSpaceTop != mLocation[1];
+            final boolean layoutSizeChanged = getWidth() != mScreenRect.width()
+                    || getHeight() != mScreenRect.height();
+            if (positionChanged || layoutSizeChanged) { // Only the position has changed
+                mWindowSpaceLeft = mLocation[0];
+                mWindowSpaceTop = mLocation[1];
+                // For our size changed check, we keep mScreenRect.width() and mScreenRect.height()
+                // in view local space.
+                mLocation[0] = getWidth();
+                mLocation[1] = getHeight();
+
+                mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop,
+                        mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]);
+
+                if (mTranslator != null) {
+                    mTranslator.translateRectInAppWindowToScreen(mScreenRect);
+                }
+
+                if (mSurfaceControl == null) {
+                    return;
+                }
+
+                if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) {
+                    try {
+                        if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " +
+                                "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
+                                mScreenRect.left, mScreenRect.top,
+                                mScreenRect.right, mScreenRect.bottom));
+                        setParentSpaceRectangle(mScreenRect, -1);
+                    } catch (Exception ex) {
+                        Log.e(TAG, "Exception configuring surface", ex);
+                    }
+                }
+            }
+        }
+    }
+
+    private void onDrawFinished() {
+        if (DEBUG) {
+            Log.i(TAG, System.identityHashCode(this) + " "
+                    + "finishedDrawing");
+        }
+
+        if (mDeferredDestroySurfaceControl != null) {
+            mDeferredDestroySurfaceControl.destroy();
+            mDeferredDestroySurfaceControl = null;
+        }
+
+        runOnUiThread(() -> {
+            performDrawFinished();
+        });
+    }
+
+    /**
+     * A place to over-ride for applying child-surface transactions.
+     * These can be synchronized with the viewroot surface using deferTransaction.
+     *
+     * Called from RenderWorker while UI thread is paused.
+     * @hide
+     */
+    protected void applyChildSurfaceTransaction_renderWorker(SurfaceControl.Transaction t,
+            Surface viewRootSurface, long nextViewRootFrameNumber) {
+    }
+
+    private void applySurfaceTransforms(SurfaceControl surface, Rect position, long frameNumber) {
+        if (frameNumber > 0) {
+            final ViewRootImpl viewRoot = getViewRootImpl();
+
+            mRtTransaction.deferTransactionUntilSurface(surface, viewRoot.mSurface,
+                    frameNumber);
+        }
+
+        mRtTransaction.setPosition(surface, position.left, position.top);
+        mRtTransaction.setMatrix(surface,
+                position.width() / (float) mSurfaceWidth,
+                0.0f, 0.0f,
+                position.height() / (float) mSurfaceHeight);
+    }
+
+    private void setParentSpaceRectangle(Rect position, long frameNumber) {
+        final ViewRootImpl viewRoot = getViewRootImpl();
+
+        applySurfaceTransforms(mSurfaceControl, position, frameNumber);
+        applySurfaceTransforms(mSurfaceControl.mBackgroundControl, position, frameNumber);
+
+        applyChildSurfaceTransaction_renderWorker(mRtTransaction, viewRoot.mSurface,
+                frameNumber);
+
+        mRtTransaction.apply();
+    }
+
+    private Rect mRTLastReportedPosition = new Rect();
+
+    /**
+     * Called by native by a Rendering Worker thread to update the window position
+     * @hide
+     */
+    public final void updateSurfacePosition_renderWorker(long frameNumber,
+            int left, int top, int right, int bottom) {
+        if (mSurfaceControl == null) {
+            return;
+        }
+
+        // TODO: This is teensy bit racey in that a brand new SurfaceView moving on
+        // its 2nd frame if RenderThread is running slowly could potentially see
+        // this as false, enter the branch, get pre-empted, then this comes along
+        // and reports a new position, then the UI thread resumes and reports
+        // its position. This could therefore be de-sync'd in that interval, but
+        // the synchronization would violate the rule that RT must never block
+        // on the UI thread which would open up potential deadlocks. The risk of
+        // a single-frame desync is therefore preferable for now.
+        mRtHandlingPositionUpdates = true;
+        if (mRTLastReportedPosition.left == left
+                && mRTLastReportedPosition.top == top
+                && mRTLastReportedPosition.right == right
+                && mRTLastReportedPosition.bottom == bottom) {
+            return;
+        }
+        try {
+            if (DEBUG) {
+                Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " +
+                        "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
+                        frameNumber, left, top, right, bottom));
+            }
+            mRTLastReportedPosition.set(left, top, right, bottom);
+            setParentSpaceRectangle(mRTLastReportedPosition, frameNumber);
+            // Now overwrite mRTLastReportedPosition with our values
+        } catch (Exception ex) {
+            Log.e(TAG, "Exception from repositionChild", ex);
+        }
+    }
+
+    /**
+     * Called by native on RenderThread to notify that the view is no longer in the
+     * draw tree. UI thread is blocked at this point.
+     * @hide
+     */
+    public final void surfacePositionLost_uiRtSync(long frameNumber) {
+        if (DEBUG) {
+            Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d",
+                    System.identityHashCode(this), frameNumber));
+        }
+        mRTLastReportedPosition.setEmpty();
+
+        if (mSurfaceControl == null) {
+            return;
+        }
+        if (mRtHandlingPositionUpdates) {
+            mRtHandlingPositionUpdates = false;
+            // This callback will happen while the UI thread is blocked, so we can
+            // safely access other member variables at this time.
+            // So do what the UI thread would have done if RT wasn't handling position
+            // updates.
+            if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) {
+                try {
+                    if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " +
+                            "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
+                            mScreenRect.left, mScreenRect.top,
+                            mScreenRect.right, mScreenRect.bottom));
+                    setParentSpaceRectangle(mScreenRect, frameNumber);
+                } catch (Exception ex) {
+                    Log.e(TAG, "Exception configuring surface", ex);
+                }
+            }
+        }
+    }
+
+    private SurfaceHolder.Callback[] getSurfaceCallbacks() {
+        SurfaceHolder.Callback callbacks[];
+        synchronized (mCallbacks) {
+            callbacks = new SurfaceHolder.Callback[mCallbacks.size()];
+            mCallbacks.toArray(callbacks);
+        }
+        return callbacks;
+    }
+
+    private void runOnUiThread(Runnable runnable) {
+        Handler handler = getHandler();
+        if (handler != null && handler.getLooper() != Looper.myLooper()) {
+            handler.post(runnable);
+        } else {
+            runnable.run();
+        }
+    }
+
+    /**
+     * Check to see if the surface has fixed size dimensions or if the surface's
+     * dimensions are dimensions are dependent on its current layout.
+     *
+     * @return true if the surface has dimensions that are fixed in size
+     * @hide
+     */
+    public boolean isFixedSize() {
+        return (mRequestedWidth != -1 || mRequestedHeight != -1);
+    }
+
+    private boolean isAboveParent() {
+        return mSubLayer >= 0;
+    }
+
+    /**
+     * Set an opaque background color to use with this {@link SurfaceView} when it's being resized
+     * and size of the content hasn't updated yet. This color will fill the expanded area when the
+     * view becomes larger.
+     * @param bgColor An opaque color to fill the background. Alpha component will be ignored.
+     * @hide
+     */
+    public void setResizeBackgroundColor(int bgColor) {
+        mSurfaceControl.setBackgroundColor(bgColor);
+    }
+
+    private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
+        private static final String LOG_TAG = "SurfaceHolder";
 
         @Override
         public boolean isCreating() {
-            return false;
+            return mIsCreating;
         }
 
         @Override
         public void addCallback(Callback callback) {
+            synchronized (mCallbacks) {
+                // This is a linear search, but in practice we'll
+                // have only a couple callbacks, so it doesn't matter.
+                if (mCallbacks.contains(callback) == false) {
+                    mCallbacks.add(callback);
+                }
+            }
         }
 
         @Override
         public void removeCallback(Callback callback) {
+            synchronized (mCallbacks) {
+                mCallbacks.remove(callback);
+            }
         }
 
         @Override
         public void setFixedSize(int width, int height) {
+            if (mRequestedWidth != width || mRequestedHeight != height) {
+                mRequestedWidth = width;
+                mRequestedHeight = height;
+                requestLayout();
+            }
         }
 
         @Override
         public void setSizeFromLayout() {
+            if (mRequestedWidth != -1 || mRequestedHeight != -1) {
+                mRequestedWidth = mRequestedHeight = -1;
+                requestLayout();
+            }
         }
 
         @Override
         public void setFormat(int format) {
+            // for backward compatibility reason, OPAQUE always
+            // means 565 for SurfaceView
+            if (format == PixelFormat.OPAQUE)
+                format = PixelFormat.RGB_565;
+
+            mRequestedFormat = format;
+            if (mSurfaceControl != null) {
+                updateSurface();
+            }
         }
 
+        /**
+         * @deprecated setType is now ignored.
+         */
         @Override
-        public void setType(int type) {
-        }
+        @Deprecated
+        public void setType(int type) { }
 
         @Override
         public void setKeepScreenOn(boolean screenOn) {
+            runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn));
         }
 
+        /**
+         * Gets a {@link Canvas} for drawing into the SurfaceView's Surface
+         *
+         * After drawing into the provided {@link Canvas}, the caller must
+         * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
+         *
+         * The caller must redraw the entire surface.
+         * @return A canvas for drawing into the surface.
+         */
         @Override
         public Canvas lockCanvas() {
-            return null;
+            return internalLockCanvas(null, false);
+        }
+
+        /**
+         * Gets a {@link Canvas} for drawing into the SurfaceView's Surface
+         *
+         * After drawing into the provided {@link Canvas}, the caller must
+         * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
+         *
+         * @param inOutDirty A rectangle that represents the dirty region that the caller wants
+         * to redraw.  This function may choose to expand the dirty rectangle if for example
+         * the surface has been resized or if the previous contents of the surface were
+         * not available.  The caller must redraw the entire dirty region as represented
+         * by the contents of the inOutDirty rectangle upon return from this function.
+         * The caller may also pass <code>null</code> instead, in the case where the
+         * entire surface should be redrawn.
+         * @return A canvas for drawing into the surface.
+         */
+        @Override
+        public Canvas lockCanvas(Rect inOutDirty) {
+            return internalLockCanvas(inOutDirty, false);
         }
 
         @Override
-        public Canvas lockCanvas(Rect dirty) {
+        public Canvas lockHardwareCanvas() {
+            return internalLockCanvas(null, true);
+        }
+
+        private Canvas internalLockCanvas(Rect dirty, boolean hardware) {
+            mSurfaceLock.lock();
+
+            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped="
+                    + mDrawingStopped + ", surfaceControl=" + mSurfaceControl);
+
+            Canvas c = null;
+            if (!mDrawingStopped && mSurfaceControl != null) {
+                try {
+                    if (hardware) {
+                        c = mSurface.lockHardwareCanvas();
+                    } else {
+                        c = mSurface.lockCanvas(dirty);
+                    }
+                } catch (Exception e) {
+                    Log.e(LOG_TAG, "Exception locking surface", e);
+                }
+            }
+
+            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c);
+            if (c != null) {
+                mLastLockTime = SystemClock.uptimeMillis();
+                return c;
+            }
+
+            // If the Surface is not ready to be drawn, then return null,
+            // but throttle calls to this function so it isn't called more
+            // than every 100ms.
+            long now = SystemClock.uptimeMillis();
+            long nextTime = mLastLockTime + 100;
+            if (nextTime > now) {
+                try {
+                    Thread.sleep(nextTime-now);
+                } catch (InterruptedException e) {
+                }
+                now = SystemClock.uptimeMillis();
+            }
+            mLastLockTime = now;
+            mSurfaceLock.unlock();
+
             return null;
         }
 
+        /**
+         * Posts the new contents of the {@link Canvas} to the surface and
+         * releases the {@link Canvas}.
+         *
+         * @param canvas The canvas previously obtained from {@link #lockCanvas}.
+         */
         @Override
         public void unlockCanvasAndPost(Canvas canvas) {
+            mSurface.unlockCanvasAndPost(canvas);
+            mSurfaceLock.unlock();
         }
 
         @Override
         public Surface getSurface() {
-            return null;
+            return mSurface;
         }
 
         @Override
         public Rect getSurfaceFrame() {
-            return null;
+            return mSurfaceFrame;
         }
     };
-}
 
+    class SurfaceControlWithBackground extends SurfaceControl {
+        SurfaceControl mBackgroundControl;
+        private boolean mOpaque = true;
+        public boolean mVisible = false;
+
+        public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b)
+                       throws Exception {
+            super(b.setName(name).build());
+
+            mBackgroundControl = b.setName("Background for -" + name)
+                    .setFormat(OPAQUE)
+                    .setColorLayer(true)
+                    .build();
+            mOpaque = opaque;
+        }
+
+        @Override
+        public void setAlpha(float alpha) {
+            super.setAlpha(alpha);
+            mBackgroundControl.setAlpha(alpha);
+        }
+
+        @Override
+        public void setLayer(int zorder) {
+            super.setLayer(zorder);
+            // -3 is below all other child layers as SurfaceView never goes below -2
+            mBackgroundControl.setLayer(-3);
+        }
+
+        @Override
+        public void setPosition(float x, float y) {
+            super.setPosition(x, y);
+            mBackgroundControl.setPosition(x, y);
+        }
+
+        @Override
+        public void setSize(int w, int h) {
+            super.setSize(w, h);
+            mBackgroundControl.setSize(w, h);
+        }
+
+        @Override
+        public void setWindowCrop(Rect crop) {
+            super.setWindowCrop(crop);
+            mBackgroundControl.setWindowCrop(crop);
+        }
+
+        @Override
+        public void setFinalCrop(Rect crop) {
+            super.setFinalCrop(crop);
+            mBackgroundControl.setFinalCrop(crop);
+        }
+
+        @Override
+        public void setLayerStack(int layerStack) {
+            super.setLayerStack(layerStack);
+            mBackgroundControl.setLayerStack(layerStack);
+        }
+
+        @Override
+        public void setOpaque(boolean isOpaque) {
+            super.setOpaque(isOpaque);
+            mOpaque = isOpaque;
+            updateBackgroundVisibility();
+        }
+
+        @Override
+        public void setSecure(boolean isSecure) {
+            super.setSecure(isSecure);
+        }
+
+        @Override
+        public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) {
+            super.setMatrix(dsdx, dtdx, dsdy, dtdy);
+            mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy);
+        }
+
+        @Override
+        public void hide() {
+            super.hide();
+            mVisible = false;
+            updateBackgroundVisibility();
+        }
+
+        @Override
+        public void show() {
+            super.show();
+            mVisible = true;
+            updateBackgroundVisibility();
+        }
+
+        @Override
+        public void destroy() {
+            super.destroy();
+            mBackgroundControl.destroy();
+         }
+
+        @Override
+        public void release() {
+            super.release();
+            mBackgroundControl.release();
+        }
+
+        @Override
+        public void setTransparentRegionHint(Region region) {
+            super.setTransparentRegionHint(region);
+            mBackgroundControl.setTransparentRegionHint(region);
+        }
+
+        @Override
+        public void deferTransactionUntil(IBinder handle, long frame) {
+            super.deferTransactionUntil(handle, frame);
+            mBackgroundControl.deferTransactionUntil(handle, frame);
+        }
+
+        @Override
+        public void deferTransactionUntil(Surface barrier, long frame) {
+            super.deferTransactionUntil(barrier, frame);
+            mBackgroundControl.deferTransactionUntil(barrier, frame);
+        }
+
+        /** Set the color to fill the background with. */
+        private void setBackgroundColor(int bgColor) {
+            final float[] colorComponents = new float[] { Color.red(bgColor) / 255.f,
+                    Color.green(bgColor) / 255.f, Color.blue(bgColor) / 255.f };
+
+            SurfaceControl.openTransaction();
+            try {
+                mBackgroundControl.setColor(colorComponents);
+            } finally {
+                SurfaceControl.closeTransaction();
+            }
+        }
+
+        void updateBackgroundVisibility() {
+            if (mOpaque && mVisible) {
+                mBackgroundControl.show();
+            } else {
+                mBackgroundControl.hide();
+            }
+        }
+    }
+}
diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java
index 5eb7e9c..e03f5fa 100644
--- a/android/view/ThreadedRenderer.java
+++ b/android/view/ThreadedRenderer.java
@@ -190,6 +190,10 @@
      */
     public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor";
 
+    public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101;
+    public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102;
+    public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103;
+
     static {
         // Try to check OpenGL support early if possible.
         isAvailable();
@@ -1140,6 +1144,16 @@
         nHackySetRTAnimationsEnabled(divisor <= 1);
     }
 
+    /**
+     * Changes the OpenGL context priority if IMG_context_priority extension is available. Must be
+     * called before any OpenGL context is created.
+     *
+     * @param priority The priority to use. Must be one of EGL_CONTEXT_PRIORITY_* values.
+     */
+    public static void setContextPriority(int priority) {
+        nSetContextPriority(priority);
+    }
+
     /** Not actually public - internal use only. This doc to make lint happy */
     public static native void disableVsync();
 
@@ -1213,4 +1227,5 @@
     private static native void nHackySetRTAnimationsEnabled(boolean enabled);
     private static native void nSetDebuggingEnabled(boolean enabled);
     private static native void nSetIsolatedProcess(boolean enabled);
+    private static native void nSetContextPriority(int priority);
 }
diff --git a/android/view/View.java b/android/view/View.java
index 97e11b1..71b6084 100644
--- a/android/view/View.java
+++ b/android/view/View.java
@@ -697,6 +697,7 @@
  * security policy. See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}.
  * </p>
  *
+ * @attr ref android.R.styleable#View_accessibilityHeading
  * @attr ref android.R.styleable#View_alpha
  * @attr ref android.R.styleable#View_background
  * @attr ref android.R.styleable#View_clickable
@@ -2955,7 +2956,7 @@
      *     1                             PFLAG3_SCREEN_READER_FOCUSABLE
      *    1                              PFLAG3_AGGREGATED_VISIBLE
      *   1                               PFLAG3_AUTOFILLID_EXPLICITLY_SET
-     *  1                                available
+     *  1                                PFLAG3_ACCESSIBILITY_HEADING
      * |-------|-------|-------|-------|
      */
 
@@ -3252,6 +3253,11 @@
      */
     private static final int PFLAG3_AUTOFILLID_EXPLICITLY_SET = 0x40000000;
 
+    /**
+     * Indicates if the View is a heading for accessibility purposes
+     */
+    private static final int PFLAG3_ACCESSIBILITY_HEADING = 0x80000000;
+
     /* End of masks for mPrivateFlags3 */
 
     /**
@@ -5475,6 +5481,8 @@
                 case R.styleable.View_outlineAmbientShadowColor:
                     setOutlineAmbientShadowColor(a.getColor(attr, Color.BLACK));
                     break;
+                case com.android.internal.R.styleable.View_accessibilityHeading:
+                    setAccessibilityHeading(a.getBoolean(attr, false));
             }
         }
 
@@ -8795,6 +8803,7 @@
         info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN);
         populateAccessibilityNodeInfoDrawingOrderInParent(info);
         info.setPaneTitle(mAccessibilityPaneTitle);
+        info.setHeading(isAccessibilityHeading());
     }
 
     /**
@@ -10398,7 +10407,21 @@
      *
      * @param willNotCacheDrawing true if this view does not cache its
      *        drawing, false otherwise
+     *
+     * @deprecated The view drawing cache was largely made obsolete with the introduction of
+     * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+     * layers are largely unnecessary and can easily result in a net loss in performance due to the
+     * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+     * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+     * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+     * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+     * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+     * software-rendered usages are discouraged and have compatibility issues with hardware-only
+     * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+     * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+     * reports or unit testing the {@link PixelCopy} API is recommended.
      */
+    @Deprecated
     public void setWillNotCacheDrawing(boolean willNotCacheDrawing) {
         setFlags(willNotCacheDrawing ? WILL_NOT_CACHE_DRAWING : 0, WILL_NOT_CACHE_DRAWING);
     }
@@ -10407,8 +10430,22 @@
      * Returns whether or not this View can cache its drawing or not.
      *
      * @return true if this view does not cache its drawing, false otherwise
+     *
+     * @deprecated The view drawing cache was largely made obsolete with the introduction of
+     * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+     * layers are largely unnecessary and can easily result in a net loss in performance due to the
+     * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+     * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+     * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+     * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+     * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+     * software-rendered usages are discouraged and have compatibility issues with hardware-only
+     * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+     * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+     * reports or unit testing the {@link PixelCopy} API is recommended.
      */
     @ViewDebug.ExportedProperty(category = "drawing")
+    @Deprecated
     public boolean willNotCacheDrawing() {
         return (mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING;
     }
@@ -10754,11 +10791,37 @@
      *                              accessibility tools.
      */
     public void setScreenReaderFocusable(boolean screenReaderFocusable) {
+        updatePflags3AndNotifyA11yIfChanged(PFLAG3_SCREEN_READER_FOCUSABLE, screenReaderFocusable);
+    }
+
+    /**
+     * Gets whether this view is a heading for accessibility purposes.
+     *
+     * @return {@code true} if the view is a heading, {@code false} otherwise.
+     *
+     * @attr ref android.R.styleable#View_accessibilityHeading
+     */
+    public boolean isAccessibilityHeading() {
+        return (mPrivateFlags3 & PFLAG3_ACCESSIBILITY_HEADING) != 0;
+    }
+
+    /**
+     * Set if view is a heading for a section of content for accessibility purposes.
+     *
+     * @param isHeading {@code true} if the view is a heading, {@code false} otherwise.
+     *
+     * @attr ref android.R.styleable#View_accessibilityHeading
+     */
+    public void setAccessibilityHeading(boolean isHeading) {
+        updatePflags3AndNotifyA11yIfChanged(PFLAG3_ACCESSIBILITY_HEADING, isHeading);
+    }
+
+    private void updatePflags3AndNotifyA11yIfChanged(int mask, boolean newValue) {
         int pflags3 = mPrivateFlags3;
-        if (screenReaderFocusable) {
-            pflags3 |= PFLAG3_SCREEN_READER_FOCUSABLE;
+        if (newValue) {
+            pflags3 |= mask;
         } else {
-            pflags3 &= ~PFLAG3_SCREEN_READER_FOCUSABLE;
+            pflags3 &= ~mask;
         }
 
         if (pflags3 != mPrivateFlags3) {
@@ -11763,6 +11826,14 @@
         return null;
     }
 
+    /** @hide */
+    View getSelfOrParentImportantForA11y() {
+        if (isImportantForAccessibility()) return this;
+        ViewParent parent = getParentForAccessibility();
+        if (parent instanceof View) return (View) parent;
+        return null;
+    }
+
     /**
      * Adds the children of this View relevant for accessibility to the given list
      * as output. Since some Views are not important for accessibility the added
@@ -14978,10 +15049,7 @@
     public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
         ensureTransformationInfo();
         if (mTransformationInfo.mAlpha != alpha) {
-            // Report visibility changes, which can affect children, to accessibility
-            if ((alpha == 0) ^ (mTransformationInfo.mAlpha == 0)) {
-                notifySubtreeAccessibilityStateChangedIfNeeded();
-            }
+            float oldAlpha = mTransformationInfo.mAlpha;
             mTransformationInfo.mAlpha = alpha;
             if (onSetAlpha((int) (alpha * 255))) {
                 mPrivateFlags |= PFLAG_ALPHA_SET;
@@ -14993,6 +15061,10 @@
                 invalidateViewProperty(true, false);
                 mRenderNode.setAlpha(getFinalAlpha());
             }
+            // Report visibility changes, which can affect children, to accessibility
+            if ((alpha == 0) ^ (oldAlpha == 0)) {
+                notifySubtreeAccessibilityStateChangedIfNeeded();
+            }
         }
     }
 
diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java
index 6002fe5..2ec42c0 100644
--- a/android/view/ViewGroup.java
+++ b/android/view/ViewGroup.java
@@ -5692,6 +5692,7 @@
         }
         dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE
                 && isShown());
+        notifySubtreeAccessibilityStateChangedIfNeeded();
     }
 
     /**
diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java
index 433c90b..730c372 100644
--- a/android/view/ViewRootImpl.java
+++ b/android/view/ViewRootImpl.java
@@ -3719,7 +3719,7 @@
         checkThread();
         if (mView != null) {
             if (!mView.hasFocus()) {
-                if (sAlwaysAssignFocus || !isInTouchMode()) {
+                if (sAlwaysAssignFocus || !mAttachInfo.mInTouchMode) {
                     v.requestFocus();
                 }
             } else {
@@ -6482,17 +6482,17 @@
                     params.type = mOrigWindowType;
                 }
             }
-
-            if (mSurface.isValid()) {
-                params.frameNumber = mSurface.getNextFrameNumber();
-            }
         }
 
-        int relayoutResult = mWindowSession.relayout(
-                mWindow, mSeq, params,
+        long frameNumber = -1;
+        if (mSurface.isValid()) {
+            frameNumber = mSurface.getNextFrameNumber();
+        }
+
+        int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
                 (int) (mView.getMeasuredWidth() * appScale + 0.5f),
-                (int) (mView.getMeasuredHeight() * appScale + 0.5f),
-                viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
+                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
+                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                 mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                 mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
                 mPendingMergedConfiguration, mSurface);
@@ -8305,6 +8305,12 @@
 
         public View mSource;
         public long mLastEventTimeMillis;
+        /**
+         * Override for {@link AccessibilityEvent#originStackTrace} to provide the stack trace
+         * of the original {@link #runOrPost} call instead of one for sending the delayed event
+         * from a looper.
+         */
+        public StackTraceElement[] mOrigin;
 
         @Override
         public void run() {
@@ -8322,6 +8328,7 @@
                 AccessibilityEvent event = AccessibilityEvent.obtain();
                 event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
                 event.setContentChangeTypes(mChangeTypes);
+                if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin;
                 source.sendAccessibilityEventUnchecked(event);
             } else {
                 mLastEventTimeMillis = 0;
@@ -8329,6 +8336,7 @@
             // In any case reset to initial state.
             source.resetSubtreeAccessibilityStateChanged();
             mChangeTypes = 0;
+            if (AccessibilityEvent.DEBUG_ORIGIN) mOrigin = null;
         }
 
         public void runOrPost(View source, int changeType) {
@@ -8352,12 +8360,18 @@
                 // If there is no common predecessor, then mSource points to
                 // a removed view, hence in this case always prefer the source.
                 View predecessor = getCommonPredecessor(mSource, source);
+                if (predecessor != null) {
+                    predecessor = predecessor.getSelfOrParentImportantForA11y();
+                }
                 mSource = (predecessor != null) ? predecessor : source;
                 mChangeTypes |= changeType;
                 return;
             }
             mSource = source;
             mChangeTypes = changeType;
+            if (AccessibilityEvent.DEBUG_ORIGIN) {
+                mOrigin = Thread.currentThread().getStackTrace();
+            }
             final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis;
             final long minEventIntevalMillis =
                     ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java
index f6181d7..0f5c23f 100644
--- a/android/view/WindowManager.java
+++ b/android/view/WindowManager.java
@@ -2438,13 +2438,6 @@
         public long hideTimeoutMilliseconds = -1;
 
         /**
-         * A frame number in which changes requested in this layout will be rendered.
-         *
-         * @hide
-         */
-        public long frameNumber = -1;
-
-        /**
          * The color mode requested by this window. The target display may
          * not be able to honor the request. When the color mode is not set
          * to {@link ActivityInfo#COLOR_MODE_DEFAULT}, it might override the
@@ -2617,7 +2610,6 @@
             TextUtils.writeToParcel(accessibilityTitle, out, parcelableFlags);
             out.writeInt(mColorMode);
             out.writeLong(hideTimeoutMilliseconds);
-            out.writeLong(frameNumber);
         }
 
         public static final Parcelable.Creator<LayoutParams> CREATOR
@@ -2674,7 +2666,6 @@
             accessibilityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
             mColorMode = in.readInt();
             hideTimeoutMilliseconds = in.readLong();
-            frameNumber = in.readLong();
         }
 
         @SuppressWarnings({"PointlessBitwiseExpression"})
@@ -2875,10 +2866,6 @@
                 changes |= SURFACE_INSETS_CHANGED;
             }
 
-            // The frame number changing is only relevant in the context of other
-            // changes, and so we don't need to track it with a flag.
-            frameNumber = o.frameNumber;
-
             if (hasManualSurfaceInsets != o.hasManualSurfaceInsets) {
                 hasManualSurfaceInsets = o.hasManualSurfaceInsets;
                 changes |= SURFACE_INSETS_CHANGED;
diff --git a/android/view/WindowManagerGlobal.java b/android/view/WindowManagerGlobal.java
index cca66d6..08c2d0b 100644
--- a/android/view/WindowManagerGlobal.java
+++ b/android/view/WindowManagerGlobal.java
@@ -610,6 +610,10 @@
                     ViewRootImpl root = mRoots.get(i);
                     // Client might remove the view by "stopped" event.
                     root.setWindowStopped(stopped);
+                    // Recursively forward stopped state to View's attached
+                    // to this Window rather than the root application token,
+                    // e.g. PopupWindow's.
+                    setStoppedState(root.mAttachInfo.mWindowToken, stopped);
                 }
             }
         }
diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java
index e0f74a7..7946e9e 100644
--- a/android/view/accessibility/AccessibilityEvent.java
+++ b/android/view/accessibility/AccessibilityEvent.java
@@ -201,6 +201,7 @@
  * <em>Properties:</em></br>
  * <ul>
  *   <li>{@link #getEventType()} - The type of the event.</li>
+ *   <li>{@link #getContentChangeTypes()} - The type of state changes.</li>
  *   <li>{@link #getSource()} - The source info (for registered clients).</li>
  *   <li>{@link #getClassName()} - The class name of the source.</li>
  *   <li>{@link #getPackageName()} - The package name of the source.</li>
@@ -388,6 +389,8 @@
  */
 public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable {
     private static final boolean DEBUG = false;
+    /** @hide */
+    public static final boolean DEBUG_ORIGIN = false;
 
     /**
      * Invalid selection/focus position.
@@ -748,7 +751,7 @@
 
     private static final int MAX_POOL_SIZE = 10;
     private static final SynchronizedPool<AccessibilityEvent> sPool =
-            new SynchronizedPool<AccessibilityEvent>(MAX_POOL_SIZE);
+            new SynchronizedPool<>(MAX_POOL_SIZE);
 
     private @EventType int mEventType;
     private CharSequence mPackageName;
@@ -758,6 +761,17 @@
     int mContentChangeTypes;
     int mWindowChangeTypes;
 
+    /**
+     * The stack trace describing where this event originated from on the app side.
+     * Only populated if {@link #DEBUG_ORIGIN} is enabled
+     * Can be inspected(e.g. printed) from an
+     * {@link android.accessibilityservice.AccessibilityService} to trace where particular events
+     * are being dispatched from.
+     *
+     * @hide
+     */
+    public StackTraceElement[] originStackTrace = null;
+
     private ArrayList<AccessibilityRecord> mRecords;
 
     /*
@@ -780,6 +794,7 @@
         mWindowChangeTypes = event.mWindowChangeTypes;
         mEventTime = event.mEventTime;
         mPackageName = event.mPackageName;
+        if (DEBUG_ORIGIN) originStackTrace = event.originStackTrace;
     }
 
     /**
@@ -849,16 +864,17 @@
     }
 
     /**
-     * Gets the bit mask of change types signaled by an
-     * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. A single event may represent
-     * multiple change types.
+     * Gets the bit mask of change types signaled by a
+     * {@link #TYPE_WINDOW_CONTENT_CHANGED} event or {@link #TYPE_WINDOW_STATE_CHANGED}. A single
+     * event may represent multiple change types.
      *
      * @return The bit mask of change types. One or more of:
      *         <ul>
-     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
-     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
-     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
-     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
+     *         <li>{@link #CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
+     *         <li>{@link #CONTENT_CHANGE_TYPE_SUBTREE}
+     *         <li>{@link #CONTENT_CHANGE_TYPE_TEXT}
+     *         <li>{@link #CONTENT_CHANGE_TYPE_PANE_TITLE}
+     *         <li>{@link #CONTENT_CHANGE_TYPE_UNDEFINED}
      *         </ul>
      */
     @ContentChangeTypes
@@ -877,6 +893,7 @@
             }
             case CONTENT_CHANGE_TYPE_SUBTREE: return "CONTENT_CHANGE_TYPE_SUBTREE";
             case CONTENT_CHANGE_TYPE_TEXT: return "CONTENT_CHANGE_TYPE_TEXT";
+            case CONTENT_CHANGE_TYPE_PANE_TITLE: return "CONTENT_CHANGE_TYPE_PANE_TITLE";
             case CONTENT_CHANGE_TYPE_UNDEFINED: return "CONTENT_CHANGE_TYPE_UNDEFINED";
             default: return Integer.toHexString(type);
         }
@@ -1104,7 +1121,9 @@
      */
     public static AccessibilityEvent obtain() {
         AccessibilityEvent event = sPool.acquire();
-        return (event != null) ? event : new AccessibilityEvent();
+        if (event == null) event = new AccessibilityEvent();
+        if (DEBUG_ORIGIN) event.originStackTrace = Thread.currentThread().getStackTrace();
+        return event;
     }
 
     /**
@@ -1142,6 +1161,7 @@
                 record.recycle();
             }
         }
+        if (DEBUG_ORIGIN) originStackTrace = null;
     }
 
     /**
@@ -1164,7 +1184,7 @@
         // Read the records.
         final int recordCount = parcel.readInt();
         if (recordCount > 0) {
-            mRecords = new ArrayList<AccessibilityRecord>(recordCount);
+            mRecords = new ArrayList<>(recordCount);
             for (int i = 0; i < recordCount; i++) {
                 AccessibilityRecord record = AccessibilityRecord.obtain();
                 readAccessibilityRecordFromParcel(record, parcel);
@@ -1172,6 +1192,17 @@
                 mRecords.add(record);
             }
         }
+
+        if (DEBUG_ORIGIN) {
+            originStackTrace = new StackTraceElement[parcel.readInt()];
+            for (int i = 0; i < originStackTrace.length; i++) {
+                originStackTrace[i] = new StackTraceElement(
+                        parcel.readString(),
+                        parcel.readString(),
+                        parcel.readString(),
+                        parcel.readInt());
+            }
+        }
     }
 
     /**
@@ -1227,6 +1258,17 @@
             AccessibilityRecord record = mRecords.get(i);
             writeAccessibilityRecordToParcel(record, parcel, flags);
         }
+
+        if (DEBUG_ORIGIN) {
+            if (originStackTrace == null) originStackTrace = Thread.currentThread().getStackTrace();
+            parcel.writeInt(originStackTrace.length);
+            for (StackTraceElement element : originStackTrace) {
+                parcel.writeString(element.getClassName());
+                parcel.writeString(element.getMethodName());
+                parcel.writeString(element.getFileName());
+                parcel.writeInt(element.getLineNumber());
+            }
+        }
     }
 
     /**
@@ -1285,7 +1327,7 @@
         }
         if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) {
             builder.append("; WindowChangeTypes: ").append(
-                    contentChangeTypesToString(mWindowChangeTypes));
+                    windowChangeTypesToString(mWindowChangeTypes));
         }
         super.appendTo(builder);
         if (DEBUG || DEBUG_CONCISE_TOSTRING) {
diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java
index 72af203..d60c481 100644
--- a/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/android/view/accessibility/AccessibilityInteractionClient.java
@@ -326,12 +326,14 @@
                             accessibilityWindowId, accessibilityNodeId);
                     if (cachedInfo != null) {
                         if (DEBUG) {
-                            Log.i(LOG_TAG, "Node cache hit");
+                            Log.i(LOG_TAG, "Node cache hit for "
+                                    + idToString(accessibilityWindowId, accessibilityNodeId));
                         }
                         return cachedInfo;
                     }
                     if (DEBUG) {
-                        Log.i(LOG_TAG, "Node cache miss");
+                        Log.i(LOG_TAG, "Node cache miss for "
+                                + idToString(accessibilityWindowId, accessibilityNodeId));
                     }
                 }
                 final int interactionId = mInteractionIdCounter.getAndIncrement();
@@ -368,6 +370,11 @@
         return null;
     }
 
+    private static String idToString(int accessibilityWindowId, long accessibilityNodeId) {
+        return accessibilityWindowId + "/"
+                + AccessibilityNodeInfo.idToString(accessibilityNodeId);
+    }
+
     /**
      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
      * the window whose id is specified and starts from the node whose accessibility
diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java
index 84b4064..cbb23f1 100644
--- a/android/view/accessibility/AccessibilityManager.java
+++ b/android/view/accessibility/AccessibilityManager.java
@@ -16,48 +16,156 @@
 
 package android.view.accessibility;
 
+import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME;
+
+import android.Manifest;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemService;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.Binder;
 import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
 import android.view.IWindow;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent.EventType;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IntPair;
+
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 /**
- * System level service that serves as an event dispatch for {@link AccessibilityEvent}s.
- * Such events are generated when something notable happens in the user interface,
+ * System level service that serves as an event dispatch for {@link AccessibilityEvent}s,
+ * and provides facilities for querying the accessibility state of the system.
+ * Accessibility events are generated when something notable happens in the user interface,
  * for example an {@link android.app.Activity} starts, the focus or selection of a
  * {@link android.view.View} changes etc. Parties interested in handling accessibility
  * events implement and register an accessibility service which extends
- * {@code android.accessibilityservice.AccessibilityService}.
+ * {@link android.accessibilityservice.AccessibilityService}.
  *
  * @see AccessibilityEvent
- * @see android.content.Context#getSystemService
+ * @see AccessibilityNodeInfo
+ * @see android.accessibilityservice.AccessibilityService
+ * @see Context#getSystemService
+ * @see Context#ACCESSIBILITY_SERVICE
  */
-@SuppressWarnings("UnusedDeclaration")
+@SystemService(Context.ACCESSIBILITY_SERVICE)
 public final class AccessibilityManager {
+    private static final boolean DEBUG = false;
 
-    private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0);
+    private static final String LOG_TAG = "AccessibilityManager";
 
+    /** @hide */
+    public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001;
+
+    /** @hide */
+    public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002;
+
+    /** @hide */
+    public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004;
+
+    /** @hide */
+    public static final int DALTONIZER_DISABLED = -1;
+
+    /** @hide */
+    public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0;
+
+    /** @hide */
+    public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12;
+
+    /** @hide */
+    public static final int AUTOCLICK_DELAY_DEFAULT = 600;
 
     /**
-     * Listener for the accessibility state.
+     * Activity action: Launch UI to manage which accessibility service or feature is assigned
+     * to the navigation bar Accessibility button.
+     * <p>
+     * Input: Nothing.
+     * </p>
+     * <p>
+     * Output: Nothing.
+     * </p>
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON =
+            "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON";
+
+    static final Object sInstanceSync = new Object();
+
+    private static AccessibilityManager sInstance;
+
+    private final Object mLock = new Object();
+
+    private IAccessibilityManager mService;
+
+    final int mUserId;
+
+    final Handler mHandler;
+
+    final Handler.Callback mCallback;
+
+    boolean mIsEnabled;
+
+    int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK;
+
+    boolean mIsTouchExplorationEnabled;
+
+    boolean mIsHighTextContrastEnabled;
+
+    AccessibilityPolicy mAccessibilityPolicy;
+
+    private final ArrayMap<AccessibilityStateChangeListener, Handler>
+            mAccessibilityStateChangeListeners = new ArrayMap<>();
+
+    private final ArrayMap<TouchExplorationStateChangeListener, Handler>
+            mTouchExplorationStateChangeListeners = new ArrayMap<>();
+
+    private final ArrayMap<HighTextContrastChangeListener, Handler>
+            mHighTextContrastStateChangeListeners = new ArrayMap<>();
+
+    private final ArrayMap<AccessibilityServicesStateChangeListener, Handler>
+            mServicesStateChangeListeners = new ArrayMap<>();
+
+    /**
+     * Map from a view's accessibility id to the list of request preparers set for that view
+     */
+    private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists;
+
+    /**
+     * Listener for the system accessibility state. To listen for changes to the
+     * accessibility state on the device, implement this interface and register
+     * it with the system by calling {@link #addAccessibilityStateChangeListener}.
      */
     public interface AccessibilityStateChangeListener {
 
         /**
-         * Called back on change in the accessibility state.
+         * Called when the accessibility enabled state changes.
          *
          * @param enabled Whether accessibility is enabled.
          */
-        public void onAccessibilityStateChanged(boolean enabled);
+        void onAccessibilityStateChanged(boolean enabled);
     }
 
     /**
@@ -73,7 +181,24 @@
          *
          * @param enabled Whether touch exploration is enabled.
          */
-        public void onTouchExplorationStateChanged(boolean enabled);
+        void onTouchExplorationStateChanged(boolean enabled);
+    }
+
+    /**
+     * Listener for changes to the state of accessibility services. Changes include services being
+     * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service.
+     * {@see #addAccessibilityServicesStateChangeListener}.
+     *
+     * @hide
+     */
+    public interface AccessibilityServicesStateChangeListener {
+
+        /**
+         * Called when the state of accessibility services changes.
+         *
+         * @param manager The manager that is calling back
+         */
+        void onAccessibilityServicesStateChanged(AccessibilityManager manager);
     }
 
     /**
@@ -81,6 +206,8 @@
      * the high text contrast state on the device, implement this interface and
      * register it with the system by calling
      * {@link #addHighTextContrastStateChangeListener}.
+     *
+     * @hide
      */
     public interface HighTextContrastChangeListener {
 
@@ -89,7 +216,7 @@
          *
          * @param enabled Whether high text contrast is enabled.
          */
-        public void onHighTextContrastStateChanged(boolean enabled);
+        void onHighTextContrastStateChanged(boolean enabled);
     }
 
     /**
@@ -148,21 +275,67 @@
 
     private final IAccessibilityManagerClient.Stub mClient =
             new IAccessibilityManagerClient.Stub() {
-                public void setState(int state) {
-                }
+        @Override
+        public void setState(int state) {
+            // We do not want to change this immediately as the application may
+            // have already checked that accessibility is on and fired an event,
+            // that is now propagating up the view tree, Hence, if accessibility
+            // is now off an exception will be thrown. We want to have the exception
+            // enforcement to guard against apps that fire unnecessary accessibility
+            // events when accessibility is off.
+            mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget();
+        }
 
-                public void notifyServicesStateChanged() {
+        @Override
+        public void notifyServicesStateChanged() {
+            final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners;
+            synchronized (mLock) {
+                if (mServicesStateChangeListeners.isEmpty()) {
+                    return;
                 }
+                listeners = new ArrayMap<>(mServicesStateChangeListeners);
+            }
 
-                public void setRelevantEventTypes(int eventTypes) {
-                }
-            };
+            int numListeners = listeners.size();
+            for (int i = 0; i < numListeners; i++) {
+                final AccessibilityServicesStateChangeListener listener =
+                        mServicesStateChangeListeners.keyAt(i);
+                mServicesStateChangeListeners.valueAt(i).post(() -> listener
+                        .onAccessibilityServicesStateChanged(AccessibilityManager.this));
+            }
+        }
+
+        @Override
+        public void setRelevantEventTypes(int eventTypes) {
+            mRelevantEventTypes = eventTypes;
+        }
+    };
 
     /**
      * Get an AccessibilityManager instance (create one if necessary).
      *
+     * @param context Context in which this manager operates.
+     *
+     * @hide
      */
     public static AccessibilityManager getInstance(Context context) {
+        synchronized (sInstanceSync) {
+            if (sInstance == null) {
+                final int userId;
+                if (Binder.getCallingUid() == Process.SYSTEM_UID
+                        || context.checkCallingOrSelfPermission(
+                                Manifest.permission.INTERACT_ACROSS_USERS)
+                                        == PackageManager.PERMISSION_GRANTED
+                        || context.checkCallingOrSelfPermission(
+                                Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+                                        == PackageManager.PERMISSION_GRANTED) {
+                    userId = UserHandle.USER_CURRENT;
+                } else {
+                    userId = context.getUserId();
+                }
+                sInstance = new AccessibilityManager(context, null, userId);
+            }
+        }
         return sInstance;
     }
 
@@ -170,21 +343,65 @@
      * Create an instance.
      *
      * @param context A {@link Context}.
+     * @param service An interface to the backing service.
+     * @param userId User id under which to run.
+     *
+     * @hide
      */
     public AccessibilityManager(Context context, IAccessibilityManager service, int userId) {
+        // Constructor can't be chained because we can't create an instance of an inner class
+        // before calling another constructor.
+        mCallback = new MyCallback();
+        mHandler = new Handler(context.getMainLooper(), mCallback);
+        mUserId = userId;
+        synchronized (mLock) {
+            tryConnectToServiceLocked(service);
+        }
     }
 
+    /**
+     * Create an instance.
+     *
+     * @param handler The handler to use
+     * @param service An interface to the backing service.
+     * @param userId User id under which to run.
+     *
+     * @hide
+     */
+    public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) {
+        mCallback = new MyCallback();
+        mHandler = handler;
+        mUserId = userId;
+        synchronized (mLock) {
+            tryConnectToServiceLocked(service);
+        }
+    }
+
+    /**
+     * @hide
+     */
     public IAccessibilityManagerClient getClient() {
         return mClient;
     }
 
     /**
-     * Returns if the {@link AccessibilityManager} is enabled.
+     * @hide
+     */
+    @VisibleForTesting
+    public Handler.Callback getCallback() {
+        return mCallback;
+    }
+
+    /**
+     * Returns if the accessibility in the system is enabled.
      *
-     * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
+     * @return True if accessibility is enabled, false otherwise.
      */
     public boolean isEnabled() {
-        return false;
+        synchronized (mLock) {
+            return mIsEnabled || (mAccessibilityPolicy != null
+                    && mAccessibilityPolicy.isEnabled(mIsEnabled));
+        }
     }
 
     /**
@@ -193,7 +410,13 @@
      * @return True if touch exploration is enabled, false otherwise.
      */
     public boolean isTouchExplorationEnabled() {
-        return true;
+        synchronized (mLock) {
+            IAccessibilityManager service = getServiceLocked();
+            if (service == null) {
+                return false;
+            }
+            return mIsTouchExplorationEnabled;
+        }
     }
 
     /**
@@ -203,47 +426,188 @@
      * doing its own rendering and does not rely on the platform rendering pipeline.
      * </p>
      *
+     * @return True if high text contrast is enabled, false otherwise.
+     *
+     * @hide
      */
     public boolean isHighTextContrastEnabled() {
-        return false;
+        synchronized (mLock) {
+            IAccessibilityManager service = getServiceLocked();
+            if (service == null) {
+                return false;
+            }
+            return mIsHighTextContrastEnabled;
+        }
     }
 
     /**
      * Sends an {@link AccessibilityEvent}.
+     *
+     * @param event The event to send.
+     *
+     * @throws IllegalStateException if accessibility is not enabled.
+     *
+     * <strong>Note:</strong> The preferred mechanism for sending custom accessibility
+     * events is through calling
+     * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
+     * instead of this method to allow predecessors to augment/filter events sent by
+     * their descendants.
      */
     public void sendAccessibilityEvent(AccessibilityEvent event) {
+        final IAccessibilityManager service;
+        final int userId;
+        final AccessibilityEvent dispatchedEvent;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+            event.setEventTime(SystemClock.uptimeMillis());
+            if (mAccessibilityPolicy != null) {
+                dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event,
+                        mIsEnabled, mRelevantEventTypes);
+                if (dispatchedEvent == null) {
+                    return;
+                }
+            } else {
+                dispatchedEvent = event;
+            }
+            if (!isEnabled()) {
+                Looper myLooper = Looper.myLooper();
+                if (myLooper == Looper.getMainLooper()) {
+                    throw new IllegalStateException(
+                            "Accessibility off. Did you forget to check that?");
+                } else {
+                    // If we're not running on the thread with the main looper, it's possible for
+                    // the state of accessibility to change between checking isEnabled and
+                    // calling this method. So just log the error rather than throwing the
+                    // exception.
+                    Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled");
+                    return;
+                }
+            }
+            if ((dispatchedEvent.getEventType() & mRelevantEventTypes) == 0) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "Not dispatching irrelevant event: " + dispatchedEvent
+                            + " that is not among "
+                            + AccessibilityEvent.eventTypeToString(mRelevantEventTypes));
+                }
+                return;
+            }
+            userId = mUserId;
+        }
+        try {
+            // it is possible that this manager is in the same process as the service but
+            // client using it is called through Binder from another process. Example: MMS
+            // app adds a SMS notification and the NotificationManagerService calls this method
+            long identityToken = Binder.clearCallingIdentity();
+            try {
+                service.sendAccessibilityEvent(dispatchedEvent, userId);
+            } finally {
+                Binder.restoreCallingIdentity(identityToken);
+            }
+            if (DEBUG) {
+                Log.i(LOG_TAG, dispatchedEvent + " sent");
+            }
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re);
+        } finally {
+            if (event != dispatchedEvent) {
+                event.recycle();
+            }
+            dispatchedEvent.recycle();
+        }
     }
 
     /**
-     * Returns whether there are observers registered for this event type. If
-     * this method returns false you shuold not generate events of this type
-     * to conserve resources.
-     *
-     * @param type The event type.
-     * @return Whether the event is being observed.
-     */
-    public boolean isObservedEventType(@AccessibilityEvent.EventType int type) {
-        return false;
-    }
-
-    /**
-     * Requests interruption of the accessibility feedback from all accessibility services.
+     * Requests feedback interruption from all accessibility services.
      */
     public void interrupt() {
+        final IAccessibilityManager service;
+        final int userId;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+            if (!isEnabled()) {
+                Looper myLooper = Looper.myLooper();
+                if (myLooper == Looper.getMainLooper()) {
+                    throw new IllegalStateException(
+                            "Accessibility off. Did you forget to check that?");
+                } else {
+                    // If we're not running on the thread with the main looper, it's possible for
+                    // the state of accessibility to change between checking isEnabled and
+                    // calling this method. So just log the error rather than throwing the
+                    // exception.
+                    Log.e(LOG_TAG, "Interrupt called with accessibility disabled");
+                    return;
+                }
+            }
+            userId = mUserId;
+        }
+        try {
+            service.interrupt(userId);
+            if (DEBUG) {
+                Log.i(LOG_TAG, "Requested interrupt from all services");
+            }
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re);
+        }
     }
 
     /**
      * Returns the {@link ServiceInfo}s of the installed accessibility services.
      *
      * @return An unmodifiable list with {@link ServiceInfo}s.
+     *
+     * @deprecated Use {@link #getInstalledAccessibilityServiceList()}
      */
     @Deprecated
     public List<ServiceInfo> getAccessibilityServiceList() {
-        return Collections.emptyList();
+        List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList();
+        List<ServiceInfo> services = new ArrayList<>();
+        final int infoCount = infos.size();
+        for (int i = 0; i < infoCount; i++) {
+            AccessibilityServiceInfo info = infos.get(i);
+            services.add(info.getResolveInfo().serviceInfo);
+        }
+        return Collections.unmodifiableList(services);
     }
 
+    /**
+     * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services.
+     *
+     * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
+     */
     public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
-        return Collections.emptyList();
+        final IAccessibilityManager service;
+        final int userId;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return Collections.emptyList();
+            }
+            userId = mUserId;
+        }
+
+        List<AccessibilityServiceInfo> services = null;
+        try {
+            services = service.getInstalledAccessibilityServiceList(userId);
+            if (DEBUG) {
+                Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
+            }
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
+        }
+        if (mAccessibilityPolicy != null) {
+            services = mAccessibilityPolicy.getInstalledAccessibilityServiceList(services);
+        }
+        if (services != null) {
+            return Collections.unmodifiableList(services);
+        } else {
+            return Collections.emptyList();
+        }
     }
 
     /**
@@ -258,21 +622,52 @@
      * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC
      * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN
      * @see AccessibilityServiceInfo#FEEDBACK_VISUAL
+     * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE
      */
     public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
             int feedbackTypeFlags) {
-        return Collections.emptyList();
+        final IAccessibilityManager service;
+        final int userId;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return Collections.emptyList();
+            }
+            userId = mUserId;
+        }
+
+        List<AccessibilityServiceInfo> services = null;
+        try {
+            services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId);
+            if (DEBUG) {
+                Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
+            }
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
+        }
+        if (mAccessibilityPolicy != null) {
+            services = mAccessibilityPolicy.getEnabledAccessibilityServiceList(
+                    feedbackTypeFlags, services);
+        }
+        if (services != null) {
+            return Collections.unmodifiableList(services);
+        } else {
+            return Collections.emptyList();
+        }
     }
 
     /**
      * Registers an {@link AccessibilityStateChangeListener} for changes in
-     * the global accessibility state of the system.
+     * the global accessibility state of the system. Equivalent to calling
+     * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)}
+     * with a null handler.
      *
      * @param listener The listener.
-     * @return True if successfully registered.
+     * @return Always returns {@code true}.
      */
     public boolean addAccessibilityStateChangeListener(
-            AccessibilityStateChangeListener listener) {
+            @NonNull AccessibilityStateChangeListener listener) {
+        addAccessibilityStateChangeListener(listener, null);
         return true;
     }
 
@@ -286,22 +681,40 @@
      *                for a callback on the process's main handler.
      */
     public void addAccessibilityStateChangeListener(
-            @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {}
+            @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {
+        synchronized (mLock) {
+            mAccessibilityStateChangeListeners
+                    .put(listener, (handler == null) ? mHandler : handler);
+        }
+    }
 
+    /**
+     * Unregisters an {@link AccessibilityStateChangeListener}.
+     *
+     * @param listener The listener.
+     * @return True if the listener was previously registered.
+     */
     public boolean removeAccessibilityStateChangeListener(
-            AccessibilityStateChangeListener listener) {
-        return true;
+            @NonNull AccessibilityStateChangeListener listener) {
+        synchronized (mLock) {
+            int index = mAccessibilityStateChangeListeners.indexOfKey(listener);
+            mAccessibilityStateChangeListeners.remove(listener);
+            return (index >= 0);
+        }
     }
 
     /**
      * Registers a {@link TouchExplorationStateChangeListener} for changes in
-     * the global touch exploration state of the system.
+     * the global touch exploration state of the system. Equivalent to calling
+     * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)}
+     * with a null handler.
      *
      * @param listener The listener.
-     * @return True if successfully registered.
+     * @return Always returns {@code true}.
      */
     public boolean addTouchExplorationStateChangeListener(
             @NonNull TouchExplorationStateChangeListener listener) {
+        addTouchExplorationStateChangeListener(listener, null);
         return true;
     }
 
@@ -315,17 +728,104 @@
      *                for a callback on the process's main handler.
      */
     public void addTouchExplorationStateChangeListener(
-            @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {}
+            @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {
+        synchronized (mLock) {
+            mTouchExplorationStateChangeListeners
+                    .put(listener, (handler == null) ? mHandler : handler);
+        }
+    }
 
     /**
      * Unregisters a {@link TouchExplorationStateChangeListener}.
      *
      * @param listener The listener.
-     * @return True if successfully unregistered.
+     * @return True if listener was previously registered.
      */
     public boolean removeTouchExplorationStateChangeListener(
             @NonNull TouchExplorationStateChangeListener listener) {
-        return true;
+        synchronized (mLock) {
+            int index = mTouchExplorationStateChangeListeners.indexOfKey(listener);
+            mTouchExplorationStateChangeListeners.remove(listener);
+            return (index >= 0);
+        }
+    }
+
+    /**
+     * Registers a {@link AccessibilityServicesStateChangeListener}.
+     *
+     * @param listener The listener.
+     * @param handler The handler on which the listener should be called back, or {@code null}
+     *                for a callback on the process's main handler.
+     * @hide
+     */
+    public void addAccessibilityServicesStateChangeListener(
+            @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) {
+        synchronized (mLock) {
+            mServicesStateChangeListeners
+                    .put(listener, (handler == null) ? mHandler : handler);
+        }
+    }
+
+    /**
+     * Unregisters a {@link AccessibilityServicesStateChangeListener}.
+     *
+     * @param listener The listener.
+     *
+     * @hide
+     */
+    public void removeAccessibilityServicesStateChangeListener(
+            @NonNull AccessibilityServicesStateChangeListener listener) {
+        synchronized (mLock) {
+            mServicesStateChangeListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Registers a {@link AccessibilityRequestPreparer}.
+     */
+    public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
+        if (mRequestPreparerLists == null) {
+            mRequestPreparerLists = new SparseArray<>(1);
+        }
+        int id = preparer.getView().getAccessibilityViewId();
+        List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id);
+        if (requestPreparerList == null) {
+            requestPreparerList = new ArrayList<>(1);
+            mRequestPreparerLists.put(id, requestPreparerList);
+        }
+        requestPreparerList.add(preparer);
+    }
+
+    /**
+     * Unregisters a {@link AccessibilityRequestPreparer}.
+     */
+    public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
+        if (mRequestPreparerLists == null) {
+            return;
+        }
+        int viewId = preparer.getView().getAccessibilityViewId();
+        List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId);
+        if (requestPreparerList != null) {
+            requestPreparerList.remove(preparer);
+            if (requestPreparerList.isEmpty()) {
+                mRequestPreparerLists.remove(viewId);
+            }
+        }
+    }
+
+    /**
+     * Get the preparers that are registered for an accessibility ID
+     *
+     * @param id The ID of interest
+     * @return The list of preparers, or {@code null} if there are none.
+     *
+     * @hide
+     */
+    public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) {
+        if (mRequestPreparerLists == null) {
+            return null;
+        }
+        return mRequestPreparerLists.get(id);
     }
 
     /**
@@ -337,7 +837,12 @@
      * @hide
      */
     public void addHighTextContrastStateChangeListener(
-            @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {}
+            @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {
+        synchronized (mLock) {
+            mHighTextContrastStateChangeListeners
+                    .put(listener, (handler == null) ? mHandler : handler);
+        }
+    }
 
     /**
      * Unregisters a {@link HighTextContrastChangeListener}.
@@ -347,7 +852,64 @@
      * @hide
      */
     public void removeHighTextContrastStateChangeListener(
-            @NonNull HighTextContrastChangeListener listener) {}
+            @NonNull HighTextContrastChangeListener listener) {
+        synchronized (mLock) {
+            mHighTextContrastStateChangeListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Sets the {@link AccessibilityPolicy} controlling this manager.
+     *
+     * @param policy The policy.
+     *
+     * @hide
+     */
+    public void setAccessibilityPolicy(@Nullable AccessibilityPolicy policy) {
+        synchronized (mLock) {
+            mAccessibilityPolicy = policy;
+        }
+    }
+
+    /**
+     * Check if the accessibility volume stream is active.
+     *
+     * @return True if accessibility volume is active (i.e. some service has requested it). False
+     * otherwise.
+     * @hide
+     */
+    public boolean isAccessibilityVolumeStreamActive() {
+        List<AccessibilityServiceInfo> serviceInfos =
+                getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+        for (int i = 0; i < serviceInfos.size(); i++) {
+            if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Report a fingerprint gesture to accessibility. Only available for the system process.
+     *
+     * @param keyCode The key code of the gesture
+     * @return {@code true} if accessibility consumes the event. {@code false} if not.
+     * @hide
+     */
+    public boolean sendFingerprintGesture(int keyCode) {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return false;
+            }
+        }
+        try {
+            return service.sendFingerprintGesture(keyCode);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
 
     /**
      * Sets the current state and notifies listeners, if necessary.
@@ -355,14 +917,312 @@
      * @param stateFlags The state flags.
      */
     private void setStateLocked(int stateFlags) {
+        final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0;
+        final boolean touchExplorationEnabled =
+                (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;
+        final boolean highTextContrastEnabled =
+                (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;
+
+        final boolean wasEnabled = isEnabled();
+        final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
+        final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
+
+        // Ensure listeners get current state from isZzzEnabled() calls.
+        mIsEnabled = enabled;
+        mIsTouchExplorationEnabled = touchExplorationEnabled;
+        mIsHighTextContrastEnabled = highTextContrastEnabled;
+
+        if (wasEnabled != isEnabled()) {
+            notifyAccessibilityStateChanged();
+        }
+
+        if (wasTouchExplorationEnabled != touchExplorationEnabled) {
+            notifyTouchExplorationStateChanged();
+        }
+
+        if (wasHighTextContrastEnabled != highTextContrastEnabled) {
+            notifyHighTextContrastStateChanged();
+        }
     }
 
+    /**
+     * Find an installed service with the specified {@link ComponentName}.
+     *
+     * @param componentName The name to match to the service.
+     *
+     * @return The info corresponding to the installed service, or {@code null} if no such service
+     * is installed.
+     * @hide
+     */
+    public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName(
+            ComponentName componentName) {
+        final List<AccessibilityServiceInfo> installedServiceInfos =
+                getInstalledAccessibilityServiceList();
+        if ((installedServiceInfos == null) || (componentName == null)) {
+            return null;
+        }
+        for (int i = 0; i < installedServiceInfos.size(); i++) {
+            if (componentName.equals(installedServiceInfos.get(i).getComponentName())) {
+                return installedServiceInfos.get(i);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds an accessibility interaction connection interface for a given window.
+     * @param windowToken The window token to which a connection is added.
+     * @param connection The connection.
+     *
+     * @hide
+     */
     public int addAccessibilityInteractionConnection(IWindow windowToken,
-            IAccessibilityInteractionConnection connection) {
+            String packageName, IAccessibilityInteractionConnection connection) {
+        final IAccessibilityManager service;
+        final int userId;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return View.NO_ID;
+            }
+            userId = mUserId;
+        }
+        try {
+            return service.addAccessibilityInteractionConnection(windowToken, connection,
+                    packageName, userId);
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re);
+        }
         return View.NO_ID;
     }
 
+    /**
+     * Removed an accessibility interaction connection interface for a given window.
+     * @param windowToken The window token to which a connection is removed.
+     *
+     * @hide
+     */
     public void removeAccessibilityInteractionConnection(IWindow windowToken) {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+        }
+        try {
+            service.removeAccessibilityInteractionConnection(windowToken);
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re);
+        }
     }
 
+    /**
+     * Perform the accessibility shortcut if the caller has permission.
+     *
+     * @hide
+     */
+    public void performAccessibilityShortcut() {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+        }
+        try {
+            service.performAccessibilityShortcut();
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re);
+        }
+    }
+
+    /**
+     * Notifies that the accessibility button in the system's navigation area has been clicked
+     *
+     * @hide
+     */
+    public void notifyAccessibilityButtonClicked() {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+        }
+        try {
+            service.notifyAccessibilityButtonClicked();
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error while dispatching accessibility button click", re);
+        }
+    }
+
+    /**
+     * Notifies that the visibility of the accessibility button in the system's navigation area
+     * has changed.
+     *
+     * @param shown {@code true} if the accessibility button is visible within the system
+     *                  navigation area, {@code false} otherwise
+     * @hide
+     */
+    public void notifyAccessibilityButtonVisibilityChanged(boolean shown) {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+        }
+        try {
+            service.notifyAccessibilityButtonVisibilityChanged(shown);
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re);
+        }
+    }
+
+    /**
+     * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture
+     * window. Intended for use by the System UI only.
+     *
+     * @param connection The connection to handle the actions. Set to {@code null} to avoid
+     * affecting the actions.
+     *
+     * @hide
+     */
+    public void setPictureInPictureActionReplacingConnection(
+            @Nullable IAccessibilityInteractionConnection connection) {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+        }
+        try {
+            service.setPictureInPictureActionReplacingConnection(connection);
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error setting picture in picture action replacement", re);
+        }
+    }
+
+    private IAccessibilityManager getServiceLocked() {
+        if (mService == null) {
+            tryConnectToServiceLocked(null);
+        }
+        return mService;
+    }
+
+    private void tryConnectToServiceLocked(IAccessibilityManager service) {
+        if (service == null) {
+            IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
+            if (iBinder == null) {
+                return;
+            }
+            service = IAccessibilityManager.Stub.asInterface(iBinder);
+        }
+
+        try {
+            final long userStateAndRelevantEvents = service.addClient(mClient, mUserId);
+            setStateLocked(IntPair.first(userStateAndRelevantEvents));
+            mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents);
+            mService = service;
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);
+        }
+    }
+
+    /**
+     * Notifies the registered {@link AccessibilityStateChangeListener}s.
+     */
+    private void notifyAccessibilityStateChanged() {
+        final boolean isEnabled;
+        final ArrayMap<AccessibilityStateChangeListener, Handler> listeners;
+        synchronized (mLock) {
+            if (mAccessibilityStateChangeListeners.isEmpty()) {
+                return;
+            }
+            isEnabled = isEnabled();
+            listeners = new ArrayMap<>(mAccessibilityStateChangeListeners);
+        }
+
+        final int numListeners = listeners.size();
+        for (int i = 0; i < numListeners; i++) {
+            final AccessibilityStateChangeListener listener = listeners.keyAt(i);
+            listeners.valueAt(i).post(() ->
+                    listener.onAccessibilityStateChanged(isEnabled));
+        }
+    }
+
+    /**
+     * Notifies the registered {@link TouchExplorationStateChangeListener}s.
+     */
+    private void notifyTouchExplorationStateChanged() {
+        final boolean isTouchExplorationEnabled;
+        final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners;
+        synchronized (mLock) {
+            if (mTouchExplorationStateChangeListeners.isEmpty()) {
+                return;
+            }
+            isTouchExplorationEnabled = mIsTouchExplorationEnabled;
+            listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners);
+        }
+
+        final int numListeners = listeners.size();
+        for (int i = 0; i < numListeners; i++) {
+            final TouchExplorationStateChangeListener listener = listeners.keyAt(i);
+            listeners.valueAt(i).post(() ->
+                    listener.onTouchExplorationStateChanged(isTouchExplorationEnabled));
+        }
+    }
+
+    /**
+     * Notifies the registered {@link HighTextContrastChangeListener}s.
+     */
+    private void notifyHighTextContrastStateChanged() {
+        final boolean isHighTextContrastEnabled;
+        final ArrayMap<HighTextContrastChangeListener, Handler> listeners;
+        synchronized (mLock) {
+            if (mHighTextContrastStateChangeListeners.isEmpty()) {
+                return;
+            }
+            isHighTextContrastEnabled = mIsHighTextContrastEnabled;
+            listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners);
+        }
+
+        final int numListeners = listeners.size();
+        for (int i = 0; i < numListeners; i++) {
+            final HighTextContrastChangeListener listener = listeners.keyAt(i);
+            listeners.valueAt(i).post(() ->
+                    listener.onHighTextContrastStateChanged(isHighTextContrastEnabled));
+        }
+    }
+
+    /**
+     * Determines if the accessibility button within the system navigation area is supported.
+     *
+     * @return {@code true} if the accessibility button is supported on this device,
+     * {@code false} otherwise
+     */
+    public static boolean isAccessibilityButtonSupported() {
+        final Resources res = Resources.getSystem();
+        return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
+    }
+
+    private final class MyCallback implements Handler.Callback {
+        public static final int MSG_SET_STATE = 1;
+
+        @Override
+        public boolean handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_SET_STATE: {
+                    // See comment at mClient
+                    final int state = message.arg1;
+                    synchronized (mLock) {
+                        setStateLocked(state);
+                    }
+                } break;
+            }
+            return true;
+        }
+    }
 }
diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java
index 4c437dd..03f1c12 100644
--- a/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/android/view/accessibility/AccessibilityNodeInfo.java
@@ -3874,6 +3874,24 @@
                         | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null);
     }
 
+    /** @hide */
+    public static String idToString(long accessibilityId) {
+        int accessibilityViewId = getAccessibilityViewId(accessibilityId);
+        int virtualDescendantId = getVirtualDescendantId(accessibilityId);
+        return virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID
+                ? idItemToString(accessibilityViewId)
+                : idItemToString(accessibilityViewId) + ":" + idItemToString(virtualDescendantId);
+    }
+
+    private static String idItemToString(int item) {
+        switch (item) {
+            case ROOT_ITEM_ID: return "ROOT";
+            case UNDEFINED_ITEM_ID: return "UNDEFINED";
+            case AccessibilityNodeProvider.HOST_VIEW_ID: return "HOST";
+            default: return "" + item;
+        }
+    }
+
     /**
      * A class defining an action that can be performed on an {@link AccessibilityNodeInfo}.
      * Each action has a unique id that is mandatory and optional data.
diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java
index 1da998d..a6495d1 100644
--- a/android/view/autofill/AutofillPopupWindow.java
+++ b/android/view/autofill/AutofillPopupWindow.java
@@ -79,6 +79,11 @@
     public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) {
         mWindowPresenter = new WindowPresenter(presenter);
 
+        // We want to show the window as system controlled one so it covers app windows, but it has
+        // to be an application type (so it's contained inside the application area).
+        // Hence, we set it to the application type with the highest z-order, which currently
+        // is TYPE_APPLICATION_ABOVE_SUB_PANEL.
+        setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
         setTouchModal(false);
         setOutsideTouchable(true);
         setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
diff --git a/android/view/inputmethod/BaseInputConnection.java b/android/view/inputmethod/BaseInputConnection.java
index 5f7a0f7..090e19f 100644
--- a/android/view/inputmethod/BaseInputConnection.java
+++ b/android/view/inputmethod/BaseInputConnection.java
@@ -522,7 +522,7 @@
             b = tmp;
         }
 
-        if (a == b) return null;
+        if (a == b || a < 0) return null;
 
         if ((flags&GET_TEXT_WITH_STYLES) != 0) {
             return content.subSequence(a, b);
diff --git a/android/view/textclassifier/GenerateLinksLogger.java b/android/view/textclassifier/GenerateLinksLogger.java
index 73cf43b..067513f 100644
--- a/android/view/textclassifier/GenerateLinksLogger.java
+++ b/android/view/textclassifier/GenerateLinksLogger.java
@@ -19,13 +19,13 @@
 import android.annotation.Nullable;
 import android.metrics.LogMaker;
 import android.util.ArrayMap;
-import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.Preconditions;
 
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Random;
@@ -39,6 +39,7 @@
 public final class GenerateLinksLogger {
 
     private static final String LOG_TAG = "GenerateLinksLogger";
+    private static final boolean DEBUG_LOG_ENABLED = false;
     private static final String ZERO = "0";
 
     private final MetricsLogger mMetricsLogger;
@@ -127,7 +128,7 @@
     }
 
     private static void debugLog(LogMaker log) {
-        if (!Logger.DEBUG_LOG_ENABLED) return;
+        if (!DEBUG_LOG_ENABLED) return;
 
         final String callId = Objects.toString(
                 log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), "");
@@ -142,8 +143,9 @@
         final int latencyMs = Integer.parseInt(
                 Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO));
 
-        Log.d(LOG_TAG, String.format("%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
-                numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
+        Log.d(LOG_TAG,
+                String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
+                        numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
     }
 
     /** Helper class for storing per-entity type statistics. */
diff --git a/android/view/textclassifier/Logger.java b/android/view/textclassifier/Logger.java
deleted file mode 100644
index f03906a..0000000
--- a/android/view/textclassifier/Logger.java
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.view.textclassifier;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-
-import com.android.internal.util.Preconditions;
-
-import java.text.BreakIterator;
-import java.util.Locale;
-import java.util.Objects;
-
-/**
- * A helper for logging TextClassifier related events.
- * @hide
- */
-public abstract class Logger {
-
-    private static final String LOG_TAG = "Logger";
-    /* package */ static final boolean DEBUG_LOG_ENABLED = true;
-
-    private @SelectionEvent.InvocationMethod int mInvocationMethod;
-    private SelectionEvent mPrevEvent;
-    private SelectionEvent mSmartEvent;
-    private SelectionEvent mStartEvent;
-
-    /**
-     * Logger that does not log anything.
-     * @hide
-     */
-    public static final Logger DISABLED = new Logger() {
-        @Override
-        public void writeEvent(SelectionEvent event) {}
-    };
-
-    @Nullable
-    private final Config mConfig;
-
-    public Logger(Config config) {
-        mConfig = Preconditions.checkNotNull(config);
-    }
-
-    private Logger() {
-        mConfig = null;
-    }
-
-    /**
-     * Writes the selection event to a log.
-     */
-    public abstract void writeEvent(@NonNull SelectionEvent event);
-
-    /**
-     * Returns true if the resultId matches that of a smart selection event (i.e.
-     * {@link SelectionEvent#EVENT_SMART_SELECTION_SINGLE} or
-     * {@link SelectionEvent#EVENT_SMART_SELECTION_MULTI}).
-     * Returns false otherwise.
-     */
-    public boolean isSmartSelection(@NonNull String resultId) {
-        return false;
-    }
-
-    /**
-     * Returns a token iterator for tokenizing text for logging purposes.
-     */
-    public BreakIterator getTokenIterator(@NonNull Locale locale) {
-        return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale));
-    }
-
-    /**
-     * Logs a "selection started" event.
-     *
-     * @param invocationMethod  the way the selection was triggered
-     * @param start  the token index of the selected token
-     */
-    public final void logSelectionStartedEvent(
-            @SelectionEvent.InvocationMethod int invocationMethod, int start) {
-        if (mConfig == null) {
-            return;
-        }
-
-        mInvocationMethod = invocationMethod;
-        logEvent(new SelectionEvent(
-                start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
-                TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig));
-    }
-
-    /**
-     * Logs a "selection modified" event.
-     * Use when the user modifies the selection.
-     *
-     * @param start  the start token (inclusive) index of the selection
-     * @param end  the end token (exclusive) index of the selection
-     */
-    public final void logSelectionModifiedEvent(int start, int end) {
-        Preconditions.checkArgument(end >= start, "end cannot be less than start");
-
-        if (mConfig == null) {
-            return;
-        }
-
-        logEvent(new SelectionEvent(
-                start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
-                TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig));
-    }
-
-    /**
-     * Logs a "selection modified" event.
-     * Use when the user modifies the selection and the selection's entity type is known.
-     *
-     * @param start  the start token (inclusive) index of the selection
-     * @param end  the end token (exclusive) index of the selection
-     * @param classification  the TextClassification object returned by the TextClassifier that
-     *      classified the selected text
-     */
-    public final void logSelectionModifiedEvent(
-            int start, int end, @NonNull TextClassification classification) {
-        Preconditions.checkArgument(end >= start, "end cannot be less than start");
-        Preconditions.checkNotNull(classification);
-
-        if (mConfig == null) {
-            return;
-        }
-
-        final String entityType = classification.getEntityCount() > 0
-                ? classification.getEntity(0)
-                : TextClassifier.TYPE_UNKNOWN;
-        logEvent(new SelectionEvent(
-                start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
-                entityType, mInvocationMethod, classification.getId(), mConfig));
-    }
-
-    /**
-     * Logs a "selection modified" event.
-     * Use when a TextClassifier modifies the selection.
-     *
-     * @param start  the start token (inclusive) index of the selection
-     * @param end  the end token (exclusive) index of the selection
-     * @param selection  the TextSelection object returned by the TextClassifier for the
-     *      specified selection
-     */
-    public final void logSelectionModifiedEvent(
-            int start, int end, @NonNull TextSelection selection) {
-        Preconditions.checkArgument(end >= start, "end cannot be less than start");
-        Preconditions.checkNotNull(selection);
-
-        if (mConfig == null) {
-            return;
-        }
-
-        final int eventType;
-        if (isSmartSelection(selection.getId())) {
-            eventType = end - start > 1
-                    ? SelectionEvent.EVENT_SMART_SELECTION_MULTI
-                    : SelectionEvent.EVENT_SMART_SELECTION_SINGLE;
-
-        } else {
-            eventType = SelectionEvent.EVENT_AUTO_SELECTION;
-        }
-        final String entityType = selection.getEntityCount() > 0
-                ? selection.getEntity(0)
-                : TextClassifier.TYPE_UNKNOWN;
-        logEvent(new SelectionEvent(start, end, eventType, entityType, mInvocationMethod,
-                selection.getId(), mConfig));
-    }
-
-    /**
-     * Logs an event specifying an action taken on a selection.
-     * Use when the user clicks on an action to act on the selected text.
-     *
-     * @param start  the start token (inclusive) index of the selection
-     * @param end  the end token (exclusive) index of the selection
-     * @param actionType  the action that was performed on the selection
-     */
-    public final void logSelectionActionEvent(
-            int start, int end, @SelectionEvent.ActionType int actionType) {
-        Preconditions.checkArgument(end >= start, "end cannot be less than start");
-        checkActionType(actionType);
-
-        if (mConfig == null) {
-            return;
-        }
-
-        logEvent(new SelectionEvent(
-                start, end, actionType, TextClassifier.TYPE_UNKNOWN, mInvocationMethod,
-                null, mConfig));
-    }
-
-    /**
-     * Logs an event specifying an action taken on a selection.
-     * Use when the user clicks on an action to act on the selected text and the selection's
-     * entity type is known.
-     *
-     * @param start  the start token (inclusive) index of the selection
-     * @param end  the end token (exclusive) index of the selection
-     * @param actionType  the action that was performed on the selection
-     * @param classification  the TextClassification object returned by the TextClassifier that
-     *      classified the selected text
-     *
-     * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
-     */
-    public final void logSelectionActionEvent(
-            int start, int end, @SelectionEvent.ActionType int actionType,
-            @NonNull TextClassification classification) {
-        Preconditions.checkArgument(end >= start, "end cannot be less than start");
-        Preconditions.checkNotNull(classification);
-        checkActionType(actionType);
-
-        if (mConfig == null) {
-            return;
-        }
-
-        final String entityType = classification.getEntityCount() > 0
-                ? classification.getEntity(0)
-                : TextClassifier.TYPE_UNKNOWN;
-        logEvent(new SelectionEvent(start, end, actionType, entityType, mInvocationMethod,
-                classification.getId(), mConfig));
-    }
-
-    private void logEvent(@NonNull SelectionEvent event) {
-        Preconditions.checkNotNull(event);
-
-        if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED
-                && mStartEvent == null) {
-            if (DEBUG_LOG_ENABLED) {
-                Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
-            }
-            return;
-        }
-
-        final long now = System.currentTimeMillis();
-        switch (event.getEventType()) {
-            case SelectionEvent.EVENT_SELECTION_STARTED:
-                Preconditions.checkArgument(event.getAbsoluteEnd() == event.getAbsoluteStart() + 1);
-                event.setSessionId(startNewSession());
-                mStartEvent = event;
-                break;
-            case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:  // fall through
-            case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
-                mSmartEvent = event;
-                break;
-            case SelectionEvent.EVENT_SELECTION_MODIFIED:  // fall through
-            case SelectionEvent.EVENT_AUTO_SELECTION:
-                if (mPrevEvent != null
-                        && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart()
-                        && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) {
-                    // Selection did not change. Ignore event.
-                    return;
-                }
-                break;
-            default:
-                // do nothing.
-        }
-
-        event.setEventTime(now);
-        if (mStartEvent != null) {
-            event.setSessionId(mStartEvent.getSessionId())
-                    .setDurationSinceSessionStart(now - mStartEvent.getEventTime())
-                    .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
-                    .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
-        }
-        if (mSmartEvent != null) {
-            event.setResultId(mSmartEvent.getResultId())
-                    .setSmartStart(mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
-                    .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
-        }
-        if (mPrevEvent != null) {
-            event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime())
-                    .setEventIndex(mPrevEvent.getEventIndex() + 1);
-        }
-        writeEvent(event);
-        mPrevEvent = event;
-
-        if (event.isTerminal()) {
-            endSession();
-        }
-    }
-
-    private TextClassificationSessionId startNewSession() {
-        endSession();
-        return new TextClassificationSessionId();
-    }
-
-    private void endSession() {
-        mPrevEvent = null;
-        mSmartEvent = null;
-        mStartEvent = null;
-    }
-
-    /**
-     * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType}
-     */
-    private static void checkActionType(@SelectionEvent.EventType int eventType)
-            throws IllegalArgumentException {
-        switch (eventType) {
-            case SelectionEvent.ACTION_OVERTYPE:  // fall through
-            case SelectionEvent.ACTION_COPY:  // fall through
-            case SelectionEvent.ACTION_PASTE:  // fall through
-            case SelectionEvent.ACTION_CUT:  // fall through
-            case SelectionEvent.ACTION_SHARE:  // fall through
-            case SelectionEvent.ACTION_SMART_SHARE:  // fall through
-            case SelectionEvent.ACTION_DRAG:  // fall through
-            case SelectionEvent.ACTION_ABANDON:  // fall through
-            case SelectionEvent.ACTION_SELECT_ALL:  // fall through
-            case SelectionEvent.ACTION_RESET:  // fall through
-                return;
-            default:
-                throw new IllegalArgumentException(
-                        String.format(Locale.US, "%d is not an eventType", eventType));
-        }
-    }
-
-
-    /**
-     * A Logger config.
-     */
-    public static final class Config {
-
-        private final String mPackageName;
-        private final String mWidgetType;
-        @Nullable private final String mWidgetVersion;
-
-        /**
-         * @param context Context of the widget the logger logs for
-         * @param widgetType a name for the widget being logged for. e.g.
-         *      {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}
-         * @param widgetVersion a string version info for the widget the logger logs for
-         */
-        public Config(
-                @NonNull Context context,
-                @TextClassifier.WidgetType String widgetType,
-                @Nullable String widgetVersion) {
-            mPackageName = Preconditions.checkNotNull(context).getPackageName();
-            mWidgetType = widgetType;
-            mWidgetVersion = widgetVersion;
-        }
-
-        /**
-         * Returns the package name of the application the logger logs for.
-         */
-        public String getPackageName() {
-            return mPackageName;
-        }
-
-        /**
-         * Returns the name for the widget being logged for. e.g.
-         * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}.
-         */
-        public String getWidgetType() {
-            return mWidgetType;
-        }
-
-        /**
-         * Returns string version info for the logger. This is specific to the text classifier.
-         */
-        @Nullable
-        public String getWidgetVersion() {
-            return mWidgetVersion;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mPackageName, mWidgetType, mWidgetVersion);
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj == this) {
-                return true;
-            }
-
-            if (!(obj instanceof Config)) {
-                return false;
-            }
-
-            final Config other = (Config) obj;
-            return Objects.equals(mPackageName, other.mPackageName)
-                    && Objects.equals(mWidgetType, other.mWidgetType)
-                    && Objects.equals(mWidgetVersion, other.mWidgetType);
-        }
-    }
-}
diff --git a/android/view/textclassifier/SelectionEvent.java b/android/view/textclassifier/SelectionEvent.java
index 1e978cc..b073596 100644
--- a/android/view/textclassifier/SelectionEvent.java
+++ b/android/view/textclassifier/SelectionEvent.java
@@ -150,20 +150,6 @@
         mInvocationMethod = invocationMethod;
     }
 
-    SelectionEvent(
-            int start, int end,
-            @EventType int eventType, @EntityType String entityType,
-            @InvocationMethod int invocationMethod, @Nullable String resultId,
-            Logger.Config config) {
-        this(start, end, eventType, entityType, invocationMethod, resultId);
-        Preconditions.checkNotNull(config);
-        setTextClassificationSessionContext(
-                new TextClassificationContext.Builder(
-                        config.getPackageName(), config.getWidgetType())
-                        .setWidgetVersion(config.getWidgetVersion())
-                        .build());
-    }
-
     private SelectionEvent(Parcel in) {
         mAbsoluteStart = in.readInt();
         mAbsoluteEnd = in.readInt();
@@ -362,6 +348,7 @@
             case SelectionEvent.ACTION_ABANDON:  // fall through
             case SelectionEvent.ACTION_SELECT_ALL:  // fall through
             case SelectionEvent.ACTION_RESET:  // fall through
+            case SelectionEvent.ACTION_OTHER:  // fall through
                 return;
             default:
                 throw new IllegalArgumentException(
@@ -667,4 +654,4 @@
             return new SelectionEvent[size];
         }
     };
-}
\ No newline at end of file
+}
diff --git a/android/view/textclassifier/DefaultLogger.java b/android/view/textclassifier/SelectionSessionLogger.java
similarity index 88%
rename from android/view/textclassifier/DefaultLogger.java
rename to android/view/textclassifier/SelectionSessionLogger.java
index 203ca56..f2fb63e 100644
--- a/android/view/textclassifier/DefaultLogger.java
+++ b/android/view/textclassifier/SelectionSessionLogger.java
@@ -17,28 +17,29 @@
 package android.view.textclassifier;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.metrics.LogMaker;
-import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.Preconditions;
 
+import java.text.BreakIterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.StringJoiner;
 
 /**
- * Default Logger.
- * Used internally by TextClassifierImpl.
+ * A helper for logging selection session events.
  * @hide
  */
-public final class DefaultLogger extends Logger {
+public final class SelectionSessionLogger {
 
-    private static final String LOG_TAG = "DefaultLogger";
+    private static final String LOG_TAG = "SelectionSessionLogger";
+    private static final boolean DEBUG_LOG_ENABLED = false;
     static final String CLASSIFIER_ID = "androidtc";
 
     private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START;
@@ -59,23 +60,16 @@
 
     private final MetricsLogger mMetricsLogger;
 
-    public DefaultLogger(@NonNull Config config) {
-        super(config);
+    public SelectionSessionLogger() {
         mMetricsLogger = new MetricsLogger();
     }
 
     @VisibleForTesting
-    public DefaultLogger(@NonNull Config config, @NonNull MetricsLogger metricsLogger) {
-        super(config);
+    public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) {
         mMetricsLogger = Preconditions.checkNotNull(metricsLogger);
     }
 
-    @Override
-    public boolean isSmartSelection(@NonNull String signature) {
-        return CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature));
-    }
-
-    @Override
+    /** Emits a selection event to the logs. */
     public void writeEvent(@NonNull SelectionEvent event) {
         Preconditions.checkNotNull(event);
         final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
@@ -93,7 +87,7 @@
                 .addTaggedData(SMART_END, event.getSmartEnd())
                 .addTaggedData(EVENT_START, event.getStart())
                 .addTaggedData(EVENT_END, event.getEnd())
-                .addTaggedData(SESSION_ID, event.getSessionId());
+                .addTaggedData(SESSION_ID, event.getSessionId().flattenToString());
         mMetricsLogger.write(log);
         debugLog(log);
     }
@@ -225,9 +219,17 @@
         final int eventEnd = Integer.parseInt(
                 Objects.toString(log.getTaggedData(EVENT_END), ZERO));
 
-        Log.d(LOG_TAG, String.format("%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
-                index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd, widget,
-                model));
+        Log.d(LOG_TAG,
+                String.format(Locale.US, "%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
+                        index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd,
+                        widget, model));
+    }
+
+    /**
+     * Returns a token iterator for tokenizing text for logging purposes.
+     */
+    public static BreakIterator getTokenIterator(@NonNull Locale locale) {
+        return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale));
     }
 
     /**
@@ -260,8 +262,10 @@
             return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash);
         }
 
-        static String getClassifierId(String signature) {
-            Preconditions.checkNotNull(signature);
+        static String getClassifierId(@Nullable String signature) {
+            if (signature == null) {
+                return "";
+            }
             final int end = signature.indexOf("|");
             if (end >= 0) {
                 return signature.substring(0, end);
@@ -269,8 +273,10 @@
             return "";
         }
 
-        static String getModelName(String signature) {
-            Preconditions.checkNotNull(signature);
+        static String getModelName(@Nullable String signature) {
+            if (signature == null) {
+                return "";
+            }
             final int start = signature.indexOf("|") + 1;
             final int end = signature.indexOf("|", start);
             if (start >= 1 && end >= start) {
@@ -279,8 +285,10 @@
             return "";
         }
 
-        static int getHash(String signature) {
-            Preconditions.checkNotNull(signature);
+        static int getHash(@Nullable String signature) {
+            if (signature == null) {
+                return 0;
+            }
             final int index1 = signature.indexOf("|");
             final int index2 = signature.indexOf("|", index1);
             if (index2 > 0) {
diff --git a/android/view/textclassifier/SystemTextClassifier.java b/android/view/textclassifier/SystemTextClassifier.java
index 45fd6bf..490c389 100644
--- a/android/view/textclassifier/SystemTextClassifier.java
+++ b/android/view/textclassifier/SystemTextClassifier.java
@@ -28,7 +28,6 @@
 import android.service.textclassifier.ITextLinksCallback;
 import android.service.textclassifier.ITextSelectionCallback;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.Preconditions;
@@ -49,13 +48,6 @@
     private final TextClassificationConstants mSettings;
     private final TextClassifier mFallback;
     private final String mPackageName;
-
-    private final Object mLoggerLock = new Object();
-    @GuardedBy("mLoggerLock")
-    private Logger.Config mLoggerConfig;
-    @GuardedBy("mLoggerLock")
-    private Logger mLogger;
-    @GuardedBy("mLoggerLock")
     private TextClassificationSessionId mSessionId;
 
     public SystemTextClassifier(Context context, TextClassificationConstants settings)
@@ -147,27 +139,6 @@
     }
 
     @Override
-    public Logger getLogger(@NonNull Logger.Config config) {
-        Preconditions.checkNotNull(config);
-        synchronized (mLoggerLock) {
-            if (mLogger == null || !config.equals(mLoggerConfig)) {
-                mLoggerConfig = config;
-                mLogger = new Logger(config) {
-                    @Override
-                    public void writeEvent(SelectionEvent event) {
-                        try {
-                            mManagerService.onSelectionEvent(mSessionId, event);
-                        } catch (RemoteException e) {
-                            Log.e(LOG_TAG, "Error reporting selection event.", e);
-                        }
-                    }
-                };
-            }
-        }
-        return mLogger;
-    }
-
-    @Override
     public void destroy() {
         try {
             if (mSessionId != null) {
diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java
index 37a5d9a..96016b4 100644
--- a/android/view/textclassifier/TextClassification.java
+++ b/android/view/textclassifier/TextClassification.java
@@ -375,13 +375,13 @@
      */
     public static final class Builder {
 
-        @NonNull private String mText;
         @NonNull private List<RemoteAction> mActions = new ArrayList<>();
         @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
-        @Nullable Drawable mLegacyIcon;
-        @Nullable String mLegacyLabel;
-        @Nullable Intent mLegacyIntent;
-        @Nullable OnClickListener mLegacyOnClickListener;
+        @Nullable private String mText;
+        @Nullable private Drawable mLegacyIcon;
+        @Nullable private String mLegacyLabel;
+        @Nullable private Intent mLegacyIntent;
+        @Nullable private OnClickListener mLegacyOnClickListener;
         @Nullable private String mId;
 
         /**
@@ -721,4 +721,67 @@
         mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
         mId = in.readString();
     }
+
+    // TODO: Remove once apps can build against the latest sdk.
+    /**
+     * Optional input parameters for generating TextClassification.
+     * @hide
+     */
+    public static final class Options {
+
+        @Nullable private final TextClassificationSessionId mSessionId;
+        @Nullable private final Request mRequest;
+        @Nullable private LocaleList mDefaultLocales;
+        @Nullable private ZonedDateTime mReferenceTime;
+
+        public Options() {
+            this(null, null);
+        }
+
+        private Options(
+                @Nullable TextClassificationSessionId sessionId, @Nullable Request request) {
+            mSessionId = sessionId;
+            mRequest = request;
+        }
+
+        /** Helper to create Options from a Request. */
+        public static Options from(TextClassificationSessionId sessionId, Request request) {
+            final Options options = new Options(sessionId, request);
+            options.setDefaultLocales(request.getDefaultLocales());
+            options.setReferenceTime(request.getReferenceTime());
+            return options;
+        }
+
+        /** @param defaultLocales ordered list of locale preferences. */
+        public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+            mDefaultLocales = defaultLocales;
+            return this;
+        }
+
+        /** @param referenceTime refrence time used for interpreting relatives dates */
+        public Options setReferenceTime(@Nullable ZonedDateTime referenceTime) {
+            mReferenceTime = referenceTime;
+            return this;
+        }
+
+        @Nullable
+        public LocaleList getDefaultLocales() {
+            return mDefaultLocales;
+        }
+
+        @Nullable
+        public ZonedDateTime getReferenceTime() {
+            return mReferenceTime;
+        }
+
+        @Nullable
+        public Request getRequest() {
+            return mRequest;
+        }
+
+        @Nullable
+        public TextClassificationSessionId getSessionId() {
+            return mSessionId;
+        }
+    }
 }
diff --git a/android/view/textclassifier/TextClassificationSession.java b/android/view/textclassifier/TextClassificationSession.java
index e8e300a..4c64198 100644
--- a/android/view/textclassifier/TextClassificationSession.java
+++ b/android/view/textclassifier/TextClassificationSession.java
@@ -17,7 +17,6 @@
 package android.view.textclassifier;
 
 import android.annotation.WorkerThread;
-import android.view.textclassifier.DefaultLogger.SignatureParser;
 import android.view.textclassifier.SelectionEvent.InvocationMethod;
 
 import com.android.internal.util.Preconditions;
@@ -222,7 +221,8 @@
         }
 
         private static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
-            return DefaultLogger.CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature));
+            return SelectionSessionLogger.CLASSIFIER_ID.equals(
+                    SelectionSessionLogger.SignatureParser.getClassifierId(signature));
         }
     }
 }
diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java
index 54261be..da47bcb 100644
--- a/android/view/textclassifier/TextClassifier.java
+++ b/android/view/textclassifier/TextClassifier.java
@@ -41,8 +41,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Interface for providing text classification related features.
@@ -208,6 +209,26 @@
         return suggestSelection(request);
     }
 
+    // TODO: Remove once apps can build against the latest sdk.
+    /** @hide */
+    default TextSelection suggestSelection(
+            @NonNull CharSequence text,
+            @IntRange(from = 0) int selectionStartIndex,
+            @IntRange(from = 0) int selectionEndIndex,
+            @Nullable TextSelection.Options options) {
+        if (options == null) {
+            return suggestSelection(new TextSelection.Request.Builder(
+                    text, selectionStartIndex, selectionEndIndex).build());
+        } else if (options.getRequest() != null) {
+            return suggestSelection(options.getRequest());
+        } else {
+            return suggestSelection(
+                    new TextSelection.Request.Builder(text, selectionStartIndex, selectionEndIndex)
+                            .setDefaultLocales(options.getDefaultLocales())
+                            .build());
+        }
+    }
+
     /**
      * Classifies the specified text and returns a {@link TextClassification} object that can be
      * used to generate a widget for handling the classified text.
@@ -267,6 +288,26 @@
         return classifyText(request);
     }
 
+    // TODO: Remove once apps can build against the latest sdk.
+    /** @hide */
+    default TextClassification classifyText(
+            @NonNull CharSequence text,
+            @IntRange(from = 0) int startIndex,
+            @IntRange(from = 0) int endIndex,
+            @Nullable TextClassification.Options options) {
+        if (options == null) {
+            return classifyText(
+                    new TextClassification.Request.Builder(text, startIndex, endIndex).build());
+        } else if (options.getRequest() != null) {
+            return classifyText(options.getRequest());
+        } else {
+            return classifyText(new TextClassification.Request.Builder(text, startIndex, endIndex)
+                    .setDefaultLocales(options.getDefaultLocales())
+                    .setReferenceTime(options.getReferenceTime())
+                    .build());
+        }
+    }
+
     /**
      * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
      * links information.
@@ -288,6 +329,22 @@
         return new TextLinks.Builder(request.getText().toString()).build();
     }
 
+    // TODO: Remove once apps can build against the latest sdk.
+    /** @hide */
+    default TextLinks generateLinks(
+            @NonNull CharSequence text, @Nullable TextLinks.Options options) {
+        if (options == null) {
+            return generateLinks(new TextLinks.Request.Builder(text).build());
+        } else if (options.getRequest() != null) {
+            return generateLinks(options.getRequest());
+        } else {
+            return generateLinks(new TextLinks.Request.Builder(text)
+                    .setDefaultLocales(options.getDefaultLocales())
+                    .setEntityConfig(options.getEntityConfig())
+                    .build());
+        }
+    }
+
     /**
      * Returns the maximal length of text that can be processed by generateLinks.
      *
@@ -302,18 +359,6 @@
     }
 
     /**
-     * Returns a helper for logging TextClassifier related events.
-     *
-     * @param config logger configuration
-     * @hide
-     */
-    @WorkerThread
-    default Logger getLogger(@NonNull Logger.Config config) {
-        Preconditions.checkNotNull(config);
-        return Logger.DISABLED;
-    }
-
-    /**
      * Reports a selection event.
      *
      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
@@ -377,6 +422,12 @@
                     /* includedEntityTypes */null, /* excludedEntityTypes */ null);
         }
 
+        // TODO: Remove once apps can build against the latest sdk.
+        /** @hide */
+        public static EntityConfig create(@Nullable Collection<String> hints) {
+            return createWithHints(hints);
+        }
+
         /**
          * Creates an EntityConfig.
          *
@@ -406,6 +457,12 @@
                     /* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null);
         }
 
+        // TODO: Remove once apps can build against the latest sdk.
+        /** @hide */
+        public static EntityConfig createWithEntityList(@Nullable Collection<String> entityTypes) {
+            return createWithExplicitEntityList(entityTypes);
+        }
+
         /**
          * Returns a list of the final set of entities to find.
          *
@@ -413,21 +470,15 @@
          *
          * This method is intended for use by TextClassifier implementations.
          */
-        public List<String> resolveEntityListModifications(@NonNull Collection<String> entities) {
-            final ArrayList<String> finalList = new ArrayList<>();
+        public Collection<String> resolveEntityListModifications(
+                @NonNull Collection<String> entities) {
+            final Set<String> finalSet = new HashSet();
             if (mUseHints) {
-                for (String entity : entities) {
-                    if (!mExcludedEntityTypes.contains(entity)) {
-                        finalList.add(entity);
-                    }
-                }
+                finalSet.addAll(entities);
             }
-            for (String entity : mIncludedEntityTypes) {
-                if (!mExcludedEntityTypes.contains(entity) && !finalList.contains(entity)) {
-                    finalList.add(entity);
-                }
-            }
-            return finalList;
+            finalSet.addAll(mIncludedEntityTypes);
+            finalSet.removeAll(mExcludedEntityTypes);
+            return finalSet;
         }
 
         /**
@@ -508,7 +559,7 @@
             final String string = request.getText().toString();
             final TextLinks.Builder links = new TextLinks.Builder(string);
 
-            final List<String> entities = request.getEntityConfig()
+            final Collection<String> entities = request.getEntityConfig()
                     .resolveEntityListModifications(Collections.emptyList());
             if (entities.contains(TextClassifier.TYPE_URL)) {
                 addLinks(links, string, TextClassifier.TYPE_URL);
diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java
index 7e3748a..2213355 100644
--- a/android/view/textclassifier/TextClassifierImpl.java
+++ b/android/view/textclassifier/TextClassifierImpl.java
@@ -94,11 +94,7 @@
 
     private final Object mLoggerLock = new Object();
     @GuardedBy("mLoggerLock") // Do not access outside this lock.
-    private Logger.Config mLoggerConfig;
-    @GuardedBy("mLoggerLock") // Do not access outside this lock.
-    private Logger mLogger;
-    @GuardedBy("mLoggerLock") // Do not access outside this lock.
-    private Logger mLogger2;  // This is the new logger. Will replace mLogger.
+    private SelectionSessionLogger mSessionLogger;
 
     private final TextClassificationConstants mSettings;
 
@@ -283,28 +279,14 @@
         }
     }
 
-    /** @inheritDoc */
-    @Override
-    public Logger getLogger(@NonNull Logger.Config config) {
-        Preconditions.checkNotNull(config);
-        synchronized (mLoggerLock) {
-            if (mLogger == null || !config.equals(mLoggerConfig)) {
-                mLoggerConfig = config;
-                mLogger = new DefaultLogger(config);
-            }
-        }
-        return mLogger;
-    }
-
     @Override
     public void onSelectionEvent(SelectionEvent event) {
         Preconditions.checkNotNull(event);
         synchronized (mLoggerLock) {
-            if (mLogger2 == null) {
-                mLogger2 = new DefaultLogger(
-                        new Logger.Config(mContext, WIDGET_TYPE_UNKNOWN, null));
+            if (mSessionLogger == null) {
+                mSessionLogger = new SelectionSessionLogger();
             }
-            mLogger2.writeEvent(event);
+            mSessionLogger.writeEvent(event);
         }
     }
 
@@ -331,7 +313,7 @@
 
     private String createId(String text, int start, int end) {
         synchronized (mLock) {
-            return DefaultLogger.createId(text, start, end, mContext, mModel.getVersion(),
+            return SelectionSessionLogger.createId(text, start, end, mContext, mModel.getVersion(),
                     mModel.getSupportedLocales());
         }
     }
diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java
index 17c7b13..851b2c9 100644
--- a/android/view/textclassifier/TextLinks.java
+++ b/android/view/textclassifier/TextLinks.java
@@ -28,6 +28,8 @@
 import android.text.method.MovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.text.util.Linkify.LinkifyMask;
 import android.view.View;
 import android.view.textclassifier.TextClassifier.EntityType;
 import android.widget.TextView;
@@ -337,7 +339,7 @@
 
         /**
          * @return The config representing the set of entities to look for
-         * @see #setEntityConfig(TextClassifier.EntityConfig)
+         * @see Builder#setEntityConfig(TextClassifier.EntityConfig)
          */
         @Nullable
         public TextClassifier.EntityConfig getEntityConfig() {
@@ -607,4 +609,124 @@
             return new TextLinks(mFullText, mLinks);
         }
     }
+
+    // TODO: Remove once apps can build against the latest sdk.
+    /**
+     * Optional input parameters for generating TextLinks.
+     * @hide
+     */
+    public static final class Options {
+
+        @Nullable private final TextClassificationSessionId mSessionId;
+        @Nullable private final Request mRequest;
+        @Nullable private LocaleList mDefaultLocales;
+        @Nullable private TextClassifier.EntityConfig mEntityConfig;
+        private boolean mLegacyFallback;
+
+        private @ApplyStrategy int mApplyStrategy;
+        private Function<TextLink, TextLinkSpan> mSpanFactory;
+
+        private String mCallingPackageName;
+
+        public Options() {
+            this(null, null);
+        }
+
+        private Options(
+                @Nullable TextClassificationSessionId sessionId, @Nullable Request request) {
+            mSessionId = sessionId;
+            mRequest = request;
+        }
+
+        /** Helper to create Options from a Request. */
+        public static Options from(TextClassificationSessionId sessionId, Request request) {
+            final Options options = new Options(sessionId, request);
+            options.setDefaultLocales(request.getDefaultLocales());
+            options.setEntityConfig(request.getEntityConfig());
+            return options;
+        }
+
+        /** Returns a new options object based on the specified link mask. */
+        public static Options fromLinkMask(@LinkifyMask int mask) {
+            final List<String> entitiesToFind = new ArrayList<>();
+
+            if ((mask & Linkify.WEB_URLS) != 0) {
+                entitiesToFind.add(TextClassifier.TYPE_URL);
+            }
+            if ((mask & Linkify.EMAIL_ADDRESSES) != 0) {
+                entitiesToFind.add(TextClassifier.TYPE_EMAIL);
+            }
+            if ((mask & Linkify.PHONE_NUMBERS) != 0) {
+                entitiesToFind.add(TextClassifier.TYPE_PHONE);
+            }
+            if ((mask & Linkify.MAP_ADDRESSES) != 0) {
+                entitiesToFind.add(TextClassifier.TYPE_ADDRESS);
+            }
+
+            return new Options().setEntityConfig(
+                    TextClassifier.EntityConfig.createWithEntityList(entitiesToFind));
+        }
+
+        /** @param defaultLocales ordered list of locale preferences. */
+        public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+            mDefaultLocales = defaultLocales;
+            return this;
+        }
+
+        /** @param entityConfig definition of which entity types to look for. */
+        public Options setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) {
+            mEntityConfig = entityConfig;
+            return this;
+        }
+
+        /** @param applyStrategy strategy to use when resolving conflicts. */
+        public Options setApplyStrategy(@ApplyStrategy int applyStrategy) {
+            checkValidApplyStrategy(applyStrategy);
+            mApplyStrategy = applyStrategy;
+            return this;
+        }
+
+        /** @param spanFactory factory for converting TextLink to TextLinkSpan. */
+        public Options setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) {
+            mSpanFactory = spanFactory;
+            return this;
+        }
+
+        @Nullable
+        public LocaleList getDefaultLocales() {
+            return mDefaultLocales;
+        }
+
+        @Nullable
+        public TextClassifier.EntityConfig getEntityConfig() {
+            return mEntityConfig;
+        }
+
+        @ApplyStrategy
+        public int getApplyStrategy() {
+            return mApplyStrategy;
+        }
+
+        @Nullable
+        public Function<TextLink, TextLinkSpan> getSpanFactory() {
+            return mSpanFactory;
+        }
+
+        @Nullable
+        public Request getRequest() {
+            return mRequest;
+        }
+
+        @Nullable
+        public TextClassificationSessionId getSessionId() {
+            return mSessionId;
+        }
+
+        private static void checkValidApplyStrategy(int applyStrategy) {
+            if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) {
+                throw new IllegalArgumentException(
+                        "Invalid apply strategy. See TextLinks.ApplyStrategy for options.");
+            }
+        }
+    }
 }
diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java
index 939e717..17687c9 100644
--- a/android/view/textclassifier/TextSelection.java
+++ b/android/view/textclassifier/TextSelection.java
@@ -375,4 +375,56 @@
         mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
         mId = in.readString();
     }
+
+
+    // TODO: Remove once apps can build against the latest sdk.
+    /**
+     * Optional input parameters for generating TextSelection.
+     * @hide
+     */
+    public static final class Options {
+
+        @Nullable private final TextClassificationSessionId mSessionId;
+        @Nullable private final Request mRequest;
+        @Nullable private LocaleList mDefaultLocales;
+        private boolean mDarkLaunchAllowed;
+
+        public Options() {
+            this(null, null);
+        }
+
+        private Options(
+                @Nullable TextClassificationSessionId sessionId, @Nullable Request request) {
+            mSessionId = sessionId;
+            mRequest = request;
+        }
+
+        /** Helper to create Options from a Request. */
+        public static Options from(TextClassificationSessionId sessionId, Request request) {
+            final Options options = new Options(sessionId, request);
+            options.setDefaultLocales(request.getDefaultLocales());
+            return options;
+        }
+
+        /** @param defaultLocales ordered list of locale preferences. */
+        public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+            mDefaultLocales = defaultLocales;
+            return this;
+        }
+
+        @Nullable
+        public LocaleList getDefaultLocales() {
+            return mDefaultLocales;
+        }
+
+        @Nullable
+        public Request getRequest() {
+            return mRequest;
+        }
+
+        @Nullable
+        public TextClassificationSessionId getSessionId() {
+            return mSessionId;
+        }
+    }
 }
diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java
index 8e1f218..21ec42b 100644
--- a/android/view/textservice/TextServicesManager.java
+++ b/android/view/textservice/TextServicesManager.java
@@ -1,58 +1,219 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2011 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
+ * 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
+ * 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.
+ * 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 android.view.textservice;
 
+import android.annotation.SystemService;
+import android.content.Context;
 import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
 import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
 
+import com.android.internal.textservice.ITextServicesManager;
+
 import java.util.Locale;
 
 /**
- * A stub class of TextServicesManager for Layout-Lib.
+ * System API to the overall text services, which arbitrates interaction between applications
+ * and text services.
+ *
+ * The user can change the current text services in Settings. And also applications can specify
+ * the target text services.
+ *
+ * <h3>Architecture Overview</h3>
+ *
+ * <p>There are three primary parties involved in the text services
+ * framework (TSF) architecture:</p>
+ *
+ * <ul>
+ * <li> The <strong>text services manager</strong> as expressed by this class
+ * is the central point of the system that manages interaction between all
+ * other parts.  It is expressed as the client-side API here which exists
+ * in each application context and communicates with a global system service
+ * that manages the interaction across all processes.
+ * <li> A <strong>text service</strong> implements a particular
+ * interaction model allowing the client application to retrieve information of text.
+ * The system binds to the current text service that is in use, causing it to be created and run.
+ * <li> Multiple <strong>client applications</strong> arbitrate with the text service
+ * manager for connections to text services.
+ * </ul>
+ *
+ * <h3>Text services sessions</h3>
+ * <ul>
+ * <li>The <strong>spell checker session</strong> is one of the text services.
+ * {@link android.view.textservice.SpellCheckerSession}</li>
+ * </ul>
+ *
  */
+@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)
 public final class TextServicesManager {
-    private static final TextServicesManager sInstance = new TextServicesManager();
-    private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0];
+    private static final String TAG = TextServicesManager.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    /**
+     * A compile time switch to control per-profile spell checker, which is not yet ready.
+     * @hide
+     */
+    public static final boolean DISABLE_PER_PROFILE_SPELL_CHECKER = true;
+
+    private static TextServicesManager sInstance;
+
+    private final ITextServicesManager mService;
+
+    private TextServicesManager() throws ServiceNotFoundException {
+        mService = ITextServicesManager.Stub.asInterface(
+                ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE));
+    }
 
     /**
      * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist.
      * @hide
      */
     public static TextServicesManager getInstance() {
-        return sInstance;
+        synchronized (TextServicesManager.class) {
+            if (sInstance == null) {
+                try {
+                    sInstance = new TextServicesManager();
+                } catch (ServiceNotFoundException e) {
+                    throw new IllegalStateException(e);
+                }
+            }
+            return sInstance;
+        }
     }
 
+    /**
+     * Returns the language component of a given locale string.
+     */
+    private static String parseLanguageFromLocaleString(String locale) {
+        final int idx = locale.indexOf('_');
+        if (idx < 0) {
+            return locale;
+        } else {
+            return locale.substring(0, idx);
+        }
+    }
+
+    /**
+     * Get a spell checker session for the specified spell checker
+     * @param locale the locale for the spell checker. If {@code locale} is null and
+     * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be
+     * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true,
+     * the locale specified in Settings will be returned only when it is same as {@code locale}.
+     * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is
+     * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
+     * selected.
+     * @param listener a spell checker session lister for getting results from a spell checker.
+     * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled
+     * languages in settings will be returned.
+     * @return the spell checker session of the spell checker
+     */
     public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale,
             SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) {
-        return null;
+        if (listener == null) {
+            throw new NullPointerException();
+        }
+        if (!referToSpellCheckerLanguageSettings && locale == null) {
+            throw new IllegalArgumentException("Locale should not be null if you don't refer"
+                    + " settings.");
+        }
+
+        if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) {
+            return null;
+        }
+
+        final SpellCheckerInfo sci;
+        try {
+            sci = mService.getCurrentSpellChecker(null);
+        } catch (RemoteException e) {
+            return null;
+        }
+        if (sci == null) {
+            return null;
+        }
+        SpellCheckerSubtype subtypeInUse = null;
+        if (referToSpellCheckerLanguageSettings) {
+            subtypeInUse = getCurrentSpellCheckerSubtype(true);
+            if (subtypeInUse == null) {
+                return null;
+            }
+            if (locale != null) {
+                final String subtypeLocale = subtypeInUse.getLocale();
+                final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale);
+                if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) {
+                    return null;
+                }
+            }
+        } else {
+            final String localeStr = locale.toString();
+            for (int i = 0; i < sci.getSubtypeCount(); ++i) {
+                final SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
+                final String tempSubtypeLocale = subtype.getLocale();
+                final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale);
+                if (tempSubtypeLocale.equals(localeStr)) {
+                    subtypeInUse = subtype;
+                    break;
+                } else if (tempSubtypeLanguage.length() >= 2 &&
+                        locale.getLanguage().equals(tempSubtypeLanguage)) {
+                    subtypeInUse = subtype;
+                }
+            }
+        }
+        if (subtypeInUse == null) {
+            return null;
+        }
+        final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener);
+        try {
+            mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(),
+                    session.getTextServicesSessionListener(),
+                    session.getSpellCheckerSessionListener(), bundle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return session;
     }
 
     /**
      * @hide
      */
     public SpellCheckerInfo[] getEnabledSpellCheckers() {
-        return EMPTY_SPELL_CHECKER_INFO;
+        try {
+            final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers();
+            if (DBG) {
+                Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null"));
+            }
+            return retval;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
      * @hide
      */
     public SpellCheckerInfo getCurrentSpellChecker() {
-        return null;
+        try {
+            // Passing null as a locale for ICS
+            return mService.getCurrentSpellChecker(null);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -60,13 +221,22 @@
      */
     public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
             boolean allowImplicitlySelectedSubtype) {
-        return null;
+        try {
+            // Passing null as a locale until we support multiple enabled spell checker subtypes.
+            return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
      * @hide
      */
     public boolean isSpellCheckerEnabled() {
-        return false;
+        try {
+            return mService.isSpellCheckerEnabled();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 }
diff --git a/android/webkit/FindAddress.java b/android/webkit/FindAddress.java
index 31b2427..9183227 100644
--- a/android/webkit/FindAddress.java
+++ b/android/webkit/FindAddress.java
@@ -429,20 +429,21 @@
 
                     // At this point we've matched a state; try to match a zip code after it.
                     Matcher zipMatcher = sWordRe.matcher(content);
-                    if (zipMatcher.find(stateMatch.end())
-                            && isValidZipCode(zipMatcher.group(0), stateMatch)) {
-                        return zipMatcher.end();
+                    if (zipMatcher.find(stateMatch.end())) {
+                        if (isValidZipCode(zipMatcher.group(0), stateMatch)) {
+                            return zipMatcher.end();
+                        }
+                    } else {
+                        // The content ends with a state but no zip
+                        // code. This is a legal match according to the
+                        // documentation. N.B. This is equivalent to the
+                        // original c++ implementation, which only allowed
+                        // the zip code to be optional at the end of the
+                        // string, which presumably is a bug.  We tried
+                        // relaxing this to work in other places but it
+                        // caused too many false positives.
+                        nonZipMatch = stateMatch.end();
                     }
-                    // The content ends with a state but no zip
-                    // code. This is a legal match according to the
-                    // documentation. N.B. This differs from the
-                    // original c++ implementation, which only allowed
-                    // the zip code to be optional at the end of the
-                    // string, which presumably is a bug.  Now we
-                    // prefer to find a match with a zip code, but
-                    // remember non-zip matches and return them if
-                    // necessary.
-                    nonZipMatch = stateMatch.end();
                 }
             }
         }
diff --git a/android/webkit/TracingConfig.java b/android/webkit/TracingConfig.java
index d95ca61..2080168 100644
--- a/android/webkit/TracingConfig.java
+++ b/android/webkit/TracingConfig.java
@@ -54,37 +54,37 @@
 
     /**
      * Predefined set of categories typically useful for analyzing WebViews.
-     * Typically includes android_webview and Java.
+     * Typically includes "android_webview" and "Java" categories.
      */
     public static final int CATEGORIES_ANDROID_WEBVIEW = 1 << 1;
 
     /**
      * Predefined set of categories typically useful for web developers.
-     * Typically includes blink, compositor, renderer.scheduler and v8 categories.
+     * Typically includes "blink", "compositor", "renderer.scheduler" and "v8" categories.
      */
     public static final int CATEGORIES_WEB_DEVELOPER = 1 << 2;
 
     /**
      * Predefined set of categories for analyzing input latency issues.
-     * Typically includes input, renderer.scheduler categories.
+     * Typically includes "input", "renderer.scheduler" categories.
      */
     public static final int CATEGORIES_INPUT_LATENCY = 1 << 3;
 
     /**
      * Predefined set of categories for analyzing rendering issues.
-     * Typically includes blink, compositor and gpu categories.
+     * Typically includes "blink", "compositor" and "gpu" categories.
      */
     public static final int CATEGORIES_RENDERING = 1 << 4;
 
     /**
      * Predefined set of categories for analyzing javascript and rendering issues.
-     * Typically includes blink, compositor, gpu, renderer.scheduler and v8 categories.
+     * Typically includes "blink", "compositor", "gpu", "renderer.scheduler" and "v8" categories.
      */
     public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 1 << 5;
 
     /**
      * Predefined set of categories for studying difficult rendering performance problems.
-     * Typically includes blink, compositor, gpu, renderer.scheduler, v8 and
+     * Typically includes "blink", "compositor", "gpu", "renderer.scheduler", "v8" and
      * some other compositor categories which are disabled by default.
      */
     public static final int CATEGORIES_FRAME_VIEWER = 1 << 6;
@@ -123,7 +123,9 @@
     }
 
     /**
-     * Returns a bitmask of the predefined categories values of this configuration.
+     * Returns a bitmask of the predefined category sets of this configuration.
+     *
+     * @return Bitmask of predefined category sets.
      */
     @PredefinedCategories
     public int getPredefinedCategories() {
@@ -133,7 +135,7 @@
     /**
      * Returns the list of included custom category patterns for this configuration.
      *
-     * @return empty list if no custom category patterns are specified.
+     * @return Empty list if no custom category patterns are specified.
      */
     @NonNull
     public List<String> getCustomIncludedCategories() {
@@ -142,6 +144,8 @@
 
     /**
      * Returns the tracing mode of this configuration.
+     *
+     * @return The tracing mode of this configuration.
      */
     @TracingMode
     public int getTracingMode() {
@@ -150,28 +154,37 @@
 
     /**
      * Builder used to create {@link TracingConfig} objects.
-     *
+     * <p>
      * Examples:
-     *   new TracingConfig.Builder().build()
-     *       -- creates a configuration with default options: {@link #CATEGORIES_NONE},
-     *          {@link #RECORD_UNTIL_FULL}.
-     *   new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER).build()
-     *       -- records trace events from the "web developer" predefined category sets.
-     *   new TracingConfig.Builder().addCategories(CATEGORIES_RENDERING,
-     *                                             CATEGORIES_INPUT_LATENCY).build()
-     *       -- records trace events from the "rendering" and "input latency" predefined
-     *          category sets.
-     *   new TracingConfig.Builder().addCategories("browser").build()
-     *       -- records only the trace events from the "browser" category.
-     *   new TracingConfig.Builder().addCategories("blink*","renderer*").build()
-     *       -- records only the trace events matching the "blink*" and "renderer*" patterns
-     *          (e.g. "blink.animations", "renderer_host" and "renderer.scheduler" categories).
-     *   new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER)
+     * <pre class="prettyprint">
+     *   // Create a configuration with default options: {@link #CATEGORIES_NONE},
+     *   // {@link #RECORD_CONTINUOUSLY}.
+     *   <code>new TracingConfig.Builder().build()</code>
+     *
+     *   // Record trace events from the "web developer" predefined category sets.
+     *   // Uses a ring buffer (the default {@link #RECORD_CONTINUOUSLY} mode) for
+     *   // internal storage during tracing.
+     *   <code>new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER).build()</code>
+     *
+     *   // Record trace events from the "rendering" and "input latency" predefined
+     *   // category sets.
+     *   <code>new TracingConfig.Builder().addCategories(CATEGORIES_RENDERING,
+     *                                     CATEGORIES_INPUT_LATENCY).build()</code>
+     *
+     *   // Record only the trace events from the "browser" category.
+     *   <code>new TracingConfig.Builder().addCategories("browser").build()</code>
+     *
+     *   // Record only the trace events matching the "blink*" and "renderer*" patterns
+     *   // (e.g. "blink.animations", "renderer_host" and "renderer.scheduler" categories).
+     *   <code>new TracingConfig.Builder().addCategories("blink*","renderer*").build()</code>
+     *
+     *   // Record events from the "web developer" predefined category set and events from
+     *   // the "disabled-by-default-v8.gc" category to understand where garbage collection
+     *   // is being triggered. Uses a limited size buffer for internal storage during tracing.
+     *   <code>new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER)
      *                              .addCategories("disabled-by-default-v8.gc")
-     *                              .setTracingMode(RECORD_CONTINUOUSLY).build()
-     *       -- records events from the "web developer" predefined category set and events from
-     *          the "disabled-by-default-v8.gc" category to understand where garbage collection
-     *          is being triggered. Uses a ring buffer for internal storage during tracing.
+     *                              .setTracingMode(RECORD_UNTIL_FULL).build()</code>
+     * </pre>
      */
     public static class Builder {
         private @PredefinedCategories int mPredefinedCategories = CATEGORIES_NONE;
@@ -185,6 +198,8 @@
 
         /**
          * Build {@link TracingConfig} using the current settings.
+         *
+         * @return The {@link TracingConfig} with the current settings.
          */
         public TracingConfig build() {
             return new TracingConfig(mPredefinedCategories, mCustomIncludedCategories,
@@ -192,16 +207,15 @@
         }
 
         /**
-         * Adds categories from a predefined set of categories to be included in the trace output.
+         * Adds predefined sets of categories to be included in the trace output.
          *
-         * @param predefinedCategories list or bitmask of predefined category sets to use:
-         *                    {@link #CATEGORIES_NONE}, {@link #CATEGORIES_ALL},
-         *                    {@link #CATEGORIES_ANDROID_WEBVIEW},
-         *                    {@link #CATEGORIES_WEB_DEVELOPER},
-         *                    {@link #CATEGORIES_INPUT_LATENCY},
-         *                    {@link #CATEGORIES_RENDERING},
-         *                    {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or
-         *                    {@link #CATEGORIES_FRAME_VIEWER}.
+         * A predefined category set can be one of {@link #CATEGORIES_NONE},
+         * {@link #CATEGORIES_ALL}, {@link #CATEGORIES_ANDROID_WEBVIEW},
+         * {@link #CATEGORIES_WEB_DEVELOPER}, {@link #CATEGORIES_INPUT_LATENCY},
+         * {@link #CATEGORIES_RENDERING}, {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or
+         * {@link #CATEGORIES_FRAME_VIEWER}.
+         *
+         * @param predefinedCategories A list or bitmask of predefined category sets.
          * @return The builder to facilitate chaining.
          */
         public Builder addCategories(@PredefinedCategories int... predefinedCategories) {
@@ -215,11 +229,11 @@
          * Adds custom categories to be included in trace output.
          *
          * Note that the categories are defined by the currently-in-use version of WebView. They
-         * live in chromium code and are not part of the Android API. See
+         * live in chromium code and are not part of the Android API.
          * See <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">
          * chromium documentation on tracing</a> for more details.
          *
-         * @param categories a list of category patterns. A category pattern can contain wilcards,
+         * @param categories A list of category patterns. A category pattern can contain wildcards,
          *        e.g. "blink*" or full category name e.g. "renderer.scheduler".
          * @return The builder to facilitate chaining.
          */
@@ -235,7 +249,7 @@
          *
          * Same as {@link #addCategories(String...)} but allows to pass a Collection as a parameter.
          *
-         * @param categories a list of category patters.
+         * @param categories A list of category patterns.
          * @return The builder to facilitate chaining.
          */
         public Builder addCategories(Collection<String> categories) {
@@ -245,8 +259,9 @@
 
         /**
          * Sets the tracing mode for this configuration.
+         * When tracingMode is not set explicitly, the default is {@link #RECORD_CONTINUOUSLY}.
          *
-         * @param tracingMode tracing mode to use, one of {@link #RECORD_UNTIL_FULL} or
+         * @param tracingMode The tracing mode to use, one of {@link #RECORD_UNTIL_FULL} or
          *                    {@link #RECORD_CONTINUOUSLY}.
          * @return The builder to facilitate chaining.
          */
diff --git a/android/webkit/TracingController.java b/android/webkit/TracingController.java
index 50068f5..05c0304 100644
--- a/android/webkit/TracingController.java
+++ b/android/webkit/TracingController.java
@@ -35,9 +35,9 @@
  * Example usage:
  * <pre class="prettyprint">
  * TracingController tracingController = TracingController.getInstance();
- * tracingController.start(new TraceConfig.Builder()
+ * tracingController.start(new TracingConfig.Builder()
  *                  .addCategories(CATEGORIES_WEB_DEVELOPER).build());
- * [..]
+ * ...
  * tracingController.stop(new FileOutputStream("trace.json"),
  *                        Executors.newSingleThreadExecutor());
  * </pre></p>
@@ -49,7 +49,7 @@
      * only one TracingController instance for all WebView instances,
      * however this restriction may be relaxed in a future Android release.
      *
-     * @return the default TracingController instance
+     * @return The default TracingController instance.
      */
     @NonNull
     public static TracingController getInstance() {
@@ -65,8 +65,10 @@
      * using an internal buffer and flushed to the outputStream when
      * {@link #stop(OutputStream, Executor)} is called.
      *
-     * @param tracingConfig configuration options to use for tracing
-     * @throws IllegalStateException if the system is already tracing.
+     * @param tracingConfig Configuration options to use for tracing.
+     * @throws IllegalStateException If the system is already tracing.
+     * @throws IllegalArgumentException If the configuration is invalid (e.g.
+     *         invalid category pattern or invalid tracing mode).
      */
     public abstract void start(@NonNull TracingConfig tracingConfig);
 
@@ -77,17 +79,22 @@
      * in chunks by invoking {@link java.io.OutputStream#write(byte[])}. On completion
      * the {@link java.io.OutputStream#close()} method is called.
      *
-     * @param outputStream the output steam the tracing data will be sent to. If null
+     * @param outputStream The output stream the tracing data will be sent to. If null
      *                     the tracing data will be discarded.
-     * @param executor the {@link java.util.concurrent.Executor} on which the
-     *        outputStream #write and #close methods will be invoked.
-     * @return false if the system was not tracing at the time of the call, true
-     *         otherwise.
+     * @param executor The {@link java.util.concurrent.Executor} on which the
+     *        outputStream {@link java.io.OutputStream#write(byte[])} and
+     *        {@link java.io.OutputStream#close()} methods will be invoked.
+     * @return False if the WebView framework was not tracing at the time of the call,
+     *         true otherwise.
      */
     public abstract boolean stop(@Nullable OutputStream outputStream,
             @NonNull @CallbackExecutor Executor executor);
 
-    /** True if the system is tracing */
+    /**
+     * Returns whether the WebView framework is tracing.
+     *
+     * @return True if tracing is enabled.
+     */
     public abstract boolean isTracing();
 
 }
diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java
index 202f204..10748ac 100644
--- a/android/webkit/WebView.java
+++ b/android/webkit/WebView.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2006 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.
@@ -16,223 +16,3136 @@
 
 package android.webkit;
 
-import com.android.layoutlib.bridge.MockView;
-
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.Widget;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.res.Configuration;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
 import android.graphics.Picture;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.net.http.SslCertificate;
+import android.os.Build;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.Message;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.print.PrintDocumentAdapter;
+import android.security.KeyChain;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.DragEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewStructure;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.textclassifier.TextClassifier;
+import android.widget.AbsoluteLayout;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
 
 /**
- * Mock version of the WebView.
- * Only non override public methods from the real WebView have been added in there.
- * Methods that take an unknown class as parameter or as return object, have been removed for now.
- * 
- * TODO: generate automatically.
+ * <p>A View that displays web pages. This class is the basis upon which you
+ * can roll your own web browser or simply display some online content within your Activity.
+ * It uses the WebKit rendering engine to display
+ * web pages and includes methods to navigate forward and backward
+ * through a history, zoom in and out, perform text searches and more.
+ *
+ * <p>Note that, in order for your Activity to access the Internet and load web pages
+ * in a WebView, you must add the {@code INTERNET} permissions to your
+ * Android Manifest file:
+ *
+ * <pre>
+ * {@code <uses-permission android:name="android.permission.INTERNET" />}
+ * </pre>
+ *
+ * <p>This must be a child of the <a
+ * href="{@docRoot}guide/topics/manifest/manifest-element.html">{@code <manifest>}</a>
+ * element.
+ *
+ * <p>For more information, read
+ * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>.
+ *
+ * <h3>Basic usage</h3>
+ *
+ * <p>By default, a WebView provides no browser-like widgets, does not
+ * enable JavaScript and web page errors are ignored. If your goal is only
+ * to display some HTML as a part of your UI, this is probably fine;
+ * the user won't need to interact with the web page beyond reading
+ * it, and the web page won't need to interact with the user. If you
+ * actually want a full-blown web browser, then you probably want to
+ * invoke the Browser application with a URL Intent rather than show it
+ * with a WebView. For example:
+ * <pre>
+ * Uri uri = Uri.parse("https://www.example.com");
+ * Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ * startActivity(intent);
+ * </pre>
+ * <p>See {@link android.content.Intent} for more information.
+ *
+ * <p>To provide a WebView in your own Activity, include a {@code <WebView>} in your layout,
+ * or set the entire Activity window as a WebView during {@link
+ * android.app.Activity#onCreate(Bundle) onCreate()}:
+ *
+ * <pre class="prettyprint">
+ * WebView webview = new WebView(this);
+ * setContentView(webview);
+ * </pre>
+ *
+ * <p>Then load the desired web page:
+ *
+ * <pre>
+ * // Simplest usage: note that an exception will NOT be thrown
+ * // if there is an error loading this page (see below).
+ * webview.loadUrl("https://example.com/");
+ *
+ * // OR, you can also load from an HTML string:
+ * String summary = "&lt;html>&lt;body>You scored &lt;b>192&lt;/b> points.&lt;/body>&lt;/html>";
+ * webview.loadData(summary, "text/html", null);
+ * // ... although note that there are restrictions on what this HTML can do.
+ * // See {@link #loadData(String,String,String)} and {@link
+ * #loadDataWithBaseURL(String,String,String,String,String)} for more info.
+ * // Also see {@link #loadData(String,String,String)} for information on encoding special
+ * // characters.
+ * </pre>
+ *
+ * <p>A WebView has several customization points where you can add your
+ * own behavior. These are:
+ *
+ * <ul>
+ *   <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass.
+ *       This class is called when something that might impact a
+ *       browser UI happens, for instance, progress updates and
+ *       JavaScript alerts are sent here (see <a
+ * href="{@docRoot}guide/developing/debug-tasks.html#DebuggingWebPages">Debugging Tasks</a>).
+ *   </li>
+ *   <li>Creating and setting a {@link android.webkit.WebViewClient} subclass.
+ *       It will be called when things happen that impact the
+ *       rendering of the content, eg, errors or form submissions. You
+ *       can also intercept URL loading here (via {@link
+ * android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView,String)
+ * shouldOverrideUrlLoading()}).</li>
+ *   <li>Modifying the {@link android.webkit.WebSettings}, such as
+ * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean)
+ * setJavaScriptEnabled()}. </li>
+ *   <li>Injecting Java objects into the WebView using the
+ *       {@link android.webkit.WebView#addJavascriptInterface} method. This
+ *       method allows you to inject Java objects into a page's JavaScript
+ *       context, so that they can be accessed by JavaScript in the page.</li>
+ * </ul>
+ *
+ * <p>Here's a more complicated example, showing error handling,
+ *    settings, and progress notification:
+ *
+ * <pre class="prettyprint">
+ * // Let's display the progress in the activity title bar, like the
+ * // browser app does.
+ * getWindow().requestFeature(Window.FEATURE_PROGRESS);
+ *
+ * webview.getSettings().setJavaScriptEnabled(true);
+ *
+ * final Activity activity = this;
+ * webview.setWebChromeClient(new WebChromeClient() {
+ *   public void onProgressChanged(WebView view, int progress) {
+ *     // Activities and WebViews measure progress with different scales.
+ *     // The progress meter will automatically disappear when we reach 100%
+ *     activity.setProgress(progress * 1000);
+ *   }
+ * });
+ * webview.setWebViewClient(new WebViewClient() {
+ *   public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ *     Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show();
+ *   }
+ * });
+ *
+ * webview.loadUrl("https://developer.android.com/");
+ * </pre>
+ *
+ * <h3>Zoom</h3>
+ *
+ * <p>To enable the built-in zoom, set
+ * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)}
+ * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}).
+ *
+ * <p class="note"><b>Note:</b> Using zoom if either the height or width is set to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} may lead to undefined behavior
+ * and should be avoided.
+ *
+ * <h3>Cookie and window management</h3>
+ *
+ * <p>For obvious security reasons, your application has its own
+ * cache, cookie store etc.&mdash;it does not share the Browser
+ * application's data.
+ *
+ * <p>By default, requests by the HTML to open new windows are
+ * ignored. This is {@code true} whether they be opened by JavaScript or by
+ * the target attribute on a link. You can customize your
+ * {@link WebChromeClient} to provide your own behavior for opening multiple windows,
+ * and render them in whatever manner you want.
+ *
+ * <p>The standard behavior for an Activity is to be destroyed and
+ * recreated when the device orientation or any other configuration changes. This will cause
+ * the WebView to reload the current page. If you don't want that, you
+ * can set your Activity to handle the {@code orientation} and {@code keyboardHidden}
+ * changes, and then just leave the WebView alone. It'll automatically
+ * re-orient itself as appropriate. Read <a
+ * href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a> for
+ * more information about how to handle configuration changes during runtime.
+ *
+ *
+ * <h3>Building web pages to support different screen densities</h3>
+ *
+ * <p>The screen density of a device is based on the screen resolution. A screen with low density
+ * has fewer available pixels per inch, where a screen with high density
+ * has more &mdash; sometimes significantly more &mdash; pixels per inch. The density of a
+ * screen is important because, other things being equal, a UI element (such as a button) whose
+ * height and width are defined in terms of screen pixels will appear larger on the lower density
+ * screen and smaller on the higher density screen.
+ * For simplicity, Android collapses all actual screen densities into three generalized densities:
+ * high, medium, and low.
+ * <p>By default, WebView scales a web page so that it is drawn at a size that matches the default
+ * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen
+ * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels
+ * are bigger).
+ * Starting with API level {@link android.os.Build.VERSION_CODES#ECLAIR}, WebView supports DOM, CSS,
+ * and meta tag features to help you (as a web developer) target screens with different screen
+ * densities.
+ * <p>Here's a summary of the features you can use to handle different screen densities:
+ * <ul>
+ * <li>The {@code window.devicePixelRatio} DOM property. The value of this property specifies the
+ * default scaling factor used for the current device. For example, if the value of {@code
+ * window.devicePixelRatio} is "1.0", then the device is considered a medium density (mdpi) device
+ * and default scaling is not applied to the web page; if the value is "1.5", then the device is
+ * considered a high density device (hdpi) and the page content is scaled 1.5x; if the
+ * value is "0.75", then the device is considered a low density device (ldpi) and the content is
+ * scaled 0.75x.</li>
+ * <li>The {@code -webkit-device-pixel-ratio} CSS media query. Use this to specify the screen
+ * densities for which this style sheet is to be used. The corresponding value should be either
+ * "0.75", "1", or "1.5", to indicate that the styles are for devices with low density, medium
+ * density, or high density screens, respectively. For example:
+ * <pre>
+ * &lt;link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio:1.5)" href="hdpi.css" /&gt;</pre>
+ * <p>The {@code hdpi.css} stylesheet is only used for devices with a screen pixel ratio of 1.5,
+ * which is the high density pixel ratio.
+ * </li>
+ * </ul>
+ *
+ * <h3>HTML5 Video support</h3>
+ *
+ * <p>In order to support inline HTML5 video in your application you need to have hardware
+ * acceleration turned on.
+ *
+ * <h3>Full screen support</h3>
+ *
+ * <p>In order to support full screen &mdash; for video or other HTML content &mdash; you need to set a
+ * {@link android.webkit.WebChromeClient} and implement both
+ * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)}
+ * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is
+ * missing then the web contents will not be allowed to enter full screen. Optionally you can implement
+ * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video
+ * is loading.
+ *
+ * <h3>HTML5 Geolocation API support</h3>
+ *
+ * <p>For applications targeting Android N and later releases
+ * (API level > {@link android.os.Build.VERSION_CODES#M}) the geolocation api is only supported on
+ * secure origins such as https. For such applications requests to geolocation api on non-secure
+ * origins are automatically denied without invoking the corresponding
+ * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)}
+ * method.
+ *
+ * <h3>Layout size</h3>
+ * <p>
+ * It is recommended to set the WebView layout height to a fixed value or to
+ * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.
+ * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+ * for the height none of the WebView's parents should use a
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in
+ * incorrect sizing of the views.
+ *
+ * <p>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+ * enables the following behaviors:
+ * <ul>
+ * <li>The HTML body layout height is set to a fixed value. This means that elements with a height
+ * relative to the HTML body may not be sized correctly. </li>
+ * <li>For applications targeting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the
+ * HTML viewport meta tag will be ignored in order to preserve backwards compatibility. </li>
+ * </ul>
+ *
+ * <p>
+ * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not
+ * supported. If such a width is used the WebView will attempt to use the width of the parent
+ * instead.
+ *
+ * <h3>Metrics</h3>
+ *
+ * <p>
+ * WebView may upload anonymous diagnostic data to Google when the user has consented. This data
+ * helps Google improve WebView. Data is collected on a per-app basis for each app which has
+ * instantiated a WebView. An individual app can opt out of this feature by putting the following
+ * tag in its manifest's {@code <application>} element:
+ * <pre>
+ * &lt;manifest&gt;
+ *     &lt;application&gt;
+ *         ...
+ *         &lt;meta-data android:name=&quot;android.webkit.WebView.MetricsOptOut&quot;
+ *             android:value=&quot;true&quot; /&gt;
+ *     &lt;/application&gt;
+ * &lt;/manifest&gt;
+ * </pre>
+ * <p>
+ * Data will only be uploaded for a given app if the user has consented AND the app has not opted
+ * out.
+ *
+ * <h3>Safe Browsing</h3>
+ *
+ * <p>
+ * With Safe Browsing, WebView will block malicious URLs and present a warning UI to the user to
+ * allow them to navigate back safely or proceed to the malicious page.
+ * <p>
+ * Safe Browsing is enabled by default on devices which support it. If your app needs to disable
+ * Safe Browsing for all WebViews, it can do so in the manifest's {@code <application>} element:
+ * <p>
+ * <pre>
+ * &lt;manifest&gt;
+ *     &lt;application&gt;
+ *         ...
+ *         &lt;meta-data android:name=&quot;android.webkit.WebView.EnableSafeBrowsing&quot;
+ *             android:value=&quot;false&quot; /&gt;
+ *     &lt;/application&gt;
+ * &lt;/manifest&gt;
+ * </pre>
+ *
+ * <p>
+ * Otherwise, see {@link WebSettings#setSafeBrowsingEnabled}.
  *
  */
-public class WebView extends MockView {
+// Implementation notes.
+// The WebView is a thin API class that delegates its public API to a backend WebViewProvider
+// class instance. WebView extends {@link AbsoluteLayout} for backward compatibility reasons.
+// Methods are delegated to the provider implementation: all public API methods introduced in this
+// file are fully delegated, whereas public and protected methods from the View base classes are
+// only delegated where a specific need exists for them to do so.
+@Widget
+public class WebView extends AbsoluteLayout
+        implements ViewTreeObserver.OnGlobalFocusChangeListener,
+        ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
+
+    private static final String LOGTAG = "WebView";
+
+    // Throwing an exception for incorrect thread usage if the
+    // build target is JB MR2 or newer. Defaults to false, and is
+    // set in the WebView constructor.
+    private static volatile boolean sEnforceThreadChecking = false;
 
     /**
-     * Construct a new WebView with a Context object.
-     * @param context A Context object used to access application assets.
+     *  Transportation object for returning WebView across thread boundaries.
+     */
+    public class WebViewTransport {
+        private WebView mWebview;
+
+        /**
+         * Sets the WebView to the transportation object.
+         *
+         * @param webview the WebView to transport
+         */
+        public synchronized void setWebView(WebView webview) {
+            mWebview = webview;
+        }
+
+        /**
+         * Gets the WebView object.
+         *
+         * @return the transported WebView object
+         */
+        public synchronized WebView getWebView() {
+            return mWebview;
+        }
+    }
+
+    /**
+     * URI scheme for telephone number.
+     */
+    public static final String SCHEME_TEL = "tel:";
+    /**
+     * URI scheme for email address.
+     */
+    public static final String SCHEME_MAILTO = "mailto:";
+    /**
+     * URI scheme for map address.
+     */
+    public static final String SCHEME_GEO = "geo:0,0?q=";
+
+    /**
+     * Interface to listen for find results.
+     */
+    public interface FindListener {
+        /**
+         * Notifies the listener about progress made by a find operation.
+         *
+         * @param activeMatchOrdinal the zero-based ordinal of the currently selected match
+         * @param numberOfMatches how many matches have been found
+         * @param isDoneCounting whether the find operation has actually completed. The listener
+         *                       may be notified multiple times while the
+         *                       operation is underway, and the numberOfMatches
+         *                       value should not be considered final unless
+         *                       isDoneCounting is {@code true}.
+         */
+        public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
+            boolean isDoneCounting);
+    }
+
+    /**
+     * Callback interface supplied to {@link #postVisualStateCallback} for receiving
+     * notifications about the visual state.
+     */
+    public static abstract class VisualStateCallback {
+        /**
+         * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}.
+         *
+         * @param requestId The identifier passed to {@link #postVisualStateCallback} when this
+         *                  callback was posted.
+         */
+        public abstract void onComplete(long requestId);
+    }
+
+    /**
+     * Interface to listen for new pictures as they change.
+     *
+     * @deprecated This interface is now obsolete.
+     */
+    @Deprecated
+    public interface PictureListener {
+        /**
+         * Used to provide notification that the WebView's picture has changed.
+         * See {@link WebView#capturePicture} for details of the picture.
+         *
+         * @param view the WebView that owns the picture
+         * @param picture the new picture. Applications targeting
+         *     {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above
+         *     will always receive a {@code null} Picture.
+         * @deprecated Deprecated due to internal changes.
+         */
+        @Deprecated
+        void onNewPicture(WebView view, @Nullable Picture picture);
+    }
+
+    public static class HitTestResult {
+        /**
+         * Default HitTestResult, where the target is unknown.
+         */
+        public static final int UNKNOWN_TYPE = 0;
+        /**
+         * @deprecated This type is no longer used.
+         */
+        @Deprecated
+        public static final int ANCHOR_TYPE = 1;
+        /**
+         * HitTestResult for hitting a phone number.
+         */
+        public static final int PHONE_TYPE = 2;
+        /**
+         * HitTestResult for hitting a map address.
+         */
+        public static final int GEO_TYPE = 3;
+        /**
+         * HitTestResult for hitting an email address.
+         */
+        public static final int EMAIL_TYPE = 4;
+        /**
+         * HitTestResult for hitting an HTML::img tag.
+         */
+        public static final int IMAGE_TYPE = 5;
+        /**
+         * @deprecated This type is no longer used.
+         */
+        @Deprecated
+        public static final int IMAGE_ANCHOR_TYPE = 6;
+        /**
+         * HitTestResult for hitting a HTML::a tag with src=http.
+         */
+        public static final int SRC_ANCHOR_TYPE = 7;
+        /**
+         * HitTestResult for hitting a HTML::a tag with src=http + HTML::img.
+         */
+        public static final int SRC_IMAGE_ANCHOR_TYPE = 8;
+        /**
+         * HitTestResult for hitting an edit text area.
+         */
+        public static final int EDIT_TEXT_TYPE = 9;
+
+        private int mType;
+        private String mExtra;
+
+        /**
+         * @hide Only for use by WebViewProvider implementations
+         */
+        @SystemApi
+        public HitTestResult() {
+            mType = UNKNOWN_TYPE;
+        }
+
+        /**
+         * @hide Only for use by WebViewProvider implementations
+         */
+        @SystemApi
+        public void setType(int type) {
+            mType = type;
+        }
+
+        /**
+         * @hide Only for use by WebViewProvider implementations
+         */
+        @SystemApi
+        public void setExtra(String extra) {
+            mExtra = extra;
+        }
+
+        /**
+         * Gets the type of the hit test result. See the XXX_TYPE constants
+         * defined in this class.
+         *
+         * @return the type of the hit test result
+         */
+        public int getType() {
+            return mType;
+        }
+
+        /**
+         * Gets additional type-dependant information about the result. See
+         * {@link WebView#getHitTestResult()} for details. May either be {@code null}
+         * or contain extra information about this result.
+         *
+         * @return additional type-dependant information about the result
+         */
+        @Nullable
+        public String getExtra() {
+            return mExtra;
+        }
+    }
+
+    /**
+     * Constructs a new WebView with an Activity Context object.
+     *
+     * <p class="note"><b>Note:</b> WebView should always be instantiated with an Activity Context.
+     * If instantiated with an Application Context, WebView will be unable to provide several
+     * features, such as JavaScript dialogs and autofill.
+     *
+     * @param context an Activity Context to access application assets
      */
     public WebView(Context context) {
         this(context, null);
     }
 
     /**
-     * Construct a new WebView with layout parameters.
-     * @param context A Context object used to access application assets.
-     * @param attrs An AttributeSet passed to our parent.
+     * Constructs a new WebView with layout parameters.
+     *
+     * @param context an Activity Context to access application assets
+     * @param attrs an AttributeSet passed to our parent
      */
     public WebView(Context context, AttributeSet attrs) {
         this(context, attrs, com.android.internal.R.attr.webViewStyle);
     }
 
     /**
-     * Construct a new WebView with layout parameters and a default style.
-     * @param context A Context object used to access application assets.
-     * @param attrs An AttributeSet passed to our parent.
-     * @param defStyle The default style resource ID.
+     * Constructs a new WebView with layout parameters and a default style.
+     *
+     * @param context an Activity Context to access application assets
+     * @param attrs an AttributeSet passed to our parent
+     * @param defStyleAttr an attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
      */
-    public WebView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
+    public WebView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
     }
-    
-    // START FAKE PUBLIC METHODS
-    
+
+    /**
+     * Constructs a new WebView with layout parameters and a default style.
+     *
+     * @param context an Activity Context to access application assets
+     * @param attrs an AttributeSet passed to our parent
+     * @param defStyleAttr an attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     * @param defStyleRes a resource identifier of a style resource that
+     *        supplies default values for the view, used only if
+     *        defStyleAttr is 0 or can not be found in the theme. Can be 0
+     *        to not look for defaults.
+     */
+    public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        this(context, attrs, defStyleAttr, defStyleRes, null, false);
+    }
+
+    /**
+     * Constructs a new WebView with layout parameters and a default style.
+     *
+     * @param context an Activity Context to access application assets
+     * @param attrs an AttributeSet passed to our parent
+     * @param defStyleAttr an attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     * @param privateBrowsing whether this WebView will be initialized in
+     *                        private mode
+     *
+     * @deprecated Private browsing is no longer supported directly via
+     * WebView and will be removed in a future release. Prefer using
+     * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager}
+     * and {@link WebStorage} for fine-grained control of privacy data.
+     */
+    @Deprecated
+    public WebView(Context context, AttributeSet attrs, int defStyleAttr,
+            boolean privateBrowsing) {
+        this(context, attrs, defStyleAttr, 0, null, privateBrowsing);
+    }
+
+    /**
+     * Constructs a new WebView with layout parameters, a default style and a set
+     * of custom JavaScript interfaces to be added to this WebView at initialization
+     * time. This guarantees that these interfaces will be available when the JS
+     * context is initialized.
+     *
+     * @param context an Activity Context to access application assets
+     * @param attrs an AttributeSet passed to our parent
+     * @param defStyleAttr an attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     * @param javaScriptInterfaces a Map of interface names, as keys, and
+     *                             object implementing those interfaces, as
+     *                             values
+     * @param privateBrowsing whether this WebView will be initialized in
+     *                        private mode
+     * @hide This is used internally by dumprendertree, as it requires the JavaScript interfaces to
+     *       be added synchronously, before a subsequent loadUrl call takes effect.
+     */
+    protected WebView(Context context, AttributeSet attrs, int defStyleAttr,
+            Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
+        this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing);
+    }
+
+    /**
+     * @hide
+     */
+    @SuppressWarnings("deprecation")  // for super() call into deprecated base class constructor.
+    protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
+            Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // WebView is important by default, unless app developer overrode attribute.
+        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+        }
+
+        if (context == null) {
+            throw new IllegalArgumentException("Invalid context argument");
+        }
+        if (mWebViewThread == null) {
+            throw new RuntimeException(
+                "WebView cannot be initialized on a thread that has no Looper.");
+        }
+        sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >=
+                Build.VERSION_CODES.JELLY_BEAN_MR2;
+        checkThread();
+
+        ensureProviderCreated();
+        mProvider.init(javaScriptInterfaces, privateBrowsing);
+        // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed.
+        CookieSyncManager.setGetInstanceIsAllowed();
+    }
+
+    /**
+     * Specifies whether the horizontal scrollbar has overlay style.
+     *
+     * @deprecated This method has no effect.
+     * @param overlay {@code true} if horizontal scrollbar should have overlay style
+     */
+    @Deprecated
     public void setHorizontalScrollbarOverlay(boolean overlay) {
     }
 
+    /**
+     * Specifies whether the vertical scrollbar has overlay style.
+     *
+     * @deprecated This method has no effect.
+     * @param overlay {@code true} if vertical scrollbar should have overlay style
+     */
+    @Deprecated
     public void setVerticalScrollbarOverlay(boolean overlay) {
     }
 
+    /**
+     * Gets whether horizontal scrollbar has overlay style.
+     *
+     * @deprecated This method is now obsolete.
+     * @return {@code true}
+     */
+    @Deprecated
     public boolean overlayHorizontalScrollbar() {
-        return false;
+        // The old implementation defaulted to true, so return true for consistency
+        return true;
     }
 
+    /**
+     * Gets whether vertical scrollbar has overlay style.
+     *
+     * @deprecated This method is now obsolete.
+     * @return {@code false}
+     */
+    @Deprecated
     public boolean overlayVerticalScrollbar() {
+        // The old implementation defaulted to false, so return false for consistency
         return false;
     }
 
-    public void savePassword(String host, String username, String password) {
+    /**
+     * Gets the visible height (in pixels) of the embedded title bar (if any).
+     *
+     * @deprecated This method is now obsolete.
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     */
+    @Deprecated
+    public int getVisibleTitleHeight() {
+        checkThread();
+        return mProvider.getVisibleTitleHeight();
     }
 
+    /**
+     * Gets the SSL certificate for the main top-level page or {@code null} if there is
+     * no certificate (the site is not secure).
+     *
+     * @return the SSL certificate for the main top-level page
+     */
+    @Nullable
+    public SslCertificate getCertificate() {
+        checkThread();
+        return mProvider.getCertificate();
+    }
+
+    /**
+     * Sets the SSL certificate for the main top-level page.
+     *
+     * @deprecated Calling this function has no useful effect, and will be
+     * ignored in future releases.
+     */
+    @Deprecated
+    public void setCertificate(SslCertificate certificate) {
+        checkThread();
+        mProvider.setCertificate(certificate);
+    }
+
+    //-------------------------------------------------------------------------
+    // Methods called by activity
+    //-------------------------------------------------------------------------
+
+    /**
+     * Sets a username and password pair for the specified host. This data is
+     * used by the WebView to autocomplete username and password fields in web
+     * forms. Note that this is unrelated to the credentials used for HTTP
+     * authentication.
+     *
+     * @param host the host that required the credentials
+     * @param username the username for the given host
+     * @param password the password for the given host
+     * @see WebViewDatabase#clearUsernamePassword
+     * @see WebViewDatabase#hasUsernamePassword
+     * @deprecated Saving passwords in WebView will not be supported in future versions.
+     */
+    @Deprecated
+    public void savePassword(String host, String username, String password) {
+        checkThread();
+        mProvider.savePassword(host, username, password);
+    }
+
+    /**
+     * Stores HTTP authentication credentials for a given host and realm to the {@link WebViewDatabase}
+     * instance.
+     *
+     * @param host the host to which the credentials apply
+     * @param realm the realm to which the credentials apply
+     * @param username the username
+     * @param password the password
+     * @deprecated Use {@link WebViewDatabase#setHttpAuthUsernamePassword} instead
+     */
+    @Deprecated
     public void setHttpAuthUsernamePassword(String host, String realm,
             String username, String password) {
+        checkThread();
+        mProvider.setHttpAuthUsernamePassword(host, realm, username, password);
     }
 
+    /**
+     * Retrieves HTTP authentication credentials for a given host and realm from the {@link
+     * WebViewDatabase} instance.
+     * @param host the host to which the credentials apply
+     * @param realm the realm to which the credentials apply
+     * @return the credentials as a String array, if found. The first element
+     *         is the username and the second element is the password. {@code null} if
+     *         no credentials are found.
+     * @deprecated Use {@link WebViewDatabase#getHttpAuthUsernamePassword} instead
+     */
+    @Deprecated
+    @Nullable
     public String[] getHttpAuthUsernamePassword(String host, String realm) {
-        return null;
+        checkThread();
+        return mProvider.getHttpAuthUsernamePassword(host, realm);
     }
 
+    /**
+     * Destroys the internal state of this WebView. This method should be called
+     * after this WebView has been removed from the view system. No other
+     * methods may be called on this WebView after destroy.
+     */
     public void destroy() {
+        checkThread();
+        mProvider.destroy();
     }
 
+    /**
+     * Enables platform notifications of data state and proxy changes.
+     * Notifications are enabled by default.
+     *
+     * @deprecated This method is now obsolete.
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     */
+    @Deprecated
     public static void enablePlatformNotifications() {
+        // noop
     }
 
+    /**
+     * Disables platform notifications of data state and proxy changes.
+     * Notifications are enabled by default.
+     *
+     * @deprecated This method is now obsolete.
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     */
+    @Deprecated
     public static void disablePlatformNotifications() {
+        // noop
     }
 
+    /**
+     * Used only by internal tests to free up memory.
+     *
+     * @hide
+     */
+    public static void freeMemoryForTests() {
+        getFactory().getStatics().freeMemoryForTests();
+    }
+
+    /**
+     * Informs WebView of the network state. This is used to set
+     * the JavaScript property window.navigator.isOnline and
+     * generates the online/offline event as specified in HTML5, sec. 5.7.7
+     *
+     * @param networkUp a boolean indicating if network is available
+     */
+    public void setNetworkAvailable(boolean networkUp) {
+        checkThread();
+        mProvider.setNetworkAvailable(networkUp);
+    }
+
+    /**
+     * Saves the state of this WebView used in
+     * {@link android.app.Activity#onSaveInstanceState}. Please note that this
+     * method no longer stores the display data for this WebView. The previous
+     * behavior could potentially leak files if {@link #restoreState} was never
+     * called.
+     *
+     * @param outState the Bundle to store this WebView's state
+     * @return the same copy of the back/forward list used to save the state, {@code null} if the
+     *         method fails.
+     */
+    @Nullable
+    public WebBackForwardList saveState(Bundle outState) {
+        checkThread();
+        return mProvider.saveState(outState);
+    }
+
+    /**
+     * Saves the current display data to the Bundle given. Used in conjunction
+     * with {@link #saveState}.
+     * @param b a Bundle to store the display data
+     * @param dest the file to store the serialized picture data. Will be
+     *             overwritten with this WebView's picture data.
+     * @return {@code true} if the picture was successfully saved
+     * @deprecated This method is now obsolete.
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     */
+    @Deprecated
+    public boolean savePicture(Bundle b, final File dest) {
+        checkThread();
+        return mProvider.savePicture(b, dest);
+    }
+
+    /**
+     * Restores the display data that was saved in {@link #savePicture}. Used in
+     * conjunction with {@link #restoreState}. Note that this will not work if
+     * this WebView is hardware accelerated.
+     *
+     * @param b a Bundle containing the saved display data
+     * @param src the file where the picture data was stored
+     * @return {@code true} if the picture was successfully restored
+     * @deprecated This method is now obsolete.
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     */
+    @Deprecated
+    public boolean restorePicture(Bundle b, File src) {
+        checkThread();
+        return mProvider.restorePicture(b, src);
+    }
+
+    /**
+     * Restores the state of this WebView from the given Bundle. This method is
+     * intended for use in {@link android.app.Activity#onRestoreInstanceState}
+     * and should be called to restore the state of this WebView. If
+     * it is called after this WebView has had a chance to build state (load
+     * pages, create a back/forward list, etc.) there may be undesirable
+     * side-effects. Please note that this method no longer restores the
+     * display data for this WebView.
+     *
+     * @param inState the incoming Bundle of state
+     * @return the restored back/forward list or {@code null} if restoreState failed
+     */
+    @Nullable
+    public WebBackForwardList restoreState(Bundle inState) {
+        checkThread();
+        return mProvider.restoreState(inState);
+    }
+
+    /**
+     * Loads the given URL with the specified additional HTTP headers.
+     * <p>
+     * Also see compatibility note on {@link #evaluateJavascript}.
+     *
+     * @param url the URL of the resource to load
+     * @param additionalHttpHeaders the additional headers to be used in the
+     *            HTTP request for this URL, specified as a map from name to
+     *            value. Note that if this map contains any of the headers
+     *            that are set by default by this WebView, such as those
+     *            controlling caching, accept types or the User-Agent, their
+     *            values may be overridden by this WebView's defaults.
+     */
+    public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
+        checkThread();
+        mProvider.loadUrl(url, additionalHttpHeaders);
+    }
+
+    /**
+     * Loads the given URL.
+     * <p>
+     * Also see compatibility note on {@link #evaluateJavascript}.
+     *
+     * @param url the URL of the resource to load
+     */
     public void loadUrl(String url) {
+        checkThread();
+        mProvider.loadUrl(url);
     }
 
-    public void loadData(String data, String mimeType, String encoding) {
+    /**
+     * Loads the URL with postData using "POST" method into this WebView. If url
+     * is not a network URL, it will be loaded with {@link #loadUrl(String)}
+     * instead, ignoring the postData param.
+     *
+     * @param url the URL of the resource to load
+     * @param postData the data will be passed to "POST" request, which must be
+     *     be "application/x-www-form-urlencoded" encoded.
+     */
+    public void postUrl(String url, byte[] postData) {
+        checkThread();
+        if (URLUtil.isNetworkUrl(url)) {
+            mProvider.postUrl(url, postData);
+        } else {
+            mProvider.loadUrl(url);
+        }
     }
 
-    public void loadDataWithBaseURL(String baseUrl, String data,
-            String mimeType, String encoding, String failUrl) {
+    /**
+     * Loads the given data into this WebView using a 'data' scheme URL.
+     * <p>
+     * Note that JavaScript's same origin policy means that script running in a
+     * page loaded using this method will be unable to access content loaded
+     * using any scheme other than 'data', including 'http(s)'. To avoid this
+     * restriction, use {@link
+     * #loadDataWithBaseURL(String,String,String,String,String)
+     * loadDataWithBaseURL()} with an appropriate base URL.
+     * <p>
+     * The {@code encoding} parameter specifies whether the data is base64 or URL
+     * encoded. If the data is base64 encoded, the value of the encoding
+     * parameter must be 'base64'. HTML can be encoded with {@link
+     * android.util.Base64#encodeToString(byte[],int)} like so:
+     * <pre>
+     * String unencodedHtml =
+     *     "&lt;html&gt;&lt;body&gt;'%28' is the code for '('&lt;/body&gt;&lt;/html&gt;";
+     * String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(), Base64.NO_PADDING);
+     * webView.loadData(encodedHtml, "text/html", "base64");
+     * </pre>
+     * <p>
+     * For all other values of {@code encoding} (including {@code null}) it is assumed that the
+     * data uses ASCII encoding for octets inside the range of safe URL characters and use the
+     * standard %xx hex encoding of URLs for octets outside that range. See <a
+     * href="https://tools.ietf.org/html/rfc3986#section-2.2">RFC 3986</a> for more information.
+     * <p>
+     * The {@code mimeType} parameter specifies the format of the data.
+     * If WebView can't handle the specified MIME type, it will download the data.
+     * If {@code null}, defaults to 'text/html'.
+     * <p>
+     * The 'data' scheme URL formed by this method uses the default US-ASCII
+     * charset. If you need need to set a different charset, you should form a
+     * 'data' scheme URL which explicitly specifies a charset parameter in the
+     * mediatype portion of the URL and call {@link #loadUrl(String)} instead.
+     * Note that the charset obtained from the mediatype portion of a data URL
+     * always overrides that specified in the HTML or XML document itself.
+     *
+     * @param data a String of data in the given encoding
+     * @param mimeType the MIME type of the data, e.g. 'text/html'.
+     * @param encoding the encoding of the data
+     */
+    public void loadData(String data, @Nullable String mimeType, @Nullable String encoding) {
+        checkThread();
+        mProvider.loadData(data, mimeType, encoding);
     }
 
+    /**
+     * Loads the given data into this WebView, using baseUrl as the base URL for
+     * the content. The base URL is used both to resolve relative URLs and when
+     * applying JavaScript's same origin policy. The historyUrl is used for the
+     * history entry.
+     * <p>
+     * The {@code mimeType} parameter specifies the format of the data.
+     * If WebView can't handle the specified MIME type, it will download the data.
+     * If {@code null}, defaults to 'text/html'.
+     * <p>
+     * Note that content specified in this way can access local device files
+     * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than
+     * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'.
+     * <p>
+     * If the base URL uses the data scheme, this method is equivalent to
+     * calling {@link #loadData(String,String,String) loadData()} and the
+     * historyUrl is ignored, and the data will be treated as part of a data: URL.
+     * If the base URL uses any other scheme, then the data will be loaded into
+     * the WebView as a plain string (i.e. not part of a data URL) and any URL-encoded
+     * entities in the string will not be decoded.
+     * <p>
+     * Note that the baseUrl is sent in the 'Referer' HTTP header when
+     * requesting subresources (images, etc.) of the page loaded using this method.
+     *
+     * @param baseUrl the URL to use as the page's base URL. If {@code null} defaults to
+     *                'about:blank'.
+     * @param data a String of data in the given encoding
+     * @param mimeType the MIME type of the data, e.g. 'text/html'.
+     * @param encoding the encoding of the data
+     * @param historyUrl the URL to use as the history entry. If {@code null} defaults
+     *                   to 'about:blank'. If non-null, this must be a valid URL.
+     */
+    public void loadDataWithBaseURL(@Nullable String baseUrl, String data,
+            @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl) {
+        checkThread();
+        mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
+    }
+
+    /**
+     * Asynchronously evaluates JavaScript in the context of the currently displayed page.
+     * If non-null, |resultCallback| will be invoked with any result returned from that
+     * execution. This method must be called on the UI thread and the callback will
+     * be made on the UI thread.
+     * <p>
+     * Compatibility note. Applications targeting {@link android.os.Build.VERSION_CODES#N} or
+     * later, JavaScript state from an empty WebView is no longer persisted across navigations like
+     * {@link #loadUrl(String)}. For example, global variables and functions defined before calling
+     * {@link #loadUrl(String)} will not exist in the loaded page. Applications should use
+     * {@link #addJavascriptInterface} instead to persist JavaScript objects across navigations.
+     *
+     * @param script the JavaScript to execute.
+     * @param resultCallback A callback to be invoked when the script execution
+     *                       completes with the result of the execution (if any).
+     *                       May be {@code null} if no notification of the result is required.
+     */
+    public void evaluateJavascript(String script, @Nullable ValueCallback<String> resultCallback) {
+        checkThread();
+        mProvider.evaluateJavaScript(script, resultCallback);
+    }
+
+    /**
+     * Saves the current view as a web archive.
+     *
+     * @param filename the filename where the archive should be placed
+     */
+    public void saveWebArchive(String filename) {
+        checkThread();
+        mProvider.saveWebArchive(filename);
+    }
+
+    /**
+     * Saves the current view as a web archive.
+     *
+     * @param basename the filename where the archive should be placed
+     * @param autoname if {@code false}, takes basename to be a file. If {@code true}, basename
+     *                 is assumed to be a directory in which a filename will be
+     *                 chosen according to the URL of the current page.
+     * @param callback called after the web archive has been saved. The
+     *                 parameter for onReceiveValue will either be the filename
+     *                 under which the file was saved, or {@code null} if saving the
+     *                 file failed.
+     */
+    public void saveWebArchive(String basename, boolean autoname, @Nullable ValueCallback<String>
+            callback) {
+        checkThread();
+        mProvider.saveWebArchive(basename, autoname, callback);
+    }
+
+    /**
+     * Stops the current load.
+     */
     public void stopLoading() {
+        checkThread();
+        mProvider.stopLoading();
     }
 
+    /**
+     * Reloads the current URL.
+     */
     public void reload() {
+        checkThread();
+        mProvider.reload();
     }
 
+    /**
+     * Gets whether this WebView has a back history item.
+     *
+     * @return {@code true} if this WebView has a back history item
+     */
     public boolean canGoBack() {
-        return false;
+        checkThread();
+        return mProvider.canGoBack();
     }
 
+    /**
+     * Goes back in the history of this WebView.
+     */
     public void goBack() {
+        checkThread();
+        mProvider.goBack();
     }
 
+    /**
+     * Gets whether this WebView has a forward history item.
+     *
+     * @return {@code true} if this WebView has a forward history item
+     */
     public boolean canGoForward() {
-        return false;
+        checkThread();
+        return mProvider.canGoForward();
     }
 
+    /**
+     * Goes forward in the history of this WebView.
+     */
     public void goForward() {
+        checkThread();
+        mProvider.goForward();
     }
 
+    /**
+     * Gets whether the page can go back or forward the given
+     * number of steps.
+     *
+     * @param steps the negative or positive number of steps to move the
+     *              history
+     */
     public boolean canGoBackOrForward(int steps) {
-        return false;
+        checkThread();
+        return mProvider.canGoBackOrForward(steps);
     }
 
+    /**
+     * Goes to the history item that is the number of steps away from
+     * the current item. Steps is negative if backward and positive
+     * if forward.
+     *
+     * @param steps the number of steps to take back or forward in the back
+     *              forward list
+     */
     public void goBackOrForward(int steps) {
+        checkThread();
+        mProvider.goBackOrForward(steps);
     }
 
+    /**
+     * Gets whether private browsing is enabled in this WebView.
+     */
+    public boolean isPrivateBrowsingEnabled() {
+        checkThread();
+        return mProvider.isPrivateBrowsingEnabled();
+    }
+
+    /**
+     * Scrolls the contents of this WebView up by half the view size.
+     *
+     * @param top {@code true} to jump to the top of the page
+     * @return {@code true} if the page was scrolled
+     */
     public boolean pageUp(boolean top) {
-        return false;
+        checkThread();
+        return mProvider.pageUp(top);
     }
-    
+
+    /**
+     * Scrolls the contents of this WebView down by half the page size.
+     *
+     * @param bottom {@code true} to jump to bottom of page
+     * @return {@code true} if the page was scrolled
+     */
     public boolean pageDown(boolean bottom) {
-        return false;
+        checkThread();
+        return mProvider.pageDown(bottom);
     }
 
+    /**
+     * Posts a {@link VisualStateCallback}, which will be called when
+     * the current state of the WebView is ready to be drawn.
+     *
+     * <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not
+     * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The
+     * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of
+     * the DOM at the current time are ready to be drawn the next time the {@link WebView}
+     * draws.
+     *
+     * <p>The next draw after the callback completes is guaranteed to reflect all the updates to the
+     * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may also
+     * contain updates applied after the callback was posted.
+     *
+     * <p>The state of the DOM covered by this API includes the following:
+     * <ul>
+     * <li>primitive HTML elements (div, img, span, etc..)</li>
+     * <li>images</li>
+     * <li>CSS animations</li>
+     * <li>WebGL</li>
+     * <li>canvas</li>
+     * </ul>
+     * It does not include the state of:
+     * <ul>
+     * <li>the video tag</li>
+     * </ul>
+     *
+     * <p>To guarantee that the {@link WebView} will successfully render the first frame
+     * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions
+     * must be met:
+     * <ul>
+     * <li>If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then
+     * the {@link WebView} must be attached to the view hierarchy.</li>
+     * <li>If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE}
+     * then the {@link WebView} must be attached to the view hierarchy and must be made
+     * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.</li>
+     * <li>If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the
+     * {@link WebView} must be attached to the view hierarchy and its
+     * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed
+     * values and must be made {@link View#VISIBLE VISIBLE} from the
+     * {@link VisualStateCallback#onComplete} method.</li>
+     * </ul>
+     *
+     * <p>When using this API it is also recommended to enable pre-rasterization if the {@link
+     * WebView} is off screen to avoid flickering. See {@link WebSettings#setOffscreenPreRaster} for
+     * more details and do consider its caveats.
+     *
+     * @param requestId An id that will be returned in the callback to allow callers to match
+     *                  requests with callbacks.
+     * @param callback  The callback to be invoked.
+     */
+    public void postVisualStateCallback(long requestId, VisualStateCallback callback) {
+        checkThread();
+        mProvider.insertVisualStateCallback(requestId, callback);
+    }
+
+    /**
+     * Clears this WebView so that onDraw() will draw nothing but white background,
+     * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY.
+     * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state
+     *             and release page resources (including any running JavaScript).
+     */
+    @Deprecated
     public void clearView() {
+        checkThread();
+        mProvider.clearView();
     }
-    
+
+    /**
+     * Gets a new picture that captures the current contents of this WebView.
+     * The picture is of the entire document being displayed, and is not
+     * limited to the area currently displayed by this WebView. Also, the
+     * picture is a static copy and is unaffected by later changes to the
+     * content being displayed.
+     * <p>
+     * Note that due to internal changes, for API levels between
+     * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and
+     * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} inclusive, the
+     * picture does not include fixed position elements or scrollable divs.
+     * <p>
+     * Note that from {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} the returned picture
+     * should only be drawn into bitmap-backed Canvas - using any other type of Canvas will involve
+     * additional conversion at a cost in memory and performance. Also the
+     * {@link android.graphics.Picture#createFromStream} and
+     * {@link android.graphics.Picture#writeToStream} methods are not supported on the
+     * returned object.
+     *
+     * @deprecated Use {@link #onDraw} to obtain a bitmap snapshot of the WebView, or
+     * {@link #saveWebArchive} to save the content to a file.
+     *
+     * @return a picture that captures the current contents of this WebView
+     */
+    @Deprecated
     public Picture capturePicture() {
-        return null;
+        checkThread();
+        return mProvider.capturePicture();
     }
 
+    /**
+     * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user
+     *             to provide a print document name.
+     */
+    @Deprecated
+    public PrintDocumentAdapter createPrintDocumentAdapter() {
+        checkThread();
+        return mProvider.createPrintDocumentAdapter("default");
+    }
+
+    /**
+     * Creates a PrintDocumentAdapter that provides the content of this WebView for printing.
+     *
+     * The adapter works by converting the WebView contents to a PDF stream. The WebView cannot
+     * be drawn during the conversion process - any such draws are undefined. It is recommended
+     * to use a dedicated off screen WebView for the printing. If necessary, an application may
+     * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance
+     * wrapped around the object returned and observing the onStart and onFinish methods. See
+     * {@link android.print.PrintDocumentAdapter} for more information.
+     *
+     * @param documentName  The user-facing name of the printed document. See
+     *                      {@link android.print.PrintDocumentInfo}
+     */
+    public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) {
+        checkThread();
+        return mProvider.createPrintDocumentAdapter(documentName);
+    }
+
+    /**
+     * Gets the current scale of this WebView.
+     *
+     * @return the current scale
+     *
+     * @deprecated This method is prone to inaccuracy due to race conditions
+     * between the web rendering and UI threads; prefer
+     * {@link WebViewClient#onScaleChanged}.
+     */
+    @Deprecated
+    @ViewDebug.ExportedProperty(category = "webview")
     public float getScale() {
-        return 0;
+        checkThread();
+        return mProvider.getScale();
     }
 
+    /**
+     * Sets the initial scale for this WebView. 0 means default.
+     * The behavior for the default scale depends on the state of
+     * {@link WebSettings#getUseWideViewPort()} and
+     * {@link WebSettings#getLoadWithOverviewMode()}.
+     * If the content fits into the WebView control by width, then
+     * the zoom is set to 100%. For wide content, the behavior
+     * depends on the state of {@link WebSettings#getLoadWithOverviewMode()}.
+     * If its value is {@code true}, the content will be zoomed out to be fit
+     * by width into the WebView control, otherwise not.
+     *
+     * If initial scale is greater than 0, WebView starts with this value
+     * as initial scale.
+     * Please note that unlike the scale properties in the viewport meta tag,
+     * this method doesn't take the screen density into account.
+     *
+     * @param scaleInPercent the initial scale in percent
+     */
     public void setInitialScale(int scaleInPercent) {
+        checkThread();
+        mProvider.setInitialScale(scaleInPercent);
     }
 
+    /**
+     * Invokes the graphical zoom picker widget for this WebView. This will
+     * result in the zoom widget appearing on the screen to control the zoom
+     * level of this WebView.
+     */
     public void invokeZoomPicker() {
+        checkThread();
+        mProvider.invokeZoomPicker();
     }
 
-    public void requestFocusNodeHref(Message hrefMsg) {
+    /**
+     * Gets a HitTestResult based on the current cursor node. If a HTML::a
+     * tag is found and the anchor has a non-JavaScript URL, the HitTestResult
+     * type is set to SRC_ANCHOR_TYPE and the URL is set in the "extra" field.
+     * If the anchor does not have a URL or if it is a JavaScript URL, the type
+     * will be UNKNOWN_TYPE and the URL has to be retrieved through
+     * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is
+     * found, the HitTestResult type is set to IMAGE_TYPE and the URL is set in
+     * the "extra" field. A type of
+     * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a URL that has an image as
+     * a child node. If a phone number is found, the HitTestResult type is set
+     * to PHONE_TYPE and the phone number is set in the "extra" field of
+     * HitTestResult. If a map address is found, the HitTestResult type is set
+     * to GEO_TYPE and the address is set in the "extra" field of HitTestResult.
+     * If an email address is found, the HitTestResult type is set to EMAIL_TYPE
+     * and the email is set in the "extra" field of HitTestResult. Otherwise,
+     * HitTestResult type is set to UNKNOWN_TYPE.
+     */
+    public HitTestResult getHitTestResult() {
+        checkThread();
+        return mProvider.getHitTestResult();
     }
 
+    /**
+     * Requests the anchor or image element URL at the last tapped point.
+     * If hrefMsg is {@code null}, this method returns immediately and does not
+     * dispatch hrefMsg to its target. If the tapped point hits an image,
+     * an anchor, or an image in an anchor, the message associates
+     * strings in named keys in its data. The value paired with the key
+     * may be an empty string.
+     *
+     * @param hrefMsg the message to be dispatched with the result of the
+     *                request. The message data contains three keys. "url"
+     *                returns the anchor's href attribute. "title" returns the
+     *                anchor's text. "src" returns the image's src attribute.
+     */
+    public void requestFocusNodeHref(@Nullable Message hrefMsg) {
+        checkThread();
+        mProvider.requestFocusNodeHref(hrefMsg);
+    }
+
+    /**
+     * Requests the URL of the image last touched by the user. msg will be sent
+     * to its target with a String representing the URL as its object.
+     *
+     * @param msg the message to be dispatched with the result of the request
+     *            as the data member with "url" as key. The result can be {@code null}.
+     */
     public void requestImageRef(Message msg) {
+        checkThread();
+        mProvider.requestImageRef(msg);
     }
 
+    /**
+     * Gets the URL for the current page. This is not always the same as the URL
+     * passed to WebViewClient.onPageStarted because although the load for
+     * that URL has begun, the current page may not have changed.
+     *
+     * @return the URL for the current page
+     */
+    @ViewDebug.ExportedProperty(category = "webview")
     public String getUrl() {
-        return null;
+        checkThread();
+        return mProvider.getUrl();
     }
 
+    /**
+     * Gets the original URL for the current page. This is not always the same
+     * as the URL passed to WebViewClient.onPageStarted because although the
+     * load for that URL has begun, the current page may not have changed.
+     * Also, there may have been redirects resulting in a different URL to that
+     * originally requested.
+     *
+     * @return the URL that was originally requested for the current page
+     */
+    @ViewDebug.ExportedProperty(category = "webview")
+    public String getOriginalUrl() {
+        checkThread();
+        return mProvider.getOriginalUrl();
+    }
+
+    /**
+     * Gets the title for the current page. This is the title of the current page
+     * until WebViewClient.onReceivedTitle is called.
+     *
+     * @return the title for the current page
+     */
+    @ViewDebug.ExportedProperty(category = "webview")
     public String getTitle() {
-        return null;
+        checkThread();
+        return mProvider.getTitle();
     }
 
+    /**
+     * Gets the favicon for the current page. This is the favicon of the current
+     * page until WebViewClient.onReceivedIcon is called.
+     *
+     * @return the favicon for the current page
+     */
     public Bitmap getFavicon() {
-        return null;
+        checkThread();
+        return mProvider.getFavicon();
     }
 
+    /**
+     * Gets the touch icon URL for the apple-touch-icon <link> element, or
+     * a URL on this site's server pointing to the standard location of a
+     * touch icon.
+     *
+     * @hide
+     */
+    public String getTouchIconUrl() {
+        return mProvider.getTouchIconUrl();
+    }
+
+    /**
+     * Gets the progress for the current page.
+     *
+     * @return the progress for the current page between 0 and 100
+     */
     public int getProgress() {
-        return 0;
+        checkThread();
+        return mProvider.getProgress();
     }
-    
+
+    /**
+     * Gets the height of the HTML content.
+     *
+     * @return the height of the HTML content
+     */
+    @ViewDebug.ExportedProperty(category = "webview")
     public int getContentHeight() {
-        return 0;
+        checkThread();
+        return mProvider.getContentHeight();
     }
 
+    /**
+     * Gets the width of the HTML content.
+     *
+     * @return the width of the HTML content
+     * @hide
+     */
+    @ViewDebug.ExportedProperty(category = "webview")
+    public int getContentWidth() {
+        return mProvider.getContentWidth();
+    }
+
+    /**
+     * Pauses all layout, parsing, and JavaScript timers for all WebViews. This
+     * is a global requests, not restricted to just this WebView. This can be
+     * useful if the application has been paused.
+     */
     public void pauseTimers() {
+        checkThread();
+        mProvider.pauseTimers();
     }
 
+    /**
+     * Resumes all layout, parsing, and JavaScript timers for all WebViews.
+     * This will resume dispatching all timers.
+     */
     public void resumeTimers() {
+        checkThread();
+        mProvider.resumeTimers();
     }
 
-    public void clearCache() {
+    /**
+     * Does a best-effort attempt to pause any processing that can be paused
+     * safely, such as animations and geolocation. Note that this call
+     * does not pause JavaScript. To pause JavaScript globally, use
+     * {@link #pauseTimers}.
+     *
+     * To resume WebView, call {@link #onResume}.
+     */
+    public void onPause() {
+        checkThread();
+        mProvider.onPause();
     }
 
+    /**
+     * Resumes a WebView after a previous call to {@link #onPause}.
+     */
+    public void onResume() {
+        checkThread();
+        mProvider.onResume();
+    }
+
+    /**
+     * Gets whether this WebView is paused, meaning onPause() was called.
+     * Calling onResume() sets the paused state back to {@code false}.
+     *
+     * @hide
+     */
+    public boolean isPaused() {
+        return mProvider.isPaused();
+    }
+
+    /**
+     * Informs this WebView that memory is low so that it can free any available
+     * memory.
+     * @deprecated Memory caches are automatically dropped when no longer needed, and in response
+     *             to system memory pressure.
+     */
+    @Deprecated
+    public void freeMemory() {
+        checkThread();
+        mProvider.freeMemory();
+    }
+
+    /**
+     * Clears the resource cache. Note that the cache is per-application, so
+     * this will clear the cache for all WebViews used.
+     *
+     * @param includeDiskFiles if {@code false}, only the RAM cache is cleared
+     */
+    public void clearCache(boolean includeDiskFiles) {
+        checkThread();
+        mProvider.clearCache(includeDiskFiles);
+    }
+
+    /**
+     * Removes the autocomplete popup from the currently focused form field, if
+     * present. Note this only affects the display of the autocomplete popup,
+     * it does not remove any saved form data from this WebView's store. To do
+     * that, use {@link WebViewDatabase#clearFormData}.
+     */
     public void clearFormData() {
+        checkThread();
+        mProvider.clearFormData();
     }
 
+    /**
+     * Tells this WebView to clear its internal back/forward list.
+     */
     public void clearHistory() {
+        checkThread();
+        mProvider.clearHistory();
     }
 
+    /**
+     * Clears the SSL preferences table stored in response to proceeding with
+     * SSL certificate errors.
+     */
     public void clearSslPreferences() {
+        checkThread();
+        mProvider.clearSslPreferences();
     }
 
+    /**
+     * Clears the client certificate preferences stored in response
+     * to proceeding/cancelling client cert requests. Note that WebView
+     * automatically clears these preferences when it receives a
+     * {@link KeyChain#ACTION_STORAGE_CHANGED} intent. The preferences are
+     * shared by all the WebViews that are created by the embedder application.
+     *
+     * @param onCleared  A runnable to be invoked when client certs are cleared.
+     *                   The runnable will be called in UI thread.
+     */
+    public static void clearClientCertPreferences(@Nullable Runnable onCleared) {
+        getFactory().getStatics().clearClientCertPreferences(onCleared);
+    }
+
+    /**
+     * Starts Safe Browsing initialization.
+     * <p>
+     * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is
+     * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those
+     * devices {@code callback} will receive {@code false}.
+     * <p>
+     * This should not be called if Safe Browsing has been disabled by manifest tag or {@link
+     * WebSettings#setSafeBrowsingEnabled}. This prepares resources used for Safe Browsing.
+     * <p>
+     * This should be called with the Application Context (and will always use the Application
+     * context to do its work regardless).
+     *
+     * @param context Application Context.
+     * @param callback will be called on the UI thread with {@code true} if initialization is
+     * successful, {@code false} otherwise.
+     */
+    public static void startSafeBrowsing(@NonNull Context context,
+            @Nullable ValueCallback<Boolean> callback) {
+        getFactory().getStatics().initSafeBrowsing(context, callback);
+    }
+
+    /**
+     * Sets the list of hosts (domain names/IP addresses) that are exempt from SafeBrowsing checks.
+     * The list is global for all the WebViews.
+     * <p>
+     * Each rule should take one of these:
+     * <table>
+     * <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr>
+     * <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr>
+     * <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr>
+     * <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr>
+     * <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr>
+     * </table>
+     * <p>
+     * All other rules, including wildcards, are invalid.
+     * <p>
+     * The correct syntax for hosts is defined by <a
+     * href="https://tools.ietf.org/html/rfc3986#section-3.2.2">RFC 3986</a>.
+     *
+     * @param hosts the list of hosts
+     * @param callback will be called with {@code true} if hosts are successfully added to the
+     * whitelist. It will be called with {@code false} if any hosts are malformed. The callback
+     * will be run on the UI thread
+     */
+    public static void setSafeBrowsingWhitelist(@NonNull List<String> hosts,
+            @Nullable ValueCallback<Boolean> callback) {
+        getFactory().getStatics().setSafeBrowsingWhitelist(hosts, callback);
+    }
+
+    /**
+     * Returns a URL pointing to the privacy policy for Safe Browsing reporting.
+     *
+     * @return the url pointing to a privacy policy document which can be displayed to users.
+     */
+    @NonNull
+    public static Uri getSafeBrowsingPrivacyPolicyUrl() {
+        return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl();
+    }
+
+    /**
+     * Gets the WebBackForwardList for this WebView. This contains the
+     * back/forward list for use in querying each item in the history stack.
+     * This is a copy of the private WebBackForwardList so it contains only a
+     * snapshot of the current state. Multiple calls to this method may return
+     * different objects. The object returned from this method will not be
+     * updated to reflect any new state.
+     */
+    public WebBackForwardList copyBackForwardList() {
+        checkThread();
+        return mProvider.copyBackForwardList();
+
+    }
+
+    /**
+     * Registers the listener to be notified as find-on-page operations
+     * progress. This will replace the current listener.
+     *
+     * @param listener an implementation of {@link FindListener}
+     */
+    public void setFindListener(FindListener listener) {
+        checkThread();
+        setupFindListenerIfNeeded();
+        mFindListener.mUserFindListener = listener;
+    }
+
+    /**
+     * Highlights and scrolls to the next match found by
+     * {@link #findAllAsync}, wrapping around page boundaries as necessary.
+     * Notifies any registered {@link FindListener}. If {@link #findAllAsync(String)}
+     * has not been called yet, or if {@link #clearMatches} has been called since the
+     * last find operation, this function does nothing.
+     *
+     * @param forward the direction to search
+     * @see #setFindListener
+     */
+    public void findNext(boolean forward) {
+        checkThread();
+        mProvider.findNext(forward);
+    }
+
+    /**
+     * Finds all instances of find on the page and highlights them.
+     * Notifies any registered {@link FindListener}.
+     *
+     * @param find the string to find
+     * @return the number of occurrences of the String "find" that were found
+     * @deprecated {@link #findAllAsync} is preferred.
+     * @see #setFindListener
+     */
+    @Deprecated
+    public int findAll(String find) {
+        checkThread();
+        StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync");
+        return mProvider.findAll(find);
+    }
+
+    /**
+     * Finds all instances of find on the page and highlights them,
+     * asynchronously. Notifies any registered {@link FindListener}.
+     * Successive calls to this will cancel any pending searches.
+     *
+     * @param find the string to find.
+     * @see #setFindListener
+     */
+    public void findAllAsync(String find) {
+        checkThread();
+        mProvider.findAllAsync(find);
+    }
+
+    /**
+     * Starts an ActionMode for finding text in this WebView.  Only works if this
+     * WebView is attached to the view system.
+     *
+     * @param text if non-null, will be the initial text to search for.
+     *             Otherwise, the last String searched for in this WebView will
+     *             be used to start.
+     * @param showIme if {@code true}, show the IME, assuming the user will begin typing.
+     *                If {@code false} and text is non-null, perform a find all.
+     * @return {@code true} if the find dialog is shown, {@code false} otherwise
+     * @deprecated This method does not work reliably on all Android versions;
+     *             implementing a custom find dialog using WebView.findAllAsync()
+     *             provides a more robust solution.
+     */
+    @Deprecated
+    public boolean showFindDialog(@Nullable String text, boolean showIme) {
+        checkThread();
+        return mProvider.showFindDialog(text, showIme);
+    }
+
+    /**
+     * Gets the first substring consisting of the address of a physical
+     * location. Currently, only addresses in the United States are detected,
+     * and consist of:
+     * <ul>
+     *   <li>a house number</li>
+     *   <li>a street name</li>
+     *   <li>a street type (Road, Circle, etc), either spelled out or
+     *       abbreviated</li>
+     *   <li>a city name</li>
+     *   <li>a state or territory, either spelled out or two-letter abbr</li>
+     *   <li>an optional 5 digit or 9 digit zip code</li>
+     * </ul>
+     * All names must be correctly capitalized, and the zip code, if present,
+     * must be valid for the state. The street type must be a standard USPS
+     * spelling or abbreviation. The state or territory must also be spelled
+     * or abbreviated using USPS standards. The house number may not exceed
+     * five digits.
+     *
+     * @param addr the string to search for addresses
+     * @return the address, or if no address is found, {@code null}
+     * @deprecated this method is superseded by {@link TextClassifier#generateLinks(
+     * android.view.textclassifier.TextLinks.Request)}. Avoid using this method even when targeting
+     * API levels where no alternative is available.
+     */
+    @Nullable
+    @Deprecated
     public static String findAddress(String addr) {
-        return null;
+        if (addr == null) {
+            throw new NullPointerException("addr is null");
+        }
+        return FindAddress.findAddress(addr);
     }
 
+    /**
+     * For apps targeting the L release, WebView has a new default behavior that reduces
+     * memory footprint and increases performance by intelligently choosing
+     * the portion of the HTML document that needs to be drawn. These
+     * optimizations are transparent to the developers. However, under certain
+     * circumstances, an App developer may want to disable them:
+     * <ol>
+     *   <li>When an app uses {@link #onDraw} to do own drawing and accesses portions
+     *       of the page that is way outside the visible portion of the page.</li>
+     *   <li>When an app uses {@link #capturePicture} to capture a very large HTML document.
+     *       Note that capturePicture is a deprecated API.</li>
+     * </ol>
+     * Enabling drawing the entire HTML document has a significant performance
+     * cost. This method should be called before any WebViews are created.
+     */
+    public static void enableSlowWholeDocumentDraw() {
+        getFactory().getStatics().enableSlowWholeDocumentDraw();
+    }
+
+    /**
+     * Clears the highlighting surrounding text matches created by
+     * {@link #findAllAsync}.
+     */
+    public void clearMatches() {
+        checkThread();
+        mProvider.clearMatches();
+    }
+
+    /**
+     * Queries the document to see if it contains any image references. The
+     * message object will be dispatched with arg1 being set to 1 if images
+     * were found and 0 if the document does not reference any images.
+     *
+     * @param response the message that will be dispatched with the result
+     */
     public void documentHasImages(Message response) {
+        checkThread();
+        mProvider.documentHasImages(response);
     }
 
+    /**
+     * Sets the WebViewClient that will receive various notifications and
+     * requests. This will replace the current handler.
+     *
+     * @param client an implementation of WebViewClient
+     * @see #getWebViewClient
+     */
     public void setWebViewClient(WebViewClient client) {
+        checkThread();
+        mProvider.setWebViewClient(client);
     }
 
+    /**
+     * Gets the WebViewClient.
+     *
+     * @return the WebViewClient, or a default client if not yet set
+     * @see #setWebViewClient
+     */
+    public WebViewClient getWebViewClient() {
+        checkThread();
+        return mProvider.getWebViewClient();
+    }
+
+    /**
+     * Registers the interface to be used when content can not be handled by
+     * the rendering engine, and should be downloaded instead. This will replace
+     * the current handler.
+     *
+     * @param listener an implementation of DownloadListener
+     */
     public void setDownloadListener(DownloadListener listener) {
+        checkThread();
+        mProvider.setDownloadListener(listener);
     }
 
+    /**
+     * Sets the chrome handler. This is an implementation of WebChromeClient for
+     * use in handling JavaScript dialogs, favicons, titles, and the progress.
+     * This will replace the current handler.
+     *
+     * @param client an implementation of WebChromeClient
+     * @see #getWebChromeClient
+     */
     public void setWebChromeClient(WebChromeClient client) {
+        checkThread();
+        mProvider.setWebChromeClient(client);
     }
 
-    public void addJavascriptInterface(Object obj, String interfaceName) {
+    /**
+     * Gets the chrome handler.
+     *
+     * @return the WebChromeClient, or {@code null} if not yet set
+     * @see #setWebChromeClient
+     */
+    @Nullable
+    public WebChromeClient getWebChromeClient() {
+        checkThread();
+        return mProvider.getWebChromeClient();
     }
 
+    /**
+     * Sets the Picture listener. This is an interface used to receive
+     * notifications of a new Picture.
+     *
+     * @param listener an implementation of WebView.PictureListener
+     * @deprecated This method is now obsolete.
+     */
+    @Deprecated
+    public void setPictureListener(PictureListener listener) {
+        checkThread();
+        mProvider.setPictureListener(listener);
+    }
+
+    /**
+     * Injects the supplied Java object into this WebView. The object is
+     * injected into the JavaScript context of the main frame, using the
+     * supplied name. This allows the Java object's methods to be
+     * accessed from JavaScript. For applications targeted to API
+     * level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     * and above, only public methods that are annotated with
+     * {@link android.webkit.JavascriptInterface} can be accessed from JavaScript.
+     * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below,
+     * all public methods (including the inherited ones) can be accessed, see the
+     * important security note below for implications.
+     * <p> Note that injected objects will not appear in JavaScript until the page is next
+     * (re)loaded. JavaScript should be enabled before injecting the object. For example:
+     * <pre>
+     * class JsObject {
+     *    {@literal @}JavascriptInterface
+     *    public String toString() { return "injectedObject"; }
+     * }
+     * webview.getSettings().setJavaScriptEnabled(true);
+     * webView.addJavascriptInterface(new JsObject(), "injectedObject");
+     * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null);
+     * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre>
+     * <p>
+     * <strong>IMPORTANT:</strong>
+     * <ul>
+     * <li> This method can be used to allow JavaScript to control the host
+     * application. This is a powerful feature, but also presents a security
+     * risk for apps targeting {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or earlier.
+     * Apps that target a version later than {@link android.os.Build.VERSION_CODES#JELLY_BEAN}
+     * are still vulnerable if the app runs on a device running Android earlier than 4.2.
+     * The most secure way to use this method is to target {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     * and to ensure the method is called only when running on Android 4.2 or later.
+     * With these older versions, JavaScript could use reflection to access an
+     * injected object's public fields. Use of this method in a WebView
+     * containing untrusted content could allow an attacker to manipulate the
+     * host application in unintended ways, executing Java code with the
+     * permissions of the host application. Use extreme care when using this
+     * method in a WebView which could contain untrusted content.</li>
+     * <li> JavaScript interacts with Java object on a private, background
+     * thread of this WebView. Care is therefore required to maintain thread
+     * safety.
+     * </li>
+     * <li> The Java object's fields are not accessible.</li>
+     * <li> For applications targeted to API level {@link android.os.Build.VERSION_CODES#LOLLIPOP}
+     * and above, methods of injected Java objects are enumerable from
+     * JavaScript.</li>
+     * </ul>
+     *
+     * @param object the Java object to inject into this WebView's JavaScript
+     *               context. {@code null} values are ignored.
+     * @param name the name used to expose the object in JavaScript
+     */
+    public void addJavascriptInterface(Object object, String name) {
+        checkThread();
+        mProvider.addJavascriptInterface(object, name);
+    }
+
+    /**
+     * Removes a previously injected Java object from this WebView. Note that
+     * the removal will not be reflected in JavaScript until the page is next
+     * (re)loaded. See {@link #addJavascriptInterface}.
+     *
+     * @param name the name used to expose the object in JavaScript
+     */
+    public void removeJavascriptInterface(@NonNull String name) {
+        checkThread();
+        mProvider.removeJavascriptInterface(name);
+    }
+
+    /**
+     * Creates a message channel to communicate with JS and returns the message
+     * ports that represent the endpoints of this message channel. The HTML5 message
+     * channel functionality is described
+     * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here
+     * </a>
+     *
+     * <p>The returned message channels are entangled and already in started state.
+     *
+     * @return the two message ports that form the message channel.
+     */
+    public WebMessagePort[] createWebMessageChannel() {
+        checkThread();
+        return mProvider.createWebMessageChannel();
+    }
+
+    /**
+     * Post a message to main frame. The embedded application can restrict the
+     * messages to a certain target origin. See
+     * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages">
+     * HTML5 spec</a> for how target origin can be used.
+     * <p>
+     * A target origin can be set as a wildcard ("*"). However this is not recommended.
+     * See the page above for security issues.
+     *
+     * @param message the WebMessage
+     * @param targetOrigin the target origin.
+     */
+    public void postWebMessage(WebMessage message, Uri targetOrigin) {
+        checkThread();
+        mProvider.postMessageToMainFrame(message, targetOrigin);
+    }
+
+    /**
+     * Gets the WebSettings object used to control the settings for this
+     * WebView.
+     *
+     * @return a WebSettings object that can be used to control this WebView's
+     *         settings
+     */
+    public WebSettings getSettings() {
+        checkThread();
+        return mProvider.getSettings();
+    }
+
+    /**
+     * Enables debugging of web contents (HTML / CSS / JavaScript)
+     * loaded into any WebViews of this application. This flag can be enabled
+     * in order to facilitate debugging of web layouts and JavaScript
+     * code running inside WebViews. Please refer to WebView documentation
+     * for the debugging guide.
+     *
+     * The default is {@code false}.
+     *
+     * @param enabled whether to enable web contents debugging
+     */
+    public static void setWebContentsDebuggingEnabled(boolean enabled) {
+        getFactory().getStatics().setWebContentsDebuggingEnabled(enabled);
+    }
+
+    /**
+     * Gets the list of currently loaded plugins.
+     *
+     * @return the list of currently loaded plugins
+     * @deprecated This was used for Gears, which has been deprecated.
+     * @hide
+     */
+    @Deprecated
+    public static synchronized PluginList getPluginList() {
+        return new PluginList();
+    }
+
+    /**
+     * Define the directory used to store WebView data for the current process.
+     * The provided suffix will be used when constructing data and cache
+     * directory paths. If this API is not called, no suffix will be used.
+     * Each directory can be used by only one process in the application. If more
+     * than one process in an app wishes to use WebView, only one process can use
+     * the default directory, and other processes must call this API to define
+     * a unique suffix.
+     * <p>
+     * This means that different processes in the same application cannot directly
+     * share WebView-related data, since the data directories must be distinct.
+     * Applications that use this API may have to explicitly pass data between
+     * processes. For example, login cookies may have to be copied from one
+     * process's cookie jar to the other using {@link CookieManager} if both
+     * processes' WebViews are intended to be logged in.
+     * <p>
+     * Most applications should simply ensure that all components of the app
+     * that rely on WebView are in the same process, to avoid needing multiple
+     * data directories. The {@link #disableWebView} method can be used to ensure
+     * that the other processes do not use WebView by accident in this case.
+     * <p>
+     * This API must be called before any instances of WebView are created in
+     * this process and before any other methods in the android.webkit package
+     * are called by this process.
+     *
+     * @param suffix The directory name suffix to be used for the current
+     *               process. Must not contain a path separator.
+     * @throws IllegalStateException if WebView has already been initialized
+     *                               in the current process.
+     * @throws IllegalArgumentException if the suffix contains a path separator.
+     */
+    public static void setDataDirectorySuffix(String suffix) {
+        WebViewFactory.setDataDirectorySuffix(suffix);
+    }
+
+    /**
+     * Indicate that the current process does not intend to use WebView, and
+     * that an exception should be thrown if a WebView is created or any other
+     * methods in the android.webkit package are used.
+     * <p>
+     * Applications with multiple processes may wish to call this in processes
+     * that are not intended to use WebView to avoid accidentally incurring
+     * the memory usage of initializing WebView in long-lived processes that
+     * have no need for it, and to prevent potential data directory conflicts
+     * (see {@link #setDataDirectorySuffix}).
+     * <p>
+     * For example, an audio player application with one process for its
+     * activities and another process for its playback service may wish to call
+     * this method in the playback service's {@link android.app.Service#onCreate}.
+     *
+     * @throws IllegalStateException if WebView has already been initialized
+     *                               in the current process.
+     */
+    public static void disableWebView() {
+        WebViewFactory.disableWebView();
+    }
+
+
+    /**
+     * @deprecated This was used for Gears, which has been deprecated.
+     * @hide
+     */
+    @Deprecated
+    public void refreshPlugins(boolean reloadOpenPages) {
+        checkThread();
+    }
+
+    /**
+     * Puts this WebView into text selection mode. Do not rely on this
+     * functionality; it will be deprecated in the future.
+     *
+     * @deprecated This method is now obsolete.
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     */
+    @Deprecated
+    public void emulateShiftHeld() {
+        checkThread();
+    }
+
+    /**
+     * @deprecated WebView no longer needs to implement
+     * ViewGroup.OnHierarchyChangeListener.  This method does nothing now.
+     */
+    @Override
+    // Cannot add @hide as this can always be accessed via the interface.
+    @Deprecated
+    public void onChildViewAdded(View parent, View child) {}
+
+    /**
+     * @deprecated WebView no longer needs to implement
+     * ViewGroup.OnHierarchyChangeListener.  This method does nothing now.
+     */
+    @Override
+    // Cannot add @hide as this can always be accessed via the interface.
+    @Deprecated
+    public void onChildViewRemoved(View p, View child) {}
+
+    /**
+     * @deprecated WebView should not have implemented
+     * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now.
+     */
+    @Override
+    // Cannot add @hide as this can always be accessed via the interface.
+    @Deprecated
+    public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+    }
+
+    /**
+     * @deprecated Only the default case, {@code true}, will be supported in a future version.
+     */
+    @Deprecated
+    public void setMapTrackballToArrowKeys(boolean setMap) {
+        checkThread();
+        mProvider.setMapTrackballToArrowKeys(setMap);
+    }
+
+
+    public void flingScroll(int vx, int vy) {
+        checkThread();
+        mProvider.flingScroll(vx, vy);
+    }
+
+    /**
+     * Gets the zoom controls for this WebView, as a separate View. The caller
+     * is responsible for inserting this View into the layout hierarchy.
+     * <p/>
+     * API level {@link android.os.Build.VERSION_CODES#CUPCAKE} introduced
+     * built-in zoom mechanisms for the WebView, as opposed to these separate
+     * zoom controls. The built-in mechanisms are preferred and can be enabled
+     * using {@link WebSettings#setBuiltInZoomControls}.
+     *
+     * @deprecated the built-in zoom mechanisms are preferred
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN}
+     */
+    @Deprecated
     public View getZoomControls() {
-        return null;
+        checkThread();
+        return mProvider.getZoomControls();
     }
 
+    /**
+     * Gets whether this WebView can be zoomed in.
+     *
+     * @return {@code true} if this WebView can be zoomed in
+     *
+     * @deprecated This method is prone to inaccuracy due to race conditions
+     * between the web rendering and UI threads; prefer
+     * {@link WebViewClient#onScaleChanged}.
+     */
+    @Deprecated
+    public boolean canZoomIn() {
+        checkThread();
+        return mProvider.canZoomIn();
+    }
+
+    /**
+     * Gets whether this WebView can be zoomed out.
+     *
+     * @return {@code true} if this WebView can be zoomed out
+     *
+     * @deprecated This method is prone to inaccuracy due to race conditions
+     * between the web rendering and UI threads; prefer
+     * {@link WebViewClient#onScaleChanged}.
+     */
+    @Deprecated
+    public boolean canZoomOut() {
+        checkThread();
+        return mProvider.canZoomOut();
+    }
+
+    /**
+     * Performs a zoom operation in this WebView.
+     *
+     * @param zoomFactor the zoom factor to apply. The zoom factor will be clamped to the WebView's
+     * zoom limits. This value must be in the range 0.01 to 100.0 inclusive.
+     */
+    public void zoomBy(float zoomFactor) {
+        checkThread();
+        if (zoomFactor < 0.01)
+            throw new IllegalArgumentException("zoomFactor must be greater than 0.01.");
+        if (zoomFactor > 100.0)
+            throw new IllegalArgumentException("zoomFactor must be less than 100.");
+        mProvider.zoomBy(zoomFactor);
+    }
+
+    /**
+     * Performs zoom in in this WebView.
+     *
+     * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes
+     */
     public boolean zoomIn() {
-        return false;
+        checkThread();
+        return mProvider.zoomIn();
     }
 
+    /**
+     * Performs zoom out in this WebView.
+     *
+     * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes
+     */
     public boolean zoomOut() {
-        return false;
+        checkThread();
+        return mProvider.zoomOut();
+    }
+
+    /**
+     * @deprecated This method is now obsolete.
+     * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+     */
+    @Deprecated
+    public void debugDump() {
+        checkThread();
+    }
+
+    /**
+     * See {@link ViewDebug.HierarchyHandler#dumpViewHierarchyWithProperties(BufferedWriter, int)}
+     * @hide
+     */
+    @Override
+    public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) {
+        mProvider.dumpViewHierarchyWithProperties(out, level);
+    }
+
+    /**
+     * See {@link ViewDebug.HierarchyHandler#findHierarchyView(String, int)}
+     * @hide
+     */
+    @Override
+    public View findHierarchyView(String className, int hashCode) {
+        return mProvider.findHierarchyView(className, hashCode);
+    }
+
+    /** @hide */
+    @IntDef(prefix = { "RENDERER_PRIORITY_" }, value = {
+            RENDERER_PRIORITY_WAIVED,
+            RENDERER_PRIORITY_BOUND,
+            RENDERER_PRIORITY_IMPORTANT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RendererPriority {}
+
+    /**
+     * The renderer associated with this WebView is bound with
+     * {@link Context#BIND_WAIVE_PRIORITY}. At this priority level
+     * {@link WebView} renderers will be strong targets for out of memory
+     * killing.
+     *
+     * Use with {@link #setRendererPriorityPolicy}.
+     */
+    public static final int RENDERER_PRIORITY_WAIVED = 0;
+    /**
+     * The renderer associated with this WebView is bound with
+     * the default priority for services.
+     *
+     * Use with {@link #setRendererPriorityPolicy}.
+     */
+    public static final int RENDERER_PRIORITY_BOUND = 1;
+    /**
+     * The renderer associated with this WebView is bound with
+     * {@link Context#BIND_IMPORTANT}.
+     *
+     * Use with {@link #setRendererPriorityPolicy}.
+     */
+    public static final int RENDERER_PRIORITY_IMPORTANT = 2;
+
+    /**
+     * Set the renderer priority policy for this {@link WebView}. The
+     * priority policy will be used to determine whether an out of
+     * process renderer should be considered to be a target for OOM
+     * killing.
+     *
+     * Because a renderer can be associated with more than one
+     * WebView, the final priority it is computed as the maximum of
+     * any attached WebViews. When a WebView is destroyed it will
+     * cease to be considerered when calculating the renderer
+     * priority. Once no WebViews remain associated with the renderer,
+     * the priority of the renderer will be reduced to
+     * {@link #RENDERER_PRIORITY_WAIVED}.
+     *
+     * The default policy is to set the priority to
+     * {@link #RENDERER_PRIORITY_IMPORTANT} regardless of visibility,
+     * and this should not be changed unless the caller also handles
+     * renderer crashes with
+     * {@link WebViewClient#onRenderProcessGone}. Any other setting
+     * will result in WebView renderers being killed by the system
+     * more aggressively than the application.
+     *
+     * @param rendererRequestedPriority the minimum priority at which
+     *        this WebView desires the renderer process to be bound.
+     * @param waivedWhenNotVisible if {@code true}, this flag specifies that
+     *        when this WebView is not visible, it will be treated as
+     *        if it had requested a priority of
+     *        {@link #RENDERER_PRIORITY_WAIVED}.
+     */
+    public void setRendererPriorityPolicy(
+            @RendererPriority int rendererRequestedPriority,
+            boolean waivedWhenNotVisible) {
+        mProvider.setRendererPriorityPolicy(rendererRequestedPriority, waivedWhenNotVisible);
+    }
+
+    /**
+     * Get the requested renderer priority for this WebView.
+     *
+     * @return the requested renderer priority policy.
+     */
+    @RendererPriority
+    public int getRendererRequestedPriority() {
+        return mProvider.getRendererRequestedPriority();
+    }
+
+    /**
+     * Return whether this WebView requests a priority of
+     * {@link #RENDERER_PRIORITY_WAIVED} when not visible.
+     *
+     * @return whether this WebView requests a priority of
+     * {@link #RENDERER_PRIORITY_WAIVED} when not visible.
+     */
+    public boolean getRendererPriorityWaivedWhenNotVisible() {
+        return mProvider.getRendererPriorityWaivedWhenNotVisible();
+    }
+
+    /**
+     * Sets the {@link TextClassifier} for this WebView.
+     */
+    public void setTextClassifier(@Nullable TextClassifier textClassifier) {
+        mProvider.setTextClassifier(textClassifier);
+    }
+
+    /**
+     * Returns the {@link TextClassifier} used by this WebView.
+     * If no TextClassifier has been set, this WebView uses the default set by the system.
+     */
+    @NonNull
+    public TextClassifier getTextClassifier() {
+        return mProvider.getTextClassifier();
+    }
+
+    /**
+     * Returns the {@link ClassLoader} used to load internal WebView classes.
+     * This method is meant for use by the WebView Support Library, there is no reason to use this
+     * method otherwise.
+     */
+    @NonNull
+    public static ClassLoader getWebViewClassLoader() {
+        return getFactory().getWebViewClassLoader();
+    }
+
+    /**
+     * Returns the {@link Looper} corresponding to the thread on which WebView calls must be made.
+     */
+    @NonNull
+    public Looper getWebViewLooper() {
+        return mWebViewThread;
+    }
+
+    //-------------------------------------------------------------------------
+    // Interface for WebView providers
+    //-------------------------------------------------------------------------
+
+    /**
+     * Gets the WebViewProvider. Used by providers to obtain the underlying
+     * implementation, e.g. when the application responds to
+     * WebViewClient.onCreateWindow() request.
+     *
+     * @hide WebViewProvider is not public API.
+     */
+    @SystemApi
+    public WebViewProvider getWebViewProvider() {
+        return mProvider;
+    }
+
+    /**
+     * Callback interface, allows the provider implementation to access non-public methods
+     * and fields, and make super-class calls in this WebView instance.
+     * @hide Only for use by WebViewProvider implementations
+     */
+    @SystemApi
+    public class PrivateAccess {
+        // ---- Access to super-class methods ----
+        public int super_getScrollBarStyle() {
+            return WebView.super.getScrollBarStyle();
+        }
+
+        public void super_scrollTo(int scrollX, int scrollY) {
+            WebView.super.scrollTo(scrollX, scrollY);
+        }
+
+        public void super_computeScroll() {
+            WebView.super.computeScroll();
+        }
+
+        public boolean super_onHoverEvent(MotionEvent event) {
+            return WebView.super.onHoverEvent(event);
+        }
+
+        public boolean super_performAccessibilityAction(int action, Bundle arguments) {
+            return WebView.super.performAccessibilityActionInternal(action, arguments);
+        }
+
+        public boolean super_performLongClick() {
+            return WebView.super.performLongClick();
+        }
+
+        public boolean super_setFrame(int left, int top, int right, int bottom) {
+            return WebView.super.setFrame(left, top, right, bottom);
+        }
+
+        public boolean super_dispatchKeyEvent(KeyEvent event) {
+            return WebView.super.dispatchKeyEvent(event);
+        }
+
+        public boolean super_onGenericMotionEvent(MotionEvent event) {
+            return WebView.super.onGenericMotionEvent(event);
+        }
+
+        public boolean super_requestFocus(int direction, Rect previouslyFocusedRect) {
+            return WebView.super.requestFocus(direction, previouslyFocusedRect);
+        }
+
+        public void super_setLayoutParams(ViewGroup.LayoutParams params) {
+            WebView.super.setLayoutParams(params);
+        }
+
+        public void super_startActivityForResult(Intent intent, int requestCode) {
+            WebView.super.startActivityForResult(intent, requestCode);
+        }
+
+        // ---- Access to non-public methods ----
+        public void overScrollBy(int deltaX, int deltaY,
+                int scrollX, int scrollY,
+                int scrollRangeX, int scrollRangeY,
+                int maxOverScrollX, int maxOverScrollY,
+                boolean isTouchEvent) {
+            WebView.this.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
+                    maxOverScrollX, maxOverScrollY, isTouchEvent);
+        }
+
+        public void awakenScrollBars(int duration) {
+            WebView.this.awakenScrollBars(duration);
+        }
+
+        public void awakenScrollBars(int duration, boolean invalidate) {
+            WebView.this.awakenScrollBars(duration, invalidate);
+        }
+
+        public float getVerticalScrollFactor() {
+            return WebView.this.getVerticalScrollFactor();
+        }
+
+        public float getHorizontalScrollFactor() {
+            return WebView.this.getHorizontalScrollFactor();
+        }
+
+        public void setMeasuredDimension(int measuredWidth, int measuredHeight) {
+            WebView.this.setMeasuredDimension(measuredWidth, measuredHeight);
+        }
+
+        public void onScrollChanged(int l, int t, int oldl, int oldt) {
+            WebView.this.onScrollChanged(l, t, oldl, oldt);
+        }
+
+        public int getHorizontalScrollbarHeight() {
+            return WebView.this.getHorizontalScrollbarHeight();
+        }
+
+        public void super_onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
+                int l, int t, int r, int b) {
+            WebView.super.onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b);
+        }
+
+        // ---- Access to (non-public) fields ----
+        /** Raw setter for the scroll X value, without invoking onScrollChanged handlers etc. */
+        public void setScrollXRaw(int scrollX) {
+            WebView.this.mScrollX = scrollX;
+        }
+
+        /** Raw setter for the scroll Y value, without invoking onScrollChanged handlers etc. */
+        public void setScrollYRaw(int scrollY) {
+            WebView.this.mScrollY = scrollY;
+        }
+
+    }
+
+    //-------------------------------------------------------------------------
+    // Package-private internal stuff
+    //-------------------------------------------------------------------------
+
+    // Only used by android.webkit.FindActionModeCallback.
+    void setFindDialogFindListener(FindListener listener) {
+        checkThread();
+        setupFindListenerIfNeeded();
+        mFindListener.mFindDialogFindListener = listener;
+    }
+
+    // Only used by android.webkit.FindActionModeCallback.
+    void notifyFindDialogDismissed() {
+        checkThread();
+        mProvider.notifyFindDialogDismissed();
+    }
+
+    //-------------------------------------------------------------------------
+    // Private internal stuff
+    //-------------------------------------------------------------------------
+
+    private WebViewProvider mProvider;
+
+    /**
+     * In addition to the FindListener that the user may set via the WebView.setFindListener
+     * API, FindActionModeCallback will register it's own FindListener. We keep them separate
+     * via this class so that the two FindListeners can potentially exist at once.
+     */
+    private class FindListenerDistributor implements FindListener {
+        private FindListener mFindDialogFindListener;
+        private FindListener mUserFindListener;
+
+        @Override
+        public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
+                boolean isDoneCounting) {
+            if (mFindDialogFindListener != null) {
+                mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches,
+                        isDoneCounting);
+            }
+
+            if (mUserFindListener != null) {
+                mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches,
+                        isDoneCounting);
+            }
+        }
+    }
+    private FindListenerDistributor mFindListener;
+
+    private void setupFindListenerIfNeeded() {
+        if (mFindListener == null) {
+            mFindListener = new FindListenerDistributor();
+            mProvider.setFindListener(mFindListener);
+        }
+    }
+
+    private void ensureProviderCreated() {
+        checkThread();
+        if (mProvider == null) {
+            // As this can get called during the base class constructor chain, pass the minimum
+            // number of dependencies here; the rest are deferred to init().
+            mProvider = getFactory().createWebView(this, new PrivateAccess());
+        }
+    }
+
+    private static WebViewFactoryProvider getFactory() {
+        return WebViewFactory.getProvider();
+    }
+
+    private final Looper mWebViewThread = Looper.myLooper();
+
+    private void checkThread() {
+        // Ignore mWebViewThread == null because this can be called during in the super class
+        // constructor, before this class's own constructor has even started.
+        if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) {
+            Throwable throwable = new Throwable(
+                    "A WebView method was called on thread '" +
+                    Thread.currentThread().getName() + "'. " +
+                    "All WebView methods must be called on the same thread. " +
+                    "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() +
+                    ", FYI main Looper is " + Looper.getMainLooper() + ")");
+            Log.w(LOGTAG, Log.getStackTraceString(throwable));
+            StrictMode.onWebViewMethodCalledOnWrongThread(throwable);
+
+            if (sEnforceThreadChecking) {
+                throw new RuntimeException(throwable);
+            }
+        }
+    }
+
+    //-------------------------------------------------------------------------
+    // Override View methods
+    //-------------------------------------------------------------------------
+
+    // TODO: Add a test that enumerates all methods in ViewDelegte & ScrollDelegate, and ensures
+    // there's a corresponding override (or better, caller) for each of them in here.
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mProvider.getViewDelegate().onAttachedToWindow();
+    }
+
+    /** @hide */
+    @Override
+    protected void onDetachedFromWindowInternal() {
+        mProvider.getViewDelegate().onDetachedFromWindow();
+        super.onDetachedFromWindowInternal();
+    }
+
+    /** @hide */
+    @Override
+    public void onMovedToDisplay(int displayId, Configuration config) {
+        mProvider.getViewDelegate().onMovedToDisplay(displayId, config);
+    }
+
+    @Override
+    public void setLayoutParams(ViewGroup.LayoutParams params) {
+        mProvider.getViewDelegate().setLayoutParams(params);
+    }
+
+    @Override
+    public void setOverScrollMode(int mode) {
+        super.setOverScrollMode(mode);
+        // This method may be called in the constructor chain, before the WebView provider is
+        // created.
+        ensureProviderCreated();
+        mProvider.getViewDelegate().setOverScrollMode(mode);
+    }
+
+    @Override
+    public void setScrollBarStyle(int style) {
+        mProvider.getViewDelegate().setScrollBarStyle(style);
+        super.setScrollBarStyle(style);
+    }
+
+    @Override
+    protected int computeHorizontalScrollRange() {
+        return mProvider.getScrollDelegate().computeHorizontalScrollRange();
+    }
+
+    @Override
+    protected int computeHorizontalScrollOffset() {
+        return mProvider.getScrollDelegate().computeHorizontalScrollOffset();
+    }
+
+    @Override
+    protected int computeVerticalScrollRange() {
+        return mProvider.getScrollDelegate().computeVerticalScrollRange();
+    }
+
+    @Override
+    protected int computeVerticalScrollOffset() {
+        return mProvider.getScrollDelegate().computeVerticalScrollOffset();
+    }
+
+    @Override
+    protected int computeVerticalScrollExtent() {
+        return mProvider.getScrollDelegate().computeVerticalScrollExtent();
+    }
+
+    @Override
+    public void computeScroll() {
+        mProvider.getScrollDelegate().computeScroll();
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        return mProvider.getViewDelegate().onHoverEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return mProvider.getViewDelegate().onTouchEvent(event);
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        return mProvider.getViewDelegate().onGenericMotionEvent(event);
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent event) {
+        return mProvider.getViewDelegate().onTrackballEvent(event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return mProvider.getViewDelegate().onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return mProvider.getViewDelegate().onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+        return mProvider.getViewDelegate().onKeyMultiple(keyCode, repeatCount, event);
+    }
+
+    /*
+    TODO: These are not currently implemented in WebViewClassic, but it seems inconsistent not
+    to be delegating them too.
+
+    @Override
+    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+        return mProvider.getViewDelegate().onKeyPreIme(keyCode, event);
+    }
+    @Override
+    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+        return mProvider.getViewDelegate().onKeyLongPress(keyCode, event);
+    }
+    @Override
+    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+        return mProvider.getViewDelegate().onKeyShortcut(keyCode, event);
+    }
+    */
+
+    @Override
+    public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+        AccessibilityNodeProvider provider =
+                mProvider.getViewDelegate().getAccessibilityNodeProvider();
+        return provider == null ? super.getAccessibilityNodeProvider() : provider;
+    }
+
+    @Deprecated
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return mProvider.getViewDelegate().shouldDelayChildPressedState();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return WebView.class.getName();
+    }
+
+    @Override
+    public void onProvideVirtualStructure(ViewStructure structure) {
+        mProvider.getViewDelegate().onProvideVirtualStructure(structure);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The {@link ViewStructure} traditionally represents a {@link View}, while for web pages
+     * it represent HTML nodes. Hence, it's necessary to "map" the HTML properties in a way that is
+     * understood by the {@link android.service.autofill.AutofillService} implementations:
+     *
+     * <ol>
+     *   <li>Only the HTML nodes inside a {@code FORM} are generated.
+     *   <li>The source of the HTML is set using {@link ViewStructure#setWebDomain(String)} in the
+     *   node representing the WebView.
+     *   <li>If a web page has multiple {@code FORM}s, only the data for the current form is
+     *   represented&mdash;if the user taps a field from another form, then the current autofill
+     *   context is canceled (by calling {@link android.view.autofill.AutofillManager#cancel()} and
+     *   a new context is created for that {@code FORM}.
+     *   <li>Similarly, if the page has {@code IFRAME} nodes, they are not initially represented in
+     *   the view structure until the user taps a field from a {@code FORM} inside the
+     *   {@code IFRAME}, in which case it would be treated the same way as multiple forms described
+     *   above, except that the {@link ViewStructure#setWebDomain(String) web domain} of the
+     *   {@code FORM} contains the {@code src} attribute from the {@code IFRAME} node.
+     *   <li>The W3C autofill field ({@code autocomplete} tag attribute) maps to
+     *   {@link ViewStructure#setAutofillHints(String[])}.
+     *   <li>If the view is editable, the {@link ViewStructure#setAutofillType(int)} and
+     *   {@link ViewStructure#setAutofillValue(AutofillValue)} must be set.
+     *   <li>The {@code placeholder} attribute maps to {@link ViewStructure#setHint(CharSequence)}.
+     *   <li>Other HTML attributes can be represented through
+     *   {@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}.
+     * </ol>
+     *
+     * <p>If the WebView implementation can determine that the value of a field was set statically
+     * (for example, not through Javascript), it should also call
+     * {@code structure.setDataIsSensitive(false)}.
+     *
+     * <p>For example, an HTML form with 2 fields for username and password:
+     *
+     * <pre class="prettyprint">
+     *    &lt;label&gt;Username:&lt;/label&gt;
+     *    &lt;input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"&gt;
+     *    &lt;label&gt;Password:&lt;/label&gt;
+     *    &lt;input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"&gt;
+     * </pre>
+     *
+     * <p>Would map to:
+     *
+     * <pre class="prettyprint">
+     *     int index = structure.addChildCount(2);
+     *     ViewStructure username = structure.newChild(index);
+     *     username.setAutofillId(structure.getAutofillId(), 1); // id 1 - first child
+     *     username.setAutofillHints("username");
+     *     username.setHtmlInfo(username.newHtmlInfoBuilder("input")
+     *         .addAttribute("type", "text")
+     *         .addAttribute("name", "username")
+     *         .addAttribute("label", "Username:")
+     *         .build());
+     *     username.setHint("Email or username");
+     *     username.setAutofillType(View.AUTOFILL_TYPE_TEXT);
+     *     username.setAutofillValue(AutofillValue.forText("Type your username"));
+     *     // Value of the field is not sensitive because it was created statically and not changed.
+     *     username.setDataIsSensitive(false);
+     *
+     *     ViewStructure password = structure.newChild(index + 1);
+     *     username.setAutofillId(structure, 2); // id 2 - second child
+     *     password.setAutofillHints("current-password");
+     *     password.setHtmlInfo(password.newHtmlInfoBuilder("input")
+     *         .addAttribute("type", "password")
+     *         .addAttribute("name", "password")
+     *         .addAttribute("label", "Password:")
+     *         .build());
+     *     password.setHint("Password");
+     *     password.setAutofillType(View.AUTOFILL_TYPE_TEXT);
+     * </pre>
+     */
+    @Override
+    public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
+        mProvider.getViewDelegate().onProvideAutofillVirtualStructure(structure, flags);
+    }
+
+    @Override
+    public void autofill(SparseArray<AutofillValue>values) {
+        mProvider.getViewDelegate().autofill(values);
+    }
+
+    @Override
+    public boolean isVisibleToUserForAutofill(int virtualId) {
+        return mProvider.getViewDelegate().isVisibleToUserForAutofill(virtualId);
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info);
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        mProvider.getViewDelegate().onInitializeAccessibilityEvent(event);
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        return mProvider.getViewDelegate().performAccessibilityAction(action, arguments);
+    }
+
+    /** @hide */
+    @Override
+    protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
+            int l, int t, int r, int b) {
+        mProvider.getViewDelegate().onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b);
+    }
+
+    @Override
+    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
+        mProvider.getViewDelegate().onOverScrolled(scrollX, scrollY, clampedX, clampedY);
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        mProvider.getViewDelegate().onWindowVisibilityChanged(visibility);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        mProvider.getViewDelegate().onDraw(canvas);
+    }
+
+    @Override
+    public boolean performLongClick() {
+        return mProvider.getViewDelegate().performLongClick();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        mProvider.getViewDelegate().onConfigurationChanged(newConfig);
+    }
+
+    /**
+     * Creates a new InputConnection for an InputMethod to interact with the WebView.
+     * This is similar to {@link View#onCreateInputConnection} but note that WebView
+     * calls InputConnection methods on a thread other than the UI thread.
+     * If these methods are overridden, then the overriding methods should respect
+     * thread restrictions when calling View methods or accessing data.
+     */
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        return mProvider.getViewDelegate().onCreateInputConnection(outAttrs);
+    }
+
+    @Override
+    public boolean onDragEvent(DragEvent event) {
+        return mProvider.getViewDelegate().onDragEvent(event);
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        // This method may be called in the constructor chain, before the WebView provider is
+        // created.
+        ensureProviderCreated();
+        mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility);
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        mProvider.getViewDelegate().onWindowFocusChanged(hasWindowFocus);
+        super.onWindowFocusChanged(hasWindowFocus);
+    }
+
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        mProvider.getViewDelegate().onFocusChanged(focused, direction, previouslyFocusedRect);
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+    }
+
+    /** @hide */
+    @Override
+    protected boolean setFrame(int left, int top, int right, int bottom) {
+        return mProvider.getViewDelegate().setFrame(left, top, right, bottom);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int ow, int oh) {
+        super.onSizeChanged(w, h, ow, oh);
+        mProvider.getViewDelegate().onSizeChanged(w, h, ow, oh);
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        mProvider.getViewDelegate().onScrollChanged(l, t, oldl, oldt);
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mProvider.getViewDelegate().dispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+        return mProvider.getViewDelegate().requestFocus(direction, previouslyFocusedRect);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        mProvider.getViewDelegate().onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+        return mProvider.getViewDelegate().requestChildRectangleOnScreen(child, rect, immediate);
+    }
+
+    @Override
+    public void setBackgroundColor(int color) {
+        mProvider.getViewDelegate().setBackgroundColor(color);
+    }
+
+    @Override
+    public void setLayerType(int layerType, Paint paint) {
+        super.setLayerType(layerType, paint);
+        mProvider.getViewDelegate().setLayerType(layerType, paint);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        mProvider.getViewDelegate().preDispatchDraw(canvas);
+        super.dispatchDraw(canvas);
+    }
+
+    @Override
+    public void onStartTemporaryDetach() {
+        super.onStartTemporaryDetach();
+        mProvider.getViewDelegate().onStartTemporaryDetach();
+    }
+
+    @Override
+    public void onFinishTemporaryDetach() {
+        super.onFinishTemporaryDetach();
+        mProvider.getViewDelegate().onFinishTemporaryDetach();
+    }
+
+    @Override
+    public Handler getHandler() {
+        return mProvider.getViewDelegate().getHandler(super.getHandler());
+    }
+
+    @Override
+    public View findFocus() {
+        return mProvider.getViewDelegate().findFocus(super.findFocus());
+    }
+
+    /**
+     * If WebView has already been loaded into the current process this method will return the
+     * package that was used to load it. Otherwise, the package that would be used if the WebView
+     * was loaded right now will be returned; this does not cause WebView to be loaded, so this
+     * information may become outdated at any time.
+     * The WebView package changes either when the current WebView package is updated, disabled, or
+     * uninstalled. It can also be changed through a Developer Setting.
+     * If the WebView package changes, any app process that has loaded WebView will be killed. The
+     * next time the app starts and loads WebView it will use the new WebView package instead.
+     * @return the current WebView package, or {@code null} if there is none.
+     */
+    @Nullable
+    public static PackageInfo getCurrentWebViewPackage() {
+        PackageInfo webviewPackage = WebViewFactory.getLoadedPackageInfo();
+        if (webviewPackage != null) {
+            return webviewPackage;
+        }
+
+        IWebViewUpdateService service = WebViewFactory.getUpdateService();
+        if (service == null) {
+            return null;
+        }
+        try {
+            return service.getCurrentWebViewPackage();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Receive the result from a previous call to {@link #startActivityForResult(Intent, int)}.
+     *
+     * @param requestCode The integer request code originally supplied to
+     *                    startActivityForResult(), allowing you to identify who this
+     *                    result came from.
+     * @param resultCode The integer result code returned by the child activity
+     *                   through its setResult().
+     * @param data An Intent, which can return result data to the caller
+     *               (various data can be attached to Intent "extras").
+     * @hide
+     */
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        mProvider.getViewDelegate().onActivityResult(requestCode, resultCode, data);
+    }
+
+    @Override
+    public boolean onCheckIsTextEditor() {
+        return mProvider.getViewDelegate().onCheckIsTextEditor();
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+
+        checkThread();
+        encoder.addProperty("webview:contentHeight", mProvider.getContentHeight());
+        encoder.addProperty("webview:contentWidth", mProvider.getContentWidth());
+        encoder.addProperty("webview:scale", mProvider.getScale());
+        encoder.addProperty("webview:title", mProvider.getTitle());
+        encoder.addProperty("webview:url", mProvider.getUrl());
+        encoder.addProperty("webview:originalUrl", mProvider.getOriginalUrl());
     }
 }
diff --git a/android/widget/Editor.java b/android/widget/Editor.java
index 9946726..dac100a 100644
--- a/android/widget/Editor.java
+++ b/android/widget/Editor.java
@@ -39,6 +39,7 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Path;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -4837,14 +4838,48 @@
             return true;
         }
 
-        private boolean handleOverlapsMagnifier() {
-            final int handleY = mContainer.getDecorViewLayoutParams().y;
-            final int magnifierBottomWhenAtWindowTop =
-                    mTextView.getRootWindowInsets().getSystemWindowInsetTop()
-                        + mMagnifierAnimator.mMagnifier.getHeight();
-            return handleY <= magnifierBottomWhenAtWindowTop;
+        private boolean handleOverlapsMagnifier(@NonNull final HandleView handle,
+                @NonNull final Rect magnifierRect) {
+            final PopupWindow window = handle.mContainer;
+            if (!window.hasDecorView()) {
+                return false;
+            }
+            final Rect handleRect = new Rect(
+                    window.getDecorViewLayoutParams().x,
+                    window.getDecorViewLayoutParams().y,
+                    window.getDecorViewLayoutParams().x + window.getContentView().getWidth(),
+                    window.getDecorViewLayoutParams().y + window.getContentView().getHeight());
+            return Rect.intersects(handleRect, magnifierRect);
         }
 
+        private @Nullable HandleView getOtherSelectionHandle() {
+            final SelectionModifierCursorController controller = getSelectionController();
+            if (controller == null || !controller.isActive()) {
+                return null;
+            }
+            return controller.mStartHandle != this
+                    ? controller.mStartHandle
+                    : controller.mEndHandle;
+        }
+
+        private final Magnifier.Callback mHandlesVisibilityCallback = new Magnifier.Callback() {
+            @Override
+            public void onOperationComplete() {
+                final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getWindowCoords();
+                if (magnifierTopLeft == null) {
+                    return;
+                }
+                final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y,
+                        magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(),
+                        magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight());
+                setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect));
+                final HandleView otherHandle = getOtherSelectionHandle();
+                if (otherHandle != null) {
+                    otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect));
+                }
+            }
+        };
+
         protected final void updateMagnifier(@NonNull final MotionEvent event) {
             if (mMagnifierAnimator == null) {
                 return;
@@ -4858,12 +4893,8 @@
                 mRenderCursorRegardlessTiming = true;
                 mTextView.invalidateCursorPath();
                 suspendBlink();
-                // Hide handle if it overlaps the magnifier.
-                if (handleOverlapsMagnifier()) {
-                    setVisible(false);
-                } else {
-                    setVisible(true);
-                }
+                mMagnifierAnimator.mMagnifier
+                        .setOnOperationCompleteCallback(mHandlesVisibilityCallback);
 
                 mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
             } else {
@@ -4877,6 +4908,10 @@
                 mRenderCursorRegardlessTiming = false;
                 resumeBlink();
                 setVisible(true);
+                final HandleView otherHandle = getOtherSelectionHandle();
+                if (otherHandle != null) {
+                    otherHandle.setVisible(true);
+                }
             }
         }
 
@@ -6031,7 +6066,9 @@
             mSwitchedLines = false;
             final int selectionStart = mTextView.getSelectionStart();
             final int selectionEnd = mTextView.getSelectionEnd();
-            if (selectionStart > selectionEnd) {
+            if (selectionStart < 0 || selectionEnd < 0) {
+                Selection.removeSelection((Spannable) mTextView.getText());
+            } else if (selectionStart > selectionEnd) {
                 Selection.setSelection((Spannable) mTextView.getText(),
                         selectionEnd, selectionStart);
             }
diff --git a/android/widget/ImageView.java b/android/widget/ImageView.java
index 4b951fa..1372987 100644
--- a/android/widget/ImageView.java
+++ b/android/widget/ImageView.java
@@ -817,8 +817,6 @@
         if (mScaleType != scaleType) {
             mScaleType = scaleType;
 
-            setWillNotCacheDrawing(mScaleType == ScaleType.CENTER);
-
             requestLayout();
             invalidate();
         }
diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java
index d32e93c..40f9652 100644
--- a/android/widget/LinearLayout.java
+++ b/android/widget/LinearLayout.java
@@ -217,6 +217,17 @@
 
     private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
 
+    /**
+     * Signals that compatibility booleans have been initialized according to
+     * target SDK versions.
+     */
+    private static boolean sCompatibilityDone = false;
+
+    /**
+     * Behavior change in P; always remeasure weighted children, regardless of excess space.
+     */
+    private static boolean sRemeasureWeightedChildren = true;
+
     public LinearLayout(Context context) {
         this(context, null);
     }
@@ -232,6 +243,15 @@
     public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
 
+        if (!sCompatibilityDone && context != null) {
+            final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+
+            // Older apps only remeasure non-zero children
+            sRemeasureWeightedChildren = targetSdkVersion >= Build.VERSION_CODES.P;
+
+            sCompatibilityDone = true;
+        }
+
         final TypedArray a = context.obtainStyledAttributes(
                 attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);
 
@@ -917,7 +937,8 @@
         // measurement on any children, we need to measure them now.
         int remainingExcess = heightSize - mTotalLength
                 + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
-        if (skippedMeasure || totalWeight > 0.0f) {
+        if (skippedMeasure
+                || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
             float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
 
             mTotalLength = 0;
@@ -1300,7 +1321,8 @@
         // measurement on any children, we need to measure them now.
         int remainingExcess = widthSize - mTotalLength
                 + (mAllowInconsistentMeasurement ? 0 : usedExcessSpace);
-        if (skippedMeasure || totalWeight > 0.0f) {
+        if (skippedMeasure
+                || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
             float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
 
             maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java
index 5eb6699..cb362e6 100644
--- a/android/widget/Magnifier.java
+++ b/android/widget/Magnifier.java
@@ -233,6 +233,17 @@
         return mZoom;
     }
 
+    /**
+     * @hide
+     */
+    @Nullable
+    public Point getWindowCoords() {
+        if (mWindow == null) {
+            return null;
+        }
+        return new Point(mWindow.mLastDrawContentPositionX, mWindow.mLastDrawContentPositionY);
+    }
+
     @Nullable
     private Surface getValidViewSurface() {
         // TODO: deduplicate this against the first part of #performPixelCopy
@@ -374,8 +385,11 @@
         private final Runnable mMagnifierUpdater;
         // The handler where the magnifier updater jobs will be post'd.
         private final Handler mHandler;
-        // The callback to be run after the next draw. Only used for testing.
+        // The callback to be run after the next draw.
         private Callback mCallback;
+        // The position of the magnifier content when the last draw was requested.
+        private int mLastDrawContentPositionX;
+        private int mLastDrawContentPositionY;
 
         // Members below describe the state of the magnifier. Reads/writes to them
         // have to be synchronized between the UI thread and the thread that handles
@@ -598,6 +612,8 @@
                     callback = null;
                 }
 
+                mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
+                mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
                 mFrameDrawScheduled = false;
             }
 
diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java
index b3327a7..1f2b90a 100644
--- a/android/widget/SelectionActionModeHelper.java
+++ b/android/widget/SelectionActionModeHelper.java
@@ -33,9 +33,9 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ActionMode;
-import android.view.textclassifier.Logger;
 import android.view.textclassifier.SelectionEvent;
 import android.view.textclassifier.SelectionEvent.InvocationMethod;
+import android.view.textclassifier.SelectionSessionLogger;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextClassificationConstants;
 import android.view.textclassifier.TextClassificationManager;
@@ -663,7 +663,6 @@
         private static final String LOG_TAG = "SelectionMetricsLogger";
         private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
 
-        private final Logger mLogger;
         private final boolean mEditTextLogger;
         private final BreakIterator mTokenIterator;
 
@@ -673,10 +672,8 @@
 
         SelectionMetricsLogger(TextView textView) {
             Preconditions.checkNotNull(textView);
-            mLogger = textView.getTextClassifier().getLogger(
-                    new Logger.Config(textView.getContext(), getWidetType(textView), null));
             mEditTextLogger = textView.isTextEditable();
-            mTokenIterator = mLogger.getTokenIterator(textView.getTextLocale());
+            mTokenIterator = SelectionSessionLogger.getTokenIterator(textView.getTextLocale());
         }
 
         @TextClassifier.WidgetType
@@ -702,8 +699,6 @@
                 }
                 mTokenIterator.setText(mText);
                 mStartIndex = index;
-                mLogger.logSelectionStartedEvent(invocationMethod, 0);
-                // TODO: Remove the above legacy logging.
                 mClassificationSession = classificationSession;
                 mClassificationSession.onSelectionEvent(
                         SelectionEvent.createSelectionStartedEvent(invocationMethod, 0));
@@ -720,27 +715,18 @@
                 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
                 int[] wordIndices = getWordDelta(start, end);
                 if (selection != null) {
-                    mLogger.logSelectionModifiedEvent(
-                            wordIndices[0], wordIndices[1], selection);
-                    // TODO: Remove the above legacy logging.
                     if (mClassificationSession != null) {
                         mClassificationSession.onSelectionEvent(
                                 SelectionEvent.createSelectionModifiedEvent(
                                         wordIndices[0], wordIndices[1], selection));
                     }
                 } else if (classification != null) {
-                    mLogger.logSelectionModifiedEvent(
-                            wordIndices[0], wordIndices[1], classification);
-                    // TODO: Remove the above legacy logging.
                     if (mClassificationSession != null) {
                         mClassificationSession.onSelectionEvent(
                                 SelectionEvent.createSelectionModifiedEvent(
                                         wordIndices[0], wordIndices[1], classification));
                     }
                 } else {
-                    mLogger.logSelectionModifiedEvent(
-                            wordIndices[0], wordIndices[1]);
-                    // TODO: Remove the above legacy logging.
                     if (mClassificationSession != null) {
                         mClassificationSession.onSelectionEvent(
                                 SelectionEvent.createSelectionModifiedEvent(
@@ -762,18 +748,12 @@
                 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
                 int[] wordIndices = getWordDelta(start, end);
                 if (classification != null) {
-                    mLogger.logSelectionActionEvent(
-                            wordIndices[0], wordIndices[1], action, classification);
-                    // TODO: Remove the above legacy logging.
                     if (mClassificationSession != null) {
                         mClassificationSession.onSelectionEvent(
                                 SelectionEvent.createSelectionActionEvent(
                                         wordIndices[0], wordIndices[1], action, classification));
                     }
                 } else {
-                    mLogger.logSelectionActionEvent(
-                            wordIndices[0], wordIndices[1], action);
-                    // TODO: Remove the above legacy logging.
                     if (mClassificationSession != null) {
                         mClassificationSession.onSelectionEvent(
                                 SelectionEvent.createSelectionActionEvent(
@@ -989,7 +969,7 @@
             mHot = true;
             trimText();
             final TextSelection selection;
-            if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) {
+            if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
                 final TextSelection.Request request = new TextSelection.Request.Builder(
                         mTrimmedText, mRelativeStart, mRelativeEnd)
                         .setDefaultLocales(mDefaultLocales)
@@ -1043,7 +1023,7 @@
 
                 trimText();
                 final TextClassification classification;
-                if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) {
+                if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
                     final TextClassification.Request request =
                             new TextClassification.Request.Builder(
                                     mTrimmedText, mRelativeStart, mRelativeEnd)
diff --git a/android/widget/TextClock.java b/android/widget/TextClock.java
index 53318c9..d8a9cca 100644
--- a/android/widget/TextClock.java
+++ b/android/widget/TextClock.java
@@ -408,6 +408,15 @@
     }
 
     /**
+     * Update the displayed time if necessary and invalidate the view.
+     * @hide
+     */
+    public void refresh() {
+        onTimeChanged();
+        invalidate();
+    }
+
+    /**
      * Indicates whether the system is currently using the 24-hour mode.
      *
      * When the system is in 24-hour mode, this view will use the pattern
diff --git a/android/widget/TextView.java b/android/widget/TextView.java
index 11db6b6..7b9ecca 100644
--- a/android/widget/TextView.java
+++ b/android/widget/TextView.java
@@ -317,7 +317,6 @@
  * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
  * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
  * @attr ref android.R.styleable#TextView_autoSizePresetSizes
- * @attr ref android.R.styleable#TextView_accessibilityHeading
  */
 @RemoteView
 public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
@@ -417,7 +416,6 @@
     private int mCurTextColor;
     private int mCurHintTextColor;
     private boolean mFreezesText;
-    private boolean mIsAccessibilityHeading;
 
     private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
     private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
@@ -1294,8 +1292,6 @@
                 case com.android.internal.R.styleable.TextView_lineHeight:
                     lineHeight = a.getDimensionPixelSize(attr, -1);
                     break;
-                case com.android.internal.R.styleable.TextView_accessibilityHeading:
-                    mIsAccessibilityHeading = a.getBoolean(attr, false);
             }
         }
 
@@ -5213,32 +5209,6 @@
     }
 
     /**
-     * Gets whether this view is a heading for accessibility purposes.
-     *
-     * @return {@code true} if the view is a heading, {@code false} otherwise.
-     *
-     * @attr ref android.R.styleable#TextView_accessibilityHeading
-     */
-    public boolean isAccessibilityHeading() {
-        return mIsAccessibilityHeading;
-    }
-
-    /**
-     * Set if view is a heading for a section of content for accessibility purposes.
-     *
-     * @param isHeading {@code true} if the view is a heading, {@code false} otherwise.
-     *
-     * @attr ref android.R.styleable#TextView_accessibilityHeading
-     */
-    public void setAccessibilityHeading(boolean isHeading) {
-        if (isHeading != mIsAccessibilityHeading) {
-            mIsAccessibilityHeading = isHeading;
-            notifyViewAccessibilityStateChangedIfNeeded(
-                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
-        }
-    }
-
-    /**
      * Convenience method to append the specified text to the TextView's
      * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
      * if it was not already editable.
@@ -9380,7 +9350,7 @@
         final int selectionStart = getSelectionStart();
         final int selectionEnd = getSelectionEnd();
 
-        return selectionStart >= 0 && selectionStart != selectionEnd;
+        return selectionStart >= 0 && selectionEnd > 0 && selectionStart != selectionEnd;
     }
 
     String getSelectedText() {
@@ -10833,7 +10803,6 @@
         info.setText(getTextForAccessibility());
         info.setHintText(mHint);
         info.setShowingHintText(isShowingHint());
-        info.setHeading(mIsAccessibilityHeading);
 
         if (mBufferType == BufferType.EDITABLE) {
             info.setEditable(true);
diff --git a/androidx/car/app/CarAlertDialog.java b/androidx/car/app/CarAlertDialog.java
index 453ad4e..58262b6 100644
--- a/androidx/car/app/CarAlertDialog.java
+++ b/androidx/car/app/CarAlertDialog.java
@@ -23,6 +23,7 @@
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.TypedValue;
+import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.TouchDelegate;
 import android.view.View;
@@ -90,17 +91,24 @@
 
     private void setTitleInternal(CharSequence title) {
         boolean hasTitle = !TextUtils.isEmpty(title);
+        boolean hasBody = mBodyView.getVisibility() == View.VISIBLE;
+        boolean hasButton = mButtonPanel.getVisibility() == View.VISIBLE;
 
         mTitleView.setText(title);
         mTitleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE);
 
+        // Center title if there is no button.
+        mTitleView.setGravity(hasButton ? Gravity.CENTER_VERTICAL | Gravity.START : Gravity.CENTER);
+
         // If there's a title, then remove the padding at the top of the content view.
         int topPadding = hasTitle ? 0 : mTopPadding;
+        // If there is only title, also remove the padding at the bottom so title is centered.
+        int bottomPadding = !hasButton && !hasBody ? 0 : mContentView.getPaddingBottom();
         mContentView.setPaddingRelative(
                 mContentView.getPaddingStart(),
                 topPadding,
                 mContentView.getPaddingEnd(),
-                mContentView.getPaddingBottom());
+                bottomPadding);
     }
 
     private void setBody(CharSequence body) {
@@ -226,10 +234,12 @@
      * contents based on what data is present.
      */
     private void initializeDialogWithData() {
-        setTitleInternal(mData.mTitle);
         setBody(mData.mBody);
         setPositiveButton(mData.mPositiveButtonText);
         setNegativeButton(mData.mNegativeButtonText);
+        // setTitleInternal() should be called last because we want to center title and adjust
+        // padding depending on body/button configuration.
+        setTitleInternal(mData.mTitle);
     }
 
     /**
diff --git a/androidx/car/utils/CarUxRestrictionsTestUtils.java b/androidx/car/utils/CarUxRestrictionsTestUtils.java
index 7100488..bc1377f 100644
--- a/androidx/car/utils/CarUxRestrictionsTestUtils.java
+++ b/androidx/car/utils/CarUxRestrictionsTestUtils.java
@@ -27,10 +27,12 @@
     private CarUxRestrictionsTestUtils() {};
 
     public static CarUxRestrictions getFullyRestricted() {
-        return new CarUxRestrictions(true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0);
+        return new CarUxRestrictions.Builder(
+                true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0).build();
     }
 
     public static CarUxRestrictions getBaseline() {
-        return new CarUxRestrictions(false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE, 0);
+        return new CarUxRestrictions.Builder(
+                false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE, 0).build();
     }
 }
diff --git a/androidx/car/widget/DayNightStyle.java b/androidx/car/widget/DayNightStyle.java
index 37d0d51..73f9ce4 100644
--- a/androidx/car/widget/DayNightStyle.java
+++ b/androidx/car/widget/DayNightStyle.java
@@ -16,8 +16,12 @@
 
 package androidx.car.widget;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import androidx.annotation.IntDef;
 
+import java.lang.annotation.Retention;
+
 /**
  * Specifies how the system UI should respond to day/night mode events.
  *
@@ -37,6 +41,7 @@
         DayNightStyle.FORCE_NIGHT,
         DayNightStyle.FORCE_DAY,
 })
+@Retention(SOURCE)
 public @interface DayNightStyle {
     /**
      * Sets the foreground color to be automatically changed based on day/night mode, assuming the
diff --git a/androidx/car/widget/PagedListView.java b/androidx/car/widget/PagedListView.java
index 3665fd6..4d16e0c 100644
--- a/androidx/car/widget/PagedListView.java
+++ b/androidx/car/widget/PagedListView.java
@@ -18,6 +18,8 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
@@ -50,6 +52,8 @@
 import androidx.recyclerview.widget.OrientationHelper;
 import androidx.recyclerview.widget.RecyclerView;
 
+import java.lang.annotation.Retention;
+
 /**
  * View that wraps a {@link RecyclerView} and a scroll bar that has
  * page up and down arrows. Interaction with this view is similar to a {@code RecyclerView} as it
@@ -187,6 +191,7 @@
             Gutter.END,
             Gutter.BOTH,
     })
+    @Retention(SOURCE)
     public @interface Gutter {
         /**
          * No gutter on either side of the list items. The items will span the full width of the
@@ -260,7 +265,7 @@
         mSnapHelper = new PagedSnapHelper(context);
         mSnapHelper.attachToRecyclerView(mRecyclerView);
 
-        mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener);
+        mRecyclerView.addOnScrollListener(mRecyclerViewOnScrollListener);
         mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
 
         int defaultGutterSize = getResources().getDimensionPixelSize(R.dimen.car_margin);
@@ -850,6 +855,13 @@
         int screenSize = mRecyclerView.getHeight();
         int scrollDistance = screenSize;
 
+        // If the last item is partially visible, page down should bring it to the top.
+        View lastChild = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1);
+        if (mRecyclerView.getLayoutManager().isViewPartiallyVisible(lastChild,
+                /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false)) {
+            scrollDistance = orientationHelper.getDecoratedStart(lastChild);
+        }
+
         // The iteration order matters. In case where there are 2 items longer than screen size, we
         // want to focus on upcoming view (the one at the bottom of screen).
         for (int i = mRecyclerView.getChildCount() - 1; i >= 0; i--) {
diff --git a/androidx/car/widget/SeekbarListItem.java b/androidx/car/widget/SeekbarListItem.java
index 368e6c0..24dd58b 100644
--- a/androidx/car/widget/SeekbarListItem.java
+++ b/androidx/car/widget/SeekbarListItem.java
@@ -21,10 +21,6 @@
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.IdRes;
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
@@ -38,6 +34,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
 import androidx.car.R;
 import androidx.car.utils.CarUxRestrictionsUtils;
 
@@ -96,7 +95,6 @@
     private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
 
     @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
-    private int mPrimaryActionIconResId;
     private Drawable mPrimaryActionIconDrawable;
 
     private String mText;
@@ -106,7 +104,7 @@
     private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
 
     @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
-    private int mSupplementalIconResId;
+    private Drawable mSupplementalIconDrawable;
     private View.OnClickListener mSupplementalIconOnClickListener;
     private boolean mShowSupplementalIconDivider;
 
@@ -250,12 +248,7 @@
             case PRIMARY_ACTION_TYPE_SMALL_ICON:
                 mBinders.add(vh -> {
                     vh.getPrimaryIcon().setVisibility(View.VISIBLE);
-
-                    if (mPrimaryActionIconDrawable != null) {
-                        vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
-                    } else if (mPrimaryActionIconResId != 0) {
-                        vh.getPrimaryIcon().setImageResource(mPrimaryActionIconResId);
-                    }
+                    vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
                 });
                 break;
             default:
@@ -405,7 +398,8 @@
                         vh.getSupplementalIconDivider().setVisibility(View.VISIBLE);
                     }
 
-                    vh.getSupplementalIcon().setImageResource(mSupplementalIconResId);
+                    vh.getSupplementalIcon().setImageDrawable(mSupplementalIconDrawable);
+
                     vh.getSupplementalIcon().setOnClickListener(
                             mSupplementalIconOnClickListener);
                     vh.getSupplementalIcon().setClickable(
@@ -423,7 +417,7 @@
      * @param iconResId the resource identifier of the drawable.
      */
     public void setPrimaryActionIcon(@DrawableRes int iconResId) {
-        setPrimaryActionIcon(null, iconResId);
+        setPrimaryActionIcon(mContext.getDrawable(iconResId));
     }
 
     /**
@@ -432,15 +426,8 @@
      * @param drawable the Drawable to set, or null to clear the content.
      */
     public void setPrimaryActionIcon(Drawable drawable) {
-        setPrimaryActionIcon(drawable, 0);
-    }
-
-    private void setPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId) {
         mPrimaryActionType = PRIMARY_ACTION_TYPE_SMALL_ICON;
-
         mPrimaryActionIconDrawable = drawable;
-        mPrimaryActionIconResId = iconResId;
-
         markDirty();
     }
 
@@ -458,18 +445,34 @@
     /**
      * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
      */
-    public void setSupplementalIcon(int iconResId, boolean showSupplementalIconDivider) {
-        setSupplementalIcon(iconResId, showSupplementalIconDivider, null);
+    public void setSupplementalIcon(@DrawableRes int iconResId,
+                                    boolean showSupplementalIconDivider) {
+        setSupplementalIcon(mContext.getDrawable(iconResId), showSupplementalIconDivider, null);
     }
 
     /**
      * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
      */
-    public void setSupplementalIcon(@IdRes int iconResId,
-            boolean showSupplementalIconDivider, @Nullable  View.OnClickListener listener) {
+    public void setSupplementalIcon(@DrawableRes int iconResId, boolean showSupplementalIconDivider,
+                                    @Nullable View.OnClickListener listener) {
+        setSupplementalIcon(mContext.getDrawable(iconResId), showSupplementalIconDivider, listener);
+    }
+
+    /**
+     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+     */
+    public void setSupplementalIcon(Drawable drawable, boolean showSupplementalIconDivider) {
+        setSupplementalIcon(drawable, showSupplementalIconDivider, null);
+    }
+
+    /**
+     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+     */
+    public void setSupplementalIcon(Drawable drawable, boolean showSupplementalIconDivider,
+                                    @Nullable  View.OnClickListener listener) {
         mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
 
-        mSupplementalIconResId = iconResId;
+        mSupplementalIconDrawable = drawable;
         mShowSupplementalIconDivider = showSupplementalIconDivider;
         mSupplementalIconOnClickListener = listener;
 
diff --git a/androidx/car/widget/SeekbarListItemTest.java b/androidx/car/widget/SeekbarListItemTest.java
index 207a51e..f8b8d33 100644
--- a/androidx/car/widget/SeekbarListItemTest.java
+++ b/androidx/car/widget/SeekbarListItemTest.java
@@ -30,6 +30,7 @@
 import android.content.pm.PackageManager;
 import android.graphics.Color;
 import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
 import android.support.test.espresso.UiController;
 import android.support.test.espresso.ViewAction;
 import android.support.test.filters.SmallTest;
@@ -278,6 +279,18 @@
         }
     }
 
+    @Test
+    public void testSettingSupplementalIconWithDrawable() {
+        Drawable drawable = mActivity.getDrawable(android.R.drawable.sym_def_app_icon);
+        SeekbarListItem item = new SeekbarListItem(mActivity, 0, 0, null, null);
+        item.setSupplementalIcon(drawable, false);
+
+        setupPagedListView(Arrays.asList(item));
+
+        assertThat(getViewHolderAtPosition(0).getSupplementalIcon().getDrawable(),
+                is(equalTo(drawable)));
+    }
+
     private static ViewAction clickChildViewWithId(final int id) {
         return new ViewAction() {
             @Override
diff --git a/androidx/car/widget/SubheaderListItem.java b/androidx/car/widget/SubheaderListItem.java
index 8724696..dd5a00b 100644
--- a/androidx/car/widget/SubheaderListItem.java
+++ b/androidx/car/widget/SubheaderListItem.java
@@ -16,6 +16,8 @@
 
 package androidx.car.widget;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
 import android.content.res.TypedArray;
@@ -23,15 +25,16 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import androidx.annotation.DimenRes;
 import androidx.annotation.IntDef;
 import androidx.annotation.StyleRes;
 import androidx.car.R;
 import androidx.car.utils.CarUxRestrictionsUtils;
 
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Class to build a sub-header list item.
  *
@@ -72,6 +75,7 @@
     @IntDef({
             TEXT_START_MARGIN_TYPE_NONE, TEXT_START_MARGIN_TYPE_LARGE,
             TEXT_START_MARGIN_TYPE_SMALL})
+    @Retention(SOURCE)
     public @interface TextStartMarginType {}
 
     /**
diff --git a/androidx/car/widget/TextListItem.java b/androidx/car/widget/TextListItem.java
index ce64a2d..3681d3e 100644
--- a/androidx/car/widget/TextListItem.java
+++ b/androidx/car/widget/TextListItem.java
@@ -109,7 +109,6 @@
     private View.OnClickListener mOnClickListener;
 
     @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
-    private int mPrimaryActionIconResId;
     private Drawable mPrimaryActionIconDrawable;
 
     private String mTitle;
@@ -117,7 +116,7 @@
     private boolean mIsBodyPrimary;
 
     @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
-    private int mSupplementalIconResId;
+    private Drawable mSupplementalIconDrawable;
     private View.OnClickListener mSupplementalIconOnClickListener;
     private boolean mShowSupplementalIconDivider;
 
@@ -257,11 +256,7 @@
                 mBinders.add(vh -> {
                     vh.getPrimaryIcon().setVisibility(View.VISIBLE);
 
-                    if (mPrimaryActionIconDrawable != null) {
-                        vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
-                    } else if (mPrimaryActionIconResId != 0) {
-                        vh.getPrimaryIcon().setImageResource(mPrimaryActionIconResId);
-                    }
+                    vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
                 });
                 break;
             case PRIMARY_ACTION_TYPE_EMPTY_ICON:
@@ -544,7 +539,7 @@
                         vh.getSupplementalIconDivider().setVisibility(View.VISIBLE);
                     }
 
-                    vh.getSupplementalIcon().setImageResource(mSupplementalIconResId);
+                    vh.getSupplementalIcon().setImageDrawable(mSupplementalIconDrawable);
                     vh.getSupplementalIcon().setOnClickListener(
                             mSupplementalIconOnClickListener);
                     vh.getSupplementalIcon().setClickable(
@@ -606,7 +601,7 @@
      * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item.
      */
     public void setPrimaryActionIcon(@DrawableRes int iconResId, boolean useLargeIcon) {
-        setPrimaryActionIcon(null, iconResId, useLargeIcon);
+        setPrimaryActionIcon(mContext.getDrawable(iconResId), useLargeIcon);
     }
 
     /**
@@ -616,15 +611,9 @@
      * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item.
      */
     public void setPrimaryActionIcon(Drawable drawable, boolean useLargeIcon) {
-        setPrimaryActionIcon(drawable, 0, useLargeIcon);
-    }
-
-    private void setPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId,
-            boolean useLargeIcon) {
         mPrimaryActionType = useLargeIcon
                 ? PRIMARY_ACTION_TYPE_LARGE_ICON
                 : PRIMARY_ACTION_TYPE_SMALL_ICON;
-        mPrimaryActionIconResId = iconResId;
         mPrimaryActionIconDrawable = drawable;
 
         markDirty();
@@ -696,7 +685,18 @@
      *                    {@code Supplemental Icon}.
      */
     public void setSupplementalIcon(int iconResId, boolean showDivider) {
-        setSupplementalIcon(iconResId, showDivider, null);
+        setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, null);
+    }
+
+    /**
+     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+     *
+     * @param drawable the Drawable to set, or null to clear the content.
+     * @param showDivider whether to display a vertical bar that separates {@code text} and
+     *                    {@code Supplemental Icon}.
+     */
+    public void setSupplementalIcon(Drawable drawable, boolean showDivider) {
+        setSupplementalIcon(drawable, showDivider, null);
     }
 
     /**
@@ -709,9 +709,22 @@
      */
     public void setSupplementalIcon(int iconResId, boolean showDivider,
             View.OnClickListener listener) {
+        setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, listener);
+    }
+
+    /**
+     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+     *
+     * @param drawable the Drawable to set, or null to clear the content.
+     * @param showDivider whether to display a vertical bar that separates {@code text} and
+     *                    {@code Supplemental Icon}.
+     * @param listener the callback that will run when icon is clicked.
+     */
+    public void setSupplementalIcon(Drawable drawable, boolean showDivider,
+                                    View.OnClickListener listener) {
         mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
 
-        mSupplementalIconResId = iconResId;
+        mSupplementalIconDrawable = drawable;
         mSupplementalIconOnClickListener = listener;
         mShowSupplementalIconDivider = showDivider;
         markDirty();
diff --git a/androidx/car/widget/TextListItemTest.java b/androidx/car/widget/TextListItemTest.java
index 9a9e46d..c96b46d 100644
--- a/androidx/car/widget/TextListItemTest.java
+++ b/androidx/car/widget/TextListItemTest.java
@@ -212,6 +212,18 @@
     }
 
     @Test
+    public void testSetSupplementalActionWithDrawable() {
+        Drawable drawable = mActivity.getDrawable(android.R.drawable.sym_def_app_icon);
+        TextListItem item = new TextListItem(mActivity);
+        item.setSupplementalIcon(drawable, true);
+
+        setupPagedListView(Arrays.asList(item));
+
+        assertThat(getViewHolderAtPosition(0).getSupplementalIcon().getDrawable(),
+                is(equalTo(drawable)));
+    }
+
+    @Test
     public void testSwitchVisibleAndCheckedState() {
         TextListItem item0 = new TextListItem(mActivity);
         item0.setSwitch(true, true, null);
diff --git a/androidx/contentpager/content/Query.java b/androidx/contentpager/content/Query.java
index 948c6da..d1cfbde 100644
--- a/androidx/contentpager/content/Query.java
+++ b/androidx/contentpager/content/Query.java
@@ -28,7 +28,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 
 import java.util.Arrays;
 
@@ -52,7 +51,6 @@
     private final CancellationSignal mCancellationSignal;
     private final ContentPager.ContentCallback mCallback;
 
-    @VisibleForTesting
     Query(
             @NonNull Uri uri,
             @Nullable String[] projection,
diff --git a/androidx/core/app/ActivityCompat.java b/androidx/core/app/ActivityCompat.java
index 9d9b9bc..f9c677b 100644
--- a/androidx/core/app/ActivityCompat.java
+++ b/androidx/core/app/ActivityCompat.java
@@ -356,6 +356,7 @@
      * @see Activity#findViewById(int)
      * @see androidx.core.view.ViewCompat#requireViewById(View, int)
      */
+    @SuppressWarnings("TypeParameterUnusedInFormals")
     @NonNull
     public static <T extends View> T requireViewById(@NonNull Activity activity, @IdRes int id) {
         // TODO: use and link to Activity#requireViewById() directly, once available
diff --git a/androidx/core/app/NotificationCompat.java b/androidx/core/app/NotificationCompat.java
index 6d1ce46..aba40d1 100644
--- a/androidx/core/app/NotificationCompat.java
+++ b/androidx/core/app/NotificationCompat.java
@@ -2214,9 +2214,14 @@
          * @see Message#Message(CharSequence, long, CharSequence)
          *
          * @return this object for method chaining
+         *
+         * @deprecated Use {@link #addMessage(CharSequence, long, Person)} or
+         * {@link #addMessage(Message)}
          */
+        @Deprecated
         public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) {
-            mMessages.add(new Message(text, timestamp, sender));
+            mMessages.add(
+                    new Message(text, timestamp, new Person.Builder().setName(sender).build()));
             if (mMessages.size() > MAXIMUM_RETAINED_MESSAGES) {
                 mMessages.remove(0);
             }
@@ -2224,8 +2229,23 @@
         }
 
         /**
+         * Adds a message for display by this notification. Convenience call for
+         * {@link #addMessage(Message)}.
+         *
+         * @see Message#Message(CharSequence, long, Person)
+         *
+         * @return this for method chaining
+         */
+        public MessagingStyle addMessage(CharSequence text, long timestamp, Person person) {
+            addMessage(new Message(text, timestamp, person));
+            return this;
+        }
+
+        /**
          * Adds a {@link Message} for display in this notification.
+         *
          * @param message The {@link Message} to be displayed
+         *
          * @return this object for method chaining
          */
         public MessagingStyle addMessage(Message message) {
@@ -2462,24 +2482,42 @@
         }
 
         public static final class Message {
-
             static final String KEY_TEXT = "text";
             static final String KEY_TIMESTAMP = "time";
             static final String KEY_SENDER = "sender";
             static final String KEY_DATA_MIME_TYPE = "type";
             static final String KEY_DATA_URI= "uri";
             static final String KEY_EXTRAS_BUNDLE = "extras";
+            static final String KEY_PERSON = "person";
 
             private final CharSequence mText;
             private final long mTimestamp;
-            private final CharSequence mSender;
+            @Nullable private final Person mPerson;
 
             private Bundle mExtras = new Bundle();
-            private String mDataMimeType;
-            private Uri mDataUri;
+            @Nullable private String mDataMimeType;
+            @Nullable private Uri mDataUri;
+
+            /**
+             * Creates a new {@link Message} with the given text, timestamp, and sender.
+             *
+             * @param text A {@link CharSequence} to be displayed as the message content
+             * @param timestamp Time at which the message arrived in ms since Unix epoch
+             * @param person A {@link Person} whose {@link Person#getName()} value is used as the
+             * display name for the sender. This should be {@code null} for messages by the current
+             * user, in which case, the platform will insert
+             * {@link MessagingStyle#getUserDisplayName()}. A {@link Person}'s key should be
+             * consistent during re-posts of the notification.
+             */
+            public Message(CharSequence text, long timestamp, @Nullable Person person) {
+                mText = text;
+                mTimestamp = timestamp;
+                mPerson = person;
+            }
 
             /**
              * Constructor
+             *
              * @param text A {@link CharSequence} to be displayed as the message content
              * @param timestamp Time at which the message arrived in ms since Unix epoch
              * @param sender A {@link CharSequence} to be used for displaying the name of the
@@ -2487,17 +2525,19 @@
              * the platform will insert {@link MessagingStyle#getUserDisplayName()}.
              * Should be unique amongst all individuals in the conversation, and should be
              * consistent during re-posts of the notification.
+             *
+             * @deprecated Use the alternative constructor instead.
              */
+            @Deprecated
             public Message(CharSequence text, long timestamp, CharSequence sender){
-                mText = text;
-                mTimestamp = timestamp;
-                mSender = sender;
+                this(text, timestamp, new Person.Builder().setName(sender).build());
             }
 
             /**
              * Sets a binary blob of data and an associated MIME type for a message. In the case
              * where the platform doesn't support the MIME type, the original text provided in the
              * constructor will be used.
+             *
              * @param dataMimeType The MIME type of the content. See
              * <a href="{@docRoot}notifications/messaging.html"> for the list of supported MIME
              * types on Android and Android Wear.
@@ -2519,6 +2559,7 @@
              *       Note that once added to the system MediaStore the content is accessible to any
              *       app on the device.</li>
              * </ol>
+             *
              * @return this object for method chaining
              */
             public Message setData(String dataMimeType, Uri dataUri) {
@@ -2531,34 +2572,41 @@
              * Get the text to be used for this message, or the fallback text if a type and content
              * Uri have been set
              */
+            @NonNull
             public CharSequence getText() {
                 return mText;
             }
 
-            /**
-             * Get the time at which this message arrived in ms since Unix epoch
-             */
+            /** Get the time at which this message arrived in ms since Unix epoch. */
             public long getTimestamp() {
                 return mTimestamp;
             }
 
-            /**
-             * Get the extras Bundle for this message.
-             */
+            /** Get the extras Bundle for this message. */
+            @NonNull
             public Bundle getExtras() {
                 return mExtras;
             }
 
             /**
              * Get the text used to display the contact's name in the messaging experience
+             *
+             * @deprecated Use {@link #getPerson()}
              */
+            @Deprecated
+            @Nullable
             public CharSequence getSender() {
-                return mSender;
+                return mPerson.getName();
             }
 
-            /**
-             * Get the MIME type of the data pointed to by the Uri
-             */
+            /** Returns the {@link Person} sender of this message. */
+            @Nullable
+            public Person getPerson() {
+                return mPerson;
+            }
+
+            /** Get the MIME type of the data pointed to by the URI. */
+            @Nullable
             public String getDataMimeType() {
                 return mDataMimeType;
             }
@@ -2567,6 +2615,7 @@
              * Get the the Uri pointing to the content of the message. Can be null, in which case
              * {@see #getText()} is used.
              */
+            @Nullable
             public Uri getDataUri() {
                 return mDataUri;
             }
@@ -2577,8 +2626,8 @@
                     bundle.putCharSequence(KEY_TEXT, mText);
                 }
                 bundle.putLong(KEY_TIMESTAMP, mTimestamp);
-                if (mSender != null) {
-                    bundle.putCharSequence(KEY_SENDER, mSender);
+                if (mPerson != null) {
+                    bundle.putBundle(KEY_PERSON, mPerson.toBundle());
                 }
                 if (mDataMimeType != null) {
                     bundle.putString(KEY_DATA_MIME_TYPE, mDataMimeType);
@@ -2592,6 +2641,7 @@
                 return bundle;
             }
 
+            @NonNull
             static Bundle[] getBundleArrayForMessages(List<Message> messages) {
                 Bundle[] bundles = new Bundle[messages.size()];
                 final int N = messages.size();
@@ -2601,6 +2651,7 @@
                 return bundles;
             }
 
+            @NonNull
             static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) {
                 List<Message> messages = new ArrayList<>(bundles.length);
                 for (int i = 0; i < bundles.length; i++) {
@@ -2614,23 +2665,38 @@
                 return messages;
             }
 
+            @Nullable
             static Message getMessageFromBundle(Bundle bundle) {
                 try {
                     if (!bundle.containsKey(KEY_TEXT) || !bundle.containsKey(KEY_TIMESTAMP)) {
                         return null;
-                    } else {
-                        Message message = new Message(bundle.getCharSequence(KEY_TEXT),
-                                bundle.getLong(KEY_TIMESTAMP), bundle.getCharSequence(KEY_SENDER));
-                        if (bundle.containsKey(KEY_DATA_MIME_TYPE) &&
-                                bundle.containsKey(KEY_DATA_URI)) {
-                            message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
-                                    (Uri) bundle.getParcelable(KEY_DATA_URI));
-                        }
-                        if (bundle.containsKey(KEY_EXTRAS_BUNDLE)) {
-                            message.getExtras().putAll(bundle.getBundle(KEY_EXTRAS_BUNDLE));
-                        }
-                        return message;
                     }
+
+                    Message message;
+                    if (bundle.containsKey(KEY_SENDER)) {
+                        // Legacy sender
+                        message = new Message(
+                                bundle.getCharSequence(KEY_TEXT),
+                                bundle.getLong(KEY_TIMESTAMP),
+                                new Person.Builder()
+                                        .setName(bundle.getCharSequence(KEY_SENDER))
+                                        .build());
+                    } else {
+                        message = new Message(
+                                bundle.getCharSequence(KEY_TEXT),
+                                bundle.getLong(KEY_TIMESTAMP),
+                                Person.fromBundle(bundle.getBundle(KEY_PERSON)));
+                    }
+
+                    if (bundle.containsKey(KEY_DATA_MIME_TYPE)
+                            && bundle.containsKey(KEY_DATA_URI)) {
+                        message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
+                                (Uri) bundle.getParcelable(KEY_DATA_URI));
+                    }
+                    if (bundle.containsKey(KEY_EXTRAS_BUNDLE)) {
+                        message.getExtras().putAll(bundle.getBundle(KEY_EXTRAS_BUNDLE));
+                    }
+                    return message;
                 } catch (ClassCastException e) {
                     return null;
                 }
diff --git a/androidx/core/app/NotificationCompatTest.java b/androidx/core/app/NotificationCompatTest.java
index 2c28cc8..a882891 100644
--- a/androidx/core/app/NotificationCompatTest.java
+++ b/androidx/core/app/NotificationCompatTest.java
@@ -44,6 +44,8 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.BaseInstrumentationTestCase;
 
+import androidx.core.app.NotificationCompat.MessagingStyle.Message;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -315,18 +317,18 @@
     public void testMessage_setAndGetExtras() throws Throwable {
         String extraKey = "extra_key";
         CharSequence extraValue = "extra_value";
-        NotificationCompat.MessagingStyle.Message m =
-                new NotificationCompat.MessagingStyle.Message("text", 0 /*timestamp */, "sender");
+        Message m =
+                new Message("text", 0 /*timestamp */, "sender");
         m.getExtras().putCharSequence(extraKey, extraValue);
         assertEquals(extraValue, m.getExtras().getCharSequence(extraKey));
 
-        ArrayList<NotificationCompat.MessagingStyle.Message> messages = new ArrayList<>(1);
+        ArrayList<Message> messages = new ArrayList<>(1);
         messages.add(m);
         Bundle[] bundleArray =
-                NotificationCompat.MessagingStyle.Message.getBundleArrayForMessages(messages);
+                Message.getBundleArrayForMessages(messages);
         assertEquals(1, bundleArray.length);
-        NotificationCompat.MessagingStyle.Message fromBundle =
-                NotificationCompat.MessagingStyle.Message.getMessageFromBundle(bundleArray[0]);
+        Message fromBundle =
+                Message.getMessageFromBundle(bundleArray[0]);
         assertEquals(extraValue, fromBundle.getExtras().getCharSequence(extraKey));
     }
 
@@ -526,6 +528,38 @@
     }
 
     @Test
+    public void testMessagingStyle_message() {
+        NotificationCompat.MessagingStyle messagingStyle =
+                new NotificationCompat.MessagingStyle("self name");
+        Person person = new Person.Builder().setName("test name").setKey("key").build();
+        Person person2 = new Person.Builder()
+                .setName("test name 2").setKey("key 2").setImportant(true).build();
+        messagingStyle.addMessage("text", 200, person);
+        messagingStyle.addMessage("text2", 300, person2);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(messagingStyle)
+                .build();
+
+        List<Message> result = NotificationCompat.MessagingStyle
+                .extractMessagingStyleFromNotification(notification)
+                .getMessages();
+
+        assertEquals(2, result.size());
+        assertEquals("text", result.get(0).getText());
+        assertEquals(200, result.get(0).getTimestamp());
+        assertEquals("test name", result.get(0).getPerson().getName());
+        assertEquals("key", result.get(0).getPerson().getKey());
+        assertEquals("text2", result.get(1).getText());
+        assertEquals(300, result.get(1).getTimestamp());
+        assertEquals("test name 2", result.get(1).getPerson().getName());
+        assertEquals("key 2", result.get(1).getPerson().getKey());
+        assertTrue(result.get(1).getPerson().isImportant());
+    }
+
+    @Test
     public void testMessagingStyle_isGroupConversation() {
         mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P;
         NotificationCompat.MessagingStyle messagingStyle =
@@ -665,6 +699,17 @@
     }
 
     @Test
+    public void testMessagingStyleMessage_bundle_legacySender() {
+        Bundle legacyBundle = new Bundle();
+        legacyBundle.putCharSequence(Message.KEY_TEXT, "message");
+        legacyBundle.putLong(Message.KEY_TIMESTAMP, 100);
+        legacyBundle.putCharSequence(Message.KEY_SENDER, "sender");
+
+        Message result = Message.getMessageFromBundle(legacyBundle);
+        assertEquals("sender", result.getPerson().getName());
+    }
+
+    @Test
     public void action_builder_hasDefault() {
         NotificationCompat.Action action =
                 new NotificationCompat.Action.Builder(0, "Test Title", null).build();
diff --git a/androidx/core/app/Person.java b/androidx/core/app/Person.java
index 3bda510..e79076e 100644
--- a/androidx/core/app/Person.java
+++ b/androidx/core/app/Person.java
@@ -16,11 +16,11 @@
 
 package androidx.core.app;
 
-import android.graphics.Bitmap;
 import android.os.Bundle;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.graphics.drawable.IconCompat;
 
 /**
  * Provides an immutable reference to an entity that appears repeatedly on different surfaces of the
@@ -39,9 +39,10 @@
      * created from a {@link Person} using {@link #toBundle()}.
      */
     public static Person fromBundle(Bundle bundle) {
+        Bundle iconBundle = bundle.getBundle(ICON_KEY);
         return new Builder()
                 .setName(bundle.getCharSequence(NAME_KEY))
-                .setIcon((Bitmap) bundle.getParcelable(ICON_KEY))
+                .setIcon(iconBundle != null ? IconCompat.createFromBundle(iconBundle) : null)
                 .setUri(bundle.getString(URI_KEY))
                 .setKey(bundle.getString(KEY_KEY))
                 .setBot(bundle.getBoolean(IS_BOT_KEY))
@@ -50,7 +51,7 @@
     }
 
     @Nullable private CharSequence mName;
-    @Nullable private Bitmap mIcon;
+    @Nullable private IconCompat mIcon;
     @Nullable private String mUri;
     @Nullable private String mKey;
     private boolean mIsBot;
@@ -72,7 +73,7 @@
     public Bundle toBundle() {
         Bundle result = new Bundle();
         result.putCharSequence(NAME_KEY, mName);
-        result.putParcelable(ICON_KEY, mIcon);
+        result.putBundle(ICON_KEY, mIcon != null ? mIcon.toBundle() : null);
         result.putString(URI_KEY, mUri);
         result.putString(KEY_KEY, mKey);
         result.putBoolean(IS_BOT_KEY, mIsBot);
@@ -96,7 +97,7 @@
 
     /** Returns the icon for this {@link Person} or {@code null} if no icon was provided. */
     @Nullable
-    public Bitmap getIcon() {
+    public IconCompat getIcon() {
         return mIcon;
     }
 
@@ -146,7 +147,7 @@
     /** Builder for the immutable {@link Person} class. */
     public static class Builder {
         @Nullable private CharSequence mName;
-        @Nullable private Bitmap mIcon;
+        @Nullable private IconCompat mIcon;
         @Nullable private String mUri;
         @Nullable private String mKey;
         private boolean mIsBot;
@@ -181,7 +182,7 @@
          * {@link #setUri(String)}.
          */
         @NonNull
-        public Builder setIcon(@Nullable Bitmap icon) {
+        public Builder setIcon(@Nullable IconCompat icon) {
             mIcon = icon;
             return this;
         }
diff --git a/androidx/core/app/PersonTest.java b/androidx/core/app/PersonTest.java
index 20b2090..12f6117 100644
--- a/androidx/core/app/PersonTest.java
+++ b/androidx/core/app/PersonTest.java
@@ -17,14 +17,16 @@
 package androidx.core.app;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 
 import android.graphics.Bitmap;
 import android.os.Bundle;
-import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import androidx.core.graphics.drawable.IconCompat;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -32,14 +34,14 @@
 @RunWith(AndroidJUnit4.class)
 public class PersonTest {
     private static final CharSequence TEST_NAME = "Example Name";
-    private static final Bitmap TEST_ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+    private static final IconCompat TEST_ICON =
+            IconCompat.createWithBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888));
     private static final String TEST_URI = "mailto:example@example.com";
     private static final String TEST_KEY = "example-key";
     private static final boolean TEST_IS_BOT = true;
     private static final boolean TEST_IS_IMPORTANT = true;
 
     @Test
-    @SdkSuppress(minSdkVersion = 12)
     public void bundle() {
         Person person = new Person.Builder()
                 .setImportant(TEST_IS_IMPORTANT)
@@ -58,13 +60,25 @@
         assertEquals(TEST_KEY, result.getKey());
         assertEquals(TEST_IS_BOT, result.isBot());
         assertEquals(TEST_IS_IMPORTANT, result.isImportant());
-
-        // Requires SDK >= 12
-        assertTrue(TEST_ICON.sameAs(result.getIcon()));
+        assertEquals(TEST_ICON.toBundle().toString(), result.getIcon().toBundle().toString());
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 12)
+    public void bundle_defaultValues() {
+        Person person = new Person.Builder().build();
+
+        Bundle personBundle = person.toBundle();
+        Person result = Person.fromBundle(personBundle);
+
+        assertNull(result.getIcon());
+        assertNull(result.getKey());
+        assertNull(result.getName());
+        assertNull(result.getUri());
+        assertFalse(result.isImportant());
+        assertFalse(result.isBot());
+    }
+
+    @Test
     public void toBuilder() {
         Person person = new Person.Builder()
                 .setImportant(TEST_IS_IMPORTANT)
@@ -81,9 +95,7 @@
         assertEquals(TEST_KEY, result.getKey());
         assertEquals(TEST_IS_BOT, result.isBot());
         assertEquals(TEST_IS_IMPORTANT, result.isImportant());
-
-        // Requires SDK >= 12
-        assertTrue(TEST_ICON.sameAs(result.getIcon()));
+        assertEquals(TEST_ICON.toBundle().toString(), result.getIcon().toBundle().toString());
     }
 
     @Test
diff --git a/androidx/core/content/pm/PackageInfoCompat.java b/androidx/core/content/pm/PackageInfoCompat.java
new file mode 100644
index 0000000..71c53f2
--- /dev/null
+++ b/androidx/core/content/pm/PackageInfoCompat.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 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 androidx.core.content.pm;
+
+import android.content.pm.PackageInfo;
+
+import androidx.annotation.NonNull;
+import androidx.core.os.BuildCompat;
+
+/** Helper for accessing features in {@link PackageInfo}. */
+public final class PackageInfoCompat {
+    /**
+     * Return {@link android.R.attr#versionCode} and {@link android.R.attr#versionCodeMajor}
+     * combined together as a single long value. The {@code versionCodeMajor} is placed in the
+     * upper 32 bits on Android P or newer, otherwise these bits are all set to 0.
+     *
+     * @see PackageInfo#getLongVersionCode()
+     */
+    public static long getLongVersionCode(@NonNull PackageInfo info) {
+        if (BuildCompat.isAtLeastP()) {
+            return info.getLongVersionCode();
+        }
+
+        return info.versionCode;
+    }
+
+    private PackageInfoCompat() {
+    }
+}
diff --git a/androidx/core/content/pm/PackageInfoCompatTest.java b/androidx/core/content/pm/PackageInfoCompatTest.java
new file mode 100644
index 0000000..e3e5504
--- /dev/null
+++ b/androidx/core/content/pm/PackageInfoCompatTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 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 androidx.core.content.pm;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.pm.PackageInfo;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public final class PackageInfoCompatTest {
+    @Test
+    public void getLongVersionCodeLowerBitsOnly() {
+        PackageInfo info = new PackageInfo();
+        info.versionCode = 12345;
+
+        assertEquals(12345L, PackageInfoCompat.getLongVersionCode(info));
+    }
+
+    @SdkSuppress(minSdkVersion = P)
+    @Test
+    public void getLongVersionCodeLowerAndUpperBits() {
+        PackageInfo info = new PackageInfo();
+        info.setLongVersionCode(Long.MAX_VALUE);
+
+        assertEquals(Long.MAX_VALUE, PackageInfoCompat.getLongVersionCode(info));
+    }
+}
diff --git a/androidx/core/content/pm/ShortcutInfoCompat.java b/androidx/core/content/pm/ShortcutInfoCompat.java
index 7eea551..67415ac 100644
--- a/androidx/core/content/pm/ShortcutInfoCompat.java
+++ b/androidx/core/content/pm/ShortcutInfoCompat.java
@@ -26,7 +26,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.util.Arrays;
@@ -74,7 +73,6 @@
         return builder.build();
     }
 
-    @VisibleForTesting
     Intent addToIntent(Intent outIntent) {
         outIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, mIntents[mIntents.length - 1])
                 .putExtra(Intent.EXTRA_SHORTCUT_NAME, mLabel.toString());
diff --git a/androidx/core/text/HtmlCompat.java b/androidx/core/text/HtmlCompat.java
index b846805..a5b97ea 100644
--- a/androidx/core/text/HtmlCompat.java
+++ b/androidx/core/text/HtmlCompat.java
@@ -18,6 +18,8 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.annotation.SuppressLint;
 import android.graphics.Color;
 import android.os.Build;
@@ -33,6 +35,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
+import java.lang.annotation.Retention;
+
 /**
  * Backwards compatible version of {@link Html}.
  */
@@ -119,6 +123,7 @@
             FROM_HTML_MODE_LEGACY
     }, flag = true)
     @RestrictTo(LIBRARY)
+    @Retention(SOURCE)
     @interface FromHtmlFlags {
     }
 
@@ -128,6 +133,7 @@
             TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
     })
     @RestrictTo(LIBRARY)
+    @Retention(SOURCE)
     @interface ToHtmlOptions {
     }
 
diff --git a/androidx/core/text/util/FindAddress.java b/androidx/core/text/util/FindAddress.java
index d66fa05..0602428 100644
--- a/androidx/core/text/util/FindAddress.java
+++ b/androidx/core/text/util/FindAddress.java
@@ -495,8 +495,7 @@
      * @param content The string to search.
      * @return The first valid address, or null if no address was matched.
      */
-    @VisibleForTesting
-    public static String findAddress(String content) {
+    static String findAddress(String content) {
         Matcher houseNumberMatcher = sHouseNumberRe.matcher(content);
         int start = 0;
         while (houseNumberMatcher.find(start)) {
diff --git a/androidx/core/view/ViewCompat.java b/androidx/core/view/ViewCompat.java
index 7b76c38..3302634 100644
--- a/androidx/core/view/ViewCompat.java
+++ b/androidx/core/view/ViewCompat.java
@@ -1344,6 +1344,7 @@
      * @return a view with given ID
      * @see View#findViewById(int)
      */
+    @SuppressWarnings("TypeParameterUnusedInFormals")
     @NonNull
     public static <T extends View> T requireViewById(@NonNull View view, @IdRes int id) {
         // TODO: use and link to View#requireViewById() directly, once available
diff --git a/androidx/core/view/WindowCompat.java b/androidx/core/view/WindowCompat.java
index 5ee42a2..ee5d44c 100644
--- a/androidx/core/view/WindowCompat.java
+++ b/androidx/core/view/WindowCompat.java
@@ -78,6 +78,7 @@
      * @see ViewCompat#requireViewById(View, int)
      * @see Window#findViewById(int)
      */
+    @SuppressWarnings("TypeParameterUnusedInFormals")
     @NonNull
     public static <T extends View> T requireViewById(@NonNull Window window, @IdRes int id) {
         // TODO: use and link to Window#requireViewById() directly, once available
diff --git a/androidx/leanback/system/Settings.java b/androidx/leanback/system/Settings.java
index 04f66d1..011c6a5 100644
--- a/androidx/leanback/system/Settings.java
+++ b/androidx/leanback/system/Settings.java
@@ -43,7 +43,7 @@
     // The intent action that must be provided by a broadcast receiver
     // in a customization package.
     private static final String ACTION_PARTNER_CUSTOMIZATION =
-            "androidx.leanback.action.PARTNER_CUSTOMIZATION";
+            "android.support.v17.leanback.action.PARTNER_CUSTOMIZATION";
 
     public static final String PREFER_STATIC_SHADOWS = "PREFER_STATIC_SHADOWS";
 
diff --git a/androidx/lifecycle/ComputableLiveData.java b/androidx/lifecycle/ComputableLiveData.java
index e2e2a1b..da5a15e 100644
--- a/androidx/lifecycle/ComputableLiveData.java
+++ b/androidx/lifecycle/ComputableLiveData.java
@@ -1,148 +1,9 @@
-/*
- * Copyright (C) 2017 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.
- */
-
+//ComputableLiveData interface for tests
 package androidx.lifecycle;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.executor.ArchTaskExecutor;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A LiveData class that can be invalidated & computed when there are active observers.
- * <p>
- * It can be invalidated via {@link #invalidate()}, which will result in a call to
- * {@link #compute()} if there are active observers (or when they start observing)
- * <p>
- * This is an internal class for now, might be public if we see the necessity.
- *
- * @param <T> The type of the live data
- * @hide internal
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+import androidx.lifecycle.LiveData;
 public abstract class ComputableLiveData<T> {
-
-    private final Executor mExecutor;
-    private final LiveData<T> mLiveData;
-
-    private AtomicBoolean mInvalid = new AtomicBoolean(true);
-    private AtomicBoolean mComputing = new AtomicBoolean(false);
-
-    /**
-     * Creates a computable live data that computes values on the arch IO thread executor.
-     */
-    @SuppressWarnings("WeakerAccess")
-    public ComputableLiveData() {
-        this(ArchTaskExecutor.getIOThreadExecutor());
-    }
-
-    /**
-     *
-     * Creates a computable live data that computes values on the specified executor.
-     *
-     * @param executor Executor that is used to compute new LiveData values.
-     */
-    @SuppressWarnings("WeakerAccess")
-    public ComputableLiveData(@NonNull Executor executor) {
-        mExecutor = executor;
-        mLiveData = new LiveData<T>() {
-            @Override
-            protected void onActive() {
-                mExecutor.execute(mRefreshRunnable);
-            }
-        };
-    }
-
-    /**
-     * Returns the LiveData managed by this class.
-     *
-     * @return A LiveData that is controlled by ComputableLiveData.
-     */
-    @SuppressWarnings("WeakerAccess")
-    @NonNull
-    public LiveData<T> getLiveData() {
-        return mLiveData;
-    }
-
-    @VisibleForTesting
-    final Runnable mRefreshRunnable = new Runnable() {
-        @WorkerThread
-        @Override
-        public void run() {
-            boolean computed;
-            do {
-                computed = false;
-                // compute can happen only in 1 thread but no reason to lock others.
-                if (mComputing.compareAndSet(false, true)) {
-                    // as long as it is invalid, keep computing.
-                    try {
-                        T value = null;
-                        while (mInvalid.compareAndSet(true, false)) {
-                            computed = true;
-                            value = compute();
-                        }
-                        if (computed) {
-                            mLiveData.postValue(value);
-                        }
-                    } finally {
-                        // release compute lock
-                        mComputing.set(false);
-                    }
-                }
-                // check invalid after releasing compute lock to avoid the following scenario.
-                // Thread A runs compute()
-                // Thread A checks invalid, it is false
-                // Main thread sets invalid to true
-                // Thread B runs, fails to acquire compute lock and skips
-                // Thread A releases compute lock
-                // We've left invalid in set state. The check below recovers.
-            } while (computed && mInvalid.get());
-        }
-    };
-
-    // invalidation check always happens on the main thread
-    @VisibleForTesting
-    final Runnable mInvalidationRunnable = new Runnable() {
-        @MainThread
-        @Override
-        public void run() {
-            boolean isActive = mLiveData.hasActiveObservers();
-            if (mInvalid.compareAndSet(false, true)) {
-                if (isActive) {
-                    mExecutor.execute(mRefreshRunnable);
-                }
-            }
-        }
-    };
-
-    /**
-     * Invalidates the LiveData.
-     * <p>
-     * When there are active observers, this will trigger a call to {@link #compute()}.
-     */
-    public void invalidate() {
-        ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
-    }
-
-    @SuppressWarnings("WeakerAccess")
-    @WorkerThread
-    protected abstract T compute();
+    public ComputableLiveData(){}
+    abstract protected T compute();
+    public LiveData<T> getLiveData() {return null;}
+    public void invalidate() {}
 }
diff --git a/androidx/lifecycle/LiveData.java b/androidx/lifecycle/LiveData.java
index 8c94a95..c961d1c 100644
--- a/androidx/lifecycle/LiveData.java
+++ b/androidx/lifecycle/LiveData.java
@@ -1,441 +1,4 @@
-/*
- * Copyright (C) 2017 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.
- */
-
+//LiveData interface for tests
 package androidx.lifecycle;
-
-import static androidx.lifecycle.Lifecycle.State.DESTROYED;
-import static androidx.lifecycle.Lifecycle.State.STARTED;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.arch.core.internal.SafeIterableMap;
-import androidx.arch.core.executor.ArchTaskExecutor;
-
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * LiveData is a data holder class that can be observed within a given lifecycle.
- * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and
- * this observer will be notified about modifications of the wrapped data only if the paired
- * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is
- * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via
- * {@link #observeForever(Observer)} is considered as always active and thus will be always notified
- * about modifications. For those observers, you should manually call
- * {@link #removeObserver(Observer)}.
- *
- * <p> An observer added with a Lifecycle will be automatically removed if the corresponding
- * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for
- * activities and fragments where they can safely observe LiveData and not worry about leaks:
- * they will be instantly unsubscribed when they are destroyed.
- *
- * <p>
- * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods
- * to get notified when number of active {@link Observer}s change between 0 and 1.
- * This allows LiveData to release any heavy resources when it does not have any Observers that
- * are actively observing.
- * <p>
- * This class is designed to hold individual data fields of {@link ViewModel},
- * but can also be used for sharing data between different modules in your application
- * in a decoupled fashion.
- *
- * @param <T> The type of data held by this instance
- * @see ViewModel
- */
-public abstract class LiveData<T> {
-    private final Object mDataLock = new Object();
-    static final int START_VERSION = -1;
-    private static final Object NOT_SET = new Object();
-
-    private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =
-            new SafeIterableMap<>();
-
-    // how many observers are in active state
-    private int mActiveCount = 0;
-    private volatile Object mData = NOT_SET;
-    // when setData is called, we set the pending data and actual data swap happens on the main
-    // thread
-    private volatile Object mPendingData = NOT_SET;
-    private int mVersion = START_VERSION;
-
-    private boolean mDispatchingValue;
-    @SuppressWarnings("FieldCanBeLocal")
-    private boolean mDispatchInvalidated;
-    private final Runnable mPostValueRunnable = new Runnable() {
-        @Override
-        public void run() {
-            Object newValue;
-            synchronized (mDataLock) {
-                newValue = mPendingData;
-                mPendingData = NOT_SET;
-            }
-            //noinspection unchecked
-            setValue((T) newValue);
-        }
-    };
-
-    private void considerNotify(ObserverWrapper observer) {
-        if (!observer.mActive) {
-            return;
-        }
-        // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
-        //
-        // we still first check observer.active to keep it as the entrance for events. So even if
-        // the observer moved to an active state, if we've not received that event, we better not
-        // notify for a more predictable notification order.
-        if (!observer.shouldBeActive()) {
-            observer.activeStateChanged(false);
-            return;
-        }
-        if (observer.mLastVersion >= mVersion) {
-            return;
-        }
-        observer.mLastVersion = mVersion;
-        //noinspection unchecked
-        observer.mObserver.onChanged((T) mData);
-    }
-
-    private void dispatchingValue(@Nullable ObserverWrapper initiator) {
-        if (mDispatchingValue) {
-            mDispatchInvalidated = true;
-            return;
-        }
-        mDispatchingValue = true;
-        do {
-            mDispatchInvalidated = false;
-            if (initiator != null) {
-                considerNotify(initiator);
-                initiator = null;
-            } else {
-                for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
-                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
-                    considerNotify(iterator.next().getValue());
-                    if (mDispatchInvalidated) {
-                        break;
-                    }
-                }
-            }
-        } while (mDispatchInvalidated);
-        mDispatchingValue = false;
-    }
-
-    /**
-     * Adds the given observer to the observers list within the lifespan of the given
-     * owner. The events are dispatched on the main thread. If LiveData already has data
-     * set, it will be delivered to the observer.
-     * <p>
-     * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED}
-     * or {@link Lifecycle.State#RESUMED} state (active).
-     * <p>
-     * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will
-     * automatically be removed.
-     * <p>
-     * When data changes while the {@code owner} is not active, it will not receive any updates.
-     * If it becomes active again, it will receive the last available data automatically.
-     * <p>
-     * LiveData keeps a strong reference to the observer and the owner as long as the
-     * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to
-     * the observer &amp; the owner.
-     * <p>
-     * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData
-     * ignores the call.
-     * <p>
-     * If the given owner, observer tuple is already in the list, the call is ignored.
-     * If the observer is already in the list with another owner, LiveData throws an
-     * {@link IllegalArgumentException}.
-     *
-     * @param owner    The LifecycleOwner which controls the observer
-     * @param observer The observer that will receive the events
-     */
-    @MainThread
-    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
-        assertMainThread("observe");
-        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
-            // ignore
-            return;
-        }
-        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
-        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
-        if (existing != null && !existing.isAttachedTo(owner)) {
-            throw new IllegalArgumentException("Cannot add the same observer"
-                    + " with different lifecycles");
-        }
-        if (existing != null) {
-            return;
-        }
-        owner.getLifecycle().addObserver(wrapper);
-    }
-
-    /**
-     * Adds the given observer to the observers list. This call is similar to
-     * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which
-     * is always active. This means that the given observer will receive all events and will never
-     * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop
-     * observing this LiveData.
-     * While LiveData has one of such observers, it will be considered
-     * as active.
-     * <p>
-     * If the observer was already added with an owner to this LiveData, LiveData throws an
-     * {@link IllegalArgumentException}.
-     *
-     * @param observer The observer that will receive the events
-     */
-    @MainThread
-    public void observeForever(@NonNull Observer<? super T> observer) {
-        assertMainThread("observeForever");
-        AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
-        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
-        if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) {
-            throw new IllegalArgumentException("Cannot add the same observer"
-                    + " with different lifecycles");
-        }
-        if (existing != null) {
-            return;
-        }
-        wrapper.activeStateChanged(true);
-    }
-
-    /**
-     * Removes the given observer from the observers list.
-     *
-     * @param observer The Observer to receive events.
-     */
-    @MainThread
-    public void removeObserver(@NonNull final Observer<? super T> observer) {
-        assertMainThread("removeObserver");
-        ObserverWrapper removed = mObservers.remove(observer);
-        if (removed == null) {
-            return;
-        }
-        removed.detachObserver();
-        removed.activeStateChanged(false);
-    }
-
-    /**
-     * Removes all observers that are tied to the given {@link LifecycleOwner}.
-     *
-     * @param owner The {@code LifecycleOwner} scope for the observers to be removed.
-     */
-    @SuppressWarnings("WeakerAccess")
-    @MainThread
-    public void removeObservers(@NonNull final LifecycleOwner owner) {
-        assertMainThread("removeObservers");
-        for (Map.Entry<Observer<? super T>, ObserverWrapper> entry : mObservers) {
-            if (entry.getValue().isAttachedTo(owner)) {
-                removeObserver(entry.getKey());
-            }
-        }
-    }
-
-    /**
-     * Posts a task to a main thread to set the given value. So if you have a following code
-     * executed in the main thread:
-     * <pre class="prettyprint">
-     * liveData.postValue("a");
-     * liveData.setValue("b");
-     * </pre>
-     * The value "b" would be set at first and later the main thread would override it with
-     * the value "a".
-     * <p>
-     * If you called this method multiple times before a main thread executed a posted task, only
-     * the last value would be dispatched.
-     *
-     * @param value The new value
-     */
-    protected void postValue(T value) {
-        boolean postTask;
-        synchronized (mDataLock) {
-            postTask = mPendingData == NOT_SET;
-            mPendingData = value;
-        }
-        if (!postTask) {
-            return;
-        }
-        ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
-    }
-
-    /**
-     * Sets the value. If there are active observers, the value will be dispatched to them.
-     * <p>
-     * This method must be called from the main thread. If you need set a value from a background
-     * thread, you can use {@link #postValue(Object)}
-     *
-     * @param value The new value
-     */
-    @MainThread
-    protected void setValue(T value) {
-        assertMainThread("setValue");
-        mVersion++;
-        mData = value;
-        dispatchingValue(null);
-    }
-
-    /**
-     * Returns the current value.
-     * Note that calling this method on a background thread does not guarantee that the latest
-     * value set will be received.
-     *
-     * @return the current value
-     */
-    @Nullable
-    public T getValue() {
-        Object data = mData;
-        if (data != NOT_SET) {
-            //noinspection unchecked
-            return (T) data;
-        }
-        return null;
-    }
-
-    int getVersion() {
-        return mVersion;
-    }
-
-    /**
-     * Called when the number of active observers change to 1 from 0.
-     * <p>
-     * This callback can be used to know that this LiveData is being used thus should be kept
-     * up to date.
-     */
-    protected void onActive() {
-
-    }
-
-    /**
-     * Called when the number of active observers change from 1 to 0.
-     * <p>
-     * This does not mean that there are no observers left, there may still be observers but their
-     * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}
-     * (like an Activity in the back stack).
-     * <p>
-     * You can check if there are observers via {@link #hasObservers()}.
-     */
-    protected void onInactive() {
-
-    }
-
-    /**
-     * Returns true if this LiveData has observers.
-     *
-     * @return true if this LiveData has observers
-     */
-    @SuppressWarnings("WeakerAccess")
-    public boolean hasObservers() {
-        return mObservers.size() > 0;
-    }
-
-    /**
-     * Returns true if this LiveData has active observers.
-     *
-     * @return true if this LiveData has active observers
-     */
-    @SuppressWarnings("WeakerAccess")
-    public boolean hasActiveObservers() {
-        return mActiveCount > 0;
-    }
-
-    class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
-        @NonNull final LifecycleOwner mOwner;
-
-        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
-            super(observer);
-            mOwner = owner;
-        }
-
-        @Override
-        boolean shouldBeActive() {
-            return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
-        }
-
-        @Override
-        public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
-            if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
-                removeObserver(mObserver);
-                return;
-            }
-            activeStateChanged(shouldBeActive());
-        }
-
-        @Override
-        boolean isAttachedTo(LifecycleOwner owner) {
-            return mOwner == owner;
-        }
-
-        @Override
-        void detachObserver() {
-            mOwner.getLifecycle().removeObserver(this);
-        }
-    }
-
-    private abstract class ObserverWrapper {
-        final Observer<? super T> mObserver;
-        boolean mActive;
-        int mLastVersion = START_VERSION;
-
-        ObserverWrapper(Observer<? super T> observer) {
-            mObserver = observer;
-        }
-
-        abstract boolean shouldBeActive();
-
-        boolean isAttachedTo(LifecycleOwner owner) {
-            return false;
-        }
-
-        void detachObserver() {
-        }
-
-        void activeStateChanged(boolean newActive) {
-            if (newActive == mActive) {
-                return;
-            }
-            // immediately set active state, so we'd never dispatch anything to inactive
-            // owner
-            mActive = newActive;
-            boolean wasInactive = LiveData.this.mActiveCount == 0;
-            LiveData.this.mActiveCount += mActive ? 1 : -1;
-            if (wasInactive && mActive) {
-                onActive();
-            }
-            if (LiveData.this.mActiveCount == 0 && !mActive) {
-                onInactive();
-            }
-            if (mActive) {
-                dispatchingValue(this);
-            }
-        }
-    }
-
-    private class AlwaysActiveObserver extends ObserverWrapper {
-
-        AlwaysActiveObserver(Observer<? super T> observer) {
-            super(observer);
-        }
-
-        @Override
-        boolean shouldBeActive() {
-            return true;
-        }
-    }
-
-    private static void assertMainThread(String methodName) {
-        if (!ArchTaskExecutor.getInstance().isMainThread()) {
-            throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
-                    + " thread");
-        }
-    }
+public class LiveData<T> {
 }
diff --git a/androidx/media/DataSourceDesc.java b/androidx/media/DataSourceDesc.java
new file mode 100644
index 0000000..f76f651
--- /dev/null
+++ b/androidx/media/DataSourceDesc.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Preconditions;
+
+import java.io.FileDescriptor;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Structure for data source descriptor. Used by {@link MediaItem2}.
+ * <p>
+ * Users should use {@link Builder} to change {@link DataSourceDesc}.
+ *
+ * @see MediaItem2
+ */
+public final class DataSourceDesc {
+    /* No data source has been set yet */
+    public static final int TYPE_NONE     = 0;
+    /* data source is type of MediaDataSource */
+    public static final int TYPE_CALLBACK = 1;
+    /* data source is type of FileDescriptor */
+    public static final int TYPE_FD       = 2;
+    /* data source is type of Uri */
+    public static final int TYPE_URI      = 3;
+
+    // intentionally less than long.MAX_VALUE.
+    // Declare this first to avoid 'illegal forward reference'.
+    private static final long LONG_MAX = 0x7ffffffffffffffL;
+
+    /**
+     * Used when a position is unknown.
+     *
+     * @see #getEndPosition()
+     */
+    public static final long POSITION_UNKNOWN = LONG_MAX;
+
+    /**
+     * Used when the length of file descriptor is unknown.
+     *
+     * @see #getFileDescriptorLength()
+     */
+    public static final long FD_LENGTH_UNKNOWN = LONG_MAX;
+
+    private int mType = TYPE_NONE;
+
+    private Media2DataSource mMedia2DataSource;
+
+    private FileDescriptor mFD;
+    private long mFDOffset = 0;
+    private long mFDLength = FD_LENGTH_UNKNOWN;
+
+    private Uri mUri;
+    private Map<String, String> mUriHeader;
+    private List<HttpCookie> mUriCookies;
+    private Context mUriContext;
+
+    private String mMediaId;
+    private long mStartPositionMs = 0;
+    private long mEndPositionMs = POSITION_UNKNOWN;
+
+    private DataSourceDesc() {
+    }
+
+    /**
+     * Return the media Id of data source.
+     * @return the media Id of data source
+     */
+    public @Nullable String getMediaId() {
+        return mMediaId;
+    }
+
+    /**
+     * Return the position in milliseconds at which the playback will start.
+     * @return the position in milliseconds at which the playback will start
+     */
+    public long getStartPosition() {
+        return mStartPositionMs;
+    }
+
+    /**
+     * Return the position in milliseconds at which the playback will end.
+     * {@link #POSITION_UNKNOWN} means ending at the end of source content.
+     * @return the position in milliseconds at which the playback will end
+     */
+    public long getEndPosition() {
+        return mEndPositionMs;
+    }
+
+    /**
+     * Return the type of data source.
+     * @return the type of data source
+     */
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Return the Media2DataSource of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}.
+     * @return the Media2DataSource of this data source
+     */
+    public @Nullable Media2DataSource getMedia2DataSource() {
+        return mMedia2DataSource;
+    }
+
+    /**
+     * Return the FileDescriptor of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+     * @return the FileDescriptor of this data source
+     */
+    public @Nullable FileDescriptor getFileDescriptor() {
+        return mFD;
+    }
+
+    /**
+     * Return the offset associated with the FileDescriptor of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_FD} and it has
+     * been set by the {@link Builder}.
+     * @return the offset associated with the FileDescriptor of this data source
+     */
+    public long getFileDescriptorOffset() {
+        return mFDOffset;
+    }
+
+    /**
+     * Return the content length associated with the FileDescriptor of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+     * {@link #FD_LENGTH_UNKNOWN} means same as the length of source content.
+     * @return the content length associated with the FileDescriptor of this data source
+     */
+    public long getFileDescriptorLength() {
+        return mFDLength;
+    }
+
+    /**
+     * Return the Uri of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+     * @return the Uri of this data source
+     */
+    public @Nullable Uri getUri() {
+        return mUri;
+    }
+
+    /**
+     * Return the Uri headers of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+     * @return the Uri headers of this data source
+     */
+    public @Nullable Map<String, String> getUriHeaders() {
+        if (mUriHeader == null) {
+            return null;
+        }
+        return new HashMap<String, String>(mUriHeader);
+    }
+
+    /**
+     * Return the Uri cookies of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+     * @return the Uri cookies of this data source
+     */
+    public @Nullable List<HttpCookie> getUriCookies() {
+        if (mUriCookies == null) {
+            return null;
+        }
+        return new ArrayList<HttpCookie>(mUriCookies);
+    }
+
+    /**
+     * Return the Context used for resolving the Uri of this data source.
+     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+     * @return the Context used for resolving the Uri of this data source
+     */
+    public @Nullable Context getUriContext() {
+        return mUriContext;
+    }
+
+    /**
+     * Builder class for {@link DataSourceDesc} objects.
+     */
+    public static class Builder {
+        private int mType = TYPE_NONE;
+
+        private Media2DataSource mMedia2DataSource;
+
+        private FileDescriptor mFD;
+        private long mFDOffset = 0;
+        private long mFDLength = FD_LENGTH_UNKNOWN;
+
+        private Uri mUri;
+        private Map<String, String> mUriHeader;
+        private List<HttpCookie> mUriCookies;
+        private Context mUriContext;
+
+        private String mMediaId;
+        private long mStartPositionMs = 0;
+        private long mEndPositionMs = POSITION_UNKNOWN;
+
+        /**
+         * Constructs a new Builder with the defaults.
+         */
+        public Builder() {
+        }
+
+        /**
+         * Constructs a new Builder from a given {@link DataSourceDesc} instance
+         * @param dsd the {@link DataSourceDesc} object whose data will be reused
+         * in the new Builder.
+         */
+        public Builder(@NonNull DataSourceDesc dsd) {
+            mType = dsd.mType;
+            mMedia2DataSource = dsd.mMedia2DataSource;
+            mFD = dsd.mFD;
+            mFDOffset = dsd.mFDOffset;
+            mFDLength = dsd.mFDLength;
+            mUri = dsd.mUri;
+            mUriHeader = dsd.mUriHeader;
+            mUriCookies = dsd.mUriCookies;
+            mUriContext = dsd.mUriContext;
+
+            mMediaId = dsd.mMediaId;
+            mStartPositionMs = dsd.mStartPositionMs;
+            mEndPositionMs = dsd.mEndPositionMs;
+        }
+
+        /**
+         * Combines all of the fields that have been set and return a new
+         * {@link DataSourceDesc} object. <code>IllegalStateException</code> will be
+         * thrown if there is conflict between fields.
+         *
+         * @return a new {@link DataSourceDesc} object
+         */
+        public @NonNull DataSourceDesc build() {
+            if (mType != TYPE_CALLBACK
+                    && mType != TYPE_FD
+                    && mType != TYPE_URI) {
+                throw new IllegalStateException("Illegal type: " + mType);
+            }
+            if (mStartPositionMs > mEndPositionMs) {
+                throw new IllegalStateException("Illegal start/end position: "
+                        + mStartPositionMs + " : " + mEndPositionMs);
+            }
+
+            DataSourceDesc dsd = new DataSourceDesc();
+            dsd.mType = mType;
+            dsd.mMedia2DataSource = mMedia2DataSource;
+            dsd.mFD = mFD;
+            dsd.mFDOffset = mFDOffset;
+            dsd.mFDLength = mFDLength;
+            dsd.mUri = mUri;
+            dsd.mUriHeader = mUriHeader;
+            dsd.mUriCookies = mUriCookies;
+            dsd.mUriContext = mUriContext;
+
+            dsd.mMediaId = mMediaId;
+            dsd.mStartPositionMs = mStartPositionMs;
+            dsd.mEndPositionMs = mEndPositionMs;
+
+            return dsd;
+        }
+
+        /**
+         * Sets the media Id of this data source.
+         *
+         * @param mediaId the media Id of this data source
+         * @return the same Builder instance.
+         */
+        public @NonNull Builder setMediaId(String mediaId) {
+            mMediaId = mediaId;
+            return this;
+        }
+
+        /**
+         * Sets the start position in milliseconds at which the playback will start.
+         * Any negative number is treated as 0.
+         *
+         * @param position the start position in milliseconds at which the playback will start
+         * @return the same Builder instance.
+         *
+         */
+        public @NonNull Builder setStartPosition(long position) {
+            if (position < 0) {
+                position = 0;
+            }
+            mStartPositionMs = position;
+            return this;
+        }
+
+        /**
+         * Sets the end position in milliseconds at which the playback will end.
+         * Any negative number is treated as maximum length of the data source.
+         *
+         * @param position the end position in milliseconds at which the playback will end
+         * @return the same Builder instance.
+         */
+        public @NonNull Builder setEndPosition(long position) {
+            if (position < 0) {
+                position = POSITION_UNKNOWN;
+            }
+            mEndPositionMs = position;
+            return this;
+        }
+
+        /**
+         * Sets the data source (Media2DataSource) to use.
+         *
+         * @param m2ds the Media2DataSource for the media you want to play
+         * @return the same Builder instance.
+         * @throws NullPointerException if m2ds is null.
+         */
+        public @NonNull Builder setDataSource(@NonNull Media2DataSource m2ds) {
+            Preconditions.checkNotNull(m2ds);
+            resetDataSource();
+            mType = TYPE_CALLBACK;
+            mMedia2DataSource = m2ds;
+            return this;
+        }
+
+        /**
+         * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+         * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+         * to close the file descriptor after the source has been used.
+         *
+         * @param fd the FileDescriptor for the file you want to play
+         * @return the same Builder instance.
+         * @throws NullPointerException if fd is null.
+         */
+        public @NonNull Builder setDataSource(@NonNull FileDescriptor fd) {
+            Preconditions.checkNotNull(fd);
+            resetDataSource();
+            mType = TYPE_FD;
+            mFD = fd;
+            return this;
+        }
+
+        /**
+         * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+         * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+         * to close the file descriptor after the source has been used.
+         *
+         * Any negative number for offset is treated as 0.
+         * Any negative number for length is treated as maximum length of the data source.
+         *
+         * @param fd the FileDescriptor for the file you want to play
+         * @param offset the offset into the file where the data to be played starts, in bytes
+         * @param length the length in bytes of the data to be played
+         * @return the same Builder instance.
+         * @throws NullPointerException if fd is null.
+         */
+        public @NonNull Builder setDataSource(@NonNull FileDescriptor fd, long offset,
+                long length) {
+            Preconditions.checkNotNull(fd);
+            if (offset < 0) {
+                offset = 0;
+            }
+            if (length < 0) {
+                length = FD_LENGTH_UNKNOWN;
+            }
+            resetDataSource();
+            mType = TYPE_FD;
+            mFD = fd;
+            mFDOffset = offset;
+            mFDLength = length;
+            return this;
+        }
+
+        /**
+         * Sets the data source as a content Uri.
+         *
+         * @param context the Context to use when resolving the Uri
+         * @param uri the Content URI of the data you want to play
+         * @return the same Builder instance.
+         * @throws NullPointerException if context or uri is null.
+         */
+        public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
+            Preconditions.checkNotNull(context, "context cannot be null");
+            Preconditions.checkNotNull(uri, "uri cannot be null");
+            resetDataSource();
+            mType = TYPE_URI;
+            mUri = uri;
+            mUriContext = context;
+            return this;
+        }
+
+        /**
+         * Sets the data source as a content Uri.
+         *
+         * To provide cookies for the subsequent HTTP requests, you can install your own default
+         * cookie handler and use other variants of setDataSource APIs instead.
+         *
+         *  <p><strong>Note</strong> that the cross domain redirection is allowed by default,
+         * but that can be changed with key/value pairs through the headers parameter with
+         * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
+         * disallow or allow cross domain redirection.
+         *
+         * @param context the Context to use when resolving the Uri
+         * @param uri the Content URI of the data you want to play
+         * @param headers the headers to be sent together with the request for the data
+         *                The headers must not include cookies. Instead, use the cookies param.
+         * @param cookies the cookies to be sent together with the request
+         * @return the same Builder instance.
+         * @throws NullPointerException if context or uri is null.
+         * @throws IllegalArgumentException if the cookie handler is not of CookieManager type
+         *                                  when cookies are provided.
+         */
+        public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
+                @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
+            Preconditions.checkNotNull(context, "context cannot be null");
+            Preconditions.checkNotNull(uri);
+            if (cookies != null) {
+                CookieHandler cookieHandler = CookieHandler.getDefault();
+                if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
+                    throw new IllegalArgumentException(
+                            "The cookie handler has to be of CookieManager type "
+                                    + "when cookies are provided.");
+                }
+            }
+
+            resetDataSource();
+            mType = TYPE_URI;
+            mUri = uri;
+            if (headers != null) {
+                mUriHeader = new HashMap<String, String>(headers);
+            }
+            if (cookies != null) {
+                mUriCookies = new ArrayList<HttpCookie>(cookies);
+            }
+            mUriContext = context;
+            return this;
+        }
+
+        private void resetDataSource() {
+            mType = TYPE_NONE;
+            mMedia2DataSource = null;
+            mFD = null;
+            mFDOffset = 0;
+            mFDLength = FD_LENGTH_UNKNOWN;
+            mUri = null;
+            mUriHeader = null;
+            mUriCookies = null;
+            mUriContext = null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/androidx/media/Media2DataSource.java b/androidx/media/Media2DataSource.java
new file mode 100644
index 0000000..4990259
--- /dev/null
+++ b/androidx/media/Media2DataSource.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * @hide
+ * For supplying media data to the framework. Implement this if your app has
+ * special requirements for the way media data is obtained.
+ *
+ * <p class="note">Methods of this interface may be called on multiple different
+ * threads. There will be a thread synchronization point between each call to ensure that
+ * modifications to the state of your Media2DataSource are visible to future calls. This means
+ * you don't need to do your own synchronization unless you're modifying the
+ * Media2DataSource from another thread while it's being used by the framework.</p>
+ *
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class Media2DataSource implements Closeable {
+    /**
+     * Called to request data from the given position.
+     *
+     * Implementations should should write up to {@code size} bytes into
+     * {@code buffer}, and return the number of bytes written.
+     *
+     * Return {@code 0} if size is zero (thus no bytes are read).
+     *
+     * Return {@code -1} to indicate that end of stream is reached.
+     *
+     * @param position the position in the data source to read from.
+     * @param buffer the buffer to read the data into.
+     * @param offset the offset within buffer to read the data into.
+     * @param size the number of bytes to read.
+     * @throws IOException on fatal errors.
+     * @return the number of bytes read, or -1 if there was an error.
+     */
+    public abstract int readAt(long position, byte[] buffer, int offset, int size)
+            throws IOException;
+
+    /**
+     * Called to get the size of the data source.
+     *
+     * @throws IOException on fatal errors
+     * @return the size of data source in bytes, or -1 if the size is unknown.
+     */
+    public abstract long getSize() throws IOException;
+}
diff --git a/androidx/media/MediaBrowser2.java b/androidx/media/MediaBrowser2.java
new file mode 100644
index 0000000..6ef7fcf
--- /dev/null
+++ b/androidx/media/MediaBrowser2.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.ItemCallback;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaLibraryService2.MediaLibrarySession;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ * Browses media content offered by a {@link MediaLibraryService2}.
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class MediaBrowser2 extends MediaController2 {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String EXTRA_ITEM_COUNT = "android.media.browse.extra.ITEM_COUNT";
+
+    /**
+     * Key for Bundle version of {@link MediaSession2.ControllerInfo}.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String EXTRA_TARGET = "android.media.browse.extra.TARGET";
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final HashMap<Bundle, MediaBrowserCompat> mBrowserCompats = new HashMap<>();
+    @GuardedBy("mLock")
+    private final HashMap<String, List<SubscribeCallback>> mSubscribeCallbacks = new HashMap<>();
+
+    /**
+     * Callback to listen events from {@link MediaLibraryService2}.
+     */
+    public static class BrowserCallback extends MediaController2.ControllerCallback {
+        /**
+         * Called with the result of {@link #getLibraryRoot(Bundle)}.
+         * <p>
+         * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the library root isn't
+         * available.
+         *
+         * @param browser the browser for this event
+         * @param rootHints rootHints that you previously requested.
+         * @param rootMediaId media id of the library root. Can be {@code null}
+         * @param rootExtra extra of the library root. Can be {@code null}
+         */
+        public void onGetLibraryRootDone(@NonNull MediaBrowser2 browser, @Nullable Bundle rootHints,
+                @Nullable String rootMediaId, @Nullable Bundle rootExtra) { }
+
+        /**
+         * Called when there's change in the parent's children.
+         * <p>
+         * This API is called when the library service called
+         * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)} or
+         * {@link MediaLibrarySession#notifyChildrenChanged(String, int, Bundle)} for the parent.
+         *
+         * @param browser the browser for this event
+         * @param parentId parent id that you've specified with {@link #subscribe(String, Bundle)}
+         * @param itemCount number of children
+         * @param extras extra bundle from the library service. Can be differ from extras that
+         *               you've specified with {@link #subscribe(String, Bundle)}.
+         */
+        public void onChildrenChanged(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+                int itemCount, @Nullable Bundle extras) { }
+
+        /**
+         * Called when the list of items has been returned by the library service for the previous
+         * {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
+         *
+         * @param browser the browser for this event
+         * @param parentId parent id
+         * @param page page number that you've specified with
+         *             {@link #getChildren(String, int, int, Bundle)}
+         * @param pageSize page size that you've specified with
+         *                 {@link #getChildren(String, int, int, Bundle)}
+         * @param result result. Can be {@code null}
+         * @param extras extra bundle from the library service
+         */
+        public void onGetChildrenDone(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+                int page, int pageSize, @Nullable List<MediaItem2> result,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when the item has been returned by the library service for the previous
+         * {@link MediaBrowser2#getItem(String)} call.
+         * <p>
+         * Result can be null if there had been error.
+         *
+         * @param browser the browser for this event
+         * @param mediaId media id
+         * @param result result. Can be {@code null}
+         */
+        public void onGetItemDone(@NonNull MediaBrowser2 browser, @NonNull String mediaId,
+                @Nullable MediaItem2 result) { }
+
+        /**
+         * Called when there's change in the search result requested by the previous
+         * {@link MediaBrowser2#search(String, Bundle)}.
+         *
+         * @param browser the browser for this event
+         * @param query search query that you've specified with {@link #search(String, Bundle)}
+         * @param itemCount The item count for the search result
+         * @param extras extra bundle from the library service
+         */
+        public void onSearchResultChanged(@NonNull MediaBrowser2 browser, @NonNull String query,
+                int itemCount, @Nullable Bundle extras) { }
+
+        /**
+         * Called when the search result has been returned by the library service for the previous
+         * {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
+         * <p>
+         * Result can be null if there had been error.
+         *
+         * @param browser the browser for this event
+         * @param query search query that you've specified with
+         *              {@link #getSearchResult(String, int, int, Bundle)}
+         * @param page page number that you've specified with
+         *             {@link #getSearchResult(String, int, int, Bundle)}
+         * @param pageSize page size that you've specified with
+         *                 {@link #getSearchResult(String, int, int, Bundle)}
+         * @param result result. Can be {@code null}.
+         * @param extras extra bundle from the library service
+         */
+        public void onGetSearchResultDone(@NonNull MediaBrowser2 browser, @NonNull String query,
+                int page, int pageSize, @Nullable List<MediaItem2> result,
+                @Nullable Bundle extras) { }
+    }
+
+    public MediaBrowser2(@NonNull Context context, @NonNull SessionToken2 token,
+            @NonNull /*@CallbackExecutor*/ Executor executor, @NonNull BrowserCallback callback) {
+        super(context, token, executor, callback);
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            for (MediaBrowserCompat browser : mBrowserCompats.values()) {
+                browser.disconnect();
+            }
+            mBrowserCompats.clear();
+            // TODO: Ensure that ControllerCallback#onDisconnected() is called by super.close().
+            super.close();
+        }
+    }
+
+    /**
+     * Get the library root. Result would be sent back asynchronously with the
+     * {@link BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)}.
+     *
+     * @param extras extras for getting root
+     * @see BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)
+     */
+    public void getLibraryRoot(@Nullable final Bundle extras) {
+        final MediaBrowserCompat browser = getBrowserCompat(extras);
+        if (browser != null) {
+            // Already connected with the given extras.
+            getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    getCallback().onGetLibraryRootDone(MediaBrowser2.this, extras,
+                            browser.getRoot(), browser.getExtras());
+                }
+            });
+        } else {
+            MediaBrowserCompat newBrowser = new MediaBrowserCompat(getContext(),
+                    getSessionToken().getComponentName(), new GetLibraryRootCallback(extras),
+                    extras);
+            newBrowser.connect();
+            synchronized (mLock) {
+                mBrowserCompats.put(extras, newBrowser);
+            }
+        }
+    }
+
+    /**
+     * Subscribe to a parent id for the change in its children. When there's a change,
+     * {@link BrowserCallback#onChildrenChanged(MediaBrowser2, String, int, Bundle)} will be called
+     * with the bundle that you've specified. You should call
+     * {@link #getChildren(String, int, int, Bundle)} to get the actual contents for the parent.
+     *
+     * @param parentId parent id
+     * @param extras extra bundle
+     */
+    public void subscribe(@NonNull String parentId, @Nullable Bundle extras) {
+        if (parentId == null) {
+            throw new IllegalArgumentException("parentId shouldn't be null");
+        }
+        // TODO: Document this behavior
+        Bundle option;
+        if (extras != null && (extras.containsKey(MediaBrowserCompat.EXTRA_PAGE)
+                || extras.containsKey(MediaBrowserCompat.EXTRA_PAGE_SIZE))) {
+            option = new Bundle(extras);
+            option.remove(MediaBrowserCompat.EXTRA_PAGE);
+            option.remove(MediaBrowserCompat.EXTRA_PAGE_SIZE);
+        } else {
+            option = extras;
+        }
+        SubscribeCallback callback = new SubscribeCallback();
+        synchronized (mLock) {
+            List<SubscribeCallback> list = mSubscribeCallbacks.get(parentId);
+            if (list == null) {
+                list = new ArrayList<>();
+                mSubscribeCallbacks.put(parentId, list);
+            }
+            list.add(callback);
+        }
+        // TODO: Revisit using default browser is OK. Here's my concern.
+        //       Assume that MediaBrowser2 is connected with the MediaBrowserServiceCompat.
+        //       Since MediaBrowserServiceCompat can call MediaBrowserServiceCompat#
+        //       getBrowserRootHints(), the service may refuse calls from MediaBrowser2
+        getBrowserCompat().subscribe(parentId, option, callback);
+    }
+
+    /**
+     * Unsubscribe for changes to the children of the parent, which was previously subscribed with
+     * {@link #subscribe(String, Bundle)}.
+     * <p>
+     * This unsubscribes all previous subscription with the parent id, regardless of the extra
+     * that was previously sent to the library service.
+     *
+     * @param parentId parent id
+     */
+    public void unsubscribe(@NonNull String parentId) {
+        if (parentId == null) {
+            throw new IllegalArgumentException("parentId shouldn't be null");
+        }
+        // Note: don't use MediaBrowserCompat#unsubscribe(String) here, to keep the subscription
+        // callback for getChildren.
+        synchronized (mLock) {
+            List<SubscribeCallback> list = mSubscribeCallbacks.get(parentId);
+            if (list == null) {
+                return;
+            }
+            MediaBrowserCompat browser = getBrowserCompat();
+            for (int i = 0; i < list.size(); i++) {
+                browser.unsubscribe(parentId, list.get(i));
+            }
+        }
+    }
+
+    /**
+     * Get list of children under the parent. Result would be sent back asynchronously with the
+     * {@link BrowserCallback#onGetChildrenDone(MediaBrowser2, String, int, int, List, Bundle)}.
+     *
+     * @param parentId parent id for getting the children.
+     * @param page page number to get the result. Starts from {@code 1}
+     * @param pageSize page size. Should be greater or equal to {@code 1}
+     * @param extras extra bundle
+     */
+    public void getChildren(@NonNull String parentId, int page, int pageSize,
+            @Nullable Bundle extras) {
+        if (parentId == null) {
+            throw new IllegalArgumentException("parentId shouldn't be null");
+        }
+        if (page < 1 || pageSize < 1) {
+            throw new IllegalArgumentException("Neither page nor pageSize should be less than 1");
+        }
+        Bundle options = new Bundle(extras);
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+        // TODO: Revisit using default browser is OK. See TODO in subscribe
+        getBrowserCompat().subscribe(parentId, options,
+                new GetChildrenCallback(parentId, page, pageSize));
+    }
+
+    /**
+     * Get the media item with the given media id. Result would be sent back asynchronously with the
+     * {@link BrowserCallback#onGetItemDone(MediaBrowser2, String, MediaItem2)}.
+     *
+     * @param mediaId media id for specifying the item
+     */
+    public void getItem(@NonNull final String mediaId) {
+        // TODO: Revisit using default browser is OK. See TODO in subscribe
+        getBrowserCompat().getItem(mediaId, new ItemCallback() {
+            @Override
+            public void onItemLoaded(final MediaItem item) {
+                getCallbackExecutor().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCallback().onGetItemDone(MediaBrowser2.this, mediaId,
+                                MediaUtils2.createMediaItem2(item));
+                    }
+                });
+            }
+
+            @Override
+            public void onError(String itemId) {
+                getCallbackExecutor().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCallback().onGetItemDone(MediaBrowser2.this, mediaId, null);
+                    }
+                });
+            }
+        });
+    }
+
+    /**
+     * Send a search request to the library service. When the search result is changed,
+     * {@link BrowserCallback#onSearchResultChanged(MediaBrowser2, String, int, Bundle)} will be
+     * called. You should call {@link #getSearchResult(String, int, int, Bundle)} to get the actual
+     * search result.
+     *
+     * @param query search query. Should not be an empty string.
+     * @param extras extra bundle
+     */
+    public void search(@NonNull String query, @Nullable Bundle extras) {
+        // TODO: Implement
+    }
+
+    /**
+     * Get the search result from lhe library service. Result would be sent back asynchronously with
+     * the
+     * {@link BrowserCallback#onGetSearchResultDone(MediaBrowser2, String, int, int, List, Bundle)}.
+     *
+     * @param query search query that you've specified with {@link #search(String, Bundle)}
+     * @param page page number to get search result. Starts from {@code 1}
+     * @param pageSize page size. Should be greater or equal to {@code 1}
+     * @param extras extra bundle
+     */
+    public void getSearchResult(@NonNull String query, int page, int pageSize,
+            @Nullable Bundle extras) {
+        // TODO: Implement
+    }
+
+    @Override
+    BrowserCallback getCallback() {
+        return (BrowserCallback) super.getCallback();
+    }
+
+    private MediaBrowserCompat getBrowserCompat(Bundle extras) {
+        synchronized (mLock) {
+            return mBrowserCompats.get(extras);
+        }
+    }
+
+    private class GetLibraryRootCallback extends MediaBrowserCompat.ConnectionCallback {
+        private final Bundle mExtras;
+
+        GetLibraryRootCallback(Bundle extras) {
+            super();
+            mExtras = extras;
+        }
+
+        @Override
+        public void onConnected() {
+            getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    MediaBrowserCompat browser;
+                    synchronized (mLock) {
+                        browser = mBrowserCompats.get(mExtras);
+                    }
+                    if (browser == null) {
+                        // Shouldn't be happen.
+                        return;
+                    }
+                    getCallback().onGetLibraryRootDone(MediaBrowser2.this,
+                            mExtras, browser.getRoot(), browser.getExtras());
+                }
+            });
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            close();
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            close();
+        }
+    }
+
+    private class SubscribeCallback extends SubscriptionCallback {
+        @Override
+        public void onError(String parentId) {
+            onChildrenLoaded(parentId, null, null);
+        }
+
+        @Override
+        public void onError(String parentId, Bundle options) {
+            onChildrenLoaded(parentId, null, options);
+        }
+
+        @Override
+        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
+            onChildrenLoaded(parentId, children, null);
+        }
+
+        @Override
+        public void onChildrenLoaded(final String parentId, List<MediaItem> children,
+                final Bundle options) {
+            final int itemCount;
+            if (options != null && options.containsKey(EXTRA_ITEM_COUNT)) {
+                itemCount = options.getInt(EXTRA_ITEM_COUNT);
+            } else if (children != null) {
+                itemCount = children.size();
+            } else {
+                // Currently no way to tell failures in MediaBrowser2#subscribe().
+                return;
+            }
+            getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    getCallback().onChildrenChanged(MediaBrowser2.this, parentId, itemCount,
+                            options);
+                }
+            });
+        }
+    }
+
+    private class GetChildrenCallback extends SubscriptionCallback {
+        private final String mParentId;
+        private final int mPage;
+        private final int mPageSize;
+
+        GetChildrenCallback(String parentId, int page, int pageSize) {
+            super();
+            mParentId = parentId;
+            mPage = page;
+            mPageSize = pageSize;
+        }
+
+        @Override
+        public void onError(String parentId) {
+            onChildrenLoaded(parentId, null, null);
+        }
+
+        @Override
+        public void onError(String parentId, Bundle options) {
+            onChildrenLoaded(parentId, null, options);
+        }
+
+        @Override
+        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
+            onChildrenLoaded(parentId, children, null);
+        }
+
+        @Override
+        public void onChildrenLoaded(final String parentId, List<MediaItem> children,
+                final Bundle options) {
+            final List<MediaItem2> items;
+            if (children == null) {
+                items = null;
+            } else {
+                items = new ArrayList<>();
+                for (int i = 0; i < children.size(); i++) {
+                    items.add(MediaUtils2.createMediaItem2(children.get(i)));
+                }
+            }
+            getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    getCallback().onGetChildrenDone(MediaBrowser2.this, parentId, mPage, mPageSize,
+                            items, options);
+                    getBrowserCompat().unsubscribe(mParentId, GetChildrenCallback.this);
+                }
+            });
+        }
+    }
+}
diff --git a/androidx/media/MediaBrowser2Test.java b/androidx/media/MediaBrowser2Test.java
new file mode 100644
index 0000000..4b75986
--- /dev/null
+++ b/androidx/media/MediaBrowser2Test.java
@@ -0,0 +1,703 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.media.MockMediaLibraryService2.EXTRAS;
+import static androidx.media.MockMediaLibraryService2.ROOT_ID;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertNotEquals;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaBrowser2.BrowserCallback;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaLibraryService2.MediaLibrarySession;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import junit.framework.Assert;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link MediaBrowser2}.
+ * <p>
+ * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
+ * {@link MediaController2} works cleanly.
+ */
+// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+public class MediaBrowser2Test extends MediaController2Test {
+    private static final String TAG = "MediaBrowser2Test";
+
+    @Override
+    TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
+            @Nullable ControllerCallback callback) throws InterruptedException {
+        final BrowserCallback browserCallback =
+                callback != null ? (BrowserCallback) callback : new BrowserCallback() {};
+        final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                // Create controller on the test handler, for changing MediaBrowserCompat's Handler
+                // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
+                // and commands wouldn't be run if tests codes waits on the test handler.
+                controller.set(new TestMediaBrowser(
+                        mContext, token, new TestBrowserCallback(browserCallback)));
+            }
+        });
+        return controller.get();
+    }
+
+    /**
+     * Test if the {@link TestBrowserCallback} wraps the callback proxy without missing any method.
+     */
+    @Test
+    public void testTestBrowserCallback() {
+        prepareLooper();
+        Method[] methods = TestBrowserCallback.class.getMethods();
+        assertNotNull(methods);
+        for (int i = 0; i < methods.length; i++) {
+            // For any methods in the controller callback, TestControllerCallback should have
+            // overriden the method and call matching API in the callback proxy.
+            assertNotEquals("TestBrowserCallback should override " + methods[i]
+                            + " and call callback proxy",
+                    BrowserCallback.class, methods[i].getDeclaringClass());
+        }
+    }
+
+    @Test
+    public void testGetLibraryRoot() throws InterruptedException {
+        prepareLooper();
+        final Bundle param = new Bundle();
+        param.putString(TAG, TAG);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onGetLibraryRootDone(MediaBrowser2 browser,
+                    Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+                assertTrue(TestUtils.equals(param, rootHints));
+                assertEquals(ROOT_ID, rootMediaId);
+                assertTrue(TestUtils.equals(EXTRAS, rootExtra));
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser =
+                (MediaBrowser2) createController(token, true, callback);
+        browser.getLibraryRoot(param);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testGetItem() throws InterruptedException {
+        prepareLooper();
+        final String mediaId = MockMediaLibraryService2.MEDIA_ID_GET_ITEM;
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
+                assertEquals(mediaId, mediaIdOut);
+                assertNotNull(result);
+                assertEquals(mediaId, result.getMediaId());
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.getItem(mediaId);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testGetItemNullResult() throws InterruptedException {
+        prepareLooper();
+        final String mediaId = "random_media_id";
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
+                assertEquals(mediaId, mediaIdOut);
+                assertNull(result);
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.getItem(mediaId);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testGetChildren() throws InterruptedException {
+        prepareLooper();
+        final String parentId = MockMediaLibraryService2.PARENT_ID;
+        final int page = 4;
+        final int pageSize = 10;
+        final Bundle extras = new Bundle();
+        extras.putString(TAG, TAG);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut, int pageOut,
+                    int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+                assertEquals(parentId, parentIdOut);
+                assertEquals(page, pageOut);
+                assertEquals(pageSize, pageSizeOut);
+                assertTrue(TestUtils.equals(extras, extrasOut));
+                assertNotNull(result);
+
+                int fromIndex = (page - 1) * pageSize;
+                int toIndex = Math.min(page * pageSize, MockMediaLibraryService2.CHILDREN_COUNT);
+
+                // Compare the given results with originals.
+                for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
+                    int relativeIndex = originalIndex - fromIndex;
+                    Assert.assertEquals(
+                            MockMediaLibraryService2.GET_CHILDREN_RESULT.get(originalIndex)
+                                    .getMediaId(),
+                            result.get(relativeIndex).getMediaId());
+                }
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.getChildren(parentId, page, pageSize, extras);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testGetChildrenEmptyResult() throws InterruptedException {
+        prepareLooper();
+        final String parentId = MockMediaLibraryService2.PARENT_ID_NO_CHILDREN;
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
+                    int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+                assertNotNull(result);
+                assertEquals(0, result.size());
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.getChildren(parentId, 1, 1, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testGetChildrenNullResult() throws InterruptedException {
+        prepareLooper();
+        final String parentId = MockMediaLibraryService2.PARENT_ID_ERROR;
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
+                    int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+                assertNull(result);
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.getChildren(parentId, 1, 1, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Ignore
+    @Test
+    public void testSearch() throws InterruptedException {
+        prepareLooper();
+        final String query = MockMediaLibraryService2.SEARCH_QUERY;
+        final int page = 4;
+        final int pageSize = 10;
+        final Bundle extras = new Bundle();
+        extras.putString(TAG, TAG);
+
+        final CountDownLatch latchForSearch = new CountDownLatch(1);
+        final CountDownLatch latchForGetSearchResult = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onSearchResultChanged(MediaBrowser2 browser,
+                    String queryOut, int itemCount, Bundle extrasOut) {
+                assertEquals(query, queryOut);
+                assertTrue(TestUtils.equals(extras, extrasOut));
+                assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
+                latchForSearch.countDown();
+            }
+
+            @Override
+            public void onGetSearchResultDone(MediaBrowser2 browser, String queryOut,
+                    int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+                assertEquals(query, queryOut);
+                assertEquals(page, pageOut);
+                assertEquals(pageSize, pageSizeOut);
+                assertTrue(TestUtils.equals(extras, extrasOut));
+                assertNotNull(result);
+
+                int fromIndex = (page - 1) * pageSize;
+                int toIndex = Math.min(
+                        page * pageSize, MockMediaLibraryService2.SEARCH_RESULT_COUNT);
+
+                // Compare the given results with originals.
+                for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
+                    int relativeIndex = originalIndex - fromIndex;
+                    Assert.assertEquals(
+                            MockMediaLibraryService2.SEARCH_RESULT.get(originalIndex).getMediaId(),
+                            result.get(relativeIndex).getMediaId());
+                }
+                latchForGetSearchResult.countDown();
+            }
+        };
+
+        // Request the search.
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.search(query, extras);
+        assertTrue(latchForSearch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+
+        // Get the search result.
+        browser.getSearchResult(query, page, pageSize, extras);
+        assertTrue(latchForGetSearchResult.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSearchTakesTime() throws InterruptedException {
+        prepareLooper();
+        final String query = MockMediaLibraryService2.SEARCH_QUERY_TAKES_TIME;
+        final Bundle extras = new Bundle();
+        extras.putString(TAG, TAG);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onSearchResultChanged(
+                    MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
+                assertEquals(query, queryOut);
+                assertTrue(TestUtils.equals(extras, extrasOut));
+                assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.search(query, extras);
+        assertTrue(latch.await(
+                MockMediaLibraryService2.SEARCH_TIME_IN_MS + WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSearchEmptyResult() throws InterruptedException {
+        prepareLooper();
+        final String query = MockMediaLibraryService2.SEARCH_QUERY_EMPTY_RESULT;
+        final Bundle extras = new Bundle();
+        extras.putString(TAG, TAG);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BrowserCallback callback = new BrowserCallback() {
+            @Override
+            public void onSearchResultChanged(
+                    MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
+                assertEquals(query, queryOut);
+                assertTrue(TestUtils.equals(extras, extrasOut));
+                assertEquals(0, itemCount);
+                latch.countDown();
+            }
+        };
+
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+        browser.search(query, extras);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSubscribe() throws InterruptedException {
+        prepareLooper();
+        final String testParentId = "testSubscribeId";
+        final Bundle testExtras = new Bundle();
+        testExtras.putString(testParentId, testParentId);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
+            @Override
+            public void onSubscribe(@NonNull MediaLibraryService2.MediaLibrarySession session,
+                    @NonNull MediaSession2.ControllerInfo info, @NonNull String parentId,
+                    @Nullable Bundle extras) {
+                if (Process.myUid() == info.getUid()) {
+                    assertEquals(testParentId, parentId);
+                    assertTrue(TestUtils.equals(testExtras, extras));
+                    latch.countDown();
+                }
+            }
+        };
+        TestServiceRegistry.getInstance().setSessionCallback(callback);
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token);
+        browser.subscribe(testParentId, testExtras);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Ignore
+    @Test
+    public void testUnsubscribe() throws InterruptedException {
+        prepareLooper();
+        final String testParentId = "testUnsubscribeId";
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
+            @Override
+            public void onUnsubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo info, @NonNull String parentId) {
+                if (Process.myUid() == info.getUid()) {
+                    assertEquals(testParentId, parentId);
+                    latch.countDown();
+                }
+            }
+        };
+        TestServiceRegistry.getInstance().setSessionCallback(callback);
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser = (MediaBrowser2) createController(token);
+        browser.unsubscribe(testParentId);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testBrowserCallback_notifyChildrenChanged() throws InterruptedException {
+        prepareLooper();
+        // TODO(jaewan): Add test for the notifyChildrenChanged itself.
+        final String testParentId1 = "testBrowserCallback_notifyChildrenChanged_unexpectedParent";
+        final String testParentId2 = "testBrowserCallback_notifyChildrenChanged";
+        final int testChildrenCount = 101;
+        final Bundle testExtras = new Bundle();
+        testExtras.putString(testParentId1, testParentId1);
+
+        final CountDownLatch latch = new CountDownLatch(3);
+        final MediaLibrarySessionCallback sessionCallback =
+                new MediaLibrarySessionCallback() {
+                    @Override
+                    public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+                            @NonNull ControllerInfo controller) {
+                        if (Process.myUid() == controller.getUid()) {
+                            assertTrue(session instanceof MediaLibrarySession);
+                            if (mSession != null) {
+                                mSession.close();
+                            }
+                            mSession = session;
+                            // Shouldn't trigger onChildrenChanged() for the browser, because it
+                            // hasn't subscribed.
+                            ((MediaLibrarySession) session).notifyChildrenChanged(
+                                    testParentId1, testChildrenCount, null);
+                            ((MediaLibrarySession) session).notifyChildrenChanged(
+                                    controller, testParentId1, testChildrenCount, null);
+                        }
+                        return super.onConnect(session, controller);
+                    }
+
+                    @Override
+                    public void onSubscribe(@NonNull MediaLibrarySession session,
+                            @NonNull ControllerInfo info, @NonNull String parentId,
+                            @Nullable Bundle extras) {
+                        if (Process.myUid() == info.getUid()) {
+                            session.notifyChildrenChanged(testParentId2, testChildrenCount, null);
+                            session.notifyChildrenChanged(info, testParentId2, testChildrenCount,
+                                    testExtras);
+                        }
+                    }
+        };
+        final BrowserCallback controllerCallbackProxy =
+                new BrowserCallback() {
+                    @Override
+                    public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+                            int itemCount, Bundle extras) {
+                        switch ((int) latch.getCount()) {
+                            case 3:
+                                assertEquals(testParentId2, parentId);
+                                assertEquals(testChildrenCount, itemCount);
+                                assertNull(extras);
+                                latch.countDown();
+                                break;
+                            case 2:
+                                assertEquals(testParentId2, parentId);
+                                assertEquals(testChildrenCount, itemCount);
+                                assertTrue(TestUtils.equals(testExtras, extras));
+                                latch.countDown();
+                                break;
+                            default:
+                                // Unexpected call.
+                                fail();
+                        }
+                    }
+                };
+        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+        final MediaBrowser2 browser = (MediaBrowser2) createController(
+                token, true, controllerCallbackProxy);
+        assertTrue(mSession instanceof MediaLibrarySession);
+        browser.subscribe(testParentId2, null);
+        // This ensures that onChildrenChanged() is only called for the expected reasons.
+        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    public static class TestBrowserCallback extends BrowserCallback
+            implements TestControllerCallbackInterface {
+        private final ControllerCallback mCallbackProxy;
+        public final CountDownLatch connectLatch = new CountDownLatch(1);
+        public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+        @GuardedBy("this")
+        private Runnable mOnCustomCommandRunnable;
+
+        TestBrowserCallback(ControllerCallback callbackProxy) {
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+            }
+            mCallbackProxy = callbackProxy;
+        }
+
+        @CallSuper
+        @Override
+        public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+            connectLatch.countDown();
+        }
+
+        @CallSuper
+        @Override
+        public void onDisconnected(MediaController2 controller) {
+            disconnectLatch.countDown();
+        }
+
+        @Override
+        public void waitForConnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+
+        @Override
+        public void waitForDisconnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+
+        @Override
+        public void onPlaybackInfoChanged(MediaController2 controller,
+                MediaController2.PlaybackInfo info) {
+            mCallbackProxy.onPlaybackInfoChanged(controller, info);
+        }
+
+        @Override
+        public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+                Bundle args, ResultReceiver receiver) {
+            mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+            synchronized (this) {
+                if (mOnCustomCommandRunnable != null) {
+                    mOnCustomCommandRunnable.run();
+                }
+            }
+        }
+
+        @Override
+        public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+            mCallbackProxy.onCustomLayoutChanged(controller, layout);
+        }
+
+        @Override
+        public void onAllowedCommandsChanged(MediaController2 controller,
+                SessionCommandGroup2 commands) {
+            mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+        }
+
+        @Override
+        public void onPlayerStateChanged(MediaController2 controller, int state) {
+            mCallbackProxy.onPlayerStateChanged(controller, state);
+        }
+
+        @Override
+        public void onSeekCompleted(MediaController2 controller, long position) {
+            mCallbackProxy.onSeekCompleted(controller, position);
+        }
+
+        @Override
+        public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+            mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+        }
+
+        @Override
+        public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+                int state) {
+            mCallbackProxy.onBufferingStateChanged(controller, item, state);
+        }
+
+        @Override
+        public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+            mCallbackProxy.onError(controller, errorCode, extras);
+        }
+
+        @Override
+        public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+            mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+        }
+
+        @Override
+        public void onPlaylistChanged(MediaController2 controller,
+                List<MediaItem2> list, MediaMetadata2 metadata) {
+            mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+        }
+
+        @Override
+        public void onPlaylistMetadataChanged(MediaController2 controller,
+                MediaMetadata2 metadata) {
+            mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+        }
+
+        @Override
+        public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+            mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+        }
+
+        @Override
+        public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+            mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+        }
+
+        @Override
+        public void onGetLibraryRootDone(MediaBrowser2 browser, Bundle rootHints,
+                String rootMediaId, Bundle rootExtra) {
+            super.onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
+            if (mCallbackProxy instanceof BrowserCallback) {
+                ((BrowserCallback) mCallbackProxy)
+                        .onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
+            }
+        }
+
+        @Override
+        public void onGetItemDone(MediaBrowser2 browser, String mediaId, MediaItem2 result) {
+            super.onGetItemDone(browser, mediaId, result);
+            if (mCallbackProxy instanceof BrowserCallback) {
+                ((BrowserCallback) mCallbackProxy).onGetItemDone(browser, mediaId, result);
+            }
+        }
+
+        @Override
+        public void onGetChildrenDone(MediaBrowser2 browser, String parentId, int page,
+                int pageSize, List<MediaItem2> result, Bundle extras) {
+            super.onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
+            if (mCallbackProxy instanceof BrowserCallback) {
+                ((BrowserCallback) mCallbackProxy)
+                        .onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
+            }
+        }
+
+        @Override
+        public void onSearchResultChanged(MediaBrowser2 browser, String query, int itemCount,
+                Bundle extras) {
+            super.onSearchResultChanged(browser, query, itemCount, extras);
+            if (mCallbackProxy instanceof BrowserCallback) {
+                ((BrowserCallback) mCallbackProxy)
+                        .onSearchResultChanged(browser, query, itemCount, extras);
+            }
+        }
+
+        @Override
+        public void onGetSearchResultDone(MediaBrowser2 browser, String query, int page,
+                int pageSize, List<MediaItem2> result, Bundle extras) {
+            super.onGetSearchResultDone(browser, query, page, pageSize, result, extras);
+            if (mCallbackProxy instanceof BrowserCallback) {
+                ((BrowserCallback) mCallbackProxy)
+                        .onGetSearchResultDone(browser, query, page, pageSize, result, extras);
+            }
+        }
+
+        @Override
+        public void onChildrenChanged(MediaBrowser2 browser, String parentId, int itemCount,
+                Bundle extras) {
+            super.onChildrenChanged(browser, parentId, itemCount, extras);
+            if (mCallbackProxy instanceof BrowserCallback) {
+                ((BrowserCallback) mCallbackProxy)
+                        .onChildrenChanged(browser, parentId, itemCount, extras);
+            }
+        }
+
+        @Override
+        public void setRunnableForOnCustomCommand(Runnable runnable) {
+            synchronized (this) {
+                mOnCustomCommandRunnable = runnable;
+            }
+        }
+    }
+
+    public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
+        private final BrowserCallback mCallback;
+
+        public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
+                @NonNull ControllerCallback callback) {
+            super(context, token, sHandlerExecutor, (BrowserCallback) callback);
+            mCallback = (BrowserCallback) callback;
+        }
+
+        @Override
+        public BrowserCallback getCallback() {
+            return mCallback;
+        }
+    }
+}
diff --git a/androidx/media/MediaBrowserServiceCompat.java b/androidx/media/MediaBrowserServiceCompat.java
index 8f18cfc..430913e 100644
--- a/androidx/media/MediaBrowserServiceCompat.java
+++ b/androidx/media/MediaBrowserServiceCompat.java
@@ -49,6 +49,7 @@
 import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT;
 
 import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.os.Binder;
@@ -1012,6 +1013,18 @@
         }
     }
 
+    /**
+     * Attaches to the base context. This method is added to change the visibility of
+     * {@link Service#attachBaseContext(Context)}.
+     * <p>
+     * Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it
+     * because lint checks considers the overriden method as the new public API that needs update
+     * of current.txt.
+     */
+    void attachToBaseContext(Context base) {
+        attachBaseContext(base);
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
diff --git a/androidx/media/MediaConstants2.java b/androidx/media/MediaConstants2.java
new file mode 100644
index 0000000..68a9a19
--- /dev/null
+++ b/androidx/media/MediaConstants2.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+class MediaConstants2 {
+
+    static final int CONNECT_RESULT_CONNECTED = 0;
+    static final int CONNECT_RESULT_DISCONNECTED = -1;
+
+    // Event string used by IMediaControllerCallback.onEvent()
+    static final String SESSION_EVENT_ON_PLAYER_STATE_CHANGED =
+            "androidx.media.session.event.ON_PLAYER_STATE_CHANGED";
+    static final String SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED =
+            "androidx.media.session.event.ON_CURRENT_MEDIA_ITEM_CHANGED";
+    static final String SESSION_EVENT_ON_ERROR = "androidx.media.session.event.ON_ERROR";
+    static final String SESSION_EVENT_ON_ROUTES_INFO_CHANGED =
+            "androidx.media.session.event.ON_ROUTES_INFO_CHANGED";
+    static final String SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED =
+            "androidx.media.session.event.ON_PLAYBACK_INFO_CHANGED";
+    static final String SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED =
+            "androidx.media.session.event.ON_PLAYBACK_SPEED_CHANGED";
+    static final String SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED =
+            "androidx.media.session.event.ON_BUFFERING_STATE_CHANGED";
+    static final String SESSION_EVENT_ON_REPEAT_MODE_CHANGED =
+            "androidx.media.session.event.ON_REPEAT_MODE_CHANGED";
+    static final String SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED =
+            "androidx.media.session.event.ON_SHUFFLE_MODE_CHANGED";
+    static final String SESSION_EVENT_ON_PLAYLIST_CHANGED =
+            "androidx.media.session.event.ON_PLAYLIST_CHANGED";
+    static final String SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED =
+            "androidx.media.session.event.ON_PLAYLIST_METADATA_CHANGED";
+    static final String SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED =
+            "androidx.media.session.event.ON_ALLOWED_COMMANDS_CHANGED";
+    static final String SESSION_EVENT_SEND_CUSTOM_COMMAND =
+            "androidx.media.session.event.SEND_CUSTOM_COMMAND";
+    static final String SESSION_EVENT_SET_CUSTOM_LAYOUT =
+            "androidx.media.session.event.SET_CUSTOM_LAYOUT";
+
+    // Command string used by MediaControllerCompat.sendCommand()
+    static final String CONTROLLER_COMMAND_CONNECT = "androidx.media.controller.command.CONNECT";
+    static final String CONTROLLER_COMMAND_DISCONNECT =
+            "androidx.media.controller.command.DISCONNECT";
+    static final String CONTROLLER_COMMAND_BY_COMMAND_CODE =
+            "androidx.media.controller.command.BY_COMMAND_CODE";
+    static final String CONTROLLER_COMMAND_BY_CUSTOM_COMMAND =
+            "androidx.media.controller.command.BY_CUSTOM_COMMAND";
+
+
+    static final String ARGUMENT_COMMAND_CODE = "androidx.media.argument.COMMAND_CODE";
+    static final String ARGUMENT_CUSTOM_COMMAND = "androidx.media.argument.CUSTOM_COMMAND";
+    static final String ARGUMENT_ALLOWED_COMMANDS = "androidx.media.argument.ALLOWED_COMMANDS";
+    static final String ARGUMENT_SEEK_POSITION = "androidx.media.argument.SEEK_POSITION";
+    static final String ARGUMENT_PLAYER_STATE = "androidx.media.argument.PLAYER_STATE";
+    static final String ARGUMENT_PLAYBACK_SPEED = "androidx.media.argument.PLAYBACK_SPEED";
+    static final String ARGUMENT_BUFFERING_STATE = "androidx.media.argument.BUFFERING_STATE";
+    static final String ARGUMENT_ERROR_CODE = "androidx.media.argument.ERROR_CODE";
+    static final String ARGUMENT_REPEAT_MODE = "androidx.media.argument.REPEAT_MODE";
+    static final String ARGUMENT_SHUFFLE_MODE = "androidx.media.argument.SHUFFLE_MODE";
+    static final String ARGUMENT_PLAYLIST = "androidx.media.argument.PLAYLIST";
+    static final String ARGUMENT_PLAYLIST_INDEX = "androidx.media.argument.PLAYLIST_INDEX";
+    static final String ARGUMENT_PLAYLIST_METADATA = "androidx.media.argument.PLAYLIST_METADATA";
+    static final String ARGUMENT_RATING = "androidx.media.argument.RATING";
+    static final String ARGUMENT_MEDIA_ITEM = "androidx.media.argument.MEDIA_ITEM";
+    static final String ARGUMENT_MEDIA_ID = "androidx.media.argument.MEDIA_ID";
+    static final String ARGUMENT_QUERY = "androidx.media.argument.QUERY";
+    static final String ARGUMENT_URI = "androidx.media.argument.URI";
+    static final String ARGUMENT_PLAYBACK_STATE_COMPAT =
+            "androidx.media.argument.PLAYBACK_STATE_COMPAT";
+    static final String ARGUMENT_VOLUME = "androidx.media.argument.VOLUME";
+    static final String ARGUMENT_VOLUME_DIRECTION = "androidx.media.argument.VOLUME_DIRECTION";
+    static final String ARGUMENT_VOLUME_FLAGS = "androidx.media.argument.VOLUME_FLAGS";
+    static final String ARGUMENT_EXTRAS = "androidx.media.argument.EXTRAS";
+    static final String ARGUMENT_ARGUMENTS = "androidx.media.argument.ARGUMENTS";
+    static final String ARGUMENT_RESULT_RECEIVER = "androidx.media.argument.RESULT_RECEIVER";
+    static final String ARGUMENT_COMMAND_BUTTONS = "androidx.media.argument.COMMAND_BUTTONS";
+    static final String ARGUMENT_ROUTE_BUNDLE = "androidx.media.argument.ROUTE_BUNDLE";
+    static final String ARGUMENT_PLAYBACK_INFO = "androidx.media.argument.PLAYBACK_INFO";
+
+    static final String ARGUMENT_ICONTROLLER_CALLBACK =
+            "androidx.media.argument.ICONTROLLER_CALLBACK";
+    static final String ARGUMENT_UID = "androidx.media.argument.UID";
+    static final String ARGUMENT_PID = "androidx.media.argument.PID";
+    static final String ARGUMENT_PACKAGE_NAME = "androidx.media.argument.PACKAGE_NAME";
+
+    static final String ROOT_EXTRA_DEFAULT = "androidx.media.root_default_root";
+
+    private MediaConstants2() {
+    }
+}
diff --git a/androidx/media/MediaController2.java b/androidx/media/MediaController2.java
new file mode 100644
index 0000000..1da552d
--- /dev/null
+++ b/androidx/media/MediaController2.java
@@ -0,0 +1,1770 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_RATING;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.ARGUMENT_URI;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaPlayerBase.UNKNOWN_TIME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaPlaylistAgent.RepeatMode;
+import androidx.media.MediaPlaylistAgent.ShuffleMode;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.ErrorCode;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and
+ * other commands can be sent to the session.
+ * <p>
+ * When you're done, use {@link #close()} to clean up resources. This also helps session service
+ * to be destroyed when there's no controller associated with it.
+ * <p>
+ * When controlling {@link MediaSession2}, the controller will be available immediately after
+ * the creation.
+ * <p>
+ * MediaController2 objects are thread-safe.
+ * <p>
+ * @see MediaSession2
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class MediaController2 implements AutoCloseable {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({AudioManager.ADJUST_LOWER, AudioManager.ADJUST_RAISE, AudioManager.ADJUST_SAME,
+            AudioManager.ADJUST_MUTE, AudioManager.ADJUST_UNMUTE, AudioManager.ADJUST_TOGGLE_MUTE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VolumeDirection {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef(value = {AudioManager.FLAG_SHOW_UI, AudioManager.FLAG_ALLOW_RINGER_MODES,
+            AudioManager.FLAG_PLAY_SOUND, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
+            AudioManager.FLAG_VIBRATE}, flag = true)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VolumeFlags {}
+
+    /**
+     * Interface for listening to change in activeness of the {@link MediaSession2}.  It's
+     * active if and only if it has set a player.
+     */
+    public abstract static class ControllerCallback {
+        /**
+         * Called when the controller is successfully connected to the session. The controller
+         * becomes available afterwards.
+         *
+         * @param controller the controller for this event
+         * @param allowedCommands commands that's allowed by the session.
+         */
+        public void onConnected(@NonNull MediaController2 controller,
+                @NonNull SessionCommandGroup2 allowedCommands) { }
+
+        /**
+         * Called when the session refuses the controller or the controller is disconnected from
+         * the session. The controller becomes unavailable afterwards and the callback wouldn't
+         * be called.
+         * <p>
+         * It will be also called after the {@link #close()}, so you can put clean up code here.
+         * You don't need to call {@link #close()} after this.
+         *
+         * @param controller the controller for this event
+         */
+        public void onDisconnected(@NonNull MediaController2 controller) { }
+
+        /**
+         * Called when the session set the custom layout through the
+         * {@link MediaSession2#setCustomLayout(ControllerInfo, List)}.
+         * <p>
+         * Can be called before {@link #onConnected(MediaController2, SessionCommandGroup2)}
+         * is called.
+         *
+         * @param controller the controller for this event
+         * @param layout
+         */
+        public void onCustomLayoutChanged(@NonNull MediaController2 controller,
+                @NonNull List<CommandButton> layout) { }
+
+        /**
+         * Called when the session has changed anything related with the {@link PlaybackInfo}.
+         *
+         * @param controller the controller for this event
+         * @param info new playback info
+         */
+        public void onPlaybackInfoChanged(@NonNull MediaController2 controller,
+                @NonNull PlaybackInfo info) { }
+
+        /**
+         * Called when the allowed commands are changed by session.
+         *
+         * @param controller the controller for this event
+         * @param commands newly allowed commands
+         */
+        public void onAllowedCommandsChanged(@NonNull MediaController2 controller,
+                @NonNull SessionCommandGroup2 commands) { }
+
+        /**
+         * Called when the session sent a custom command.
+         *
+         * @param controller the controller for this event
+         * @param command
+         * @param args
+         * @param receiver
+         */
+        public void onCustomCommand(@NonNull MediaController2 controller,
+                @NonNull SessionCommand2 command, @Nullable Bundle args,
+                @Nullable ResultReceiver receiver) { }
+
+        /**
+         * Called when the player state is changed.
+         *
+         * @param controller the controller for this event
+         * @param state
+         */
+        public void onPlayerStateChanged(@NonNull MediaController2 controller, int state) { }
+
+        /**
+         * Called when playback speed is changed.
+         *
+         * @param controller the controller for this event
+         * @param speed speed
+         */
+        public void onPlaybackSpeedChanged(@NonNull MediaController2 controller,
+                float speed) { }
+
+        /**
+         * Called to report buffering events for a data source.
+         * <p>
+         * Use {@link #getBufferedPosition()} for current buffering position.
+         *
+         * @param controller the controller for this event
+         * @param item the media item for which buffering is happening.
+         * @param state the new buffering state.
+         */
+        public void onBufferingStateChanged(@NonNull MediaController2 controller,
+                @NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { }
+
+        /**
+         * Called to indicate that seeking is completed.
+         *
+         * @param controller the controller for this event.
+         * @param position the previous seeking request.
+         */
+        public void onSeekCompleted(@NonNull MediaController2 controller, long position) { }
+
+        /**
+         * Called when a error from
+         *
+         * @param controller the controller for this event
+         * @param errorCode error code
+         * @param extras extra information
+         */
+        public void onError(@NonNull MediaController2 controller, @ErrorCode int errorCode,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when the player's currently playing item is changed
+         * <p>
+         * When it's called, you should invalidate previous playback information and wait for later
+         * callbacks.
+         *
+         * @param controller the controller for this event
+         * @param item new item
+         * @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
+         */
+        public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
+                @NonNull MediaItem2 item) { }
+
+        /**
+         * Called when a playlist is changed.
+         *
+         * @param controller the controller for this event
+         * @param list new playlist
+         * @param metadata new metadata
+         */
+        public void onPlaylistChanged(@NonNull MediaController2 controller,
+                @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when a playlist metadata is changed.
+         *
+         * @param controller the controller for this event
+         * @param metadata new metadata
+         */
+        public void onPlaylistMetadataChanged(@NonNull MediaController2 controller,
+                @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when the shuffle mode is changed.
+         *
+         * @param controller the controller for this event
+         * @param shuffleMode repeat mode
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+         */
+        public void onShuffleModeChanged(@NonNull MediaController2 controller,
+                @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+        /**
+         * Called when the repeat mode is changed.
+         *
+         * @param controller the controller for this event
+         * @param repeatMode repeat mode
+         * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+         * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+         */
+        public void onRepeatModeChanged(@NonNull MediaController2 controller,
+                @MediaPlaylistAgent.RepeatMode int repeatMode) { }
+
+        /**
+         * Called when a property of the indicated media route has changed.
+         *
+         * @param controller the controller for this event
+         * @param routes The list of Bundle from MediaRouteDescriptor.asBundle().
+         *              See MediaRouteDescriptor.fromBundle(Bundle bundle) to get
+         *              MediaRouteDescriptor object from the {@code routes}
+         */
+        public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+                @Nullable List<Bundle> routes) { }
+    }
+
+    /**
+     * Holds information about the the way volume is handled for this session.
+     */
+    // The same as MediaController.PlaybackInfo
+    public static final class PlaybackInfo {
+        private static final String KEY_PLAYBACK_TYPE = "android.media.audio_info.playback_type";
+        private static final String KEY_CONTROL_TYPE = "android.media.audio_info.control_type";
+        private static final String KEY_MAX_VOLUME = "android.media.audio_info.max_volume";
+        private static final String KEY_CURRENT_VOLUME = "android.media.audio_info.current_volume";
+        private static final String KEY_AUDIO_ATTRIBUTES = "android.media.audio_info.audio_attrs";
+
+        private final int mPlaybackType;
+        private final int mControlType;
+        private final int mMaxVolume;
+        private final int mCurrentVolume;
+        private final AudioAttributesCompat mAudioAttrsCompat;
+
+        /**
+         * The session uses remote playback.
+         */
+        public static final int PLAYBACK_TYPE_REMOTE = 2;
+        /**
+         * The session uses local playback.
+         */
+        public static final int PLAYBACK_TYPE_LOCAL = 1;
+
+        PlaybackInfo(int playbackType, AudioAttributesCompat attrs, int controlType, int max,
+                int current) {
+            mPlaybackType = playbackType;
+            mAudioAttrsCompat = attrs;
+            mControlType = controlType;
+            mMaxVolume = max;
+            mCurrentVolume = current;
+        }
+
+        /**
+         * Get the type of playback which affects volume handling. One of:
+         * <ul>
+         * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
+         * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
+         * </ul>
+         *
+         * @return The type of playback this session is using.
+         */
+        public int getPlaybackType() {
+            return mPlaybackType;
+        }
+
+        /**
+         * Get the audio attributes for this session. The attributes will affect
+         * volume handling for the session. When the volume type is
+         * {@link #PLAYBACK_TYPE_REMOTE} these may be ignored by the
+         * remote volume handler.
+         *
+         * @return The attributes for this session.
+         */
+        public AudioAttributesCompat getAudioAttributes() {
+            return mAudioAttrsCompat;
+        }
+
+        /**
+         * Get the type of volume control that can be used. One of:
+         * <ul>
+         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
+         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
+         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
+         * </ul>
+         *
+         * @return The type of volume control that may be used with this session.
+         */
+        public int getControlType() {
+            return mControlType;
+        }
+
+        /**
+         * Get the maximum volume that may be set for this session.
+         *
+         * @return The maximum allowed volume where this session is playing.
+         */
+        public int getMaxVolume() {
+            return mMaxVolume;
+        }
+
+        /**
+         * Get the current volume for this session.
+         *
+         * @return The current volume where this session is playing.
+         */
+        public int getCurrentVolume() {
+            return mCurrentVolume;
+        }
+
+        Bundle toBundle() {
+            Bundle bundle = new Bundle();
+            bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType);
+            bundle.putInt(KEY_CONTROL_TYPE, mControlType);
+            bundle.putInt(KEY_MAX_VOLUME, mMaxVolume);
+            bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume);
+            if (mAudioAttrsCompat != null) {
+                bundle.putParcelable(KEY_AUDIO_ATTRIBUTES,
+                        MediaUtils2.toAudioAttributesBundle(mAudioAttrsCompat));
+            }
+            return bundle;
+        }
+
+        static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributesCompat attrs,
+                int controlType, int max, int current) {
+            return new PlaybackInfo(playbackType, attrs, controlType, max, current);
+        }
+
+        static PlaybackInfo fromBundle(Bundle bundle) {
+            if (bundle == null) {
+                return null;
+            }
+            final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE);
+            final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE);
+            final int maxVolume = bundle.getInt(KEY_MAX_VOLUME);
+            final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME);
+            final AudioAttributesCompat attrs = MediaUtils2.fromAudioAttributesBundle(
+                    bundle.getBundle(KEY_AUDIO_ATTRIBUTES));
+            return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume,
+                    currentVolume);
+        }
+    }
+
+    private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
+        @Override
+        public void onSessionReady() {
+            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle resultData) {
+                    if (!mHandlerThread.isAlive()) {
+                        return;
+                    }
+                    switch (resultCode) {
+                        case CONNECT_RESULT_CONNECTED:
+                            onConnectedNotLocked(resultData);
+                            break;
+                        case CONNECT_RESULT_DISCONNECTED:
+                            mCallback.onDisconnected(MediaController2.this);
+                            close();
+                            break;
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onSessionDestroyed() {
+            close();
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            synchronized (mLock) {
+                mPlaybackStateCompat = state;
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadataCompat metadata) {
+            synchronized (mLock) {
+                mMediaMetadataCompat = metadata;
+            }
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            switch (event) {
+                case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
+                    SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+                            extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+                    synchronized (mLock) {
+                        mAllowedCommands = allowedCommands;
+                    }
+                    mCallback.onAllowedCommandsChanged(MediaController2.this, allowedCommands);
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
+                    int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
+                    synchronized (mLock) {
+                        mPlayerState = playerState;
+                    }
+                    mCallback.onPlayerStateChanged(MediaController2.this, playerState);
+                    break;
+                }
+                case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
+                    MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                    if (item == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mCurrentMediaItem = item;
+                    }
+                    mCallback.onCurrentMediaItemChanged(MediaController2.this, item);
+                    break;
+                }
+                case SESSION_EVENT_ON_ERROR: {
+                    int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
+                    Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
+                    mCallback.onError(MediaController2.this, errorCode, errorExtras);
+                    break;
+                }
+                case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
+                    List<Bundle> routes = MediaUtils2.toBundleList(
+                            extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
+                    mCallback.onRoutesInfoChanged(MediaController2.this, routes);
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
+                    MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+                    List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
+                            extras.getParcelableArray(ARGUMENT_PLAYLIST));
+                    synchronized (mLock) {
+                        mPlaylist = playlist;
+                        mPlaylistMetadata = playlistMetadata;
+                    }
+                    mCallback.onPlaylistChanged(MediaController2.this, playlist, playlistMetadata);
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
+                    MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+                    synchronized (mLock) {
+                        mPlaylistMetadata = playlistMetadata;
+                    }
+                    mCallback.onPlaylistMetadataChanged(MediaController2.this, playlistMetadata);
+                    break;
+                }
+                case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
+                    int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
+                    synchronized (mLock) {
+                        mRepeatMode = repeatMode;
+                    }
+                    mCallback.onRepeatModeChanged(MediaController2.this, repeatMode);
+                    break;
+                }
+                case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
+                    int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
+                    synchronized (mLock) {
+                        mShuffleMode = shuffleMode;
+                    }
+                    mCallback.onShuffleModeChanged(MediaController2.this, shuffleMode);
+                    break;
+                }
+                case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
+                    Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
+                    if (commandBundle == null) {
+                        return;
+                    }
+                    SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
+                    Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
+                    ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
+                    mCallback.onCustomCommand(MediaController2.this, command, args, receiver);
+                    break;
+                }
+                case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
+                    List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
+                            extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
+                    if (layout == null) {
+                        return;
+                    }
+                    mCallback.onCustomLayoutChanged(MediaController2.this, layout);
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
+                    PlaybackInfo info = PlaybackInfo.fromBundle(
+                            extras.getBundle(ARGUMENT_PLAYBACK_INFO));
+                    if (info == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mPlaybackInfo = info;
+                    }
+                    mCallback.onPlaybackInfoChanged(MediaController2.this, info);
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
+                    PlaybackStateCompat state =
+                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+                    if (state == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mPlaybackStateCompat = state;
+                    }
+                    mCallback.onPlaybackSpeedChanged(
+                            MediaController2.this, state.getPlaybackSpeed());
+                    break;
+                }
+                case SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED: {
+                    MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                    int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
+                    if (item == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mBufferingState = bufferingState;
+                    }
+                    mCallback.onBufferingStateChanged(MediaController2.this, item, bufferingState);
+                    break;
+                }
+            }
+        }
+    }
+
+    private static final String TAG = "MediaController2";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
+    //       the rootHints so it becomes non-null.
+    static final Bundle sDefaultRootExtras = new Bundle();
+    static {
+        sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
+    }
+
+    private final Context mContext;
+    private final Object mLock = new Object();
+
+    private final SessionToken2 mToken;
+    private final ControllerCallback mCallback;
+    private final Executor mCallbackExecutor;
+    private final IBinder.DeathRecipient mDeathRecipient;
+
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+
+    @GuardedBy("mLock")
+    private MediaBrowserCompat mBrowserCompat;
+    @GuardedBy("mLock")
+    private boolean mIsReleased;
+    @GuardedBy("mLock")
+    private List<MediaItem2> mPlaylist;
+    @GuardedBy("mLock")
+    private MediaMetadata2 mPlaylistMetadata;
+    @GuardedBy("mLock")
+    private @RepeatMode int mRepeatMode;
+    @GuardedBy("mLock")
+    private @ShuffleMode int mShuffleMode;
+    @GuardedBy("mLock")
+    private int mPlayerState;
+    @GuardedBy("mLock")
+    private MediaItem2 mCurrentMediaItem;
+    @GuardedBy("mLock")
+    private int mBufferingState;
+    @GuardedBy("mLock")
+    private PlaybackInfo mPlaybackInfo;
+    @GuardedBy("mLock")
+    private SessionCommandGroup2 mAllowedCommands;
+
+    // Media 1.0 variables
+    @GuardedBy("mLock")
+    private MediaControllerCompat mControllerCompat;
+    @GuardedBy("mLock")
+    private ControllerCompatCallback mControllerCompatCallback;
+    @GuardedBy("mLock")
+    private PlaybackStateCompat mPlaybackStateCompat;
+    @GuardedBy("mLock")
+    private MediaMetadataCompat mMediaMetadataCompat;
+
+    // Assignment should be used with the lock hold, but should be used without a lock to prevent
+    // potential deadlock.
+    @GuardedBy("mLock")
+    private volatile boolean mConnected;
+
+    /**
+     * Create a {@link MediaController2} from the {@link SessionToken2}.
+     * This connects to the session and may wake up the service if it's not available.
+     *
+     * @param context Context
+     * @param token token to connect to
+     * @param executor executor to run callbacks on.
+     * @param callback controller callback to receive changes in
+     */
+    public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
+            @NonNull Executor executor, @NonNull ControllerCallback callback) {
+        super();
+        if (context == null) {
+            throw new IllegalArgumentException("context shouldn't be null");
+        }
+        if (token == null) {
+            throw new IllegalArgumentException("token shouldn't be null");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("callback shouldn't be null");
+        }
+        if (executor == null) {
+            throw new IllegalArgumentException("executor shouldn't be null");
+        }
+        mContext = context;
+        mHandlerThread = new HandlerThread("MediaController2_Thread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mToken = token;
+        mCallback = callback;
+        mCallbackExecutor = executor;
+        mDeathRecipient = new IBinder.DeathRecipient() {
+            @Override
+            public void binderDied() {
+                MediaController2.this.close();
+            }
+        };
+
+        initialize();
+    }
+
+    /**
+     * Release this object, and disconnect from the session. After this, callbacks wouldn't be
+     * received.
+     */
+    @Override
+    public void close() {
+        if (DEBUG) {
+            //Log.d(TAG, "release from " + mToken, new IllegalStateException());
+        }
+        synchronized (mLock) {
+            if (mIsReleased) {
+                // Prevent re-enterance from the ControllerCallback.onDisconnected()
+                return;
+            }
+            mHandler.removeCallbacksAndMessages(null);
+            mHandlerThread.quitSafely();
+
+            mIsReleased = true;
+
+            // Send command before the unregister callback to use mIControllerCallback in the
+            // callback.
+            sendCommand(CONTROLLER_COMMAND_DISCONNECT);
+            if (mControllerCompat != null) {
+                mControllerCompat.unregisterCallback(mControllerCompatCallback);
+            }
+            if (mBrowserCompat != null) {
+                mBrowserCompat.disconnect();
+                mBrowserCompat = null;
+            }
+            if (mControllerCompat != null) {
+                mControllerCompat.unregisterCallback(mControllerCompatCallback);
+                mControllerCompat = null;
+            }
+            mConnected = false;
+        }
+        mCallbackExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                mCallback.onDisconnected(MediaController2.this);
+            }
+        });
+    }
+
+    /**
+     * @return token
+     */
+    public @NonNull SessionToken2 getSessionToken() {
+        return mToken;
+    }
+
+    /**
+     * Returns whether this class is connected to active {@link MediaSession2} or not.
+     */
+    public boolean isConnected() {
+        synchronized (mLock) {
+            return mConnected;
+        }
+    }
+
+    /**
+     * Requests that the player starts or resumes playback.
+     */
+    public void play() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
+        }
+    }
+
+    /**
+     * Requests that the player pauses playback.
+     */
+    public void pause() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
+        }
+    }
+
+    /**
+     * Requests that the player be reset to its uninitialized state.
+     */
+    public void reset() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_RESET);
+        }
+    }
+
+    /**
+     * Request that the player prepare its playback. In other words, other sessions can continue
+     * to play during the preparation of this session. This method can be used to speed up the
+     * start of the playback. Once the preparation is done, the session will change its playback
+     * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
+     * to start playback.
+     */
+    public void prepare() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
+        }
+    }
+
+    /**
+     * Start fast forwarding. If playback is already fast forwarding this
+     * may increase the rate.
+     */
+    public void fastForward() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
+        }
+    }
+
+    /**
+     * Start rewinding. If playback is already rewinding this may increase
+     * the rate.
+     */
+    public void rewind() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            sendCommand(COMMAND_CODE_SESSION_REWIND);
+        }
+    }
+
+    /**
+     * Move to a new location in the media stream.
+     *
+     * @param pos Position to move to, in milliseconds.
+     */
+    public void seekTo(long pos) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putLong(ARGUMENT_SEEK_POSITION, pos);
+            sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void skipForward() {
+        // To match with KEYCODE_MEDIA_SKIP_FORWARD
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void skipBackward() {
+        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+    }
+
+    /**
+     * Request that the player start playback for a specific media id.
+     *
+     * @param mediaId The id of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be played.
+     */
+    public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_MEDIA_ID, mediaId);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
+        }
+    }
+
+    /**
+     * Request that the player start playback for a specific search query.
+     *
+     * @param query The search query. Should not be an empty string.
+     * @param extras Optional extras that can include extra information about the query.
+     */
+    public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_QUERY, query);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
+        }
+    }
+
+    /**
+     * Request that the player start playback for a specific {@link Uri}.
+     *
+     * @param uri The URI of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be played.
+     */
+    public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putParcelable(ARGUMENT_URI, uri);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
+        }
+    }
+
+    /**
+     * Request that the player prepare playback for a specific media id. In other words, other
+     * sessions can continue to play during the preparation of this session. This method can be
+     * used to speed up the start of the playback. Once the preparation is done, the session
+     * will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
+     * {@link #play} can be called to start playback. If the preparation is not needed,
+     * {@link #playFromMediaId} can be directly called without this method.
+     *
+     * @param mediaId The id of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be prepared.
+     */
+    public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_MEDIA_ID, mediaId);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
+        }
+    }
+
+    /**
+     * Request that the player prepare playback for a specific search query.
+     * In other words, other sessions can continue to play during the preparation of this session.
+     * This method can be used to speed up the start of the playback.
+     * Once the preparation is done, the session will change its playback state to
+     * {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
+     * {@link #play} can be called to start playback. If the preparation is not needed,
+     * {@link #playFromSearch} can be directly called without this method.
+     *
+     * @param query The search query. Should not be an empty string.
+     * @param extras Optional extras that can include extra information about the query.
+     */
+    public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_QUERY, query);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
+        }
+    }
+
+    /**
+     * Request that the player prepare playback for a specific {@link Uri}. In other words,
+     * other sessions can continue to play during the preparation of this session. This method
+     * can be used to speed up the start of the playback. Once the preparation is done, the
+     * session will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}.
+     * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
+     * {@link #playFromUri} can be directly called without this method.
+     *
+     * @param uri The URI of the requested media.
+     * @param extras Optional extras that can include extra information about the media item
+     *               to be prepared.
+     */
+    public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putParcelable(ARGUMENT_URI, uri);
+            args.putBundle(ARGUMENT_EXTRAS, extras);
+            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
+        }
+    }
+
+    /**
+     * Set the volume of the output this session is playing on. The command will be ignored if it
+     * does not support {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
+     * <p>
+     * If the session is local playback, this changes the device's volume with the stream that
+     * session's player is using. Flags will be specified for the {@link AudioManager}.
+     * <p>
+     * If the session is remote player (i.e. session has set volume provider), its volume provider
+     * will receive this request instead.
+     *
+     * @see #getPlaybackInfo()
+     * @param value The value to set it to, between 0 and the reported max.
+     * @param flags flags from {@link AudioManager} to include with the volume request for local
+     *              playback
+     */
+    public void setVolumeTo(int value, @VolumeFlags int flags) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putInt(ARGUMENT_VOLUME, value);
+            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+            sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
+        }
+    }
+
+    /**
+     * Adjust the volume of the output this session is playing on. The direction
+     * must be one of {@link AudioManager#ADJUST_LOWER},
+     * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
+     * <p>
+     * The command will be ignored if the session does not support
+     * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
+     * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
+     * <p>
+     * If the session is local playback, this changes the device's volume with the stream that
+     * session's player is using. Flags will be specified for the {@link AudioManager}.
+     * <p>
+     * If the session is remote player (i.e. session has set volume provider), its volume provider
+     * will receive this request instead.
+     *
+     * @see #getPlaybackInfo()
+     * @param direction The direction to adjust the volume in.
+     * @param flags flags from {@link AudioManager} to include with the volume request for local
+     *              playback
+     */
+    public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
+            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+            sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
+        }
+    }
+
+    /**
+     * Get an intent for launching UI associated with this session if one exists.
+     *
+     * @return A {@link PendingIntent} to launch UI or null.
+     */
+    public @Nullable PendingIntent getSessionActivity() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return null;
+            }
+            return mControllerCompat.getSessionActivity();
+        }
+    }
+
+    /**
+     * Get the lastly cached player state from
+     * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
+     *
+     * @return player state
+     */
+    public int getPlayerState() {
+        synchronized (mLock) {
+            return mPlayerState;
+        }
+    }
+
+    /**
+     * Gets the duration of the current media item, or {@link MediaPlayerBase#UNKNOWN_TIME} if
+     * unknown.
+     * @return the duration in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
+     */
+    public long getDuration() {
+        synchronized (mLock) {
+            if (mMediaMetadataCompat != null
+                    && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
+                return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
+            }
+        }
+        return MediaPlayerBase.UNKNOWN_TIME;
+    }
+
+    /**
+     * Gets the current playback position.
+     * <p>
+     * This returns the calculated value of the position, based on the difference between the
+     * update time and current time.
+     *
+     * @return position
+     */
+    public long getCurrentPosition() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return UNKNOWN_TIME;
+            }
+            if (mPlaybackStateCompat != null) {
+                long timeDiff = SystemClock.elapsedRealtime()
+                        - mPlaybackStateCompat.getLastPositionUpdateTime();
+                long expectedPosition = mPlaybackStateCompat.getPosition()
+                        + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
+                return Math.max(0, expectedPosition);
+            }
+            return UNKNOWN_TIME;
+        }
+    }
+
+    /**
+     * Get the lastly cached playback speed from
+     * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
+     *
+     * @return speed the lastly cached playback speed, or 0.0f if unknown.
+     */
+    public float getPlaybackSpeed() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return 0f;
+            }
+            return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
+        }
+    }
+
+    /**
+     * Set the playback speed.
+     */
+    public void setPlaybackSpeed(float speed) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
+            sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
+        }
+    }
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     * @return the buffering state.
+     */
+    public @MediaPlayerBase.BuffState int getBufferingState() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return BUFFERING_STATE_UNKNOWN;
+            }
+            return mBufferingState;
+        }
+    }
+
+    /**
+     * Gets the lastly cached buffered position from the session when
+     * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
+     * called.
+     *
+     * @return buffering position in millis, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
+     */
+    public long getBufferedPosition() {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return UNKNOWN_TIME;
+            }
+            return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
+                    : mPlaybackStateCompat.getBufferedPosition();
+        }
+    }
+
+    /**
+     * Get the current playback info for this session.
+     *
+     * @return The current playback info or null.
+     */
+    public @Nullable PlaybackInfo getPlaybackInfo() {
+        synchronized (mLock) {
+            return mPlaybackInfo;
+        }
+    }
+
+    /**
+     * Rate the media. This will cause the rating to be set for the current user.
+     * The rating style must follow the user rating style from the session.
+     * You can get the rating style from the session through the
+     * {@link MediaMetadata2#getRating(String)} with the key
+     * {@link MediaMetadata2#METADATA_KEY_USER_RATING}.
+     * <p>
+     * If the user rating was {@code null}, the media item does not accept setting user rating.
+     *
+     * @param mediaId The id of the media
+     * @param rating The rating to set
+     */
+    public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putString(ARGUMENT_MEDIA_ID, mediaId);
+            args.putBundle(ARGUMENT_RATING, rating.toBundle());
+            sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
+        }
+    }
+
+    /**
+     * Send custom command to the session
+     *
+     * @param command custom command
+     * @param args optional argument
+     * @param cb optional result receiver
+     */
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+            @Nullable ResultReceiver cb) {
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle bundle = new Bundle();
+            bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+            bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+            sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
+        }
+    }
+
+    /**
+     * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}.
+     * <p>
+     * This list may differ with the list that was specified with
+     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+     * implementation. Use media items returned here for other playlist agent APIs such as
+     * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+     *
+     * @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have
+     *      enough permission.
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+     */
+    public @Nullable List<MediaItem2> getPlaylist() {
+        synchronized (mLock) {
+            return mPlaylist;
+        }
+    }
+
+    /**
+     * Sets the playlist.
+     * <p>
+     * Even when the playlist is successfully set, use the playlist returned from
+     * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
+     * Otherwise the session in the remote process can't distinguish between media items.
+     *
+     * @param list playlist
+     * @param metadata metadata of the playlist
+     * @see #getPlaylist()
+     * @see ControllerCallback#onPlaylistChanged
+     */
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        if (list == null) {
+            throw new IllegalArgumentException("list shouldn't be null");
+        }
+        Bundle args = new Bundle();
+        args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
+        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
+    }
+
+    /**
+     * Updates the playlist metadata
+     *
+     * @param metadata metadata of the playlist
+     */
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
+    }
+
+    /**
+     * Gets the lastly cached playlist playlist metadata either from
+     * {@link ControllerCallback#onPlaylistMetadataChanged or
+     * {@link ControllerCallback#onPlaylistChanged}.
+     *
+     * @return metadata metadata of the playlist, or null if none is set
+     */
+    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+        synchronized (mLock) {
+            return mPlaylistMetadata;
+        }
+    }
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+     * the playlist.
+     * <p>
+     * This will not change the currently playing media item.
+     * If index is less than or equal to the current index of the playlist,
+     * the current index of the playlist will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
+    }
+
+    /**
+     * Removes the media item at index in the playlist.
+     *<p>
+     * If the item is the currently playing item of the playlist, current playback
+     * will be stopped and playback moves to next source in the list.
+     *
+     * @param item the media item you want to add
+     */
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
+    }
+
+    /**
+     * Replace the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
+     *
+     * @param index the index of the item to replace
+     * @param item the new item
+     */
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
+    }
+
+    /**
+     * Get the lastly cached current item from
+     * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
+     *
+     * @return the currently playing item, or null if unknown.
+     */
+    public MediaItem2 getCurrentMediaItem() {
+        synchronized (mLock) {
+            return mCurrentMediaItem;
+        }
+    }
+
+    /**
+     * Skips to the previous item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()}.
+     */
+    public void skipToPreviousItem() {
+        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
+    }
+
+    /**
+     * Skips to the next item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToNextItem()}.
+     */
+    public void skipToNextItem() {
+        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
+    }
+
+    /**
+     * Skips to the item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+     *
+     * @param item The item in the playlist you want to play
+     */
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
+    }
+
+    /**
+     * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}.
+     *
+     * @return repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public @RepeatMode int getRepeatMode() {
+        synchronized (mLock) {
+            return mRepeatMode;
+        }
+    }
+
+    /**
+     * Sets the repeat mode.
+     *
+     * @param repeatMode repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
+    }
+
+    /**
+     * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}.
+     *
+     * @return The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public @ShuffleMode int getShuffleMode() {
+        synchronized (mLock) {
+            return mShuffleMode;
+        }
+    }
+
+    /**
+     * Sets the shuffle mode.
+     *
+     * @param shuffleMode The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        Bundle args = new Bundle();
+        args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+        sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
+    }
+
+    /**
+     * Queries for information about the routes currently known.
+     */
+    public void subscribeRoutesInfo() {
+        sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
+    }
+
+    /**
+     * Unsubscribes for changes to the routes.
+     * <p>
+     * The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for
+     * the routes once this method returns.
+     * </p>
+     */
+    public void unsubscribeRoutesInfo() {
+        sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
+    }
+
+    /**
+     * Selects the specified route.
+     *
+     * @param route The route to select.
+     */
+    public void selectRoute(@NonNull Bundle route) {
+        if (route == null) {
+            throw new IllegalArgumentException("route shouldn't be null");
+        }
+        Bundle args = new Bundle();
+        args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
+        sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
+    }
+
+    // Should be used without a lock to prevent potential deadlock.
+    void onConnectedNotLocked(Bundle data) {
+        // is enough or should we pass it while connecting?
+        final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+                data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+        final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
+        final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
+        final PlaybackStateCompat playbackStateCompat = data.getParcelable(
+                ARGUMENT_PLAYBACK_STATE_COMPAT);
+        final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
+        final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
+        final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
+                data.getParcelableArray(ARGUMENT_PLAYLIST));
+        final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
+                data.getBundle(ARGUMENT_MEDIA_ITEM));
+        final PlaybackInfo playbackInfo =
+                PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
+        final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+                data.getBundle(ARGUMENT_PLAYLIST_METADATA));
+        if (DEBUG) {
+            Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
+                    + ", allowedCommands=" + allowedCommands);
+        }
+        boolean close = false;
+        try {
+            synchronized (mLock) {
+                if (mIsReleased) {
+                    return;
+                }
+                if (mConnected) {
+                    Log.e(TAG, "Cannot be notified about the connection result many times."
+                            + " Probably a bug or malicious app.");
+                    close = true;
+                    return;
+                }
+                mAllowedCommands = allowedCommands;
+                mPlayerState = playerState;
+                mBufferingState = bufferingState;
+                mPlaybackStateCompat = playbackStateCompat;
+                mRepeatMode = repeatMode;
+                mShuffleMode = shuffleMode;
+                mPlaylist = playlist;
+                mCurrentMediaItem = currentMediaItem;
+                mPlaylistMetadata = metadata;
+                mConnected = true;
+                mPlaybackInfo = playbackInfo;
+            }
+            mCallbackExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    // Note: We may trigger ControllerCallbacks with the initial values
+                    // But it's hard to define the order of the controller callbacks
+                    // Only notify about the
+                    mCallback.onConnected(MediaController2.this, allowedCommands);
+                }
+            });
+        } finally {
+            if (close) {
+                // Trick to call release() without holding the lock, to prevent potential deadlock
+                // with the developer's custom lock within the ControllerCallback.onDisconnected().
+                close();
+            }
+        }
+    }
+
+    private void initialize() {
+        if (mToken.getType() == SessionToken2.TYPE_SESSION) {
+            synchronized (mLock) {
+                mBrowserCompat = null;
+            }
+            connectToSession(mToken.getSessionCompatToken());
+        } else {
+            connectToService();
+        }
+    }
+
+    private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
+        MediaControllerCompat controllerCompat = null;
+        try {
+            controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+        synchronized (mLock) {
+            mControllerCompat = controllerCompat;
+            mControllerCompatCallback = new ControllerCompatCallback();
+            mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
+        }
+
+        if (controllerCompat.isSessionReady()) {
+            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle resultData) {
+                    if (!mHandlerThread.isAlive()) {
+                        return;
+                    }
+                    switch (resultCode) {
+                        case CONNECT_RESULT_CONNECTED:
+                            onConnectedNotLocked(resultData);
+                            break;
+                        case CONNECT_RESULT_DISCONNECTED:
+                            mCallback.onDisconnected(MediaController2.this);
+                            close();
+                            break;
+                    }
+                }
+            });
+        }
+    }
+
+    private void connectToService() {
+        synchronized (mLock) {
+            mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
+                    new ConnectionCallback(), sDefaultRootExtras);
+            mBrowserCompat.connect();
+        }
+    }
+
+    private void sendCommand(int commandCode) {
+        sendCommand(commandCode, null);
+    }
+
+    private void sendCommand(int commandCode, Bundle args) {
+        if (args == null) {
+            args = new Bundle();
+        }
+        args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
+        sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
+    }
+
+    private void sendCommand(String command) {
+        sendCommand(command, null, null);
+    }
+
+    private void sendCommand(String command, ResultReceiver receiver) {
+        sendCommand(command, null, receiver);
+    }
+
+    private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
+        if (args == null) {
+            args = new Bundle();
+        }
+        MediaControllerCompat controller;
+        ControllerCompatCallback callback;
+        synchronized (mLock) {
+            controller = mControllerCompat;
+            callback = mControllerCompatCallback;
+        }
+        args.putBinder(ARGUMENT_ICONTROLLER_CALLBACK, callback.getIControllerCallback().asBinder());
+        args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
+        args.putInt(ARGUMENT_UID, Process.myUid());
+        args.putInt(ARGUMENT_PID, Process.myPid());
+        controller.sendCommand(command, args, receiver);
+    }
+
+    @NonNull Context getContext() {
+        return mContext;
+    }
+
+    @NonNull ControllerCallback getCallback() {
+        return mCallback;
+    }
+
+    @NonNull Executor getCallbackExecutor() {
+        return mCallbackExecutor;
+    }
+
+    @Nullable MediaBrowserCompat getBrowserCompat() {
+        synchronized (mLock) {
+            return mBrowserCompat;
+        }
+    }
+
+    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        @Override
+        public void onConnected() {
+            MediaBrowserCompat browser = getBrowserCompat();
+            if (browser != null) {
+                connectToSession(browser.getSessionToken());
+            } else if (DEBUG) {
+                Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
+            }
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            close();
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            close();
+        }
+    }
+}
diff --git a/androidx/media/MediaController2Test.java b/androidx/media/MediaController2Test.java
new file mode 100644
index 0000000..75c9e50
--- /dev/null
+++ b/androidx/media/MediaController2Test.java
@@ -0,0 +1,1306 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.TestServiceRegistry.SessionServiceCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+public class MediaController2Test extends MediaSession2TestBase {
+    private static final String TAG = "MediaController2Test";
+
+    PendingIntent mIntent;
+    MediaSession2 mSession;
+    MediaController2 mController;
+    MockPlayer mPlayer;
+    MockPlaylistAgent mMockAgent;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        final Intent sessionActivity = new Intent(mContext, MockActivity.class);
+        // Create this test specific MediaSession2 to use our own Handler.
+        mIntent = PendingIntent.getActivity(mContext, 0, sessionActivity, 0);
+
+        mPlayer = new MockPlayer(1);
+        mMockAgent = new MockPlaylistAgent();
+        mSession = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public SessionCommandGroup2 onConnect(MediaSession2 session,
+                            ControllerInfo controller) {
+                        if (Process.myUid() == controller.getUid()) {
+                            return super.onConnect(session, controller);
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    public void onPlaylistMetadataChanged(MediaSession2 session,
+                            MediaPlaylistAgent playlistAgent,
+                            MediaMetadata2 metadata) {
+                        super.onPlaylistMetadataChanged(session, playlistAgent, metadata);
+                    }
+                })
+                .setSessionActivity(mIntent)
+                .setId(TAG).build();
+        mController = createController(mSession.getToken());
+        TestServiceRegistry.getInstance().setHandler(sHandler);
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        if (mSession != null) {
+            mSession.close();
+        }
+        TestServiceRegistry.getInstance().cleanUp();
+    }
+
+    /**
+     * Test if the {@link MediaSession2TestBase.TestControllerCallback} wraps the callback proxy
+     * without missing any method.
+     */
+    @Test
+    public void testTestControllerCallback() {
+        prepareLooper();
+        Method[] methods = TestControllerCallback.class.getMethods();
+        assertNotNull(methods);
+        for (int i = 0; i < methods.length; i++) {
+            // For any methods in the controller callback, TestControllerCallback should have
+            // overriden the method and call matching API in the callback proxy.
+            assertNotEquals("TestControllerCallback should override " + methods[i]
+                            + " and call callback proxy",
+                    ControllerCallback.class, methods[i].getDeclaringClass());
+        }
+    }
+
+    @Test
+    public void testPlay() {
+        prepareLooper();
+        mController.play();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPlayCalled);
+    }
+
+    @Test
+    public void testPause() {
+        prepareLooper();
+        mController.pause();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPauseCalled);
+    }
+
+    @Test
+    public void testReset() {
+        prepareLooper();
+        mController.reset();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mResetCalled);
+    }
+
+    @Test
+    public void testPrepare() {
+        prepareLooper();
+        mController.prepare();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPrepareCalled);
+    }
+
+    @Test
+    public void testSeekTo() {
+        prepareLooper();
+        final long seekPosition = 12125L;
+        mController.seekTo(seekPosition);
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSeekToCalled);
+        assertEquals(seekPosition, mPlayer.mSeekPosition);
+    }
+
+    @Test
+    public void testGettersAfterConnected() throws InterruptedException {
+        prepareLooper();
+        final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+        final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+        final long position = 150000;
+        final long bufferedPosition = 900000;
+        final float speed = 0.5f;
+        final MediaItem2 currentMediaItem = TestUtils.createMediaItemWithMetadata();
+
+        mPlayer.mLastPlayerState = state;
+        mPlayer.mLastBufferingState = bufferingState;
+        mPlayer.mCurrentPosition = position;
+        mPlayer.mBufferedPosition = bufferedPosition;
+        mPlayer.mPlaybackSpeed = speed;
+        mMockAgent.mCurrentMediaItem = currentMediaItem;
+
+        long time1 = System.currentTimeMillis();
+        MediaController2 controller = createController(mSession.getToken());
+        long time2 = System.currentTimeMillis();
+        assertEquals(state, controller.getPlayerState());
+        assertEquals(bufferedPosition, controller.getBufferedPosition());
+        assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
+        long positionLowerBound = (long) (position + speed * (System.currentTimeMillis() - time2));
+        long currentPosition = controller.getCurrentPosition();
+        long positionUpperBound = (long) (position + speed * (System.currentTimeMillis() - time1));
+        assertTrue("curPos=" + currentPosition + ", lowerBound=" + positionLowerBound
+                        + ", upperBound=" + positionUpperBound,
+                positionLowerBound <= currentPosition && currentPosition <= positionUpperBound);
+        assertEquals(currentMediaItem, controller.getCurrentMediaItem());
+    }
+
+    @Test
+    public void testGetSessionActivity() {
+        prepareLooper();
+        PendingIntent sessionActivity = mController.getSessionActivity();
+        assertEquals(mContext.getPackageName(), sessionActivity.getCreatorPackage());
+        assertEquals(Process.myUid(), sessionActivity.getCreatorUid());
+    }
+
+    @Test
+    public void testSetPlaylist() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        mController.setPlaylist(list, null /* Metadata */);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSetPlaylistCalled);
+        assertNull(mMockAgent.mMetadata);
+
+        assertNotNull(mMockAgent.mPlaylist);
+        assertEquals(list.size(), mMockAgent.mPlaylist.size());
+        for (int i = 0; i < list.size(); i++) {
+            // MediaController2.setPlaylist does not ensure the equality of the items.
+            assertEquals(list.get(i).getMediaId(), mMockAgent.mPlaylist.get(i).getMediaId());
+        }
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onPlaylistChanged(
+     * MediaController2, List, MediaMetadata2)}.
+     */
+    @Test
+    public void testGetPlaylist() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> testList = TestUtils.createPlaylist(2);
+        final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaylistChanged(MediaController2 controller,
+                    List<MediaItem2> playlist, MediaMetadata2 metadata) {
+                assertNotNull(playlist);
+                assertEquals(testList.size(), playlist.size());
+                for (int i = 0; i < playlist.size(); i++) {
+                    assertEquals(testList.get(i).getMediaId(), playlist.get(i).getMediaId());
+                }
+                listFromCallback.set(playlist);
+                latch.countDown();
+            }
+        };
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public List<MediaItem2> getPlaylist() {
+                return testList;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testControllerCallback_onPlaylistChanged")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                .setPlaylistAgent(agent)
+                .build()) {
+            MediaController2 controller = createController(
+                    session.getToken(), true, callback);
+            agent.notifyPlaylistChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertEquals(listFromCallback.get(), controller.getPlaylist());
+        }
+    }
+
+    @Test
+    public void testUpdatePlaylistMetadata() throws InterruptedException {
+        prepareLooper();
+        final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+        mController.updatePlaylistMetadata(testMetadata);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+        assertNotNull(mMockAgent.mMetadata);
+        assertEquals(testMetadata.getMediaId(), mMockAgent.mMetadata.getMediaId());
+    }
+
+    @Test
+    public void testGetPlaylistMetadata() throws InterruptedException {
+        prepareLooper();
+        final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+        final AtomicReference<MediaMetadata2> metadataFromCallback = new AtomicReference<>();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaylistMetadataChanged(MediaController2 controller,
+                    MediaMetadata2 metadata) {
+                assertNotNull(testMetadata);
+                assertEquals(testMetadata.getMediaId(), metadata.getMediaId());
+                metadataFromCallback.set(metadata);
+                latch.countDown();
+            }
+        };
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public MediaMetadata2 getPlaylistMetadata() {
+                return testMetadata;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testGetPlaylistMetadata")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                .setPlaylistAgent(agent)
+                .build()) {
+            MediaController2 controller = createController(session.getToken(), true, callback);
+            agent.notifyPlaylistMetadataChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertEquals(metadataFromCallback.get().getMediaId(),
+                    controller.getPlaylistMetadata().getMediaId());
+        }
+    }
+
+    @Test
+    public void testSetPlaybackSpeed() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mController.setPlaybackSpeed(speed);
+        assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+    }
+
+    /**
+     * Test whether {@link MediaSession2#setPlaylist(List, MediaMetadata2)} is notified
+     * through the
+     * {@link ControllerCallback#onPlaylistMetadataChanged(MediaController2, MediaMetadata2)}
+     * if the controller doesn't have {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST} but
+     * {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA}.
+     */
+    @Test
+    public void testControllerCallback_onPlaylistMetadataChanged() throws InterruptedException {
+        prepareLooper();
+        final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaylistMetadataChanged(MediaController2 controller,
+                    MediaMetadata2 metadata) {
+                assertNotNull(metadata);
+                assertEquals(item.getMediaId(), metadata.getMediaId());
+                latch.countDown();
+            }
+        };
+        final SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public SessionCommandGroup2 onConnect(MediaSession2 session,
+                    ControllerInfo controller) {
+                if (Process.myUid() == controller.getUid()) {
+                    SessionCommandGroup2 commands = new SessionCommandGroup2();
+                    commands.addCommand(new SessionCommand2(
+                              SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA));
+                    return commands;
+                }
+                return super.onConnect(session, controller);
+            }
+        };
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public MediaMetadata2 getPlaylistMetadata() {
+                return item.getMetadata();
+            }
+
+            @Override
+            public List<MediaItem2> getPlaylist() {
+                return list;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testControllerCallback_onPlaylistMetadataChanged")
+                .setSessionCallback(sHandlerExecutor, sessionCallback)
+                .setPlaylistAgent(agent)
+                .build()) {
+            MediaController2 controller = createController(session.getToken(), true, callback);
+            agent.notifyPlaylistMetadataChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testAddPlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final int testIndex = 12;
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mController.addPlaylistItem(testIndex, testMediaItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mAddPlaylistItemCalled);
+        assertEquals(testIndex, mMockAgent.mIndex);
+        // MediaController2.addPlaylistItem does not ensure the equality of the items.
+        assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+    }
+
+    @Test
+    public void testRemovePlaylistItem() throws InterruptedException {
+        prepareLooper();
+        mMockAgent.mPlaylist = TestUtils.createPlaylist(2);
+
+        // Recreate controller for sending removePlaylistItem.
+        // It's easier to ensure that MediaController2.getPlaylist() returns the playlist from the
+        // agent.
+        MediaController2 controller = createController(mSession.getToken());
+        MediaItem2 targetItem = controller.getPlaylist().get(0);
+        controller.removePlaylistItem(targetItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+        assertEquals(targetItem, mMockAgent.mItem);
+    }
+
+    @Test
+    public void testReplacePlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final int testIndex = 12;
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mController.replacePlaylistItem(testIndex, testMediaItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+        // MediaController2.replacePlaylistItem does not ensure the equality of the items.
+        assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+    }
+
+    @Test
+    public void testSkipToPreviousItem() throws InterruptedException {
+        prepareLooper();
+        mController.skipToPreviousItem();
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+    }
+
+    @Test
+    public void testSkipToNextItem() throws InterruptedException {
+        prepareLooper();
+        mController.skipToNextItem();
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mMockAgent.mSkipToNextItemCalled);
+    }
+
+    @Test
+    public void testSkipToPlaylistItem() throws InterruptedException {
+        prepareLooper();
+        MediaController2 controller = createController(mSession.getToken());
+        MediaItem2 targetItem = TestUtils.createMediaItemWithMetadata();
+        controller.skipToPlaylistItem(targetItem);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+        assertEquals(targetItem, mMockAgent.mItem);
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onShuffleModeChanged(MediaController2, int)}.
+     */
+    @Test
+    public void testGetShuffleMode() throws InterruptedException {
+        prepareLooper();
+        final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public int getShuffleMode() {
+                return testShuffleMode;
+            }
+        };
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+                assertEquals(testShuffleMode, shuffleMode);
+                latch.countDown();
+            }
+        };
+        mSession.updatePlayer(mPlayer, agent, null);
+        MediaController2 controller = createController(mSession.getToken(), true, callback);
+        agent.notifyShuffleModeChanged();
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(testShuffleMode, controller.getShuffleMode());
+    }
+
+    @Test
+    public void testSetShuffleMode() throws InterruptedException {
+        prepareLooper();
+        final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+        mController.setShuffleMode(testShuffleMode);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSetShuffleModeCalled);
+        assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onRepeatModeChanged(MediaController2, int)}.
+     */
+    @Test
+    public void testGetRepeatMode() throws InterruptedException {
+        prepareLooper();
+        final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+        final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+            @Override
+            public int getRepeatMode() {
+                return testRepeatMode;
+            }
+        };
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+                assertEquals(testRepeatMode, repeatMode);
+                latch.countDown();
+            }
+        };
+        mSession.updatePlayer(mPlayer, agent, null);
+        MediaController2 controller = createController(mSession.getToken(), true, callback);
+        agent.notifyRepeatModeChanged();
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(testRepeatMode, controller.getRepeatMode());
+    }
+
+    @Test
+    public void testSetRepeatMode() throws InterruptedException {
+        prepareLooper();
+        final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+        mController.setRepeatMode(testRepeatMode);
+        assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertTrue(mMockAgent.mSetRepeatModeCalled);
+        assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+    }
+
+    @Test
+    public void testSetVolumeTo() throws Exception {
+        // TODO(jaewan): Also test with local volume.
+        prepareLooper();
+        final int maxVolume = 100;
+        final int currentVolume = 23;
+        final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+        TestVolumeProvider volumeProvider =
+                new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+        mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+        final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+        final int targetVolume = 50;
+        controller.setVolumeTo(targetVolume, 0 /* flags */);
+        assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(volumeProvider.mSetVolumeToCalled);
+        assertEquals(targetVolume, volumeProvider.mVolume);
+    }
+
+    @Test
+    public void testAdjustVolume() throws Exception {
+        // TODO(jaewan): Also test with local volume.
+        prepareLooper();
+        final int maxVolume = 100;
+        final int currentVolume = 23;
+        final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+        TestVolumeProvider volumeProvider =
+                new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+        mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+        final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+        final int direction = AudioManager.ADJUST_RAISE;
+        controller.adjustVolume(direction, 0 /* flags */);
+        assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(volumeProvider.mAdjustVolumeCalled);
+        assertEquals(direction, volumeProvider.mDirection);
+    }
+
+    @Test
+    public void testGetPackageName() {
+        prepareLooper();
+        assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+    }
+
+    @Test
+    public void testSendCustomCommand() throws InterruptedException {
+        prepareLooper();
+        // TODO(jaewan): Need to revisit with the permission.
+        final SessionCommand2 testCommand =
+                new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+        final Bundle testArgs = new Bundle();
+        testArgs.putString("args", "testSendCustomCommand");
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onCustomCommand(MediaSession2 session, ControllerInfo controller,
+                    SessionCommand2 customCommand, Bundle args, ResultReceiver cb) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(testCommand, customCommand);
+                assertTrue(TestUtils.equals(testArgs, args));
+                assertNull(cb);
+                latch.countDown();
+            }
+        };
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+        final MediaController2 controller = createController(mSession.getToken());
+        controller.sendCustomCommand(testCommand, testArgs, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onConnected() throws InterruptedException {
+        prepareLooper();
+        // createController() uses controller callback to wait until the controller becomes
+        // available.
+        MediaController2 controller = createController(mSession.getToken());
+        assertNotNull(controller);
+    }
+
+    @Test
+    public void testControllerCallback_sessionRejects() throws InterruptedException {
+        prepareLooper();
+        final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public SessionCommandGroup2 onConnect(MediaSession2 session,
+                    ControllerInfo controller) {
+                return null;
+            }
+        };
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                        .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+            }
+        });
+        MediaController2 controller =
+                createController(mSession.getToken(), false, null);
+        assertNotNull(controller);
+        waitForConnect(controller, false);
+        waitForDisconnect(controller, true);
+    }
+
+    @Test
+    public void testControllerCallback_releaseSession() throws InterruptedException {
+        prepareLooper();
+        mSession.close();
+        waitForDisconnect(mController, true);
+    }
+
+    @Test
+    public void testControllerCallback_close() throws InterruptedException {
+        prepareLooper();
+        mController.close();
+        waitForDisconnect(mController, true);
+    }
+
+    @Test
+    public void testFastForward() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onFastForward(MediaSession2 session, ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testFastForward").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.fastForward();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testRewind() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onRewind(MediaSession2 session, ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testRewind").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.rewind();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPlayFromSearch() throws InterruptedException {
+        prepareLooper();
+        final String request = "random query";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+                    String query, Bundle extras) {
+                super.onPlayFromSearch(session, controller, query, extras);
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, query);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPlayFromSearch").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.playFromSearch(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPlayFromUri() throws InterruptedException {
+        prepareLooper();
+        final Uri request = Uri.parse("foo://boo");
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPlayFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+                    Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, uri);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPlayFromUri").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.playFromUri(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPlayFromMediaId() throws InterruptedException {
+        prepareLooper();
+        final String request = "media_id";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+                    String mediaId, Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, mediaId);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPlayFromMediaId").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.playFromMediaId(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPrepareFromSearch() throws InterruptedException {
+        prepareLooper();
+        final String request = "random query";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+                    String query, Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, query);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPrepareFromSearch").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.prepareFromSearch(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPrepareFromUri() throws InterruptedException {
+        prepareLooper();
+        final Uri request = Uri.parse("foo://boo");
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+                    Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, uri);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPrepareFromUri").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.prepareFromUri(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testPrepareFromMediaId() throws InterruptedException {
+        prepareLooper();
+        final String request = "media_id";
+        final Bundle bundle = new Bundle();
+        bundle.putString("key", "value");
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+                    String mediaId, Bundle extras) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(request, mediaId);
+                assertTrue(TestUtils.equals(bundle, extras));
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testPrepareFromMediaId").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.prepareFromMediaId(request, bundle);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testSetRating() throws InterruptedException {
+        prepareLooper();
+        final int ratingType = Rating2.RATING_5_STARS;
+        final float ratingValue = 3.5f;
+        final Rating2 rating = Rating2.newStarRating(ratingType, ratingValue);
+        final String mediaId = "media_id";
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onSetRating(MediaSession2 session, ControllerInfo controller,
+                    String mediaIdOut, Rating2 ratingOut) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(mediaId, mediaIdOut);
+                assertEquals(rating, ratingOut);
+                latch.countDown();
+            }
+        };
+
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testSetRating").build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.setRating(mediaId, rating);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testIsConnected() throws InterruptedException {
+        prepareLooper();
+        assertTrue(mController.isConnected());
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+            }
+        });
+        waitForDisconnect(mController, true);
+        assertFalse(mController.isConnected());
+    }
+
+    /**
+     * Test potential deadlock for calls between controller and session.
+     */
+    @Test
+    public void testDeadlock() throws InterruptedException {
+        prepareLooper();
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mSession = null;
+            }
+        });
+
+        // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+        final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+        final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+        sessionThread.start();
+        testThread.start();
+        final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+        final Handler testHandler = new Handler(testThread.getLooper());
+        final CountDownLatch latch = new CountDownLatch(1);
+        try {
+            final MockPlayer player = new MockPlayer(0);
+            sessionHandler.postAndSync(new Runnable() {
+                @Override
+                public void run() {
+                    mSession = new MediaSession2.Builder(mContext)
+                            .setPlayer(mPlayer)
+                            .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                            .setId("testDeadlock").build();
+                }
+            });
+            final MediaController2 controller = createController(mSession.getToken());
+            testHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    final int state = MediaPlayerBase.PLAYER_STATE_ERROR;
+                    for (int i = 0; i < 100; i++) {
+                        // triggers call from session to controller.
+                        player.notifyPlaybackState(state);
+                        // triggers call from controller to session.
+                        controller.play();
+
+                        // Repeat above
+                        player.notifyPlaybackState(state);
+                        controller.pause();
+                        player.notifyPlaybackState(state);
+                        controller.reset();
+                        player.notifyPlaybackState(state);
+                        controller.skipToNextItem();
+                        player.notifyPlaybackState(state);
+                        controller.skipToPreviousItem();
+                    }
+                    // This may hang if deadlock happens.
+                    latch.countDown();
+                }
+            });
+            assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            if (mSession != null) {
+                sessionHandler.postAndSync(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Clean up here because sessionHandler will be removed afterwards.
+                        mSession.close();
+                        mSession = null;
+                    }
+                });
+            }
+            if (sessionThread != null) {
+                sessionThread.quitSafely();
+            }
+            if (testThread != null) {
+                testThread.quitSafely();
+            }
+        }
+    }
+
+    @Test
+    public void testGetServiceToken() {
+        prepareLooper();
+        SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+        assertNotNull(token);
+        assertEquals(mContext.getPackageName(), token.getPackageName());
+        assertEquals(MockMediaSessionService2.ID, token.getId());
+        assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+    }
+
+    @Test
+    public void testConnectToService_sessionService() throws InterruptedException {
+        prepareLooper();
+        testConnectToService(MockMediaSessionService2.ID);
+    }
+
+    @Test
+    public void testConnectToService_libraryService() throws InterruptedException {
+        prepareLooper();
+        testConnectToService(MockMediaLibraryService2.ID);
+    }
+
+    public void testConnectToService(String id) throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+            @Override
+            public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+                    @NonNull ControllerInfo controller) {
+                if (Process.myUid() == controller.getUid()) {
+                    if (mSession != null) {
+                        mSession.close();
+                    }
+                    mSession = session;
+                    mPlayer = (MockPlayer) session.getPlayer();
+                    assertEquals(mContext.getPackageName(), controller.getPackageName());
+                    assertFalse(controller.isTrusted());
+                    latch.countDown();
+                }
+                return super.onConnect(session, controller);
+            }
+        };
+        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+
+        mController = createController(TestUtils.getServiceToken(mContext, id));
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        // Test command from controller to session service
+        // TODO: Re enable when transport control works
+        /*
+        mController.play();
+        assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mPlayer.mPlayCalled);
+        */
+
+        // Test command from session service to controller
+        // TODO(jaewan): Add equivalent tests again
+        /*
+        final CountDownLatch latch = new CountDownLatch(1);
+        mController.registerPlayerEventCallback((state) -> {
+            assertNotNull(state);
+            assertEquals(PlaybackState.STATE_REWINDING, state.getState());
+            latch.countDown();
+        }, sHandler);
+        mPlayer.notifyPlaybackState(
+                TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        */
+    }
+
+    @Test
+    public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+        prepareLooper();
+        testControllerAfterSessionIsClosed(mSession.getToken().getId());
+    }
+
+    // TODO(jaewan): Re-enable this test
+    @Ignore
+    @Test
+    public void testControllerAfterSessionIsClosed_sessionService() throws InterruptedException {
+        prepareLooper();
+        /*
+        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+        testControllerAfterSessionIsClosed(MockMediaSessionService2.ID);
+        */
+    }
+
+    @Test
+    public void testSubscribeRouteInfo() throws InterruptedException {
+        prepareLooper();
+        final TestSessionCallback callback = new TestSessionCallback() {
+            @Override
+            public void onSubscribeRoutesInfo(@NonNull MediaSession2 session,
+                    @NonNull ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                mLatch.countDown();
+            }
+
+            @Override
+            public void onUnsubscribeRoutesInfo(@NonNull MediaSession2 session,
+                    @NonNull ControllerInfo controller) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                mLatch.countDown();
+            }
+        };
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+        final MediaController2 controller = createController(mSession.getToken());
+
+        callback.resetLatchCount(1);
+        controller.subscribeRoutesInfo();
+        assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        callback.resetLatchCount(1);
+        controller.unsubscribeRoutesInfo();
+        assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSelectRouteInfo() throws InterruptedException {
+        prepareLooper();
+        final Bundle testRoute = new Bundle();
+        testRoute.putString("id", "testRoute");
+        final TestSessionCallback callback = new TestSessionCallback() {
+            @Override
+            public void onSelectRoute(@NonNull MediaSession2 session,
+                    @NonNull ControllerInfo controller, @NonNull Bundle route) {
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertTrue(TestUtils.equals(route, testRoute));
+                mLatch.countDown();
+            }
+        };
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+        final MediaController2 controller = createController(mSession.getToken());
+
+        callback.resetLatchCount(1);
+        controller.selectRoute(testRoute);
+        assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testClose_beforeConnected() throws InterruptedException {
+        prepareLooper();
+        MediaController2 controller =
+                createController(mSession.getToken(), false, null);
+        controller.close();
+    }
+
+    @Test
+    public void testClose_twice() {
+        prepareLooper();
+        mController.close();
+        mController.close();
+    }
+
+    @Test
+    public void testClose_session() throws InterruptedException {
+        prepareLooper();
+        final String id = mSession.getToken().getId();
+        mController.close();
+        // close is done immediately for session.
+        testNoInteraction();
+
+        // Test whether the controller is notified about later close of the session or
+        // re-creation.
+        testControllerAfterSessionIsClosed(id);
+    }
+
+    @Ignore
+    @Test
+    public void testClose_sessionService() throws InterruptedException {
+        prepareLooper();
+        testCloseFromService(MockMediaSessionService2.ID);
+    }
+
+    @Ignore
+    @Test
+    public void testClose_libraryService() throws InterruptedException {
+        prepareLooper();
+        testCloseFromService(MockMediaLibraryService2.ID);
+    }
+
+    private void testCloseFromService(String id) throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        TestServiceRegistry.getInstance().setSessionServiceCallback(new SessionServiceCallback() {
+            @Override
+            public void onCreated() {
+                // Do nothing.
+            }
+
+            @Override
+            public void onDestroyed() {
+                latch.countDown();
+            }
+        });
+        mController = createController(TestUtils.getServiceToken(mContext, id));
+        mController.close();
+        // Wait until close triggers onDestroy() of the session service.
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+        testNoInteraction();
+
+        // Test whether the controller is notified about later close of the session or
+        // re-creation.
+        testControllerAfterSessionIsClosed(id);
+    }
+
+    private void testControllerAfterSessionIsClosed(final String id) throws InterruptedException {
+        // This cause session service to be died.
+        mSession.close();
+        waitForDisconnect(mController, true);
+        testNoInteraction();
+
+        // Ensure that the controller cannot use newly create session with the same ID.
+        // Recreated session has different session stub, so previously created controller
+        // shouldn't be available.
+        mSession = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+                .setId(id).build();
+        testNoInteraction();
+    }
+
+    // Test that mSession and mController doesn't interact.
+    // Note that this method can be called after the mSession is died, so mSession may not have
+    // valid player.
+    private void testNoInteraction() throws InterruptedException {
+        // TODO: check that calls from the controller to session shouldn't be delivered.
+
+        // Calls from the session to controller shouldn't be delivered.
+        final CountDownLatch latch = new CountDownLatch(1);
+        setRunnableForOnCustomCommand(mController, new Runnable() {
+            @Override
+            public void run() {
+                latch.countDown();
+            }
+        });
+        SessionCommand2 customCommand = new SessionCommand2("testNoInteraction", null);
+        mSession.sendCustomCommand(customCommand, null);
+        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        setRunnableForOnCustomCommand(mController, null);
+    }
+
+    // TODO(jaewan): Add  test for service connect rejection, when we differentiate session
+    //               active/inactive and connection accept/refuse
+
+    class TestVolumeProvider extends VolumeProviderCompat {
+        final CountDownLatch mLatch = new CountDownLatch(1);
+        boolean mSetVolumeToCalled;
+        boolean mAdjustVolumeCalled;
+        int mVolume;
+        int mDirection;
+
+        TestVolumeProvider(int controlType, int maxVolume, int currentVolume) {
+            super(controlType, maxVolume, currentVolume);
+        }
+
+        @Override
+        public void onSetVolumeTo(int volume) {
+            mSetVolumeToCalled = true;
+            mVolume = volume;
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onAdjustVolume(int direction) {
+            mAdjustVolumeCalled = true;
+            mDirection = direction;
+            mLatch.countDown();
+        }
+    }
+
+    class TestSessionCallback extends SessionCallback {
+        CountDownLatch mLatch;
+
+        void resetLatchCount(int count) {
+            mLatch = new CountDownLatch(count);
+        }
+    }
+}
diff --git a/androidx/media/MediaInterface2.java b/androidx/media/MediaInterface2.java
new file mode 100644
index 0000000..93047a1
--- /dev/null
+++ b/androidx/media/MediaInterface2.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import java.util.List;
+
+class MediaInterface2 {
+    private MediaInterface2() {
+    }
+
+    // TODO: relocate methods among different interfaces and classes.
+    interface SessionPlaybackControl {
+        void prepare();
+        void play();
+        void pause();
+        void reset();
+
+        void seekTo(long pos);
+
+        int getPlayerState();
+        long getCurrentPosition();
+        long getDuration();
+
+        long getBufferedPosition();
+        int getBufferingState();
+
+        float getPlaybackSpeed();
+        void setPlaybackSpeed(float speed);
+    }
+
+    interface SessionPlaylistControl {
+        void setOnDataSourceMissingHelper(MediaSession2.OnDataSourceMissingHelper helper);
+        void clearOnDataSourceMissingHelper();
+
+        List<MediaItem2> getPlaylist();
+        MediaMetadata2 getPlaylistMetadata();
+        void setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata);
+        void updatePlaylistMetadata(MediaMetadata2 metadata);
+
+        MediaItem2 getCurrentMediaItem();
+        void skipToPlaylistItem(MediaItem2 item);
+        void skipToPreviousItem();
+        void skipToNextItem();
+
+        void addPlaylistItem(int index, MediaItem2 item);
+        void removePlaylistItem(MediaItem2 item);
+        void replacePlaylistItem(int index, MediaItem2 item);
+
+        int getRepeatMode();
+        void setRepeatMode(int repeatMode);
+        int getShuffleMode();
+        void setShuffleMode(int shuffleMode);
+    }
+
+    // Common interface for session2 and controller2
+    // TODO: consider to add fastForward, rewind.
+    abstract static class SessionPlayer implements SessionPlaybackControl, SessionPlaylistControl {
+        abstract void skipForward();
+        abstract void skipBackward();
+        abstract void notifyError(@MediaSession2.ErrorCode int errorCode, @Nullable Bundle extras);
+    }
+}
diff --git a/androidx/media/MediaItem2.java b/androidx/media/MediaItem2.java
new file mode 100644
index 0000000..b8c44c1
--- /dev/null
+++ b/androidx/media/MediaItem2.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.UUID;
+
+/**
+ * A class with information on a single media item with the metadata information.
+ * Media item are application dependent so we cannot guarantee that they contain the right values.
+ * <p>
+ * When it's sent to a controller or browser, it's anonymized and data descriptor wouldn't be sent.
+ * <p>
+ * This object isn't a thread safe.
+ */
+public class MediaItem2 {
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+    public @interface Flags { }
+
+    /**
+     * Flag: Indicates that the item has children of its own.
+     */
+    public static final int FLAG_BROWSABLE = 1 << 0;
+
+    /**
+     * Flag: Indicates that the item is playable.
+     * <p>
+     * The id of this item may be passed to
+     * {@link MediaController2#playFromMediaId(String, Bundle)}
+     */
+    public static final int FLAG_PLAYABLE = 1 << 1;
+
+    private static final String KEY_ID = "android.media.mediaitem2.id";
+    private static final String KEY_FLAGS = "android.media.mediaitem2.flags";
+    private static final String KEY_METADATA = "android.media.mediaitem2.metadata";
+    private static final String KEY_UUID = "android.media.mediaitem2.uuid";
+
+    private final String mId;
+    private final int mFlags;
+    private final UUID mUUID;
+    private MediaMetadata2 mMetadata;
+    private DataSourceDesc mDataSourceDesc;
+
+    private MediaItem2(@NonNull String mediaId, @Nullable DataSourceDesc dsd,
+            @Nullable MediaMetadata2 metadata, @Flags int flags) {
+        this(mediaId, dsd, metadata, flags, null);
+    }
+
+    private MediaItem2(@NonNull String mediaId, @Nullable DataSourceDesc dsd,
+            @Nullable MediaMetadata2 metadata, @Flags int flags, @Nullable UUID uuid) {
+        if (mediaId == null) {
+            throw new IllegalArgumentException("mediaId shouldn't be null");
+        }
+        if (metadata != null && !TextUtils.equals(mediaId, metadata.getMediaId())) {
+            throw new IllegalArgumentException("metadata's id should be matched with the mediaid");
+        }
+
+        mId = mediaId;
+        mDataSourceDesc = dsd;
+        mMetadata = metadata;
+        mFlags = flags;
+        mUUID = (uuid == null) ? UUID.randomUUID() : uuid;
+    }
+    /**
+     * Return this object as a bundle to share between processes.
+     *
+     * @return a new bundle instance
+     */
+    public Bundle toBundle() {
+        Bundle bundle = new Bundle();
+        bundle.putString(KEY_ID, mId);
+        bundle.putInt(KEY_FLAGS, mFlags);
+        if (mMetadata != null) {
+            bundle.putBundle(KEY_METADATA, mMetadata.toBundle());
+        }
+        bundle.putString(KEY_UUID, mUUID.toString());
+        return bundle;
+    }
+
+    /**
+     * Create a MediaItem2 from the {@link Bundle}.
+     *
+     * @param bundle The bundle which was published by {@link MediaItem2#toBundle()}.
+     * @return The newly created MediaItem2
+     */
+    public static MediaItem2 fromBundle(Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        final String uuidString = bundle.getString(KEY_UUID);
+        return fromBundle(bundle, UUID.fromString(uuidString));
+    }
+
+    /**
+     * Create a MediaItem2 from the {@link Bundle} with the specified {@link UUID}.
+     * If {@link UUID}
+     * can be null for creating new.
+     *
+     * @param bundle The bundle which was published by {@link MediaItem2#toBundle()}.
+     * @param uuid A {@link UUID} to override. Can be {@link null} for override.
+     * @return The newly created MediaItem2
+     */
+    static MediaItem2 fromBundle(@NonNull Bundle bundle, @Nullable UUID uuid) {
+        if (bundle == null) {
+            return null;
+        }
+        final String id = bundle.getString(KEY_ID);
+        final Bundle metadataBundle = bundle.getBundle(KEY_METADATA);
+        final MediaMetadata2 metadata = metadataBundle != null
+                ? MediaMetadata2.fromBundle(metadataBundle) : null;
+        final int flags = bundle.getInt(KEY_FLAGS);
+        return new MediaItem2(id, null, metadata, flags, uuid);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("MediaItem2{");
+        sb.append("mFlags=").append(mFlags);
+        sb.append(", mMetadata=").append(mMetadata);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    /**
+     * Gets the flags of the item.
+     */
+    public @Flags int getFlags() {
+        return mFlags;
+    }
+
+    /**
+     * Returns whether this item is browsable.
+     * @see #FLAG_BROWSABLE
+     */
+    public boolean isBrowsable() {
+        return (mFlags & FLAG_BROWSABLE) != 0;
+    }
+
+    /**
+     * Returns whether this item is playable.
+     * @see #FLAG_PLAYABLE
+     */
+    public boolean isPlayable() {
+        return (mFlags & FLAG_PLAYABLE) != 0;
+    }
+
+    /**
+     * Set a metadata. If the metadata is not null, its id should be matched with this instance's
+     * media id.
+     *
+     * @param metadata metadata to update
+     */
+    public void setMetadata(@Nullable MediaMetadata2 metadata) {
+        if (metadata != null && !TextUtils.equals(mId, metadata.getMediaId())) {
+            throw new IllegalArgumentException("metadata's id should be matched with the mediaId");
+        }
+        mMetadata = metadata;
+    }
+
+    /**
+     * Returns the metadata of the media.
+     */
+    public @Nullable MediaMetadata2 getMetadata() {
+        return mMetadata;
+    }
+
+    /**
+     * Returns the media id for this item.
+     */
+    public /*@NonNull*/ String getMediaId() {
+        return mId;
+    }
+
+    /**
+     * Return the {@link DataSourceDesc}
+     * <p>
+     * Can be {@code null} if the MediaItem2 came from another process and anonymized
+     *
+     * @return data source descriptor
+     */
+    public @Nullable DataSourceDesc getDataSourceDesc() {
+        return mDataSourceDesc;
+    }
+
+    @Override
+    public int hashCode() {
+        return mUUID.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof MediaItem2)) {
+            return false;
+        }
+        MediaItem2 other = (MediaItem2) obj;
+        return mUUID.equals(other.mUUID);
+    }
+
+    /**
+     * Build {@link MediaItem2}
+     */
+    public static final class Builder {
+        private @Flags int mFlags;
+        private String mMediaId;
+        private MediaMetadata2 mMetadata;
+        private DataSourceDesc mDataSourceDesc;
+
+        /**
+         * Constructor for {@link Builder}
+         *
+         * @param flags
+         */
+        public Builder(@Flags int flags) {
+            mFlags = flags;
+        }
+
+        /**
+         * Set the media id of this instance. {@code null} for unset.
+         * <p>
+         * Media id is used to identify a media contents between session and controller.
+         * <p>
+         * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+         * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+         * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+         * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+         *
+         * @param mediaId media id
+         * @return this instance for chaining
+         */
+        public Builder setMediaId(@Nullable String mediaId) {
+            mMediaId = mediaId;
+            return this;
+        }
+
+        /**
+         * Set the metadata of this instance. {@code null} for unset.
+         * <p>
+         * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+         * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+         * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+         * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+         *
+         * @param metadata metadata
+         * @return this instance for chaining
+         */
+        public Builder setMetadata(@Nullable MediaMetadata2 metadata) {
+            mMetadata = metadata;
+            return this;
+        }
+
+        /**
+         * Set the data source descriptor for this instance. {@code null} for unset.
+         *
+         * @param dataSourceDesc data source descriptor
+         * @return this instance for chaining
+         */
+        public Builder setDataSourceDesc(@Nullable DataSourceDesc dataSourceDesc) {
+            mDataSourceDesc = dataSourceDesc;
+            return this;
+        }
+
+        /**
+         * Build {@link MediaItem2}.
+         *
+         * @return a new {@link MediaItem2}.
+         */
+        public MediaItem2 build() {
+            String id = (mMetadata != null)
+                    ? mMetadata.getString(MediaMetadata2.METADATA_KEY_MEDIA_ID) : null;
+            if (id == null) {
+                id = (mMediaId != null) ? mMediaId : toString();
+            }
+            return new MediaItem2(id, mDataSourceDesc, mMetadata, mFlags);
+        }
+    }
+}
diff --git a/androidx/media/MediaLibraryService2.java b/androidx/media/MediaLibraryService2.java
new file mode 100644
index 0000000..edd97c3
--- /dev/null
+++ b/androidx/media/MediaLibraryService2.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE;
+import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.Builder;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ * Base class for media library services.
+ * <p>
+ * Media library services enable applications to browse media content provided by an application
+ * and ask the application to start playing it. They may also be used to control content that
+ * is already playing by way of a {@link MediaSession2}.
+ * <p>
+ * When extending this class, also add the following to your {@code AndroidManifest.xml}.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ *   &lt;intent-filter&gt;
+ *     &lt;action android:name="android.media.MediaLibraryService2" /&gt;
+ *   &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * The {@link MediaLibraryService2} class derives from {@link MediaSessionService2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ *
+ * @see MediaSessionService2
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MediaLibraryService2 extends MediaSessionService2 {
+    /**
+     * This is the interface name that a service implementing a session service should say that it
+     * support -- that is, this is the action it uses for its intent filter.
+     */
+    public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
+
+    // TODO: Revisit this value.
+
+    /**
+     * Session for the {@link MediaLibraryService2}. Build this object with
+     * {@link Builder} and return in {@link #onCreateSession(String)}.
+     */
+    public static final class MediaLibrarySession extends MediaSession2 {
+        /**
+         * Callback for the {@link MediaLibrarySession}.
+         */
+        public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
+            /**
+             * Called to get the root information for browsing by a particular client.
+             * <p>
+             * The implementation should verify that the client package has permission
+             * to access browse media information before returning the root id; it
+             * should return null if the client is not allowed to access this
+             * information.
+             * <p>
+             * Note: this callback may be called on the main thread, regardless of the callback
+             * executor.
+             *
+             * @param session the session for this event
+             * @param controllerInfo information of the controller requesting access to browse
+             *                       media.
+             * @param extras An optional bundle of service-specific arguments to send
+             * to the media library service when connecting and retrieving the
+             * root id for browsing, or null if none. The contents of this
+             * bundle may affect the information returned when browsing.
+             * @return The {@link LibraryRoot} for accessing this app's content or null.
+             * @see LibraryRoot#EXTRA_RECENT
+             * @see LibraryRoot#EXTRA_OFFLINE
+             * @see LibraryRoot#EXTRA_SUGGESTED
+             * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT
+             */
+            public @Nullable LibraryRoot onGetLibraryRoot(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controllerInfo, @Nullable Bundle extras) {
+                return null;
+            }
+
+            /**
+             * Called to get an item. Return result here for the browser.
+             * <p>
+             * Return {@code null} for no result or error.
+             *
+             * @param session the session for this event
+             * @param mediaId item id to get media item.
+             * @return a media item. {@code null} for no result or error.
+             * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_ITEM
+             */
+            public @Nullable MediaItem2 onGetItem(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controllerInfo, @NonNull String mediaId) {
+                return null;
+            }
+
+            /**
+             * Called to get children of given parent id. Return the children here for the browser.
+             * <p>
+             * Return an empty list for no children, and return {@code null} for the error.
+             *
+             * @param session the session for this event
+             * @param parentId parent id to get children
+             * @param page number of page
+             * @param pageSize size of the page
+             * @param extras extra bundle
+             * @return list of children. Can be {@code null}.
+             * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_CHILDREN
+             */
+            public @Nullable List<MediaItem2> onGetChildren(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId, int page,
+                    int pageSize, @Nullable Bundle extras) {
+                return null;
+            }
+
+            /**
+             * Called when a controller subscribes to the parent.
+             * <p>
+             * It's your responsibility to keep subscriptions by your own and call
+             * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)}
+             * when the parent is changed.
+             *
+             * @param session the session for this event
+             * @param controller controller
+             * @param parentId parent id
+             * @param extras extra bundle
+             * @see SessionCommand2#COMMAND_CODE_LIBRARY_SUBSCRIBE
+             */
+            public void onSubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId,
+                    @Nullable Bundle extras) {
+            }
+
+            /**
+             * Called when a controller unsubscribes to the parent.
+             *
+             * @param session the session for this event
+             * @param controller controller
+             * @param parentId parent id
+             * @see SessionCommand2#COMMAND_CODE_LIBRARY_UNSUBSCRIBE
+             */
+            // TODO: Make this to be called.
+            public void onUnsubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId) {
+            }
+
+            /**
+             * Called when a controller requests search.
+             *
+             * @param session the session for this event
+             * @param query The search query sent from the media browser. It contains keywords
+             *              separated by space.
+             * @param extras The bundle of service-specific arguments sent from the media browser.
+             * @see SessionCommand2#COMMAND_CODE_LIBRARY_SEARCH
+             */
+            public void onSearch(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controllerInfo, @NonNull String query,
+                    @Nullable Bundle extras) {
+            }
+
+            /**
+             * Called to get the search result. Return search result here for the browser which has
+             * requested search previously.
+             * <p>
+             * Return an empty list for no search result, and return {@code null} for the error.
+             *
+             * @param session the session for this event
+             * @param controllerInfo Information of the controller requesting the search result.
+             * @param query The search query which was previously sent through
+             *              {@link #onSearch(MediaLibrarySession, ControllerInfo, String, Bundle)}.
+             * @param page page number. Starts from {@code 1}.
+             * @param pageSize page size. Should be greater or equal to {@code 1}.
+             * @param extras The bundle of service-specific arguments sent from the media browser.
+             * @return search result. {@code null} for error.
+             * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT
+             */
+            public @Nullable List<MediaItem2> onGetSearchResult(
+                    @NonNull MediaLibrarySession session, @NonNull ControllerInfo controllerInfo,
+                    @NonNull String query, int page, int pageSize, @Nullable Bundle extras) {
+                return null;
+            }
+        }
+
+        /**
+         * Builder for {@link MediaLibrarySession}.
+         */
+        // Override all methods just to show them with the type instead of generics in Javadoc.
+        // This workarounds javadoc issue described in the MediaSession2.BuilderBase.
+        public static final class Builder extends MediaSession2.BuilderBase<MediaLibrarySession,
+                Builder, MediaLibrarySessionCallback> {
+            private MediaLibrarySessionImplBase.Builder mImpl;
+
+            // Builder requires MediaLibraryService2 instead of Context just to ensure that the
+            // builder can be only instantiated within the MediaLibraryService2.
+            // Ideally it's better to make it inner class of service to enforce, it violates API
+            // guideline that Builders should be the inner class of the building target.
+            public Builder(@NonNull MediaLibraryService2 service,
+                    @NonNull Executor callbackExecutor,
+                    @NonNull MediaLibrarySessionCallback callback) {
+                super(service);
+                mImpl = new MediaLibrarySessionImplBase.Builder(service);
+                setImpl(mImpl);
+                setSessionCallback(callbackExecutor, callback);
+            }
+
+            @Override
+            public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+                return super.setPlayer(player);
+            }
+
+            @Override
+            public @NonNull Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+                return super.setPlaylistAgent(playlistAgent);
+            }
+
+            @Override
+            public @NonNull Builder setVolumeProvider(
+                    @Nullable VolumeProviderCompat volumeProvider) {
+                return super.setVolumeProvider(volumeProvider);
+            }
+
+            @Override
+            public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) {
+                return super.setSessionActivity(pi);
+            }
+
+            @Override
+            public @NonNull Builder setId(@NonNull String id) {
+                return super.setId(id);
+            }
+
+            @Override
+            public @NonNull Builder setSessionCallback(@NonNull Executor executor,
+                    @NonNull MediaLibrarySessionCallback callback) {
+                return super.setSessionCallback(executor, callback);
+            }
+
+            @Override
+            public @NonNull MediaLibrarySession build() {
+                return super.build();
+            }
+        }
+
+        MediaLibrarySession(SupportLibraryImpl impl) {
+            super(impl);
+        }
+
+        /**
+         * Notify the controller of the change in a parent's children.
+         * <p>
+         * If the controller hasn't subscribed to the parent, the API will do nothing.
+         * <p>
+         * Controllers will use {@link MediaBrowser2#getChildren(String, int, int, Bundle)} to get
+         * the list of children.
+         *
+         * @param controller controller to notify
+         * @param parentId parent id with changes in its children
+         * @param itemCount number of children.
+         * @param extras extra information from session to controller
+         */
+        public void notifyChildrenChanged(@NonNull ControllerInfo controller,
+                @NonNull String parentId, int itemCount, @Nullable Bundle extras) {
+            Bundle options = new Bundle(extras);
+            options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
+            options.putBundle(MediaBrowser2.EXTRA_TARGET, controller.toBundle());
+        }
+
+        /**
+         * Notify all controllers that subscribed to the parent about change in the parent's
+         * children, regardless of the extra bundle supplied by
+         * {@link MediaBrowser2#subscribe(String, Bundle)}.
+         *
+         * @param parentId parent id
+         * @param itemCount number of children
+         * @param extras extra information from session to controller
+         */
+        // This is for the backward compatibility.
+        public void notifyChildrenChanged(@NonNull String parentId, int itemCount,
+                @Nullable Bundle extras) {
+            Bundle options = new Bundle(extras);
+            options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
+            getServiceCompat().notifyChildrenChanged(parentId, options);
+        }
+
+        /**
+         * Notify controller about change in the search result.
+         *
+         * @param controller controller to notify
+         * @param query previously sent search query from the controller.
+         * @param itemCount the number of items that have been found in the search.
+         * @param extras extra bundle
+         */
+        public void notifySearchResultChanged(@NonNull ControllerInfo controller,
+                @NonNull String query, int itemCount, @NonNull Bundle extras) {
+            // TODO: Implement
+        }
+
+        private MediaLibraryService2 getService() {
+            return (MediaLibraryService2) getContext();
+        }
+
+        private MediaBrowserServiceCompat getServiceCompat() {
+            return getService().getServiceCompat();
+        }
+
+        @Override
+        MediaLibrarySessionCallback getCallback() {
+            return (MediaLibrarySessionCallback) super.getCallback();
+        }
+    }
+
+    @Override
+    MediaBrowserServiceCompat createBrowserServiceCompat() {
+        return new MyBrowserService();
+    }
+
+    @Override
+    int getSessionType() {
+        return SessionToken2.TYPE_LIBRARY_SERVICE;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        MediaSession2 session = getSession();
+        if (!(session instanceof MediaLibrarySession)) {
+            throw new RuntimeException("Expected MediaLibrarySession, but returned MediaSession2");
+        }
+    }
+
+    private MediaLibrarySession getLibrarySession() {
+        return (MediaLibrarySession) getSession();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return super.onBind(intent);
+    }
+
+    /**
+     * Called when another app requested to start this service.
+     * <p>
+     * Library service will accept or reject the connection with the
+     * {@link MediaLibrarySessionCallback} in the created session.
+     * <p>
+     * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+     * expected ID that you've specified through the AndroidManifest.xml.
+     * <p>
+     * This method will be called on the main thread.
+     *
+     * @param sessionId session id written in the AndroidManifest.xml.
+     * @return a new library session
+     * @see Builder
+     * @see #getSession()
+     * @throws RuntimeException if returned session is invalid
+     */
+    @Override
+    public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
+
+    /**
+     * Contains information that the library service needs to send to the client when
+     * {@link MediaBrowser2#getLibraryRoot(Bundle)} is called.
+     */
+    public static final class LibraryRoot {
+        /**
+         * The lookup key for a boolean that indicates whether the library service should return a
+         * librar root for recently played media items.
+         *
+         * <p>When creating a media browser for a given media library service, this key can be
+         * supplied as a root hint for retrieving media items that are recently played.
+         * If the media library service can provide such media items, the implementation must return
+         * the key in the root hint when
+         * {@link MediaLibrarySessionCallback#onGetLibraryRoot}
+         * is called back.
+         *
+         * <p>The root hint may contain multiple keys.
+         *
+         * @see #EXTRA_OFFLINE
+         * @see #EXTRA_SUGGESTED
+         */
+        public static final String EXTRA_RECENT = "android.media.extra.RECENT";
+
+        /**
+         * The lookup key for a boolean that indicates whether the library service should return a
+         * library root for offline media items.
+         *
+         * <p>When creating a media browser for a given media library service, this key can be
+         * supplied as a root hint for retrieving media items that are can be played without an
+         * internet connection.
+         * If the media library service can provide such media items, the implementation must return
+         * the key in the root hint when
+         * {@link MediaLibrarySessionCallback#onGetLibraryRoot}
+         * is called back.
+         *
+         * <p>The root hint may contain multiple keys.
+         *
+         * @see #EXTRA_RECENT
+         * @see #EXTRA_SUGGESTED
+         */
+        public static final String EXTRA_OFFLINE = "android.media.extra.OFFLINE";
+
+        /**
+         * The lookup key for a boolean that indicates whether the library service should return a
+         * library root for suggested media items.
+         *
+         * <p>When creating a media browser for a given media library service, this key can be
+         * supplied as a root hint for retrieving the media items suggested by the media library
+         * service. The list of media items is considered ordered by relevance, first being the top
+         * suggestion.
+         * If the media library service can provide such media items, the implementation must return
+         * the key in the root hint when
+         * {@link MediaLibrarySessionCallback#onGetLibraryRoot}
+         * is called back.
+         *
+         * <p>The root hint may contain multiple keys.
+         *
+         * @see #EXTRA_RECENT
+         * @see #EXTRA_OFFLINE
+         */
+        public static final String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED";
+
+        private final String mRootId;
+        private final Bundle mExtras;
+
+        //private final LibraryRootProvider mProvider;
+
+        /**
+         * Constructs a library root.
+         * @param rootId The root id for browsing.
+         * @param extras Any extras about the library service.
+         */
+        public LibraryRoot(@NonNull String rootId, @Nullable Bundle extras) {
+            if (rootId == null) {
+                throw new IllegalArgumentException("rootId shouldn't be null");
+            }
+            mRootId = rootId;
+            mExtras = extras;
+        }
+
+        /**
+         * Gets the root id for browsing.
+         */
+        public String getRootId() {
+            return mRootId;
+        }
+
+        /**
+         * Gets any extras about the library service.
+         */
+        public Bundle getExtras() {
+            return mExtras;
+        }
+    }
+
+    private class MyBrowserService extends MediaBrowserServiceCompat {
+        @Override
+        public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
+                final Bundle extras) {
+            if (MediaUtils2.isDefaultLibraryRootHint(extras)) {
+                // For connection request from the MediaController2. accept the connection from
+                // here, and let MediaLibrarySession decide whether to accept or reject the
+                // controller.
+                return sDefaultBrowserRoot;
+            }
+            final CountDownLatch latch = new CountDownLatch(1);
+            // TODO: Revisit this when we support caller information.
+            final ControllerInfo info = new ControllerInfo(MediaLibraryService2.this, clientUid, -1,
+                    clientPackageName, null);
+            MediaLibrarySession session = getLibrarySession();
+            // Call onGetLibraryRoot() directly instead of execute on the executor. Here's the
+            // reason.
+            // We need to return browser root here. So if we run the callback on the executor, we
+            // should wait for the completion.
+            // However, we cannot wait if the callback executor is the main executor, which posts
+            // the runnable to the main thread's. In that case, since this onGetRoot() always runs
+            // on the main thread, the posted runnable for calling onGetLibraryRoot() wouldn't run
+            // in here. Even worse, we cannot know whether it would be run on the main thread or
+            // not.
+            // Because of the reason, just call onGetLibraryRoot directly here. onGetLibraryRoot()
+            // has documentation that it may be called on the main thread.
+            LibraryRoot libraryRoot = session.getCallback().onGetLibraryRoot(
+                    session, info, extras);
+            if (libraryRoot == null) {
+                return null;
+            }
+            return new BrowserRoot(libraryRoot.getRootId(), libraryRoot.getExtras());
+        }
+
+        @Override
+        public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
+            onLoadChildren(parentId, result, null);
+        }
+
+        @Override
+        public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result,
+                final Bundle options) {
+            final ControllerInfo controller = getController();
+            getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    int page = options.getInt(EXTRA_PAGE, -1);
+                    int pageSize = options.getInt(EXTRA_PAGE_SIZE, -1);
+                    if (page >= 0 && pageSize >= 0) {
+                        // Requesting the list of children through the pagenation.
+                        List<MediaItem2> children = getLibrarySession().getCallback().onGetChildren(
+                                getLibrarySession(), controller, parentId, page, pageSize, options);
+                        if (children == null) {
+                            result.sendError(null);
+                        } else {
+                            List<MediaItem> list = new ArrayList<>();
+                            for (int i = 0; i < children.size(); i++) {
+                                list.add(MediaUtils2.createMediaItem(children.get(i)));
+                            }
+                            result.sendResult(list);
+                        }
+                    } else {
+                        // Only wants to register callbacks
+                        getLibrarySession().getCallback().onSubscribe(getLibrarySession(),
+                                controller, parentId, options);
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onLoadItem(final String itemId, final Result<MediaItem> result) {
+            final ControllerInfo controller = getController();
+            getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    MediaItem2 item = getLibrarySession().getCallback().onGetItem(
+                            getLibrarySession(), controller, itemId);
+                    if (item == null) {
+                        result.sendError(null);
+                    } else {
+                        result.sendResult(MediaUtils2.createMediaItem(item));
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
+            // TODO: Implement
+        }
+
+        @Override
+        public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
+            // TODO: Implement
+        }
+
+        private ControllerInfo getController() {
+            // TODO: Implement, by using getBrowserRootHints() / getCurrentBrowserInfo() / ...
+            return null;
+        }
+    }
+}
diff --git a/androidx/media/MediaLibrarySessionImplBase.java b/androidx/media/MediaLibrarySessionImplBase.java
new file mode 100644
index 0000000..c21edd3
--- /dev/null
+++ b/androidx/media/MediaLibrarySessionImplBase.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import androidx.annotation.NonNull;
+import androidx.media.MediaLibraryService2.MediaLibrarySession;
+
+import java.util.concurrent.Executor;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaLibrarySessionImplBase extends MediaSession2ImplBase {
+    MediaLibrarySessionImplBase(Context context,
+            MediaSessionCompat sessionCompat, String id,
+            MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+            VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
+            Executor callbackExecutor,
+            MediaSession2.SessionCallback callback) {
+        super(context, sessionCompat, id, player, playlistAgent, volumeProvider, sessionActivity,
+                callbackExecutor, callback);
+    }
+
+    static final class Builder extends MediaSession2ImplBase.BuilderBase<
+            MediaLibrarySession, MediaLibrarySession.MediaLibrarySessionCallback> {
+        Builder(Context context) {
+            super(context);
+        }
+
+        @Override
+        public @NonNull MediaLibrarySession build() {
+            if (mCallbackExecutor == null) {
+                mCallbackExecutor = new MainHandlerExecutor(mContext);
+            }
+            if (mCallback == null) {
+                mCallback = new MediaLibrarySession.MediaLibrarySessionCallback() {};
+            }
+            return new MediaLibrarySession(new MediaLibrarySessionImplBase(mContext,
+                    new MediaSessionCompat(mContext, mId), mId, mPlayer, mPlaylistAgent,
+                    mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback));
+        }
+    }
+}
diff --git a/androidx/media/MediaMetadata2.java b/androidx/media/MediaMetadata2.java
new file mode 100644
index 0000000..0cfd237
--- /dev/null
+++ b/androidx/media/MediaMetadata2.java
@@ -0,0 +1,1097 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
+import androidx.collection.ArrayMap;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * Contains metadata about an item, such as the title, artist, etc.
+ */
+// New version of MediaMetadata with following changes
+//   - Don't implement Parcelable for updatable support.
+//   - Also support MediaDescription features. MediaDescription is deprecated instead because
+//     it was insufficient for controller to display media contents.
+public final class MediaMetadata2 {
+    private static final String TAG = "MediaMetadata2";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the title of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the artist of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * duration of the media in ms. A negative duration indicates that the duration is unknown
+     * (or infinite).
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the album title for the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the author of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the writer of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the composer of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the compilation status of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the date the media was created or published.
+     * The format is unspecified but RFC 3339 is recommended.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about the year
+     * the media was created or published.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the genre of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * track number for the media.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * number of tracks in the media's original source.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * disc number for the media's original source.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the artist for the album of the media's original source.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+    /**
+     * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+     * artwork for the media.
+     * The artwork should be relatively small and may be scaled down if it is too large.
+     * For higher resolution artwork, {@link #METADATA_KEY_ART_URI} should be used instead.
+     *
+     * @see Builder#putBitmap(String, Bitmap)
+     * @see #getBitmap(String)
+     */
+    public static final String METADATA_KEY_ART = "android.media.metadata.ART";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about Uri of the artwork for the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+
+    /**
+     * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+     * artwork for the album of the media's original source.
+     * The artwork should be relatively small and may be scaled down if it is too large.
+     * For higher resolution artwork, {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead.
+     *
+     * @see Builder#putBitmap(String, Bitmap)
+     * @see #getBitmap(String)
+     */
+    public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the Uri of the artwork for the album of the media's original source.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+
+    /**
+     * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+     * user's rating for the media.
+     *
+     * @see Builder#putRating(String, Rating2)
+     * @see #getRating(String)
+     */
+    public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+
+    /**
+     * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+     * overall rating for the media.
+     *
+     * @see Builder#putRating(String, Rating2)
+     * @see #getRating(String)
+     */
+    public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the title that is suitable for display to the user.
+     * It will generally be the same as {@link #METADATA_KEY_TITLE} but may differ for some formats.
+     * When displaying media described by this metadata, this should be preferred if present.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the subtitle that is suitable for display to the user.
+     * When displaying a second line for media described by this metadata, this should be preferred
+     * to other fields if present.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_DISPLAY_SUBTITLE =
+            "android.media.metadata.DISPLAY_SUBTITLE";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the description that is suitable for display to the user.
+     * When displaying more information for media described by this metadata,
+     * this should be preferred to other fields if present.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_DISPLAY_DESCRIPTION =
+            "android.media.metadata.DISPLAY_DESCRIPTION";
+
+    /**
+     * The metadata key for a {@link Bitmap} typed value to retrieve the information about the icon
+     * or thumbnail that is suitable for display to the user.
+     * When displaying an icon for media described by this metadata, this should be preferred to
+     * other fields if present.
+     * <p>
+     * The icon should be relatively small and may be scaled down if it is too large.
+     * For higher resolution artwork, {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead.
+     *
+     * @see Builder#putBitmap(String, Bitmap)
+     * @see #getBitmap(String)
+     */
+    public static final String METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the Uri of icon or thumbnail that is suitable for display to the user.
+     * When displaying more information for media described by this metadata, the
+     * display description should be preferred to other fields when present.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_DISPLAY_ICON_URI =
+            "android.media.metadata.DISPLAY_ICON_URI";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the media ID of the content. This value is specific to the
+     * service providing the content. If used, this should be a persistent
+     * unique key for the underlying content.  It may be used with
+     * {@link MediaController2#playFromMediaId(String, Bundle)}
+     * to initiate playback.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
+
+    /**
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the Uri of the content. This value is specific to the service providing the
+     * content. It may be used with {@link MediaController2#playFromUri(Uri, Bundle)}
+     * to initiate playback.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
+
+    /**
+     * @hide
+     * The metadata key for a {@link Float} typed value to retrieve the information about the
+     * radio frequency if this metadata represents radio content.
+     *
+     * @see Builder#putFloat(String, float)
+     * @see #getFloat(String)
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String METADATA_KEY_RADIO_FREQUENCY =
+            "android.media.metadata.RADIO_FREQUENCY";
+
+    /**
+     * @hide
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the radio program name if this metadata represents radio content.
+     *
+     * @see MediaMetadata2.Builder#putText(String, CharSequence)
+     * @see MediaMetadata2.Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String METADATA_KEY_RADIO_PROGRAM_NAME =
+            "android.media.metadata.RADIO_PROGRAM_NAME";
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth
+     * AVRCP 1.5. It should be one of the following:
+     * <ul>
+     * <li>{@link #BT_FOLDER_TYPE_MIXED}</li>
+     * <li>{@link #BT_FOLDER_TYPE_TITLES}</li>
+     * <li>{@link #BT_FOLDER_TYPE_ALBUMS}</li>
+     * <li>{@link #BT_FOLDER_TYPE_ARTISTS}</li>
+     * <li>{@link #BT_FOLDER_TYPE_GENRES}</li>
+     * <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li>
+     * <li>{@link #BT_FOLDER_TYPE_YEARS}</li>
+     * </ul>
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_BT_FOLDER_TYPE =
+            "android.media.metadata.BT_FOLDER_TYPE";
+
+    /**
+     * The type of folder that is unknown or contains media elements of mixed types as specified in
+     * the section 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     */
+    public static final long BT_FOLDER_TYPE_MIXED = 0;
+
+    /**
+     * The type of folder that contains media elements only as specified in the section 6.10.2.2 of
+     * the Bluetooth AVRCP 1.5.
+     */
+    public static final long BT_FOLDER_TYPE_TITLES = 1;
+
+    /**
+     * The type of folder that contains folders categorized by album as specified in the section
+     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     */
+    public static final long BT_FOLDER_TYPE_ALBUMS = 2;
+
+    /**
+     * The type of folder that contains folders categorized by artist as specified in the section
+     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     */
+    public static final long BT_FOLDER_TYPE_ARTISTS = 3;
+
+    /**
+     * The type of folder that contains folders categorized by genre as specified in the section
+     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     */
+    public static final long BT_FOLDER_TYPE_GENRES = 4;
+
+    /**
+     * The type of folder that contains folders categorized by playlist as specified in the section
+     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     */
+    public static final long BT_FOLDER_TYPE_PLAYLISTS = 5;
+
+    /**
+     * The type of folder that contains folders categorized by year as specified in the section
+     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     */
+    public static final long BT_FOLDER_TYPE_YEARS = 6;
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about whether
+     * the media is an advertisement. A value of 0 indicates it is not an advertisement.
+     * A value of 1 or non-zero indicates it is an advertisement.
+     * If not specified, this value is set to 0 by default.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
+
+    /**
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * download status of the media which will be used for later offline playback. It should be
+     * one of the following:
+     *
+     * <ul>
+     * <li>{@link #STATUS_NOT_DOWNLOADED}</li>
+     * <li>{@link #STATUS_DOWNLOADING}</li>
+     * <li>{@link #STATUS_DOWNLOADED}</li>
+     * </ul>
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     */
+    public static final String METADATA_KEY_DOWNLOAD_STATUS =
+            "android.media.metadata.DOWNLOAD_STATUS";
+
+    /**
+     * The status value to indicate the media item is not downloaded.
+     *
+     * @see #METADATA_KEY_DOWNLOAD_STATUS
+     */
+    public static final long STATUS_NOT_DOWNLOADED = 0;
+
+    /**
+     * The status value to indicate the media item is being downloaded.
+     *
+     * @see #METADATA_KEY_DOWNLOAD_STATUS
+     */
+    public static final long STATUS_DOWNLOADING = 1;
+
+    /**
+     * The status value to indicate the media item is downloaded for later offline playback.
+     *
+     * @see #METADATA_KEY_DOWNLOAD_STATUS
+     */
+    public static final long STATUS_DOWNLOADED = 2;
+
+    /**
+     * A {@link Bundle} extra.
+     */
+    public static final String METADATA_KEY_EXTRAS = "android.media.metadata.EXTRAS";
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR,
+            METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION,
+            METADATA_KEY_DATE, METADATA_KEY_GENRE, METADATA_KEY_ALBUM_ARTIST, METADATA_KEY_ART_URI,
+            METADATA_KEY_ALBUM_ART_URI, METADATA_KEY_DISPLAY_TITLE, METADATA_KEY_DISPLAY_SUBTITLE,
+            METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_KEY_DISPLAY_ICON_URI,
+            METADATA_KEY_MEDIA_ID, METADATA_KEY_MEDIA_URI, METADATA_KEY_RADIO_PROGRAM_NAME})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TextKey {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER,
+            METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE,
+            METADATA_KEY_ADVERTISEMENT, METADATA_KEY_DOWNLOAD_STATUS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LongKey {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BitmapKey {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RatingKey {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @StringDef({METADATA_KEY_RADIO_FREQUENCY})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FloatKey {}
+
+    static final int METADATA_TYPE_LONG = 0;
+    static final int METADATA_TYPE_TEXT = 1;
+    static final int METADATA_TYPE_BITMAP = 2;
+    static final int METADATA_TYPE_RATING = 3;
+    static final int METADATA_TYPE_FLOAT = 4;
+    static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
+
+    static {
+        METADATA_KEYS_TYPE = new ArrayMap<>();
+        METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_RADIO_FREQUENCY, METADATA_TYPE_FLOAT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_RADIO_PROGRAM_NAME, METADATA_TYPE_TEXT);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
+    }
+
+    private static final @MediaMetadata2.TextKey
+    String[] PREFERRED_DESCRIPTION_ORDER = {
+            METADATA_KEY_TITLE,
+            METADATA_KEY_ARTIST,
+            METADATA_KEY_ALBUM,
+            METADATA_KEY_ALBUM_ARTIST,
+            METADATA_KEY_WRITER,
+            METADATA_KEY_AUTHOR,
+            METADATA_KEY_COMPOSER
+    };
+
+    private static final @MediaMetadata2.BitmapKey
+    String[] PREFERRED_BITMAP_ORDER = {
+            METADATA_KEY_DISPLAY_ICON,
+            METADATA_KEY_ART,
+            METADATA_KEY_ALBUM_ART
+    };
+
+    private static final @MediaMetadata2.TextKey
+    String[] PREFERRED_URI_ORDER = {
+            METADATA_KEY_DISPLAY_ICON_URI,
+            METADATA_KEY_ART_URI,
+            METADATA_KEY_ALBUM_ART_URI
+    };
+
+    final Bundle mBundle;
+
+    MediaMetadata2(Bundle bundle) {
+        mBundle = new Bundle(bundle);
+        mBundle.setClassLoader(MediaMetadata2.class.getClassLoader());
+    }
+
+    /**
+     * Returns true if the given key is contained in the metadata
+     *
+     * @param key a String key
+     * @return true if the key exists in this metadata, false otherwise
+     */
+    public boolean containsKey(@NonNull String key) {
+        if (key == null) {
+            throw new IllegalArgumentException("key shouldn't be null");
+        }
+        return mBundle.containsKey(key);
+    }
+
+    /**
+     * Returns the value associated with the given key, or null if no mapping of
+     * the desired type exists for the given key or a null value is explicitly
+     * associated with the key.
+     *
+     * @param key The key the value is stored under
+     * @return a CharSequence value, or null
+     */
+    public @Nullable CharSequence getText(@NonNull @TextKey String key) {
+        if (key == null) {
+            throw new IllegalArgumentException("key shouldn't be null");
+        }
+        return mBundle.getCharSequence(key);
+    }
+
+    /**
+     * Returns the media id, or {@code null} if the id doesn't exist.
+     *<p>
+     * This is equivalent to the {@link #getString(String)} with the {@link #METADATA_KEY_MEDIA_ID}.
+     *
+     * @return media id. Can be {@code null}
+     * @see #METADATA_KEY_MEDIA_ID
+     */
+    public @Nullable String getMediaId() {
+        return getString(METADATA_KEY_MEDIA_ID);
+    }
+
+    /**
+     * Returns the value associated with the given key, or null if no mapping of
+     * the desired type exists for the given key or a null value is explicitly
+     * associated with the key.
+     *
+     * @param key The key the value is stored under
+     * @return a String value, or null
+     */
+    public @Nullable String getString(@NonNull @TextKey String key) {
+        if (key == null) {
+            throw new IllegalArgumentException("key shouldn't be null");
+        }
+        CharSequence text = mBundle.getCharSequence(key);
+        if (text != null) {
+            return text.toString();
+        }
+        return null;
+    }
+
+    /**
+     * Returns the value associated with the given key, or 0L if no long exists
+     * for the given key.
+     *
+     * @param key The key the value is stored under
+     * @return a long value
+     */
+    public long getLong(@NonNull @LongKey String key) {
+        if (key == null) {
+            throw new IllegalArgumentException("key shouldn't be null");
+        }
+        return mBundle.getLong(key, 0);
+    }
+
+    /**
+     * Return a {@link Rating2} for the given key or null if no rating exists for
+     * the given key.
+     * <p>
+     * For the {@link #METADATA_KEY_USER_RATING}, A {@code null} return value means that user rating
+     * cannot be set by {@link MediaController2}.
+     *
+     * @param key The key the value is stored under
+     * @return A {@link Rating2} or {@code null}
+     */
+    public @Nullable Rating2 getRating(@NonNull @RatingKey String key) {
+        if (key == null) {
+            throw new IllegalArgumentException("key shouldn't be null");
+        }
+        Rating2 rating = null;
+        try {
+            rating = Rating2.fromBundle(mBundle.getBundle(key));
+        } catch (Exception e) {
+            // ignore, value was not a rating
+            Log.w(TAG, "Failed to retrieve a key as Rating.", e);
+        }
+        return rating;
+    }
+
+    /**
+     * Return the value associated with the given key, or 0.0f if no long exists
+     * for the given key.
+     *
+     * @param key The key the value is stored under
+     * @return a float value
+     */
+    public float getFloat(@NonNull @FloatKey String key) {
+        if (key == null) {
+            throw new IllegalArgumentException("key shouldn't be null");
+        }
+        return mBundle.getFloat(key);
+    }
+
+    /**
+     * Return a {@link Bitmap} for the given key or null if no bitmap exists for
+     * the given key.
+     *
+     * @param key The key the value is stored under
+     * @return A {@link Bitmap} or null
+     */
+    public @Nullable Bitmap getBitmap(@NonNull @BitmapKey String key) {
+        if (key == null) {
+            throw new IllegalArgumentException("key shouldn't be null");
+        }
+        Bitmap bmp = null;
+        try {
+            bmp = mBundle.getParcelable(key);
+        } catch (Exception e) {
+            // ignore, value was not a bitmap
+            Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
+        }
+        return bmp;
+    }
+
+    /**
+     * Get the extra {@link Bundle} from the metadata object.
+     *
+     * @return A {@link Bundle} or {@code null}
+     */
+    public @Nullable Bundle getExtras() {
+        try {
+            return mBundle.getBundle(METADATA_KEY_EXTRAS);
+        } catch (Exception e) {
+            // ignore, value was not an bundle
+            Log.w(TAG, "Failed to retrieve an extra");
+        }
+        return null;
+    }
+
+    /**
+     * Get the number of fields in this metadata.
+     *
+     * @return The number of fields in the metadata.
+     */
+    public int size() {
+        return mBundle.size();
+    }
+
+    /**
+     * Returns a Set containing the Strings used as keys in this metadata.
+     *
+     * @return a Set of String keys
+     */
+    public @NonNull Set<String> keySet() {
+        return mBundle.keySet();
+    }
+
+    /**
+     * Gets the bundle backing the metadata object. This is available to support
+     * backwards compatibility. Apps should not modify the bundle directly.
+     *
+     * @return The Bundle backing this metadata.
+     */
+    public @NonNull Bundle toBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Creates the {@link MediaMetadata2} from the bundle that previously returned by
+     * {@link #toBundle()}.
+     *
+     * @param bundle bundle for the metadata
+     * @return a new MediaMetadata2x
+     */
+    public static @NonNull MediaMetadata2 fromBundle(@Nullable Bundle bundle) {
+        return (bundle == null) ? null : new MediaMetadata2(bundle);
+    }
+
+    /**
+     * Use to build MediaMetadata2x objects. The system defined metadata keys must
+     * use the appropriate data type.
+     */
+    public static final class Builder {
+        final Bundle mBundle;
+
+        /**
+         * Create an empty Builder. Any field that should be included in the
+         * {@link MediaMetadata2} must be added.
+         */
+        public Builder() {
+            mBundle = new Bundle();
+        }
+
+        /**
+         * Create a Builder using a {@link MediaMetadata2} instance to set the
+         * initial values. All fields in the source metadata will be included in
+         * the new metadata. Fields can be overwritten by adding the same key.
+         *
+         * @param source
+         */
+        public Builder(@NonNull MediaMetadata2 source) {
+            mBundle = new Bundle(source.toBundle());
+        }
+
+        /**
+         * Create a Builder using a {@link MediaMetadata2} instance to set
+         * initial values, but replace bitmaps with a scaled down copy if they
+         * are larger than maxBitmapSize.
+         *
+         * @param source The original metadata to copy.
+         * @param maxBitmapSize The maximum height/width for bitmaps contained
+         *            in the metadata.
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public Builder(MediaMetadata2 source, int maxBitmapSize) {
+            this(source);
+            for (String key : mBundle.keySet()) {
+                Object value = mBundle.get(key);
+                if (value instanceof Bitmap) {
+                    Bitmap bmp = (Bitmap) value;
+                    if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
+                        putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
+                    }
+                }
+            }
+        }
+
+        /**
+         * Put a CharSequence value into the metadata. Custom keys may be used,
+         * but if the METADATA_KEYs defined in this class are used they may only
+         * be one of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_TITLE}</li>
+         * <li>{@link #METADATA_KEY_ARTIST}</li>
+         * <li>{@link #METADATA_KEY_ALBUM}</li>
+         * <li>{@link #METADATA_KEY_AUTHOR}</li>
+         * <li>{@link #METADATA_KEY_WRITER}</li>
+         * <li>{@link #METADATA_KEY_COMPOSER}</li>
+         * <li>{@link #METADATA_KEY_COMPILATION}</li>
+         * <li>{@link #METADATA_KEY_DATE}</li>
+         * <li>{@link #METADATA_KEY_GENRE}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+         * <li>{@link #METADATA_KEY_ART_URI}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+         * <li>{@link #METADATA_KEY_MEDIA_ID}</li>
+         * <li>{@link #METADATA_KEY_MEDIA_URI}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The CharSequence value to store
+         * @return The Builder to allow chaining
+         */
+        public @NonNull Builder putText(@NonNull @TextKey String key,
+                @Nullable CharSequence value) {
+            if (key == null) {
+                throw new IllegalArgumentException("key shouldn't be null");
+            }
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a CharSequence");
+                }
+            }
+            mBundle.putCharSequence(key, value);
+            return this;
+        }
+
+        /**
+         * Put a String value into the metadata. Custom keys may be used, but if
+         * the METADATA_KEYs defined in this class are used they may only be one
+         * of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_TITLE}</li>
+         * <li>{@link #METADATA_KEY_ARTIST}</li>
+         * <li>{@link #METADATA_KEY_ALBUM}</li>
+         * <li>{@link #METADATA_KEY_AUTHOR}</li>
+         * <li>{@link #METADATA_KEY_WRITER}</li>
+         * <li>{@link #METADATA_KEY_COMPOSER}</li>
+         * <li>{@link #METADATA_KEY_COMPILATION}</li>
+         * <li>{@link #METADATA_KEY_DATE}</li>
+         * <li>{@link #METADATA_KEY_GENRE}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+         * <li>{@link #METADATA_KEY_ART_URI}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+         * <li>{@link #METADATA_KEY_MEDIA_ID}</li>
+         * <li>{@link #METADATA_KEY_MEDIA_URI}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public @NonNull Builder putString(@NonNull @TextKey String key,
+                @Nullable String value) {
+            if (key == null) {
+                throw new IllegalArgumentException("key shouldn't be null");
+            }
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a String");
+                }
+            }
+            mBundle.putCharSequence(key, value);
+            return this;
+        }
+
+        /**
+         * Put a long value into the metadata. Custom keys may be used, but if
+         * the METADATA_KEYs defined in this class are used they may only be one
+         * of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_DURATION}</li>
+         * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
+         * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
+         * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+         * <li>{@link #METADATA_KEY_YEAR}</li>
+         * <li>{@link #METADATA_KEY_BT_FOLDER_TYPE}</li>
+         * <li>{@link #METADATA_KEY_ADVERTISEMENT}</li>
+         * <li>{@link #METADATA_KEY_DOWNLOAD_STATUS}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public @NonNull Builder putLong(@NonNull @LongKey String key, long value) {
+            if (key == null) {
+                throw new IllegalArgumentException("key shouldn't be null");
+            }
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a long");
+                }
+            }
+            mBundle.putLong(key, value);
+            return this;
+        }
+
+        /**
+         * Put a {@link Rating2} into the metadata. Custom keys may be used, but
+         * if the METADATA_KEYs defined in this class are used they may only be
+         * one of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_RATING}</li>
+         * <li>{@link #METADATA_KEY_USER_RATING}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public @NonNull Builder putRating(@NonNull @RatingKey String key,
+                @Nullable Rating2 value) {
+            if (key == null) {
+                throw new IllegalArgumentException("key shouldn't be null");
+            }
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a Rating");
+                }
+            }
+            mBundle.putBundle(key, (value == null) ? null : value.toBundle());
+            return this;
+        }
+
+        /**
+         * Put a {@link Bitmap} into the metadata. Custom keys may be used, but
+         * if the METADATA_KEYs defined in this class are used they may only be
+         * one of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_ART}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ART}</li>
+         * <li>{@link #METADATA_KEY_DISPLAY_ICON}</li>
+         * </ul>
+         * Large bitmaps may be scaled down by the system when
+         * {@link android.media.session.MediaSession#setMetadata} is called.
+         * To pass full resolution images {@link Uri Uris} should be used with
+         * {@link #putString}.
+         *
+         * @param key The key for referencing this value
+         * @param value The Bitmap to store
+         * @return The Builder to allow chaining
+         */
+        public @NonNull Builder putBitmap(@NonNull @BitmapKey String key,
+                @Nullable Bitmap value) {
+            if (key == null) {
+                throw new IllegalArgumentException("key shouldn't be null");
+            }
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a Bitmap");
+                }
+            }
+            mBundle.putParcelable(key, value);
+            return this;
+        }
+
+        /**
+         * Put a float value into the metadata. Custom keys may be used.
+         *
+         * @param key The key for referencing this value
+         * @param value The float value to store
+         * @return The Builder to allow chaining
+         */
+        public @NonNull Builder putFloat(@NonNull @LongKey String key, float value) {
+            if (key == null) {
+                throw new IllegalArgumentException("key shouldn't be null");
+            }
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_FLOAT) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a float");
+                }
+            }
+            mBundle.putFloat(key, value);
+            return this;
+        }
+
+        /**
+         * Set a bundle of extras.
+         *
+         * @param extras The extras to include with this description or null.
+         * @return The Builder to allow chaining
+         */
+        public Builder setExtras(@Nullable Bundle extras) {
+            mBundle.putBundle(METADATA_KEY_EXTRAS, extras);
+            return this;
+        }
+
+        /**
+         * Creates a {@link MediaMetadata2} instance with the specified fields.
+         *
+         * @return The new MediaMetadata2x instance
+         */
+        public @NonNull MediaMetadata2 build() {
+            return new MediaMetadata2(mBundle);
+        }
+
+        private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
+            float maxSizeF = maxSize;
+            float widthScale = maxSizeF / bmp.getWidth();
+            float heightScale = maxSizeF / bmp.getHeight();
+            float scale = Math.min(widthScale, heightScale);
+            int height = (int) (bmp.getHeight() * scale);
+            int width = (int) (bmp.getWidth() * scale);
+            return Bitmap.createScaledBitmap(bmp, width, height, true);
+        }
+    }
+}
+
diff --git a/androidx/media/MediaMetadata2Test.java b/androidx/media/MediaMetadata2Test.java
new file mode 100644
index 0000000..f000f02
--- /dev/null
+++ b/androidx/media/MediaMetadata2Test.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.media.MediaMetadata2.Builder;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaMetadata2Test {
+    @Test
+    public void testBuilder() {
+        final Bundle extras = new Bundle();
+        extras.putString("MediaMetadata2Test", "testBuilder");
+        final String title = "title";
+        final long discNumber = 10;
+        final Rating2 rating = Rating2.newThumbRating(true);
+
+        Builder builder = new Builder();
+        builder.setExtras(extras);
+        builder.putString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, title);
+        builder.putLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER, discNumber);
+        builder.putRating(MediaMetadata2.METADATA_KEY_USER_RATING, rating);
+
+        MediaMetadata2 metadata = builder.build();
+        assertTrue(TestUtils.equals(extras, metadata.getExtras()));
+        assertEquals(title, metadata.getString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE));
+        assertEquals(discNumber, metadata.getLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER));
+        assertEquals(rating, metadata.getRating(MediaMetadata2.METADATA_KEY_USER_RATING));
+    }
+}
diff --git a/androidx/media/MediaPlayer2.java b/androidx/media/MediaPlayer2.java
new file mode 100644
index 0000000..1864d72
--- /dev/null
+++ b/androidx/media/MediaPlayer2.java
@@ -0,0 +1,2011 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.media.AudioAttributes;
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.MediaFormat;
+import android.media.MediaTimestamp;
+import android.media.PlaybackParams;
+import android.media.ResourceBusyException;
+import android.media.SyncParams;
+import android.media.TimedMetaData;
+import android.media.UnsupportedSchemeException;
+import android.os.Build;
+import android.os.PersistableBundle;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+
+
+/**
+ * @hide
+ * MediaPlayer2 class can be used to control playback
+ * of audio/video files and streams. An example on how to use the methods in
+ * this class can be found in {@link android.widget.VideoView}.
+ *
+ * <p>Topics covered here are:
+ * <ol>
+ * <li><a href="#StateDiagram">State Diagram</a>
+ * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#Callbacks">Register informational and error callbacks</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use MediaPlayer2, read the
+ * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="StateDiagram"></a>
+ * <h3>State Diagram</h3>
+ *
+ * <p>Playback control of audio/video files and streams is managed as a state
+ * machine. The following diagram shows the life cycle and the states of a
+ * MediaPlayer2 object driven by the supported playback control operations.
+ * The ovals represent the states a MediaPlayer2 object may reside
+ * in. The arcs represent the playback control operations that drive the object
+ * state transition. There are two types of arcs. The arcs with a single arrow
+ * head represent synchronous method calls, while those with
+ * a double arrow head represent asynchronous method calls.</p>
+ *
+ * <p><img src="../../../images/mediaplayer_state_diagram.gif"
+ *         alt="MediaPlayer State diagram"
+ *         border="0" /></p>
+ *
+ * <p>From this state diagram, one can see that a MediaPlayer2 object has the
+ *    following states:</p>
+ * <ul>
+ *     <li>When a MediaPlayer2 object is just created using <code>create</code> or
+ *         after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
+ *         {@link #close()} is called, it is in the <em>End</em> state. Between these
+ *         two states is the life cycle of the MediaPlayer2 object.
+ *         <ul>
+ *         <li> It is a programming error to invoke methods such
+ *         as {@link #getCurrentPosition()},
+ *         {@link #getDuration()}, {@link #getVideoHeight()},
+ *         {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
+ *         {@link #setPlayerVolume(float)}, {@link #pause()}, {@link #play()},
+ *         {@link #seekTo(long, int)} or
+ *         {@link #prepare()} in the <em>Idle</em> state.
+ *         <li>It is also recommended that once
+ *         a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
+ *         so that resources used by the internal player engine associated with the
+ *         MediaPlayer2 object can be released immediately. Resource may include
+ *         singleton resources such as hardware acceleration components and
+ *         failure to call {@link #close()} may cause subsequent instances of
+ *         MediaPlayer2 objects to fallback to software implementations or fail
+ *         altogether. Once the MediaPlayer2
+ *         object is in the <em>End</em> state, it can no longer be used and
+ *         there is no way to bring it back to any other state. </li>
+ *         <li>Furthermore,
+ *         the MediaPlayer2 objects created using <code>new</code> is in the
+ *         <em>Idle</em> state.
+ *         </li>
+ *         </ul>
+ *         </li>
+ *     <li>In general, some playback control operation may fail due to various
+ *         reasons, such as unsupported audio/video format, poorly interleaved
+ *         audio/video, resolution too high, streaming timeout, and the like.
+ *         Thus, error reporting and recovery is an important concern under
+ *         these circumstances. Sometimes, due to programming errors, invoking a playback
+ *         control operation in an invalid state may also occur. Under all these
+ *         error conditions, the internal player engine invokes a user supplied
+ *         MediaPlayer2EventCallback.onError() method if an MediaPlayer2EventCallback has been
+ *         registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ *         <ul>
+ *         <li>It is important to note that once an error occurs, the
+ *         MediaPlayer2 object enters the <em>Error</em> state (except as noted
+ *         above), even if an error listener has not been registered by the application.</li>
+ *         <li>In order to reuse a MediaPlayer2 object that is in the <em>
+ *         Error</em> state and recover from the error,
+ *         {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ *         state.</li>
+ *         <li>It is good programming practice to have your application
+ *         register a OnErrorListener to look out for error notifications from
+ *         the internal player engine.</li>
+ *         <li>IllegalStateException is
+ *         thrown to prevent programming errors such as calling
+ *         {@link #prepare()}, {@link #setDataSource(DataSourceDesc)}
+ *         methods in an invalid state. </li>
+ *         </ul>
+ *         </li>
+ *     <li>Calling
+ *         {@link #setDataSource(DataSourceDesc)} transfers a
+ *         MediaPlayer2 object in the <em>Idle</em> state to the
+ *         <em>Initialized</em> state.
+ *         <ul>
+ *         <li>An IllegalStateException is thrown if
+ *         setDataSource() is called in any other state.</li>
+ *         <li>It is good programming
+ *         practice to always look out for <code>IllegalArgumentException</code>
+ *         and <code>IOException</code> that may be thrown from
+ *         <code>setDataSource</code>.</li>
+ *         </ul>
+ *         </li>
+ *     <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ *         before playback can be started.
+ *         <ul>
+ *         <li>There are an asynchronous way that the <em>Prepared</em> state can be reached:
+ *         a call to {@link #prepare()} (asynchronous) which
+ *         first transfers the object to the <em>Preparing</em> state after the
+ *         call returns (which occurs almost right way) while the internal
+ *         player engine continues working on the rest of preparation work
+ *         until the preparation work completes. When the preparation completes,
+ *         the internal player engine then calls a user supplied callback method,
+ *         onInfo() of the MediaPlayer2EventCallback interface with {@link #MEDIA_INFO_PREPARED},
+ *         if an MediaPlayer2EventCallback is registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
+ *         <li>It is important to note that
+ *         the <em>Preparing</em> state is a transient state, and the behavior
+ *         of calling any method with side effect while a MediaPlayer2 object is
+ *         in the <em>Preparing</em> state is undefined.</li>
+ *         <li>An IllegalStateException is
+ *         thrown if {@link #prepare()} is called in
+ *         any other state.</li>
+ *         <li>While in the <em>Prepared</em> state, properties
+ *         such as audio/sound volume, screenOnWhilePlaying, looping can be
+ *         adjusted by invoking the corresponding set methods.</li>
+ *         </ul>
+ *         </li>
+ *     <li>To start the playback, {@link #play()} must be called. After
+ *         {@link #play()} returns successfully, the MediaPlayer2 object is in the
+ *         <em>Started</em> state. {@link #getPlayerState()} can be called to test
+ *         whether the MediaPlayer2 object is in the <em>Started</em> state.
+ *         <ul>
+ *         <li>While in the <em>Started</em> state, the internal player engine calls
+ *         a user supplied callback method MediaPlayer2EventCallback.onInfo() with
+ *         {@link #MEDIA_INFO_BUFFERING_UPDATE} if an MediaPlayer2EventCallback has been
+ *         registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ *         This callback allows applications to keep track of the buffering status
+ *         while streaming audio/video.</li>
+ *         <li>Calling {@link #play()} has not effect
+ *         on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ *         </ul>
+ *         </li>
+ *     <li>Playback can be paused and stopped, and the current playback position
+ *         can be adjusted. Playback can be paused via {@link #pause()}. When the call to
+ *         {@link #pause()} returns, the MediaPlayer2 object enters the
+ *         <em>Paused</em> state. Note that the transition from the <em>Started</em>
+ *         state to the <em>Paused</em> state and vice versa happens
+ *         asynchronously in the player engine. It may take some time before
+ *         the state is updated in calls to {@link #getPlayerState()}, and it can be
+ *         a number of seconds in the case of streamed content.
+ *         <ul>
+ *         <li>Calling {@link #play()} to resume playback for a paused
+ *         MediaPlayer2 object, and the resumed playback
+ *         position is the same as where it was paused. When the call to
+ *         {@link #play()} returns, the paused MediaPlayer2 object goes back to
+ *         the <em>Started</em> state.</li>
+ *         <li>Calling {@link #pause()} has no effect on
+ *         a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
+ *         </ul>
+ *         </li>
+ *     <li>The playback position can be adjusted with a call to
+ *         {@link #seekTo(long, int)}.
+ *         <ul>
+ *         <li>Although the asynchronuous {@link #seekTo(long, int)}
+ *         call returns right away, the actual seek operation may take a while to
+ *         finish, especially for audio/video being streamed. When the actual
+ *         seek operation completes, the internal player engine calls a user
+ *         supplied MediaPlayer2EventCallback.onCallCompleted() with
+ *         {@link #CALL_COMPLETED_SEEK_TO}
+ *         if an MediaPlayer2EventCallback has been registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
+ *         <li>Please
+ *         note that {@link #seekTo(long, int)} can also be called in the other states,
+ *         such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
+ *         </em> state. When {@link #seekTo(long, int)} is called in those states,
+ *         one video frame will be displayed if the stream has video and the requested
+ *         position is valid.
+ *         </li>
+ *         <li>Furthermore, the actual current playback position
+ *         can be retrieved with a call to {@link #getCurrentPosition()}, which
+ *         is helpful for applications such as a Music player that need to keep
+ *         track of the playback progress.</li>
+ *         </ul>
+ *         </li>
+ *     <li>When the playback reaches the end of stream, the playback completes.
+ *         <ul>
+ *         <li>If current source is set to loop by {@link #loopCurrent(boolean)},
+ *         the MediaPlayer2 object shall remain in the <em>Started</em> state.</li>
+ *         <li>If the looping mode was set to <var>false
+ *         </var>, the player engine calls a user supplied callback method,
+ *         MediaPlayer2EventCallback.onCompletion(), if an MediaPlayer2EventCallback is
+ *         registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ *         The invoke of the callback signals that the object is now in the <em>
+ *         PlaybackCompleted</em> state.</li>
+ *         <li>While in the <em>PlaybackCompleted</em>
+ *         state, calling {@link #play()} can restart the playback from the
+ *         beginning of the audio/video source.</li>
+ * </ul>
+ *
+ *
+ * <a name="Valid_and_Invalid_States"></a>
+ * <h3>Valid and invalid states</h3>
+ *
+ * <table border="0" cellspacing="0" cellpadding="0">
+ * <tr><td>Method Name </p></td>
+ *     <td>Valid Sates </p></td>
+ *     <td>Invalid States </p></td>
+ *     <td>Comments </p></td></tr>
+ * <tr><td>attachAuxEffect </p></td>
+ *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ *     <td>{Idle, Error} </p></td>
+ *     <td>This method must be called after setDataSource.
+ *     Calling it does not change the object state. </p></td></tr>
+ * <tr><td>getAudioSessionId </p></td>
+ *     <td>any </p></td>
+ *     <td>{} </p></td>
+ *     <td>This method can be called in any state and calling it does not change
+ *         the object state. </p></td></tr>
+ * <tr><td>getCurrentPosition </p></td>
+ *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ *         PlaybackCompleted} </p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state does not change the
+ *         state. Calling this method in an invalid state transfers the object
+ *         to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getDuration </p></td>
+ *     <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ *     <td>{Idle, Initialized, Error} </p></td>
+ *     <td>Successful invoke of this method in a valid state does not change the
+ *         state. Calling this method in an invalid state transfers the object
+ *         to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoHeight </p></td>
+ *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ *         PlaybackCompleted}</p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state does not change the
+ *         state. Calling this method in an invalid state transfers the object
+ *         to the <em>Error</em> state.  </p></td></tr>
+ * <tr><td>getVideoWidth </p></td>
+ *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ *         PlaybackCompleted}</p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state does not change
+ *         the state. Calling this method in an invalid state transfers the
+ *         object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getPlayerState </p></td>
+ *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ *          PlaybackCompleted}</p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state does not change
+ *         the state. Calling this method in an invalid state transfers the
+ *         object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>pause </p></td>
+ *     <td>{Started, Paused, PlaybackCompleted}</p></td>
+ *     <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state transfers the
+ *         object to the <em>Paused</em> state. Calling this method in an
+ *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>prepare </p></td>
+ *     <td>{Initialized, Stopped} </p></td>
+ *     <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ *     <td>Successful invoke of this method in a valid state transfers the
+ *         object to the <em>Preparing</em> state. Calling this method in an
+ *         invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>release </p></td>
+ *     <td>any </p></td>
+ *     <td>{} </p></td>
+ *     <td>After {@link #close()}, the object is no longer available. </p></td></tr>
+ * <tr><td>reset </p></td>
+ *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ *         PlaybackCompleted, Error}</p></td>
+ *     <td>{}</p></td>
+ *     <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
+ * <tr><td>seekTo </p></td>
+ *     <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
+ *     <td>{Idle, Initialized, Stopped, Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state does not change
+ *         the state. Calling this method in an invalid state transfers the
+ *         object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>setAudioAttributes </p></td>
+ *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ *          PlaybackCompleted}</p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method does not change the state. In order for the
+ *         target audio attributes type to become effective, this method must be called before
+ *         prepare().</p></td></tr>
+ * <tr><td>setAudioSessionId </p></td>
+ *     <td>{Idle} </p></td>
+ *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ *          Error} </p></td>
+ *     <td>This method must be called in idle state as the audio session ID must be known before
+ *         calling setDataSource. Calling it does not change the object
+ *         state. </p></td></tr>
+ * <tr><td>setAudioStreamType (deprecated)</p></td>
+ *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ *          PlaybackCompleted}</p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method does not change the state. In order for the
+ *         target audio stream type to become effective, this method must be called before
+ *         prepare().</p></td></tr>
+ * <tr><td>setAuxEffectSendLevel </p></td>
+ *     <td>any</p></td>
+ *     <td>{} </p></td>
+ *     <td>Calling this method does not change the object state. </p></td></tr>
+ * <tr><td>setDataSource </p></td>
+ *     <td>{Idle} </p></td>
+ *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ *          Error} </p></td>
+ *     <td>Successful invoke of this method in a valid state transfers the
+ *         object to the <em>Initialized</em> state. Calling this method in an
+ *         invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setDisplay </p></td>
+ *     <td>any </p></td>
+ *     <td>{} </p></td>
+ *     <td>This method can be called in any state and calling it does not change
+ *         the object state. </p></td></tr>
+ * <tr><td>setSurface </p></td>
+ *     <td>any </p></td>
+ *     <td>{} </p></td>
+ *     <td>This method can be called in any state and calling it does not change
+ *         the object state. </p></td></tr>
+ * <tr><td>loopCurrent </p></td>
+ *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ *         PlaybackCompleted}</p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state does not change
+ *         the state. Calling this method in an
+ *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>isLooping </p></td>
+ *     <td>any </p></td>
+ *     <td>{} </p></td>
+ *     <td>This method can be called in any state and calling it does not change
+ *         the object state. </p></td></tr>
+ * <tr><td>setDrmEventCallback </p></td>
+ *     <td>any </p></td>
+ *     <td>{} </p></td>
+ *     <td>This method can be called in any state and calling it does not change
+ *         the object state. </p></td></tr>
+ * <tr><td>setMediaPlayer2EventCallback </p></td>
+ *     <td>any </p></td>
+ *     <td>{} </p></td>
+ *     <td>This method can be called in any state and calling it does not change
+ *         the object state. </p></td></tr>
+ * <tr><td>setPlaybackParams</p></td>
+ *     <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
+ *     <td>{Idle, Stopped} </p></td>
+ *     <td>This method will change state in some cases, depending on when it's called.
+ *         </p></td></tr>
+ * <tr><td>setPlayerVolume </p></td>
+ *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ *          PlaybackCompleted}</p></td>
+ *     <td>{Error}</p></td>
+ *     <td>Successful invoke of this method does not change the state.
+ * <tr><td>play </p></td>
+ *     <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
+ *     <td>{Idle, Initialized, Stopped, Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state transfers the
+ *         object to the <em>Started</em> state. Calling this method in an
+ *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>stop </p></td>
+ *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ *     <td>{Idle, Initialized, Error}</p></td>
+ *     <td>Successful invoke of this method in a valid state transfers the
+ *         object to the <em>Stopped</em> state. Calling this method in an
+ *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>getTrackInfo </p></td>
+ *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ *     <td>{Idle, Initialized, Error}</p></td>
+ *     <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>selectTrack </p></td>
+ *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ *     <td>{Idle, Initialized, Error}</p></td>
+ *     <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>deselectTrack </p></td>
+ *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ *     <td>{Idle, Initialized, Error}</p></td>
+ *     <td>Successful invoke of this method does not change the state.</p></td></tr>
+ *
+ * </table>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
+ * android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element.
+ *
+ * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
+ * when used with network-based content.
+ *
+ * <a name="Callbacks"></a>
+ * <h3>Callbacks</h3>
+ * <p>Applications may want to register for informational and error
+ * events in order to be informed of some internal state update and
+ * possible runtime errors during playback or streaming. Registration for
+ * these events is done by properly setting the appropriate listeners (via calls
+ * to
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)},
+ * {@link #setDrmEventCallback(Executor, DrmEventCallback)}).
+ * In order to receive the respective callback
+ * associated with these listeners, applications are required to create
+ * MediaPlayer2 objects on a thread with its own Looper running (main UI
+ * thread by default has a Looper running).
+ *
+ */
+@TargetApi(Build.VERSION_CODES.P)
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MediaPlayer2 extends MediaPlayerBase {
+    /**
+     * Create a MediaPlayer2 object.
+     *
+     * @return A MediaPlayer2 object created
+     */
+    public static final MediaPlayer2 create() {
+        return new MediaPlayer2Impl();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public MediaPlayer2() { }
+
+    /**
+     * Releases the resources held by this {@code MediaPlayer2} object.
+     *
+     * It is considered good practice to call this method when you're
+     * done using the MediaPlayer2. In particular, whenever an Activity
+     * of an application is paused (its onPause() method is called),
+     * or stopped (its onStop() method is called), this method should be
+     * invoked to release the MediaPlayer2 object, unless the application
+     * has a special need to keep the object around. In addition to
+     * unnecessary resources (such as memory and instances of codecs)
+     * being held, failure to call this method immediately if a
+     * MediaPlayer2 object is no longer needed may also lead to
+     * continuous battery consumption for mobile devices, and playback
+     * failure for other applications if no multiple instances of the
+     * same codec are supported on a device. Even if multiple instances
+     * of the same codec are supported, some performance degradation
+     * may be expected when unnecessary multiple instances are used
+     * at the same time.
+     *
+     * {@code close()} may be safely called after a prior {@code close()}.
+     * This class implements the Java {@code AutoCloseable} interface and
+     * may be used with try-with-resources.
+     */
+    // This is a synchronous call.
+    @Override
+    public abstract void close();
+
+    /**
+     * Starts or resumes playback. If playback had previously been paused,
+     * playback will continue from where it was paused. If playback had
+     * reached end of stream and been paused, or never started before,
+     * playback will start at the beginning. If the source had not been
+     * prepared, the player will prepare the source and play.
+     *
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void play();
+
+    /**
+     * Prepares the player for playback, asynchronously.
+     *
+     * After setting the datasource and the display surface, you need to
+     * call prepare().
+     *
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void prepare();
+
+    /**
+     * Pauses playback. Call play() to resume.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void pause();
+
+    /**
+     * Tries to play next data source if applicable.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void skipToNext();
+
+    /**
+     * Moves the media to specified time position.
+     * Same as {@link #seekTo(long, int)} with {@code mode = SEEK_PREVIOUS_SYNC}.
+     *
+     * @param msec the offset in milliseconds from the start to seek to
+     */
+    // This is an asynchronous call.
+    @Override
+    public void seekTo(long msec) {
+        seekTo(msec, SEEK_PREVIOUS_SYNC /* mode */);
+    }
+
+    /**
+     * Gets the current playback position.
+     *
+     * @return the current position in milliseconds
+     */
+    @Override
+    public abstract long getCurrentPosition();
+
+    /**
+     * Gets the duration of the file.
+     *
+     * @return the duration in milliseconds, if no duration is available
+     *         (for example, if streaming live content), -1 is returned.
+     */
+    @Override
+    public abstract long getDuration();
+
+    /**
+     * Gets the current buffered media source position received through progressive downloading.
+     * The received buffering percentage indicates how much of the content has been buffered
+     * or played. For example a buffering update of 80 percent when half the content
+     * has already been played indicates that the next 30 percent of the
+     * content to play has been buffered.
+     *
+     * @return the current buffered media source position in milliseconds
+     */
+    @Override
+    public abstract long getBufferedPosition();
+
+    /**
+     * Gets the current player state.
+     *
+     * @return the current player state.
+     */
+    @Override
+    public abstract @PlayerState int getPlayerState();
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     * @return the buffering state, one of the following:
+     */
+    @Override
+    public abstract @BuffState int getBufferingState();
+
+    /**
+     * Sets the audio attributes for this MediaPlayer2.
+     * See {@link AudioAttributes} for how to build and configure an instance of this class.
+     * You must call this method before {@link #prepare()} in order
+     * for the audio attributes to become effective thereafter.
+     * @param attributes a non-null set of audio attributes
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
+
+    /**
+     * Gets the audio attributes for this MediaPlayer2.
+     * @return attributes a set of audio attributes
+     */
+    @Override
+    public abstract @Nullable AudioAttributesCompat getAudioAttributes();
+
+    /**
+     * Sets the data source as described by a DataSourceDesc.
+     *
+     * @param dsd the descriptor of data source you want to play
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets a single data source as described by a DataSourceDesc which will be played
+     * after current data source is finished.
+     *
+     * @param dsd the descriptor of data source you want to play after current one
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets a list of data sources to be played sequentially after current data source is done.
+     *
+     * @param dsds the list of data sources you want to play after current one
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+    /**
+     * Gets the current data source as described by a DataSourceDesc.
+     *
+     * @return the current DataSourceDesc
+     */
+    @Override
+    public abstract @NonNull DataSourceDesc getCurrentDataSource();
+
+    /**
+     * Configures the player to loop on the current data source.
+     * @param loop true if the current data source is meant to loop.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void loopCurrent(boolean loop);
+
+    /**
+     * Sets the playback speed.
+     * A value of 1.0f is the default playback value.
+     * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+     * before using negative values.<br>
+     * After changing the playback speed, it is recommended to query the actual speed supported
+     * by the player, see {@link #getPlaybackSpeed()}.
+     * @param speed the desired playback speed
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setPlaybackSpeed(float speed);
+
+    /**
+     * Returns the actual playback speed to be used by the player when playing.
+     * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+     * @return the actual playback speed
+     */
+    @Override
+    public float getPlaybackSpeed() {
+        return 1.0f;
+    }
+
+    /**
+     * Indicates whether reverse playback is supported.
+     * Reverse playback is indicated by negative playback speeds, see
+     * {@link #setPlaybackSpeed(float)}.
+     * @return true if reverse playback is supported.
+     */
+    @Override
+    public boolean isReversePlaybackSupported() {
+        return false;
+    }
+
+    /**
+     * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+     * on the audio samples.
+     * Note that this volume is specific to the player, and is separate from stream volume
+     * used across the platform.<br>
+     * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+     * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+     * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setPlayerVolume(float volume);
+
+    /**
+     * Returns the current volume of this player to this player.
+     * Note that it does not take into account the associated stream volume.
+     * @return the player volume.
+     */
+    @Override
+    public abstract float getPlayerVolume();
+
+    /**
+     * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+     */
+    @Override
+    public float getMaxPlayerVolume() {
+        return 1.0f;
+    }
+
+    /**
+     * Adds a callback to be notified of events for this player.
+     * @param e the {@link Executor} to be used for the events.
+     * @param cb the callback to receive the events.
+     */
+    // This is a synchronous call.
+    @Override
+    public abstract void registerPlayerEventCallback(@NonNull Executor e,
+                                                     @NonNull PlayerEventCallback cb);
+
+    /**
+     * Removes a previously registered callback for player events
+     * @param cb the callback to remove
+     */
+    // This is a synchronous call.
+    @Override
+    public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
+
+    /**
+     * Insert a task in the command queue to help the client to identify whether a batch
+     * of commands has been finished. When this command is processed, a notification
+     * {@code MediaPlayer2EventCallback.onCommandLabelReached} will be fired with the
+     * given {@code label}.
+     *
+     * @see MediaPlayer2EventCallback#onCommandLabelReached
+     *
+     * @param label An application specific Object used to help to identify the completeness
+     * of a batch of commands.
+     */
+    // This is an asynchronous call.
+    public void notifyWhenCommandLabelReached(@NonNull Object label) { }
+
+    /**
+     * Sets the {@link Surface} to be used as the sink for the video portion of
+     * the media.  Setting a
+     * Surface will un-set any Surface or SurfaceHolder that was previously set.
+     * A null surface will result in only the audio track being played.
+     *
+     * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+     * returned from {@link SurfaceTexture#getTimestamp()} will have an
+     * unspecified zero point.  These timestamps cannot be directly compared
+     * between different media sources, different instances of the same media
+     * source, or multiple runs of the same program.  The timestamp is normally
+     * monotonically increasing and is unaffected by time-of-day adjustments,
+     * but it is reset when the position is set.
+     *
+     * @param surface The {@link Surface} to be used for the video portion of
+     * the media.
+     * @throws IllegalStateException if the internal player engine has not been
+     * initialized or has been released.
+     */
+    // This is an asynchronous call.
+    public abstract void setSurface(Surface surface);
+
+    /* Do not change these video scaling mode values below without updating
+     * their counterparts in system/window.h! Please do not forget to update
+     * {@link #isVideoScalingModeSupported} when new video scaling modes
+     * are added.
+     */
+    /**
+     * Specifies a video scaling mode. The content is stretched to the
+     * surface rendering area. When the surface has the same aspect ratio
+     * as the content, the aspect ratio of the content is maintained;
+     * otherwise, the aspect ratio of the content is not maintained when video
+     * is being rendered.
+     * There is no content cropping with this video scaling mode.
+     */
+    public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1;
+
+    /**
+     * Discards all pending commands.
+     */
+    // This is a synchronous call.
+    public abstract void clearPendingCommands();
+
+    /**
+     * Returns the width of the video.
+     *
+     * @return the width of the video, or 0 if there is no video,
+     * no display surface was set, or the width has not been determined
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+     * is available.
+     */
+    public abstract int getVideoWidth();
+
+    /**
+     * Returns the height of the video.
+     *
+     * @return the height of the video, or 0 if there is no video,
+     * no display surface was set, or the height has not been determined
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height is
+     * available.
+     */
+    public abstract int getVideoHeight();
+
+    /**
+     * Return Metrics data about the current player.
+     *
+     * @return a {@link PersistableBundle} containing the set of attributes and values
+     * available for the media being handled by this instance of MediaPlayer2
+     * The attributes are descibed in {@link MetricsConstants}.
+     *
+     *  Additional vendor-specific fields may also be present in
+     *  the return value.
+     *  @hide
+     *  TODO: This method is not ready for public. Currently returns metrics data in MediaPlayer1.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public abstract PersistableBundle getMetrics();
+
+    /**
+     * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+     * PlaybackParams to the input, except that the object remembers previous speed
+     * when input speed is zero. This allows the object to resume at previous speed
+     * when play() is called. Calling it before the object is prepared does not change
+     * the object state. After the object is prepared, calling it with zero speed is
+     * equivalent to calling pause(). After the object is prepared, calling it with
+     * non-zero speed is equivalent to calling play().
+     *
+     * @param params the playback params.
+     */
+    // This is an asynchronous call.
+    public abstract void setPlaybackParams(@NonNull PlaybackParams params);
+
+    /**
+     * Gets the playback params, containing the current playback rate.
+     *
+     * @return the playback params.
+     */
+    @NonNull
+    public abstract PlaybackParams getPlaybackParams();
+
+    /**
+     * Sets A/V sync mode.
+     *
+     * @param params the A/V sync params to apply
+     */
+    // This is an asynchronous call.
+    public abstract void setSyncParams(@NonNull SyncParams params);
+
+    /**
+     * Gets the A/V sync mode.
+     *
+     * @return the A/V sync params
+     */
+    @NonNull
+    public abstract SyncParams getSyncParams();
+
+    /**
+     * Seek modes used in method seekTo(long, int) to move media position
+     * to a specified location.
+     *
+     * Do not change these mode values without updating their counterparts
+     * in include/media/IMediaSource.h!
+     */
+    /**
+     * This mode is used with {@link #seekTo(long, int)} to move media position to
+     * a sync (or key) frame associated with a data source that is located
+     * right before or at the given time.
+     *
+     * @see #seekTo(long, int)
+     */
+    public static final int SEEK_PREVIOUS_SYNC    = 0x00;
+    /**
+     * This mode is used with {@link #seekTo(long, int)} to move media position to
+     * a sync (or key) frame associated with a data source that is located
+     * right after or at the given time.
+     *
+     * @see #seekTo(long, int)
+     */
+    public static final int SEEK_NEXT_SYNC        = 0x01;
+    /**
+     * This mode is used with {@link #seekTo(long, int)} to move media position to
+     * a sync (or key) frame associated with a data source that is located
+     * closest to (in time) or at the given time.
+     *
+     * @see #seekTo(long, int)
+     */
+    public static final int SEEK_CLOSEST_SYNC     = 0x02;
+    /**
+     * This mode is used with {@link #seekTo(long, int)} to move media position to
+     * a frame (not necessarily a key frame) associated with a data source that
+     * is located closest to or at the given time.
+     *
+     * @see #seekTo(long, int)
+     */
+    public static final int SEEK_CLOSEST          = 0x03;
+
+    /** @hide */
+    @IntDef(flag = false, /*prefix = "SEEK",*/ value = {
+            SEEK_PREVIOUS_SYNC,
+            SEEK_NEXT_SYNC,
+            SEEK_CLOSEST_SYNC,
+            SEEK_CLOSEST,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface SeekMode {}
+
+    /**
+     * Moves the media to specified time position by considering the given mode.
+     * <p>
+     * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+     * There is at most one active seekTo processed at any time. If there is a to-be-completed
+     * seekTo, new seekTo requests will be queued in such a way that only the last request
+     * is kept. When current seekTo is completed, the queued request will be processed if
+     * that request is different from just-finished seekTo operation, i.e., the requested
+     * position or mode is different.
+     *
+     * @param msec the offset in milliseconds from the start to seek to.
+     * When seeking to the given time position, there is no guarantee that the data source
+     * has a frame located at the position. When this happens, a frame nearby will be rendered.
+     * If msec is negative, time position zero will be used.
+     * If msec is larger than duration, duration will be used.
+     * @param mode the mode indicating where exactly to seek to.
+     */
+    // This is an asynchronous call.
+    public abstract void seekTo(long msec, @SeekMode int mode);
+
+    /**
+     * Get current playback position as a {@link MediaTimestamp}.
+     * <p>
+     * The MediaTimestamp represents how the media time correlates to the system time in
+     * a linear fashion using an anchor and a clock rate. During regular playback, the media
+     * time moves fairly constantly (though the anchor frame may be rebased to a current
+     * system time, the linear correlation stays steady). Therefore, this method does not
+     * need to be called often.
+     * <p>
+     * To help users get current playback position, this method always anchors the timestamp
+     * to the current {@link System#nanoTime system time}, so
+     * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+     *
+     * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+     *         is available, e.g. because the media player has not been initialized.
+     *
+     * @see MediaTimestamp
+     */
+    @Nullable
+    public abstract MediaTimestamp getTimestamp();
+
+    /**
+     * Resets the MediaPlayer2 to its uninitialized state. After calling
+     * this method, you will have to initialize it again by setting the
+     * data source and calling prepare().
+     */
+    // This is a synchronous call.
+    @Override
+    public abstract void reset();
+
+    /**
+     * Sets the audio session ID.
+     *
+     * @param sessionId the audio session ID.
+     * The audio session ID is a system wide unique identifier for the audio stream played by
+     * this MediaPlayer2 instance.
+     * The primary use of the audio session ID  is to associate audio effects to a particular
+     * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+     * this effect will be applied only to the audio content of media players within the same
+     * audio session and not to the output mix.
+     * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+     * However, it is possible to force this player to be part of an already existing audio session
+     * by calling this method.
+     * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+     */
+    // This is an asynchronous call.
+    public abstract void setAudioSessionId(int sessionId);
+
+    /**
+     * Returns the audio session ID.
+     *
+     * @return the audio session ID. {@see #setAudioSessionId(int)}
+     * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was
+     * contructed.
+     */
+    public abstract int getAudioSessionId();
+
+    /**
+     * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+     * effect which can be applied on any sound source that directs a certain amount of its
+     * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+     * See {@link #setAuxEffectSendLevel(float)}.
+     * <p>After creating an auxiliary effect (e.g.
+     * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+     * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+     * to attach the player to the effect.
+     * <p>To detach the effect from the player, call this method with a null effect id.
+     * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+     * methods.
+     * @param effectId system wide unique id of the effect to attach
+     */
+    // This is an asynchronous call.
+    public abstract void attachAuxEffect(int effectId);
+
+
+    /**
+     * Sets the send level of the player to the attached auxiliary effect.
+     * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+     * <p>By default the send level is 0, so even if an effect is attached to the player
+     * this method must be called for the effect to be applied.
+     * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+     * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+     * so an appropriate conversion from linear UI input x to level is:
+     * x == 0 -> level = 0
+     * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+     * @param level send level scalar
+     */
+    // This is an asynchronous call.
+    public abstract void setAuxEffectSendLevel(float level);
+
+    /**
+     * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+     *
+     * @see MediaPlayer2#getTrackInfo
+     */
+    public abstract static class TrackInfo {
+        /**
+         * Gets the track type.
+         * @return TrackType which indicates if the track is video, audio, timed text.
+         */
+        public abstract int getTrackType();
+
+        /**
+         * Gets the language code of the track.
+         * @return a language code in either way of ISO-639-1 or ISO-639-2.
+         * When the language is unknown or could not be determined,
+         * ISO-639-2 language code, "und", is returned.
+         */
+        public abstract String getLanguage();
+
+        /**
+         * Gets the {@link MediaFormat} of the track.  If the format is
+         * unknown or could not be determined, null is returned.
+         */
+        public abstract MediaFormat getFormat();
+
+        public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0;
+        public static final int MEDIA_TRACK_TYPE_VIDEO = 1;
+        public static final int MEDIA_TRACK_TYPE_AUDIO = 2;
+
+        /** @hide */
+        @RestrictTo(LIBRARY_GROUP)
+        public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3;
+
+        public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
+        public static final int MEDIA_TRACK_TYPE_METADATA = 5;
+
+        @Override
+        public abstract String toString();
+    };
+
+    /**
+     * Returns a List of track information.
+     *
+     * @return List of track info. The total number of tracks is the array length.
+     * Must be called again if an external timed text source has been added after
+     * addTimedTextSource method is called.
+     */
+    public abstract List<TrackInfo> getTrackInfo();
+
+    /**
+     * Returns the index of the audio, video, or subtitle track currently selected for playback,
+     * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+     * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+     *
+     * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+     * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+     * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+     * @return index of the audio, video, or subtitle track currently selected for playback;
+     * a negative integer is returned when there is no selected track for {@code trackType} or
+     * when {@code trackType} is not one of audio, video, or subtitle.
+     * @throws IllegalStateException if called after {@link #close()}
+     *
+     * @see #getTrackInfo()
+     * @see #selectTrack(int)
+     * @see #deselectTrack(int)
+     */
+    public abstract int getSelectedTrack(int trackType);
+
+    /**
+     * Selects a track.
+     * <p>
+     * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+     * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+     * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+     * </p>
+     * <p>
+     * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+     * Audio, Timed Text), the most recent one will be chosen.
+     * </p>
+     * <p>
+     * The first audio and video tracks are selected by default if available, even though
+     * this method is not called. However, no timed text track will be selected until
+     * this function is called.
+     * </p>
+     * <p>
+     * Currently, only timed text tracks or audio tracks can be selected via this method.
+     * In addition, the support for selecting an audio track at runtime is pretty limited
+     * in that an audio track can only be selected in the <em>Prepared</em> state.
+     * </p>
+     * @param index the index of the track to be selected. The valid range of the index
+     * is 0..total number of track - 1. The total number of tracks as well as the type of
+     * each individual track can be found by calling {@link #getTrackInfo()} method.
+     * @throws IllegalStateException if called in an invalid state.
+     *
+     * @see MediaPlayer2#getTrackInfo
+     */
+    // This is an asynchronous call.
+    public abstract void selectTrack(int index);
+
+    /**
+     * Deselect a track.
+     * <p>
+     * Currently, the track must be a timed text track and no audio or video tracks can be
+     * deselected. If the timed text track identified by index has not been
+     * selected before, it throws an exception.
+     * </p>
+     * @param index the index of the track to be deselected. The valid range of the index
+     * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+     * each individual track can be found by calling {@link #getTrackInfo()} method.
+     * @throws IllegalStateException if called in an invalid state.
+     *
+     * @see MediaPlayer2#getTrackInfo
+     */
+    // This is an asynchronous call.
+    public abstract void deselectTrack(int index);
+
+    /**
+     * Interface definition for callbacks to be invoked when the player has the corresponding
+     * events.
+     */
+    public abstract static class MediaPlayer2EventCallback {
+        /**
+         * Called to indicate the video size
+         *
+         * The video size (width and height) could be 0 if there was no video,
+         * no display surface was set, or the value was not determined yet.
+         *
+         * @param mp the MediaPlayer2 associated with this callback
+         * @param dsd the DataSourceDesc of this data source
+         * @param width the width of the video
+         * @param height the height of the video
+         */
+        public void onVideoSizeChanged(
+                MediaPlayer2 mp, DataSourceDesc dsd, int width, int height) { }
+
+        /**
+         * Called to indicate available timed metadata
+         * <p>
+         * This method will be called as timed metadata is extracted from the media,
+         * in the same order as it occurs in the media. The timing of this event is
+         * not controlled by the associated timestamp.
+         * <p>
+         * Currently only HTTP live streaming data URI's embedded with timed ID3 tags generates
+         * {@link TimedMetaData}.
+         *
+         * @see MediaPlayer2#selectTrack(int)
+         * @see TimedMetaData
+         *
+         * @param mp the MediaPlayer2 associated with this callback
+         * @param dsd the DataSourceDesc of this data source
+         * @param data the timed metadata sample associated with this event
+         */
+        public void onTimedMetaDataAvailable(
+                MediaPlayer2 mp, DataSourceDesc dsd, TimedMetaData data) { }
+
+        /**
+         * Called to indicate an error.
+         *
+         * @param mp the MediaPlayer2 the error pertains to
+         * @param dsd the DataSourceDesc of this data source
+         * @param what the type of error that has occurred.
+         * @param extra an extra code, specific to the error. Typically
+         * implementation dependent.
+         */
+        public void onError(
+                MediaPlayer2 mp, DataSourceDesc dsd, @MediaError int what, int extra) { }
+
+        /**
+         * Called to indicate an info or a warning.
+         *
+         * @param mp the MediaPlayer2 the info pertains to.
+         * @param dsd the DataSourceDesc of this data source
+         * @param what the type of info or warning.
+         * @param extra an extra code, specific to the info. Typically
+         * implementation dependent.
+         */
+        public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, @MediaInfo int what, int extra) { }
+
+        /**
+         * Called to acknowledge an API call.
+         *
+         * @param mp the MediaPlayer2 the call was made on.
+         * @param dsd the DataSourceDesc of this data source
+         * @param what the enum for the API call.
+         * @param status the returned status code for the call.
+         */
+        public void onCallCompleted(
+                MediaPlayer2 mp, DataSourceDesc dsd, @CallCompleted int what,
+                @CallStatus int status) { }
+
+        /**
+         * Called to indicate media clock has changed.
+         *
+         * @param mp the MediaPlayer2 the media time pertains to.
+         * @param dsd the DataSourceDesc of this data source
+         * @param timestamp the new media clock.
+         */
+        public void onMediaTimeChanged(
+                MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) { }
+
+        /**
+         * Called to indicate {@link #notifyWhenCommandLabelReached(Object)} has been processed.
+         *
+         * @param mp the MediaPlayer2 {@link #notifyWhenCommandLabelReached(Object)} was called on.
+         * @param label the application specific Object given by
+         *        {@link #notifyWhenCommandLabelReached(Object)}.
+         */
+        public void onCommandLabelReached(MediaPlayer2 mp, @NonNull Object label) { }
+
+        /* TODO : uncomment below once API is available in supportlib.
+         * Called when when a player subtitle track has new subtitle data available.
+         * @param mp the player that reports the new subtitle data
+         * @param data the subtitle data
+         */
+        // public void onSubtitleData(MediaPlayer2 mp, @NonNull SubtitleData data) { }
+    }
+
+    /**
+     * Sets the callback to be invoked when the media source is ready for playback.
+     *
+     * @param eventCallback the callback that will be run
+     * @param executor the executor through which the callback should be invoked
+     */
+    // This is a synchronous call.
+    public abstract void setMediaPlayer2EventCallback(
+            @NonNull Executor executor, @NonNull MediaPlayer2EventCallback eventCallback);
+
+    /**
+     * Clears the {@link MediaPlayer2EventCallback}.
+     */
+    // This is a synchronous call.
+    public abstract void clearMediaPlayer2EventCallback();
+
+    /* Do not change these values without updating their counterparts
+     * in include/media/mediaplayer2.h!
+     */
+    /** Unspecified media player error.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onError
+     */
+    public static final int MEDIA_ERROR_UNKNOWN = 1;
+
+    /** The video is streamed and its container is not valid for progressive
+     * playback i.e the video's index (e.g moov atom) is not at the start of the
+     * file.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onError
+     */
+    public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;
+
+    /** File or network related operation errors. */
+    public static final int MEDIA_ERROR_IO = -1004;
+    /** Bitstream is not conforming to the related coding standard or file spec. */
+    public static final int MEDIA_ERROR_MALFORMED = -1007;
+    /** Bitstream is conforming to the related coding standard or file spec, but
+     * the media framework does not support the feature. */
+    public static final int MEDIA_ERROR_UNSUPPORTED = -1010;
+    /** Some operation takes too long to complete, usually more than 3-5 seconds. */
+    public static final int MEDIA_ERROR_TIMED_OUT = -110;
+
+    /** Unspecified low-level system error. This value originated from UNKNOWN_ERROR in
+     * system/core/include/utils/Errors.h
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onError
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int MEDIA_ERROR_SYSTEM = -2147483648;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, /*prefix = "MEDIA_ERROR",*/ value = {
+            MEDIA_ERROR_UNKNOWN,
+            MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+            MEDIA_ERROR_IO,
+            MEDIA_ERROR_MALFORMED,
+            MEDIA_ERROR_UNSUPPORTED,
+            MEDIA_ERROR_TIMED_OUT,
+            MEDIA_ERROR_SYSTEM
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface MediaError {}
+
+    /* Do not change these values without updating their counterparts
+     * in include/media/mediaplayer2.h!
+     */
+    /** Unspecified media player info.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_UNKNOWN = 1;
+
+    /** The player switched to this datas source because it is the
+     * next-to-be-played in the playlist.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
+
+    /** The player just pushed the very first video frame for rendering.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3;
+
+    /** The player just rendered the very first audio sample.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4;
+
+    /** The player just completed the playback of this data source.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5;
+
+    /** The player just completed the playback of the full playlist.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_PLAYLIST_END = 6;
+
+    /** The player just prepared a data source.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_PREPARED = 100;
+
+    /** The video is too complex for the decoder: it can't decode frames fast
+     *  enough. Possibly only the audio plays fine at this stage.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700;
+
+    /** MediaPlayer2 is temporarily pausing playback internally in order to
+     * buffer more data.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_BUFFERING_START = 701;
+
+    /** MediaPlayer2 is resuming playback after filling buffers.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_BUFFERING_END = 702;
+
+    /** Estimated network bandwidth information (kbps) is available; currently this event fires
+     * simultaneously as {@link #MEDIA_INFO_BUFFERING_START} and {@link #MEDIA_INFO_BUFFERING_END}
+     * when playing network files.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703;
+
+    /**
+     * Update status in buffering a media source received through progressive downloading.
+     * The received buffering percentage indicates how much of the content has been buffered
+     * or played. For example a buffering update of 80 percent when half the content
+     * has already been played indicates that the next 30 percent of the
+     * content to play has been buffered.
+     *
+     * The {@code extra} parameter in {@code MediaPlayer2EventCallback.onInfo} is the
+     * percentage (0-100) of the content that has been buffered or played thus far.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_BUFFERING_UPDATE = 704;
+
+    /** Bad interleaving means that a media has been improperly interleaved or
+     * not interleaved at all, e.g has all the video samples first then all the
+     * audio ones. Video is playing but a lot of disk seeks may be happening.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_BAD_INTERLEAVING = 800;
+
+    /** The media cannot be seeked (e.g live stream)
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_NOT_SEEKABLE = 801;
+
+    /** A new set of metadata is available.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_METADATA_UPDATE = 802;
+
+    /** A new set of external-only metadata is available.  Used by
+     *  JAVA framework to avoid triggering track scanning.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int MEDIA_INFO_EXTERNAL_METADATA_UPDATE = 803;
+
+    /** Informs that audio is not playing. Note that playback of the video
+     * is not interrupted.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804;
+
+    /** Informs that video is not playing. Note that playback of the audio
+     * is not interrupted.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805;
+
+    /** Failed to handle timed text track properly.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     * {@hide}
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int MEDIA_INFO_TIMED_TEXT_ERROR = 900;
+
+    /** Subtitle track was not supported by the media framework.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;
+
+    /** Reading the subtitle track takes too long.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, /*prefix = "MEDIA_INFO",*/ value = {
+            MEDIA_INFO_UNKNOWN,
+            MEDIA_INFO_STARTED_AS_NEXT,
+            MEDIA_INFO_VIDEO_RENDERING_START,
+            MEDIA_INFO_AUDIO_RENDERING_START,
+            MEDIA_INFO_PLAYBACK_COMPLETE,
+            MEDIA_INFO_PLAYLIST_END,
+            MEDIA_INFO_PREPARED,
+            MEDIA_INFO_VIDEO_TRACK_LAGGING,
+            MEDIA_INFO_BUFFERING_START,
+            MEDIA_INFO_BUFFERING_END,
+            MEDIA_INFO_NETWORK_BANDWIDTH,
+            MEDIA_INFO_BUFFERING_UPDATE,
+            MEDIA_INFO_BAD_INTERLEAVING,
+            MEDIA_INFO_NOT_SEEKABLE,
+            MEDIA_INFO_METADATA_UPDATE,
+            MEDIA_INFO_EXTERNAL_METADATA_UPDATE,
+            MEDIA_INFO_AUDIO_NOT_PLAYING,
+            MEDIA_INFO_VIDEO_NOT_PLAYING,
+            MEDIA_INFO_TIMED_TEXT_ERROR,
+            MEDIA_INFO_UNSUPPORTED_SUBTITLE,
+            MEDIA_INFO_SUBTITLE_TIMED_OUT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface MediaInfo {}
+
+    //--------------------------------------------------------------------------
+    /** The player just completed a call {@link #attachAuxEffect}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_ATTACH_AUX_EFFECT = 1;
+
+    /** The player just completed a call {@link #deselectTrack}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_DESELECT_TRACK = 2;
+
+    /** The player just completed a call {@link #loopCurrent}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_LOOP_CURRENT = 3;
+
+    /** The player just completed a call {@link #pause}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_PAUSE = 4;
+
+    /** The player just completed a call {@link #play}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_PLAY = 5;
+
+    /** The player just completed a call {@link #prepare}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_PREPARE = 6;
+
+    /** The player just completed a call {@link #releaseDrm}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_RELEASE_DRM = 12;
+
+    /** The player just completed a call {@link #restoreDrmKeys}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
+
+    /** The player just completed a call {@link #seekTo}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SEEK_TO = 14;
+
+    /** The player just completed a call {@link #selectTrack}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SELECT_TRACK = 15;
+
+    /** The player just completed a call {@link #setAudioAttributes}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_AUDIO_ATTRIBUTES = 16;
+
+    /** The player just completed a call {@link #setAudioSessionId}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_AUDIO_SESSION_ID = 17;
+
+    /** The player just completed a call {@link #setAuxEffectSendLevel}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL = 18;
+
+    /** The player just completed a call {@link #setDataSource}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_DATA_SOURCE = 19;
+
+    /** The player just completed a call {@link #setNextDataSource}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCE = 22;
+
+    /** The player just completed a call {@link #setNextDataSources}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCES = 23;
+
+    /** The player just completed a call {@link #setPlaybackParams}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_PLAYBACK_PARAMS = 24;
+
+    /** The player just completed a call {@link #setPlaybackSpeed}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_PLAYBACK_SPEED = 25;
+
+    /** The player just completed a call {@link #setPlayerVolume}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_PLAYER_VOLUME = 26;
+
+    /** The player just completed a call {@link #setSurface}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_SURFACE = 27;
+
+    /** The player just completed a call {@link #setSyncParams}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_SYNC_PARAMS = 28;
+
+    /** The player just completed a call {@link #skipToNext}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SKIP_TO_NEXT = 29;
+
+    /** The player just completed a call {@code notifyWhenCommandLabelReached}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCommandLabelReached
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED = 1003;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, /*prefix = "CALL_COMPLETED",*/ value = {
+            CALL_COMPLETED_ATTACH_AUX_EFFECT,
+            CALL_COMPLETED_DESELECT_TRACK,
+            CALL_COMPLETED_LOOP_CURRENT,
+            CALL_COMPLETED_PAUSE,
+            CALL_COMPLETED_PLAY,
+            CALL_COMPLETED_PREPARE,
+            CALL_COMPLETED_RELEASE_DRM,
+            CALL_COMPLETED_RESTORE_DRM_KEYS,
+            CALL_COMPLETED_SEEK_TO,
+            CALL_COMPLETED_SELECT_TRACK,
+            CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
+            CALL_COMPLETED_SET_AUDIO_SESSION_ID,
+            CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL,
+            CALL_COMPLETED_SET_DATA_SOURCE,
+            CALL_COMPLETED_SET_NEXT_DATA_SOURCE,
+            CALL_COMPLETED_SET_NEXT_DATA_SOURCES,
+            CALL_COMPLETED_SET_PLAYBACK_PARAMS,
+            CALL_COMPLETED_SET_PLAYBACK_SPEED,
+            CALL_COMPLETED_SET_PLAYER_VOLUME,
+            CALL_COMPLETED_SET_SURFACE,
+            CALL_COMPLETED_SET_SYNC_PARAMS,
+            CALL_COMPLETED_SKIP_TO_NEXT,
+            CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface CallCompleted {}
+
+    /** Status code represents that call is completed without an error.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_NO_ERROR = 0;
+
+    /** Status code represents that call is ended with an unknown error.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_ERROR_UNKNOWN = Integer.MIN_VALUE;
+
+    /** Status code represents that the player is not in valid state for the operation.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_INVALID_OPERATION = 1;
+
+    /** Status code represents that the argument is illegal.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_BAD_VALUE = 2;
+
+    /** Status code represents that the operation is not allowed.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_PERMISSION_DENIED = 3;
+
+    /** Status code represents a file or network related operation error.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_ERROR_IO = 4;
+
+    /** Status code represents that DRM operation is called before preparing a DRM scheme through
+     *  {@link #prepareDrm}.
+     * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, /*prefix = "CALL_STATUS",*/ value = {
+            CALL_STATUS_NO_ERROR,
+            CALL_STATUS_ERROR_UNKNOWN,
+            CALL_STATUS_INVALID_OPERATION,
+            CALL_STATUS_BAD_VALUE,
+            CALL_STATUS_PERMISSION_DENIED,
+            CALL_STATUS_ERROR_IO,
+            CALL_STATUS_NO_DRM_SCHEME})
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface CallStatus {}
+
+    // Modular DRM begin
+
+    /**
+     * Interface definition of a callback to be invoked when the app
+     * can do DRM configuration (get/set properties) before the session
+     * is opened. This facilitates configuration of the properties, like
+     * 'securityLevel', which has to be set after DRM scheme creation but
+     * before the DRM session is opened.
+     *
+     * The only allowed DRM calls in this listener are {@link #getDrmPropertyString}
+     * and {@link #setDrmPropertyString}.
+     */
+    public interface OnDrmConfigHelper {
+        /**
+         * Called to give the app the opportunity to configure DRM before the session is created
+         *
+         * @param mp the {@code MediaPlayer2} associated with this callback
+         * @param dsd the DataSourceDesc of this data source
+         */
+        void onDrmConfig(MediaPlayer2 mp, DataSourceDesc dsd);
+    }
+
+    /**
+     * Register a callback to be invoked for configuration of the DRM object before
+     * the session is created.
+     * The callback will be invoked synchronously during the execution
+     * of {@link #prepareDrm(UUID uuid)}.
+     *
+     * @param listener the callback that will be run
+     */
+    // This is a synchronous call.
+    public abstract void setOnDrmConfigHelper(OnDrmConfigHelper listener);
+
+    /**
+     * Interface definition for callbacks to be invoked when the player has the corresponding
+     * DRM events.
+     */
+    public abstract static class DrmEventCallback {
+        /**
+         * Called to indicate DRM info is available
+         *
+         * @param mp the {@code MediaPlayer2} associated with this callback
+         * @param dsd the DataSourceDesc of this data source
+         * @param drmInfo DRM info of the source including PSSH, and subset
+         *                of crypto schemes supported by this device
+         */
+        public void onDrmInfo(MediaPlayer2 mp, DataSourceDesc dsd, DrmInfo drmInfo) { }
+
+        /**
+         * Called to notify the client that {@link #prepareDrm} is finished and ready for
+         * key request/response.
+         *
+         * @param mp the {@code MediaPlayer2} associated with this callback
+         * @param dsd the DataSourceDesc of this data source
+         * @param status the result of DRM preparation.
+         */
+        public void onDrmPrepared(
+                MediaPlayer2 mp, DataSourceDesc dsd, @PrepareDrmStatusCode int status) { }
+    }
+
+    /**
+     * Sets the callback to be invoked when the media source is ready for playback.
+     *
+     * @param eventCallback the callback that will be run
+     * @param executor the executor through which the callback should be invoked
+     */
+    // This is a synchronous call.
+    public abstract void setDrmEventCallback(@NonNull Executor executor,
+                                             @NonNull DrmEventCallback eventCallback);
+
+    /**
+     * Clears the {@link DrmEventCallback}.
+     */
+    // This is a synchronous call.
+    public abstract void clearDrmEventCallback();
+
+    /**
+     * The status codes for {@link DrmEventCallback#onDrmPrepared} listener.
+     * <p>
+     *
+     * DRM preparation has succeeded.
+     */
+    public static final int PREPARE_DRM_STATUS_SUCCESS = 0;
+
+    /**
+     * The device required DRM provisioning but couldn't reach the provisioning server.
+     */
+    public static final int PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR = 1;
+
+    /**
+     * The device required DRM provisioning but the provisioning server denied the request.
+     */
+    public static final int PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR = 2;
+
+    /**
+     * The DRM preparation has failed .
+     */
+    public static final int PREPARE_DRM_STATUS_PREPARATION_ERROR = 3;
+
+
+    /** @hide */
+    @IntDef(flag = false, /*prefix = "PREPARE_DRM_STATUS",*/ value = {
+            PREPARE_DRM_STATUS_SUCCESS,
+            PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
+            PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+            PREPARE_DRM_STATUS_PREPARATION_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface PrepareDrmStatusCode {}
+
+    /**
+     * Retrieves the DRM Info associated with the current source
+     *
+     * @throws IllegalStateException if called before being prepared
+     */
+    public abstract DrmInfo getDrmInfo();
+
+    /**
+     * Prepares the DRM for the current source
+     * <p>
+     * If {@link OnDrmConfigHelper} is registered, it will be called during
+     * preparation to allow configuration of the DRM properties before opening the
+     * DRM session. Note that the callback is called synchronously in the thread that called
+     * {@link #prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+     * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+     * <p>
+     * If the device has not been provisioned before, this call also provisions the device
+     * which involves accessing the provisioning server and can take a variable time to
+     * complete depending on the network connectivity.
+     * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+     * mode by launching the provisioning in the background and returning. The listener
+     * will be called when provisioning and preparation has finished. If a
+     * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+     * and preparation has finished, i.e., runs in blocking mode.
+     * <p>
+     * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+     * session being ready. The application should not make any assumption about its call
+     * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+     * execute the listener (unless the listener is registered with a handler thread).
+     * <p>
+     *
+     * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+     * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+     *
+     * @throws IllegalStateException              if called before being prepared or the DRM was
+     *                                            prepared already
+     * @throws UnsupportedSchemeException         if the crypto scheme is not supported
+     * @throws ResourceBusyException              if required DRM resources are in use
+     * @throws ProvisioningNetworkErrorException  if provisioning is required but failed due to a
+     *                                            network error
+     * @throws ProvisioningServerErrorException   if provisioning is required but failed due to
+     *                                            the request denied by the provisioning server
+     */
+    // This is a synchronous call.
+    public abstract void prepareDrm(@NonNull UUID uuid)
+            throws UnsupportedSchemeException, ResourceBusyException,
+            ProvisioningNetworkErrorException, ProvisioningServerErrorException;
+
+    /**
+     * Releases the DRM session
+     * <p>
+     * The player has to have an active DRM session and be in stopped, or prepared
+     * state before this call is made.
+     * A {@code reset()} call will release the DRM session implicitly.
+     *
+     * @throws NoDrmSchemeException if there is no active DRM session to release
+     */
+    // This is an asynchronous call.
+    public abstract void releaseDrm() throws NoDrmSchemeException;
+
+    /**
+     * A key request/response exchange occurs between the app and a license server
+     * to obtain or release keys used to decrypt encrypted content.
+     * <p>
+     * getDrmKeyRequest() is used to obtain an opaque key request byte array that is
+     * delivered to the license server.  The opaque key request byte array is returned
+     * in KeyRequest.data.  The recommended URL to deliver the key request to is
+     * returned in KeyRequest.defaultUrl.
+     * <p>
+     * After the app has received the key request response from the server,
+     * it should deliver to the response to the DRM engine plugin using the method
+     * {@link #provideDrmKeyResponse}.
+     *
+     * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+     * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+     * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+     *
+     * @param initData is the container-specific initialization data when the keyType is
+     * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+     * interpreted based on the mime type provided in the mimeType parameter.  It could
+     * contain, for example, the content ID, key ID or other data obtained from the content
+     * metadata that is required in generating the key request.
+     * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+     *
+     * @param mimeType identifies the mime type of the content
+     *
+     * @param keyType specifies the type of the request. The request may be to acquire
+     * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+     * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+     * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+     *
+     * @param optionalParameters are included in the key request message to
+     * allow a client application to provide additional message parameters to the server.
+     * This may be {@code null} if no additional parameters are to be sent.
+     *
+     * @throws NoDrmSchemeException if there is no active DRM session
+     */
+    @NonNull
+    public abstract MediaDrm.KeyRequest getDrmKeyRequest(
+            @Nullable byte[] keySetId, @Nullable byte[] initData,
+            @Nullable String mimeType, int keyType,
+            @Nullable Map<String, String> optionalParameters)
+            throws NoDrmSchemeException;
+
+    /**
+     * A key response is received from the license server by the app, then it is
+     * provided to the DRM engine plugin using provideDrmKeyResponse. When the
+     * response is for an offline key request, a key-set identifier is returned that
+     * can be used to later restore the keys to a new session with the method
+     * {@ link # restoreDrmKeys}.
+     * When the response is for a streaming or release request, null is returned.
+     *
+     * @param keySetId When the response is for a release request, keySetId identifies
+     * the saved key associated with the release request (i.e., the same keySetId
+     * passed to the earlier {@ link # getDrmKeyRequest} call. It MUST be null when the
+     * response is for either streaming or offline key requests.
+     *
+     * @param response the byte array response from the server
+     *
+     * @throws NoDrmSchemeException if there is no active DRM session
+     * @throws DeniedByServerException if the response indicates that the
+     * server rejected the request
+     */
+    // This is a synchronous call.
+    public abstract byte[] provideDrmKeyResponse(
+            @Nullable byte[] keySetId, @NonNull byte[] response)
+            throws NoDrmSchemeException, DeniedByServerException;
+
+    /**
+     * Restore persisted offline keys into a new session.  keySetId identifies the
+     * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
+     *
+     * @param keySetId identifies the saved key set to restore
+     */
+    // This is an asynchronous call.
+    public abstract void restoreDrmKeys(@NonNull byte[] keySetId)
+            throws NoDrmSchemeException;
+
+    /**
+     * Read a DRM engine plugin String property value, given the property name string.
+     * <p>
+     * @param propertyName the property name
+     *
+     * Standard fields names are:
+     * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+     * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+     */
+    @NonNull
+    public abstract String getDrmPropertyString(
+            @NonNull String propertyName)
+            throws NoDrmSchemeException;
+
+    /**
+     * Set a DRM engine plugin String property value.
+     * <p>
+     * @param propertyName the property name
+     * @param value the property value
+     *
+     * Standard fields names are:
+     * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+     * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+     */
+    // This is a synchronous call.
+    public abstract void setDrmPropertyString(
+            @NonNull String propertyName, @NonNull String value)
+            throws NoDrmSchemeException;
+
+    /**
+     * Encapsulates the DRM properties of the source.
+     */
+    public abstract static class DrmInfo {
+        /**
+         * Returns the PSSH info of the data source for each supported DRM scheme.
+         */
+        public abstract Map<UUID, byte[]> getPssh();
+
+        /**
+         * Returns the intersection of the data source and the device DRM schemes.
+         * It effectively identifies the subset of the source's DRM schemes which
+         * are supported by the device too.
+         */
+        public abstract List<UUID> getSupportedSchemes();
+    };  // DrmInfo
+
+    /**
+     * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+     * Extends MediaDrm.MediaDrmException
+     */
+    public static class NoDrmSchemeException extends MediaDrmException {
+        public NoDrmSchemeException(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    /**
+     * Thrown when the device requires DRM provisioning but the provisioning attempt has
+     * failed due to a network error (Internet reachability, timeout, etc.).
+     * Extends MediaDrm.MediaDrmException
+     */
+    public static class ProvisioningNetworkErrorException extends MediaDrmException {
+        public ProvisioningNetworkErrorException(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    /**
+     * Thrown when the device requires DRM provisioning but the provisioning attempt has
+     * failed due to the provisioning server denying the request.
+     * Extends MediaDrm.MediaDrmException
+     */
+    public static class ProvisioningServerErrorException extends MediaDrmException {
+        public ProvisioningServerErrorException(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+}
diff --git a/androidx/media/MediaPlayer2Impl.java b/androidx/media/MediaPlayer2Impl.java
new file mode 100644
index 0000000..3b3e119
--- /dev/null
+++ b/androidx/media/MediaPlayer2Impl.java
@@ -0,0 +1,1982 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.media.AudioAttributes;
+import android.media.DeniedByServerException;
+import android.media.MediaDataSource;
+import android.media.MediaDrm;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaTimestamp;
+import android.media.PlaybackParams;
+import android.media.ResourceBusyException;
+import android.media.SyncParams;
+import android.media.TimedMetaData;
+import android.media.UnsupportedSchemeException;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.io.IOException;
+import java.nio.ByteOrder;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @hide
+ */
+@TargetApi(Build.VERSION_CODES.P)
+@RestrictTo(LIBRARY_GROUP)
+public final class MediaPlayer2Impl extends MediaPlayer2 {
+
+    private static final String TAG = "MediaPlayer2Impl";
+
+    private static final int NEXT_SOURCE_STATE_ERROR = -1;
+    private static final int NEXT_SOURCE_STATE_INIT = 0;
+    private static final int NEXT_SOURCE_STATE_PREPARING = 1;
+    private static final int NEXT_SOURCE_STATE_PREPARED = 2;
+
+    private static ArrayMap<Integer, Integer> sInfoEventMap;
+    private static ArrayMap<Integer, Integer> sErrorEventMap;
+
+    static {
+        sInfoEventMap = new ArrayMap<>();
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNKNOWN);
+        sInfoEventMap.put(2 /*MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT*/, MEDIA_INFO_STARTED_AS_NEXT);
+        sInfoEventMap.put(
+                MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_RENDERING_START);
+        sInfoEventMap.put(
+                MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING, MEDIA_INFO_VIDEO_TRACK_LAGGING);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_START, MEDIA_INFO_BUFFERING_START);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_END, MEDIA_INFO_BUFFERING_END);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BAD_INTERLEAVING);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_NOT_SEEKABLE);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_NOT_PLAYING);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_NOT_PLAYING);
+        sInfoEventMap.put(
+                MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, MEDIA_INFO_UNSUPPORTED_SUBTITLE);
+        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT, MEDIA_INFO_SUBTITLE_TIMED_OUT);
+
+        sErrorEventMap = new ArrayMap<>();
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNKNOWN);
+        sErrorEventMap.put(
+                MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+                MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_IO, MEDIA_ERROR_IO);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
+        sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);
+    }
+
+    private MediaPlayer mPlayer;  // MediaPlayer is thread-safe.
+
+    private final Object mSrcLock = new Object();
+    //--- guarded by |mSrcLock| start
+    private long mSrcIdGenerator = 0;
+    private DataSourceDesc mCurrentDSD;
+    private long mCurrentSrcId = mSrcIdGenerator++;
+    private List<DataSourceDesc> mNextDSDs;
+    private long mNextSrcId = mSrcIdGenerator++;
+    private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
+    private boolean mNextSourcePlayPending = false;
+    //--- guarded by |mSrcLock| end
+
+    private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
+    private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
+    private volatile float mVolume = 1.0f;
+
+    private HandlerThread mHandlerThread;
+    private final Handler mTaskHandler;
+    private final Object mTaskLock = new Object();
+    @GuardedBy("mTaskLock")
+    private final ArrayDeque<Task> mPendingTasks = new ArrayDeque<>();
+    @GuardedBy("mTaskLock")
+    private Task mCurrentTask;
+
+    private final Object mLock = new Object();
+    //--- guarded by |mLock| start
+    @PlayerState private int mPlayerState;
+    @BuffState private int mBufferingState;
+    private AudioAttributesCompat mAudioAttributes;
+    private ArrayList<Pair<Executor, MediaPlayer2EventCallback>> mMp2EventCallbackRecords =
+            new ArrayList<>();
+    private ArrayMap<PlayerEventCallback, Executor> mPlayerEventCallbackMap =
+            new ArrayMap<>();
+    private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
+            new ArrayList<>();
+    //--- guarded by |mLock| end
+
+    /**
+     * Default constructor.
+     * <p>When done with the MediaPlayer2Impl, you should call  {@link #close()},
+     * to free the resources. If not released, too many MediaPlayer2Impl instances may
+     * result in an exception.</p>
+     */
+    public MediaPlayer2Impl() {
+        mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
+        mHandlerThread.start();
+        Looper looper = mHandlerThread.getLooper();
+        mTaskHandler = new Handler(looper);
+        mPlayer = new MediaPlayer();
+        mPlayerState = PLAYER_STATE_IDLE;
+        mBufferingState = BUFFERING_STATE_UNKNOWN;
+        setUpListeners();
+    }
+
+    /**
+     * Releases the resources held by this {@code MediaPlayer2} object.
+     *
+     * It is considered good practice to call this method when you're
+     * done using the MediaPlayer2. In particular, whenever an Activity
+     * of an application is paused (its onPause() method is called),
+     * or stopped (its onStop() method is called), this method should be
+     * invoked to release the MediaPlayer2 object, unless the application
+     * has a special need to keep the object around. In addition to
+     * unnecessary resources (such as memory and instances of codecs)
+     * being held, failure to call this method immediately if a
+     * MediaPlayer2 object is no longer needed may also lead to
+     * continuous battery consumption for mobile devices, and playback
+     * failure for other applications if no multiple instances of the
+     * same codec are supported on a device. Even if multiple instances
+     * of the same codec are supported, some performance degradation
+     * may be expected when unnecessary multiple instances are used
+     * at the same time.
+     *
+     * {@code close()} may be safely called after a prior {@code close()}.
+     * This class implements the Java {@code AutoCloseable} interface and
+     * may be used with try-with-resources.
+     */
+    @Override
+    public void close() {
+        mPlayer.release();
+    }
+
+    /**
+     * Starts or resumes playback. If playback had previously been paused,
+     * playback will continue from where it was paused. If playback had
+     * been stopped, or never started before, playback will start at the
+     * beginning.
+     *
+     * @throws IllegalStateException if it is called in an invalid state
+     */
+    @Override
+    public void play() {
+        addTask(new Task(CALL_COMPLETED_PLAY, false) {
+            @Override
+            void process() {
+                mPlayer.start();
+                setPlayerState(PLAYER_STATE_PLAYING);
+            }
+        });
+    }
+
+    /**
+     * Prepares the player for playback, asynchronously.
+     *
+     * After setting the datasource and the display surface, you need to either
+     * call prepare(). For streams, you should call prepare(),
+     * which returns immediately, rather than blocking until enough data has been
+     * buffered.
+     *
+     * @throws IllegalStateException if it is called in an invalid state
+     */
+    @Override
+    public void prepare() {
+        addTask(new Task(CALL_COMPLETED_PREPARE, true) {
+            @Override
+            void process() throws IOException {
+                mPlayer.prepareAsync();
+                setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+            }
+        });
+    }
+
+    /**
+     * Pauses playback. Call play() to resume.
+     *
+     * @throws IllegalStateException if the internal player engine has not been initialized.
+     */
+    @Override
+    public void pause() {
+        addTask(new Task(CALL_COMPLETED_PAUSE, false) {
+            @Override
+            void process() {
+                mPlayer.pause();
+                setPlayerState(PLAYER_STATE_PAUSED);
+            }
+        });
+    }
+
+    /**
+     * Tries to play next data source if applicable.
+     *
+     * @throws IllegalStateException if it is called in an invalid state
+     */
+    @Override
+    public void skipToNext() {
+        addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
+            @Override
+            void process() {
+                // TODO: switch to next data source and play
+            }
+        });
+    }
+
+    /**
+     * Gets the current playback position.
+     *
+     * @return the current position in milliseconds
+     */
+    @Override
+    public long getCurrentPosition() {
+        return mPlayer.getCurrentPosition();
+    }
+
+    /**
+     * Gets the duration of the file.
+     *
+     * @return the duration in milliseconds, if no duration is available
+     * (for example, if streaming live content), -1 is returned.
+     */
+    @Override
+    public long getDuration() {
+        return mPlayer.getDuration();
+    }
+
+    /**
+     * Gets the current buffered media source position received through progressive downloading.
+     * The received buffering percentage indicates how much of the content has been buffered
+     * or played. For example a buffering update of 80 percent when half the content
+     * has already been played indicates that the next 30 percent of the
+     * content to play has been buffered.
+     *
+     * @return the current buffered media source position in milliseconds
+     */
+    @Override
+    public long getBufferedPosition() {
+        // Use cached buffered percent for now.
+        return getDuration() * mBufferedPercentageCurrent.get() / 100;
+    }
+
+    @Override
+    public @PlayerState int getPlayerState() {
+        synchronized (mLock) {
+            return mPlayerState;
+        }
+    }
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     */
+    @Override
+    public @BuffState int getBufferingState() {
+        synchronized (mLock) {
+            return mBufferingState;
+        }
+    }
+
+    /**
+     * Sets the audio attributes for this MediaPlayer2.
+     * See {@link AudioAttributes} for how to build and configure an instance of this class.
+     * You must call this method before {@link #prepare()} in order
+     * for the audio attributes to become effective thereafter.
+     * @param attributes a non-null set of audio attributes
+     * @throws IllegalArgumentException if the attributes are null or invalid.
+     */
+    @Override
+    public void setAudioAttributes(@NonNull final AudioAttributesCompat attributes) {
+        addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
+            @Override
+            void process() {
+                AudioAttributes attr;
+                synchronized (mLock) {
+                    mAudioAttributes = attributes;
+                    attr = (AudioAttributes) mAudioAttributes.unwrap();
+                }
+                mPlayer.setAudioAttributes(attr);
+            }
+        });
+    }
+
+    @Override
+    public @NonNull AudioAttributesCompat getAudioAttributes() {
+        synchronized (mLock) {
+            return mAudioAttributes;
+        }
+    }
+
+    /**
+     * Sets the data source as described by a DataSourceDesc.
+     *
+     * @param dsd the descriptor of data source you want to play
+     * @throws IllegalStateException if it is called in an invalid state
+     * @throws NullPointerException if dsd is null
+     */
+    @Override
+    public void setDataSource(@NonNull final DataSourceDesc dsd) {
+        addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
+            @Override
+            void process() {
+                Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+                // TODO: setDataSource could update exist data source
+                synchronized (mSrcLock) {
+                    mCurrentDSD = dsd;
+                    mCurrentSrcId = mSrcIdGenerator++;
+                    try {
+                        handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Sets a single data source as described by a DataSourceDesc which will be played
+     * after current data source is finished.
+     *
+     * @param dsd the descriptor of data source you want to play after current one
+     * @throws IllegalStateException if it is called in an invalid state
+     * @throws NullPointerException if dsd is null
+     */
+    @Override
+    public void setNextDataSource(@NonNull final DataSourceDesc dsd) {
+        addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
+            @Override
+            void process() {
+                Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+                synchronized (mSrcLock) {
+                    mNextDSDs = new ArrayList<DataSourceDesc>(1);
+                    mNextDSDs.add(dsd);
+                    mNextSrcId = mSrcIdGenerator++;
+                    mNextSourceState = NEXT_SOURCE_STATE_INIT;
+                    mNextSourcePlayPending = false;
+                }
+                /* FIXME : define and handle state.
+                int state = getMediaPlayer2State();
+                if (state != MEDIAPLAYER2_STATE_IDLE) {
+                    synchronized (mSrcLock) {
+                        prepareNextDataSource_l();
+                    }
+                }
+                */
+            }
+        });
+    }
+
+    /**
+     * Sets a list of data sources to be played sequentially after current data source is done.
+     *
+     * @param dsds the list of data sources you want to play after current one
+     * @throws IllegalStateException if it is called in an invalid state
+     * @throws IllegalArgumentException if dsds is null or empty, or contains null DataSourceDesc
+     */
+    @Override
+    public void setNextDataSources(@NonNull final List<DataSourceDesc> dsds) {
+        addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
+            @Override
+            void process() {
+                if (dsds == null || dsds.size() == 0) {
+                    throw new IllegalArgumentException("data source list cannot be null or empty.");
+                }
+                for (DataSourceDesc dsd : dsds) {
+                    if (dsd == null) {
+                        throw new IllegalArgumentException(
+                                "DataSourceDesc in the source list cannot be null.");
+                    }
+                }
+
+                synchronized (mSrcLock) {
+                    mNextDSDs = new ArrayList(dsds);
+                    mNextSrcId = mSrcIdGenerator++;
+                    mNextSourceState = NEXT_SOURCE_STATE_INIT;
+                    mNextSourcePlayPending = false;
+                }
+                /* FIXME : define and handle state.
+                int state = getMediaPlayer2State();
+                if (state != MEDIAPLAYER2_STATE_IDLE) {
+                    synchronized (mSrcLock) {
+                        prepareNextDataSource_l();
+                    }
+                }
+                */
+            }
+        });
+    }
+
+    @Override
+    public @NonNull DataSourceDesc getCurrentDataSource() {
+        synchronized (mSrcLock) {
+            return mCurrentDSD;
+        }
+    }
+
+    /**
+     * Configures the player to loop on the current data source.
+     * @param loop true if the current data source is meant to loop.
+     */
+    @Override
+    public void loopCurrent(final boolean loop) {
+        addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
+            @Override
+            void process() {
+                mPlayer.setLooping(loop);
+            }
+        });
+    }
+
+    /**
+     * Sets the playback speed.
+     * A value of 1.0f is the default playback value.
+     * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+     * before using negative values.<br>
+     * After changing the playback speed, it is recommended to query the actual speed supported
+     * by the player, see {@link #getPlaybackSpeed()}.
+     * @param speed the desired playback speed
+     */
+    @Override
+    public void setPlaybackSpeed(final float speed) {
+        addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_SPEED, false) {
+            @Override
+            void process() {
+                setPlaybackParamsInternal(getPlaybackParams().setSpeed(speed));
+            }
+        });
+    }
+
+    /**
+     * Returns the actual playback speed to be used by the player when playing.
+     * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+     * @return the actual playback speed
+     */
+    @Override
+    public float getPlaybackSpeed() {
+        return getPlaybackParams().getSpeed();
+    }
+
+    /**
+     * Indicates whether reverse playback is supported.
+     * Reverse playback is indicated by negative playback speeds, see
+     * {@link #setPlaybackSpeed(float)}.
+     * @return true if reverse playback is supported.
+     */
+    @Override
+    public boolean isReversePlaybackSupported() {
+        return false;
+    }
+
+    /**
+     * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+     * on the audio samples.
+     * Note that this volume is specific to the player, and is separate from stream volume
+     * used across the platform.<br>
+     * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+     * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+     * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+     */
+    @Override
+    public void setPlayerVolume(final float volume) {
+        addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
+            @Override
+            void process() {
+                mVolume = volume;
+                mPlayer.setVolume(volume, volume);
+            }
+        });
+    }
+
+    /**
+     * Returns the current volume of this player to this player.
+     * Note that it does not take into account the associated stream volume.
+     * @return the player volume.
+     */
+    @Override
+    public float getPlayerVolume() {
+        return mVolume;
+    }
+
+    /**
+     * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+     */
+    @Override
+    public float getMaxPlayerVolume() {
+        return 1.0f;
+    }
+
+    /**
+     * Adds a callback to be notified of events for this player.
+     * @param e the {@link Executor} to be used for the events.
+     * @param cb the callback to receive the events.
+     */
+    @Override
+    public void registerPlayerEventCallback(@NonNull Executor e,
+            @NonNull PlayerEventCallback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Illegal null PlayerEventCallback");
+        }
+        if (e == null) {
+            throw new IllegalArgumentException(
+                    "Illegal null Executor for the PlayerEventCallback");
+        }
+        synchronized (mLock) {
+            mPlayerEventCallbackMap.put(cb, e);
+        }
+    }
+
+    /**
+     * Removes a previously registered callback for player events
+     * @param cb the callback to remove
+     */
+    @Override
+    public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Illegal null PlayerEventCallback");
+        }
+        synchronized (mLock) {
+            mPlayerEventCallbackMap.remove(cb);
+        }
+    }
+
+    @Override
+    public void notifyWhenCommandLabelReached(final Object label) {
+        addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
+            @Override
+            void process() {
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onCommandLabelReached(MediaPlayer2Impl.this, label);
+                    }
+                });
+            }
+        });
+    }
+
+    /**
+     * Sets the {@link Surface} to be used as the sink for the video portion of
+     * the media. Setting a Surface will un-set any Surface or SurfaceHolder that
+     * was previously set. A null surface will result in only the audio track
+     * being played.
+     *
+     * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+     * returned from {@link SurfaceTexture#getTimestamp()} will have an
+     * unspecified zero point.  These timestamps cannot be directly compared
+     * between different media sources, different instances of the same media
+     * source, or multiple runs of the same program.  The timestamp is normally
+     * monotonically increasing and is unaffected by time-of-day adjustments,
+     * but it is reset when the position is set.
+     *
+     * @param surface The {@link Surface} to be used for the video portion of
+     * the media.
+     * @throws IllegalStateException if the internal player engine has not been
+     * initialized or has been released.
+     */
+    @Override
+    public void setSurface(final Surface surface) {
+        addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
+            @Override
+            void process() {
+                mPlayer.setSurface(surface);
+            }
+        });
+    }
+
+    /**
+     * Discards all pending commands.
+     */
+    @Override
+    public void clearPendingCommands() {
+        // TODO: implement this.
+    }
+
+    private void addTask(Task task) {
+        synchronized (mTaskLock) {
+            mPendingTasks.add(task);
+            processPendingTask_l();
+        }
+    }
+
+    @GuardedBy("mTaskLock")
+    private void processPendingTask_l() {
+        if (mCurrentTask != null) {
+            return;
+        }
+        if (!mPendingTasks.isEmpty()) {
+            Task task = mPendingTasks.removeFirst();
+            mCurrentTask = task;
+            mTaskHandler.post(task);
+        }
+    }
+
+    private void handleDataSource(boolean isCurrent, @NonNull final DataSourceDesc dsd, long srcId)
+            throws IOException {
+        Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+
+        // TODO: handle the case isCurrent is false.
+        switch (dsd.getType()) {
+            case DataSourceDesc.TYPE_CALLBACK:
+                mPlayer.setDataSource(new MediaDataSource() {
+                    Media2DataSource mDataSource = dsd.getMedia2DataSource();
+                    @Override
+                    public int readAt(long position, byte[] buffer, int offset, int size)
+                            throws IOException {
+                        return mDataSource.readAt(position, buffer, offset, size);
+                    }
+
+                    @Override
+                    public long getSize() throws IOException {
+                        return mDataSource.getSize();
+                    }
+
+                    @Override
+                    public void close() throws IOException {
+                        mDataSource.close();
+                    }
+                });
+                break;
+
+            case DataSourceDesc.TYPE_FD:
+                mPlayer.setDataSource(
+                        dsd.getFileDescriptor(),
+                        dsd.getFileDescriptorOffset(),
+                        dsd.getFileDescriptorLength());
+                break;
+
+            case DataSourceDesc.TYPE_URI:
+                mPlayer.setDataSource(
+                        dsd.getUriContext(),
+                        dsd.getUri(),
+                        dsd.getUriHeaders(),
+                        dsd.getUriCookies());
+                break;
+
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Returns the width of the video.
+     *
+     * @return the width of the video, or 0 if there is no video,
+     * no display surface was set, or the width has not been determined
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+     * is available.
+     */
+    @Override
+    public int getVideoWidth() {
+        return mPlayer.getVideoWidth();
+    }
+
+    /**
+     * Returns the height of the video.
+     *
+     * @return the height of the video, or 0 if there is no video,
+     * no display surface was set, or the height has not been determined
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height
+     * is available.
+     */
+    @Override
+    public int getVideoHeight() {
+        return mPlayer.getVideoHeight();
+    }
+
+    @Override
+    public PersistableBundle getMetrics() {
+        return mPlayer.getMetrics();
+    }
+
+    /**
+     * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+     * PlaybackParams to the input, except that the object remembers previous speed
+     * when input speed is zero. This allows the object to resume at previous speed
+     * when play() is called. Calling it before the object is prepared does not change
+     * the object state. After the object is prepared, calling it with zero speed is
+     * equivalent to calling pause(). After the object is prepared, calling it with
+     * non-zero speed is equivalent to calling play().
+     *
+     * @param params the playback params.
+     * @throws IllegalStateException if the internal player engine has not been
+     * initialized or has been released.
+     * @throws IllegalArgumentException if params is not supported.
+     */
+    @Override
+    public void setPlaybackParams(@NonNull final PlaybackParams params) {
+        addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
+            @Override
+            void process() {
+                setPlaybackParamsInternal(params);
+            }
+        });
+    }
+
+    /**
+     * Gets the playback params, containing the current playback rate.
+     *
+     * @return the playback params.
+     * @throws IllegalStateException if the internal player engine has not been
+     * initialized.
+     */
+    @Override
+    @NonNull
+    public PlaybackParams getPlaybackParams() {
+        return mPlayer.getPlaybackParams();
+    }
+
+    /**
+     * Sets A/V sync mode.
+     *
+     * @param params the A/V sync params to apply
+     * @throws IllegalStateException if the internal player engine has not been
+     * initialized.
+     * @throws IllegalArgumentException if params are not supported.
+     */
+    @Override
+    public void setSyncParams(@NonNull final SyncParams params) {
+        addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) {
+            @Override
+            void process() {
+                mPlayer.setSyncParams(params);
+            }
+        });
+    }
+
+    /**
+     * Gets the A/V sync mode.
+     *
+     * @return the A/V sync params
+     * @throws IllegalStateException if the internal player engine has not been
+     *                               initialized.
+     */
+    @Override
+    @NonNull
+    public SyncParams getSyncParams() {
+        return mPlayer.getSyncParams();
+    }
+
+    /**
+     * Moves the media to specified time position by considering the given mode.
+     * <p>
+     * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+     * There is at most one active seekTo processed at any time. If there is a to-be-completed
+     * seekTo, new seekTo requests will be queued in such a way that only the last request
+     * is kept. When current seekTo is completed, the queued request will be processed if
+     * that request is different from just-finished seekTo operation, i.e., the requested
+     * position or mode is different.
+     *
+     * @param msec the offset in milliseconds from the start to seek to.
+     * When seeking to the given time position, there is no guarantee that the data source
+     * has a frame located at the position. When this happens, a frame nearby will be rendered.
+     * If msec is negative, time position zero will be used.
+     * If msec is larger than duration, duration will be used.
+     * @param mode the mode indicating where exactly to seek to.
+     * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
+     * that has a timestamp earlier than or the same as msec. Use
+     * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
+     * that has a timestamp later than or the same as msec. Use
+     * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
+     * that has a timestamp closest to or the same as msec. Use
+     * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
+     * or may not be a sync frame but is closest to or the same as msec.
+     * {@link #SEEK_CLOSEST} often has larger performance overhead compared
+     * to the other options if there is no sync frame located at msec.
+     * @throws IllegalStateException if the internal player engine has not been
+     * initialized
+     * @throws IllegalArgumentException if the mode is invalid.
+     */
+    @Override
+    public void seekTo(final long msec, @SeekMode final int mode) {
+        addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
+            @Override
+            void process() {
+                mPlayer.seekTo(msec, mode);
+            }
+        });
+    }
+
+    /**
+     * Get current playback position as a {@link MediaTimestamp}.
+     * <p>
+     * The MediaTimestamp represents how the media time correlates to the system time in
+     * a linear fashion using an anchor and a clock rate. During regular playback, the media
+     * time moves fairly constantly (though the anchor frame may be rebased to a current
+     * system time, the linear correlation stays steady). Therefore, this method does not
+     * need to be called often.
+     * <p>
+     * To help users get current playback position, this method always anchors the timestamp
+     * to the current {@link System#nanoTime system time}, so
+     * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+     *
+     * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+     * is available, e.g. because the media player has not been initialized.
+     * @see MediaTimestamp
+     */
+    @Override
+    @Nullable
+    public MediaTimestamp getTimestamp() {
+        return mPlayer.getTimestamp();
+    }
+
+    /**
+     * Resets the MediaPlayer2 to its uninitialized state. After calling
+     * this method, you will have to initialize it again by setting the
+     * data source and calling prepare().
+     */
+    @Override
+    public void reset() {
+        mPlayer.reset();
+        setPlayerState(PLAYER_STATE_IDLE);
+        setBufferingState(BUFFERING_STATE_UNKNOWN);
+        /* FIXME: reset other internal variables. */
+    }
+
+    /**
+     * Sets the audio session ID.
+     *
+     * @param sessionId the audio session ID.
+     * The audio session ID is a system wide unique identifier for the audio stream played by
+     * this MediaPlayer2 instance.
+     * The primary use of the audio session ID  is to associate audio effects to a particular
+     * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+     * this effect will be applied only to the audio content of media players within the same
+     * audio session and not to the output mix.
+     * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+     * However, it is possible to force this player to be part of an already existing audio session
+     * by calling this method.
+     * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+     * @throws IllegalStateException if it is called in an invalid state
+     * @throws IllegalArgumentException if the sessionId is invalid.
+     */
+    @Override
+    public void setAudioSessionId(final int sessionId) {
+        addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
+            @Override
+            void process() {
+                mPlayer.setAudioSessionId(sessionId);
+            }
+        });
+    }
+
+    @Override
+    public int getAudioSessionId() {
+        return mPlayer.getAudioSessionId();
+    }
+
+    /**
+     * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+     * effect which can be applied on any sound source that directs a certain amount of its
+     * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+     * See {@link #setAuxEffectSendLevel(float)}.
+     * <p>After creating an auxiliary effect (e.g.
+     * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+     * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+     * to attach the player to the effect.
+     * <p>To detach the effect from the player, call this method with a null effect id.
+     * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+     * methods.
+     * @param effectId system wide unique id of the effect to attach
+     */
+    @Override
+    public void attachAuxEffect(final int effectId) {
+        addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
+            @Override
+            void process() {
+                mPlayer.attachAuxEffect(effectId);
+            }
+        });
+    }
+
+    /**
+     * Sets the send level of the player to the attached auxiliary effect.
+     * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+     * <p>By default the send level is 0, so even if an effect is attached to the player
+     * this method must be called for the effect to be applied.
+     * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+     * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+     * so an appropriate conversion from linear UI input x to level is:
+     * x == 0 -> level = 0
+     * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+     * @param level send level scalar
+     */
+    @Override
+    public void setAuxEffectSendLevel(final float level) {
+        addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
+            @Override
+            void process() {
+                mPlayer.setAuxEffectSendLevel(level);
+            }
+        });
+    }
+
+    /**
+     * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+     *
+     * @see MediaPlayer2#getTrackInfo
+     */
+    public static final class TrackInfoImpl extends TrackInfo {
+        final int mTrackType;
+        final MediaFormat mFormat;
+
+        /**
+         * Gets the track type.
+         * @return TrackType which indicates if the track is video, audio, timed text.
+         */
+        @Override
+        public int getTrackType() {
+            return mTrackType;
+        }
+
+        /**
+         * Gets the language code of the track.
+         * @return a language code in either way of ISO-639-1 or ISO-639-2.
+         * When the language is unknown or could not be determined,
+         * ISO-639-2 language code, "und", is returned.
+         */
+        @Override
+        public String getLanguage() {
+            String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
+            return language == null ? "und" : language;
+        }
+
+        /**
+         * Gets the {@link MediaFormat} of the track.  If the format is
+         * unknown or could not be determined, null is returned.
+         */
+        @Override
+        public MediaFormat getFormat() {
+            if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT
+                    || mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+                return mFormat;
+            }
+            return null;
+        }
+
+        TrackInfoImpl(Parcel in) {
+            mTrackType = in.readInt();
+            // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
+            // even for audio/video tracks, meaning we only set the mime and language.
+            String mime = in.readString();
+            String language = in.readString();
+            mFormat = MediaFormat.createSubtitleFormat(mime, language);
+
+            if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+                mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
+                mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
+                mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
+            }
+        }
+
+        TrackInfoImpl(int type, MediaFormat format) {
+            mTrackType = type;
+            mFormat = format;
+        }
+
+        /**
+         * Flatten this object in to a Parcel.
+         *
+         * @param dest The Parcel in which the object should be written.
+         * @param flags Additional flags about how the object should be written.
+         * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
+         */
+        /* package private */ void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mTrackType);
+            dest.writeString(getLanguage());
+
+            if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+                dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
+                dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
+                dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
+                dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder out = new StringBuilder(128);
+            out.append(getClass().getName());
+            out.append('{');
+            switch (mTrackType) {
+                case MEDIA_TRACK_TYPE_VIDEO:
+                    out.append("VIDEO");
+                    break;
+                case MEDIA_TRACK_TYPE_AUDIO:
+                    out.append("AUDIO");
+                    break;
+                case MEDIA_TRACK_TYPE_TIMEDTEXT:
+                    out.append("TIMEDTEXT");
+                    break;
+                case MEDIA_TRACK_TYPE_SUBTITLE:
+                    out.append("SUBTITLE");
+                    break;
+                default:
+                    out.append("UNKNOWN");
+                    break;
+            }
+            out.append(", " + mFormat.toString());
+            out.append("}");
+            return out.toString();
+        }
+
+        /**
+         * Used to read a TrackInfoImpl from a Parcel.
+         */
+        /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR =
+                new Parcelable.Creator<TrackInfoImpl>() {
+                    @Override
+                    public TrackInfoImpl createFromParcel(Parcel in) {
+                        return new TrackInfoImpl(in);
+                    }
+
+                    @Override
+                    public TrackInfoImpl[] newArray(int size) {
+                        return new TrackInfoImpl[size];
+                    }
+                };
+
+    };
+
+    /**
+     * Returns a List of track information.
+     *
+     * @return List of track info. The total number of tracks is the array length.
+     * Must be called again if an external timed text source has been added after
+     * addTimedTextSource method is called.
+     * @throws IllegalStateException if it is called in an invalid state.
+     */
+    @Override
+    public List<TrackInfo> getTrackInfo() {
+        MediaPlayer.TrackInfo[] list = mPlayer.getTrackInfo();
+        List<TrackInfo> trackList = new ArrayList<>();
+        for (MediaPlayer.TrackInfo info : list) {
+            trackList.add(new TrackInfoImpl(info.getTrackType(), info.getFormat()));
+        }
+        return trackList;
+    }
+
+    /**
+     * Returns the index of the audio, video, or subtitle track currently selected for playback,
+     * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+     * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+     *
+     * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+     * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+     * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+     * @return index of the audio, video, or subtitle track currently selected for playback;
+     * a negative integer is returned when there is no selected track for {@code trackType} or
+     * when {@code trackType} is not one of audio, video, or subtitle.
+     * @throws IllegalStateException if called after {@link #close()}
+     *
+     * @see #getTrackInfo()
+     * @see #selectTrack(int)
+     * @see #deselectTrack(int)
+     */
+    @Override
+    public int getSelectedTrack(int trackType) {
+        return mPlayer.getSelectedTrack(trackType);
+    }
+
+    /**
+     * Selects a track.
+     * <p>
+     * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+     * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+     * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+     * </p>
+     * <p>
+     * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+     * Audio, Timed Text), the most recent one will be chosen.
+     * </p>
+     * <p>
+     * The first audio and video tracks are selected by default if available, even though
+     * this method is not called. However, no timed text track will be selected until
+     * this function is called.
+     * </p>
+     * <p>
+     * Currently, only timed text tracks or audio tracks can be selected via this method.
+     * In addition, the support for selecting an audio track at runtime is pretty limited
+     * in that an audio track can only be selected in the <em>Prepared</em> state.
+     * </p>
+     *
+     * @param index the index of the track to be selected. The valid range of the index
+     * is 0..total number of track - 1. The total number of tracks as well as the type of
+     * each individual track can be found by calling {@link #getTrackInfo()} method.
+     * @throws IllegalStateException if called in an invalid state.
+     * @see MediaPlayer2#getTrackInfo
+     */
+    @Override
+    public void selectTrack(final int index) {
+        addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
+            @Override
+            void process() {
+                mPlayer.selectTrack(index);
+            }
+        });
+    }
+
+    /**
+     * Deselect a track.
+     * <p>
+     * Currently, the track must be a timed text track and no audio or video tracks can be
+     * deselected. If the timed text track identified by index has not been
+     * selected before, it throws an exception.
+     * </p>
+     *
+     * @param index the index of the track to be deselected. The valid range of the index
+     * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+     * each individual track can be found by calling {@link #getTrackInfo()} method.
+     * @throws IllegalStateException if called in an invalid state.
+     * @see MediaPlayer2#getTrackInfo
+     */
+    @Override
+    public void deselectTrack(final int index) {
+        addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
+            @Override
+            void process() {
+                mPlayer.deselectTrack(index);
+            }
+        });
+    }
+
+    /**
+     * Register a callback to be invoked when the media source is ready
+     * for playback.
+     *
+     * @param eventCallback the callback that will be run
+     * @param executor the executor through which the callback should be invoked
+     */
+    @Override
+    public void setMediaPlayer2EventCallback(@NonNull Executor executor,
+            @NonNull MediaPlayer2EventCallback eventCallback) {
+        if (eventCallback == null) {
+            throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
+        }
+        if (executor == null) {
+            throw new IllegalArgumentException(
+                    "Illegal null Executor for the MediaPlayer2EventCallback");
+        }
+        synchronized (mLock) {
+            mMp2EventCallbackRecords.add(new Pair(executor, eventCallback));
+        }
+    }
+
+    /**
+     * Clears the {@link MediaPlayer2EventCallback}.
+     */
+    @Override
+    public void clearMediaPlayer2EventCallback() {
+        synchronized (mLock) {
+            mMp2EventCallbackRecords.clear();
+        }
+    }
+
+    // Modular DRM begin
+
+    /**
+     * Register a callback to be invoked for configuration of the DRM object before
+     * the session is created.
+     * The callback will be invoked synchronously during the execution
+     * of {@link #prepareDrm(UUID uuid)}.
+     *
+     * @param listener the callback that will be run
+     */
+    @Override
+    public void setOnDrmConfigHelper(final OnDrmConfigHelper listener) {
+        mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
+            @Override
+            public void onDrmConfig(MediaPlayer mp) {
+                /** FIXME: pass the right DSD. */
+                listener.onDrmConfig(MediaPlayer2Impl.this, null);
+            }
+        });
+    }
+
+    /**
+     * Register a callback to be invoked when the media source is ready
+     * for playback.
+     *
+     * @param eventCallback the callback that will be run
+     * @param executor the executor through which the callback should be invoked
+     */
+    @Override
+    public void setDrmEventCallback(@NonNull Executor executor,
+                                    @NonNull DrmEventCallback eventCallback) {
+        if (eventCallback == null) {
+            throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
+        }
+        if (executor == null) {
+            throw new IllegalArgumentException(
+                    "Illegal null Executor for the MediaPlayer2EventCallback");
+        }
+        synchronized (mLock) {
+            mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
+        }
+    }
+
+    /**
+     * Clears the {@link DrmEventCallback}.
+     */
+    @Override
+    public void clearDrmEventCallback() {
+        synchronized (mLock) {
+            mDrmEventCallbackRecords.clear();
+        }
+    }
+
+
+    /**
+     * Retrieves the DRM Info associated with the current source
+     *
+     * @throws IllegalStateException if called before prepare()
+     */
+    @Override
+    public DrmInfo getDrmInfo() {
+        MediaPlayer.DrmInfo info = mPlayer.getDrmInfo();
+        return info == null ? null : new DrmInfoImpl(info.getPssh(), info.getSupportedSchemes());
+    }
+
+
+    /**
+     * Prepares the DRM for the current source
+     * <p>
+     * If {@code OnDrmConfigHelper} is registered, it will be called during
+     * preparation to allow configuration of the DRM properties before opening the
+     * DRM session. Note that the callback is called synchronously in the thread that called
+     * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+     * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+     * <p>
+     * If the device has not been provisioned before, this call also provisions the device
+     * which involves accessing the provisioning server and can take a variable time to
+     * complete depending on the network connectivity.
+     * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+     * mode by launching the provisioning in the background and returning. The listener
+     * will be called when provisioning and preparation has finished. If a
+     * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+     * and preparation has finished, i.e., runs in blocking mode.
+     * <p>
+     * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+     * session being ready. The application should not make any assumption about its call
+     * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+     * execute the listener (unless the listener is registered with a handler thread).
+     * <p>
+     *
+     * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+     * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+     * @throws IllegalStateException             if called before prepare(), or the DRM was
+     *                                           prepared already
+     * @throws UnsupportedSchemeException        if the crypto scheme is not supported
+     * @throws ResourceBusyException             if required DRM resources are in use
+     * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+     *                                           network error
+     * @throws ProvisioningServerErrorException  if provisioning is required but failed due to
+     *                                           the request denied by the provisioning server
+     */
+    @Override
+    public void prepareDrm(@NonNull UUID uuid)
+            throws UnsupportedSchemeException, ResourceBusyException,
+            ProvisioningNetworkErrorException, ProvisioningServerErrorException {
+        try {
+            mPlayer.prepareDrm(uuid);
+        } catch (MediaPlayer.ProvisioningNetworkErrorException e) {
+            throw new ProvisioningNetworkErrorException(e.getMessage());
+        } catch (MediaPlayer.ProvisioningServerErrorException e) {
+            throw new ProvisioningServerErrorException(e.getMessage());
+        }
+    }
+
+    /**
+     * Releases the DRM session
+     * <p>
+     * The player has to have an active DRM session and be in stopped, or prepared
+     * state before this call is made.
+     * A {@code reset()} call will release the DRM session implicitly.
+     *
+     * @throws NoDrmSchemeException if there is no active DRM session to release
+     */
+    @Override
+    public void releaseDrm() throws NoDrmSchemeException {
+        addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
+            @Override
+            void process() throws NoDrmSchemeException {
+                try {
+                    mPlayer.releaseDrm();
+                } catch (MediaPlayer.NoDrmSchemeException e) {
+                    throw new NoDrmSchemeException(e.getMessage());
+                }
+            }
+        });
+    }
+
+
+    /**
+     * A key request/response exchange occurs between the app and a license server
+     * to obtain or release keys used to decrypt encrypted content.
+     * <p>
+     * getDrmKeyRequest() is used to obtain an opaque key request byte array that is
+     * delivered to the license server.  The opaque key request byte array is returned
+     * in KeyRequest.data.  The recommended URL to deliver the key request to is
+     * returned in KeyRequest.defaultUrl.
+     * <p>
+     * After the app has received the key request response from the server,
+     * it should deliver to the response to the DRM engine plugin using the method
+     * {@link #provideDrmKeyResponse}.
+     *
+     * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+     * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+     * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+     *
+     * @param initData is the container-specific initialization data when the keyType is
+     * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+     * interpreted based on the mime type provided in the mimeType parameter.  It could
+     * contain, for example, the content ID, key ID or other data obtained from the content
+     * metadata that is required in generating the key request.
+     * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+     *
+     * @param mimeType identifies the mime type of the content
+     *
+     * @param keyType specifies the type of the request. The request may be to acquire
+     * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+     * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+     * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+     *
+     * @param optionalParameters are included in the key request message to
+     * allow a client application to provide additional message parameters to the server.
+     * This may be {@code null} if no additional parameters are to be sent.
+     *
+     * @throws NoDrmSchemeException if there is no active DRM session
+     */
+    @Override
+    @NonNull
+    public MediaDrm.KeyRequest getDrmKeyRequest(@Nullable byte[] keySetId,
+            @Nullable byte[] initData, @Nullable String mimeType, int keyType,
+            @Nullable Map<String, String> optionalParameters)
+            throws NoDrmSchemeException {
+        try {
+            return mPlayer.getKeyRequest(keySetId, initData, mimeType, keyType, optionalParameters);
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
+    }
+
+
+    /**
+     * A key response is received from the license server by the app, then it is
+     * provided to the DRM engine plugin using provideDrmKeyResponse. When the
+     * response is for an offline key request, a key-set identifier is returned that
+     * can be used to later restore the keys to a new session with the method
+     * {@ link # restoreDrmKeys}.
+     * When the response is for a streaming or release request, null is returned.
+     *
+     * @param keySetId When the response is for a release request, keySetId identifies
+     * the saved key associated with the release request (i.e., the same keySetId
+     * passed to the earlier {@ link #getDrmKeyRequest} call. It MUST be null when the
+     * response is for either streaming or offline key requests.
+     *
+     * @param response the byte array response from the server
+     *
+     * @throws NoDrmSchemeException if there is no active DRM session
+     * @throws DeniedByServerException if the response indicates that the
+     * server rejected the request
+     */
+    @Override
+    public byte[] provideDrmKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+            throws NoDrmSchemeException, DeniedByServerException {
+        try {
+            return mPlayer.provideKeyResponse(keySetId, response);
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
+    }
+
+
+    /**
+     * Restore persisted offline keys into a new session.  keySetId identifies the
+     * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
+     *
+     * @param keySetId identifies the saved key set to restore
+     */
+    @Override
+    public void restoreDrmKeys(@NonNull final byte[] keySetId)
+            throws NoDrmSchemeException {
+        addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
+            @Override
+            void process() throws NoDrmSchemeException {
+                try {
+                    mPlayer.restoreKeys(keySetId);
+                } catch (MediaPlayer.NoDrmSchemeException e) {
+                    throw new NoDrmSchemeException(e.getMessage());
+                }
+            }
+        });
+    }
+
+
+    /**
+     * Read a DRM engine plugin String property value, given the property name string.
+     * <p>
+     *
+
+     * @param propertyName the property name
+     *
+     * Standard fields names are:
+     * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+     * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+     */
+    @Override
+    @NonNull
+    public String getDrmPropertyString(@NonNull String propertyName)
+            throws NoDrmSchemeException {
+        try {
+            return mPlayer.getDrmPropertyString(propertyName);
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
+    }
+
+
+    /**
+     * Set a DRM engine plugin String property value.
+     * <p>
+     *
+     * @param propertyName the property name
+     * @param value the property value
+     *
+     * Standard fields names are:
+     * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+     * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+     */
+    @Override
+    public void setDrmPropertyString(@NonNull String propertyName,
+                                     @NonNull String value)
+            throws NoDrmSchemeException {
+        try {
+            mPlayer.setDrmPropertyString(propertyName, value);
+        } catch (MediaPlayer.NoDrmSchemeException e) {
+            throw new NoDrmSchemeException(e.getMessage());
+        }
+    }
+
+    private void setPlaybackParamsInternal(final PlaybackParams params) {
+        PlaybackParams current = mPlayer.getPlaybackParams();
+        mPlayer.setPlaybackParams(params);
+        if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
+            notifyPlayerEvent(new PlayerEventNotifier() {
+                @Override
+                public void notify(PlayerEventCallback cb) {
+                    cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
+                }
+            });
+        }
+    }
+
+    private void setPlayerState(@PlayerState final int state) {
+        synchronized (mLock) {
+            if (mPlayerState == state) {
+                return;
+            }
+            mPlayerState = state;
+        }
+        notifyPlayerEvent(new PlayerEventNotifier() {
+            @Override
+            public void notify(PlayerEventCallback cb) {
+                cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
+            }
+        });
+    }
+
+    private void setBufferingState(@BuffState final int state) {
+        synchronized (mLock) {
+            if (mBufferingState == state) {
+                return;
+            }
+            mBufferingState = state;
+        }
+        notifyPlayerEvent(new PlayerEventNotifier() {
+            @Override
+            public void notify(PlayerEventCallback cb) {
+                cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
+            }
+        });
+    }
+
+    private void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
+        List<Pair<Executor, MediaPlayer2EventCallback>> records;
+        synchronized (mLock) {
+            records = new ArrayList<>(mMp2EventCallbackRecords);
+        }
+        for (final Pair<Executor, MediaPlayer2EventCallback> record : records) {
+            record.first.execute(new Runnable() {
+                @Override
+                public void run() {
+                    notifier.notify(record.second);
+                }
+            });
+        }
+    }
+
+    private void notifyPlayerEvent(final PlayerEventNotifier notifier) {
+        ArrayMap<PlayerEventCallback, Executor> map;
+        synchronized (mLock) {
+            map = new ArrayMap<>(mPlayerEventCallbackMap);
+        }
+        final int callbackCount = map.size();
+        for (int i = 0; i < callbackCount; i++) {
+            final Executor executor = map.valueAt(i);
+            final PlayerEventCallback cb = map.keyAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    notifier.notify(cb);
+                }
+            });
+        }
+    }
+
+    private interface Mp2EventNotifier {
+        void notify(MediaPlayer2EventCallback callback);
+    }
+
+    private interface PlayerEventNotifier {
+        void notify(PlayerEventCallback callback);
+    }
+
+    private void setUpListeners() {
+        mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+            @Override
+            public void onPrepared(MediaPlayer mp) {
+                setPlayerState(PLAYER_STATE_PAUSED);
+                setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback callback) {
+                        callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
+                    }
+                });
+                notifyPlayerEvent(new PlayerEventNotifier() {
+                    @Override
+                    public void notify(PlayerEventCallback cb) {
+                        cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
+                    }
+                });
+                synchronized (mTaskLock) {
+                    if (mCurrentTask != null
+                            && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
+                            && mCurrentTask.mDSD == mCurrentDSD
+                            && mCurrentTask.mNeedToWaitForEventToComplete) {
+                        mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+                        mCurrentTask = null;
+                        processPendingTask_l();
+                    }
+                }
+            }
+        });
+        mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
+            @Override
+            public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
+                    }
+                });
+            }
+        });
+        mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+            @Override
+            public boolean onInfo(MediaPlayer mp, int what, int extra) {
+                switch (what) {
+                    case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
+                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                            @Override
+                            public void notify(MediaPlayer2EventCallback cb) {
+                                cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+                                        MEDIA_INFO_VIDEO_RENDERING_START, 0);
+                            }
+                        });
+                        break;
+                    case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+                        setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+                        break;
+                    case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+                        setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+                        break;
+                }
+                return false;
+            }
+        });
+        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+            @Override
+            public void onCompletion(MediaPlayer mp) {
+                setPlayerState(PLAYER_STATE_PAUSED);
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
+                                0);
+                    }
+                });
+            }
+        });
+        mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+            @Override
+            public boolean onError(MediaPlayer mp, final int what, final int extra) {
+                setPlayerState(PLAYER_STATE_ERROR);
+                setBufferingState(BUFFERING_STATE_UNKNOWN);
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
+                        cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+                    }
+                });
+                return true;
+            }
+        });
+        mPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
+            @Override
+            public void onSeekComplete(MediaPlayer mp) {
+                synchronized (mTaskLock) {
+                    if (mCurrentTask != null
+                            && mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
+                            && mCurrentTask.mNeedToWaitForEventToComplete) {
+                        mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+                        mCurrentTask = null;
+                        processPendingTask_l();
+                    }
+                }
+                final long seekPos = getCurrentPosition();
+                notifyPlayerEvent(new PlayerEventNotifier() {
+                    @Override
+                    public void notify(PlayerEventCallback cb) {
+                        // TODO: The actual seeked position might be different from the
+                        // requested position. Clarify which one is expected here.
+                        cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
+                    }
+                });
+            }
+        });
+        mPlayer.setOnTimedMetaDataAvailableListener(
+                new MediaPlayer.OnTimedMetaDataAvailableListener() {
+                    @Override
+                    public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
+                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                            @Override
+                            public void notify(MediaPlayer2EventCallback cb) {
+                                cb.onTimedMetaDataAvailable(
+                                        MediaPlayer2Impl.this, mCurrentDSD, data);
+                            }
+                        });
+                    }
+                });
+        mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+            @Override
+            public boolean onInfo(MediaPlayer mp, final int what, final int extra) {
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
+                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+                    }
+                });
+                return true;
+            }
+        });
+        mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+            @Override
+            public void onBufferingUpdate(MediaPlayer mp, final int percent) {
+                if (percent >= 100) {
+                    setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
+                }
+                mBufferedPercentageCurrent.set(percent);
+                notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                    @Override
+                    public void notify(MediaPlayer2EventCallback cb) {
+                        cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+                                MEDIA_INFO_BUFFERING_UPDATE, percent);
+                    }
+                });
+            }
+        });
+    }
+
+    /**
+     * Encapsulates the DRM properties of the source.
+     */
+    public static final class DrmInfoImpl extends DrmInfo {
+        private Map<UUID, byte[]> mMapPssh;
+        private UUID[] mSupportedSchemes;
+
+        /**
+         * Returns the PSSH info of the data source for each supported DRM scheme.
+         */
+        @Override
+        public Map<UUID, byte[]> getPssh() {
+            return mMapPssh;
+        }
+
+        /**
+         * Returns the intersection of the data source and the device DRM schemes.
+         * It effectively identifies the subset of the source's DRM schemes which
+         * are supported by the device too.
+         */
+        @Override
+        public List<UUID> getSupportedSchemes() {
+            return Arrays.asList(mSupportedSchemes);
+        }
+
+        private DrmInfoImpl(Map<UUID, byte[]> pssh, UUID[] supportedSchemes) {
+            mMapPssh = pssh;
+            mSupportedSchemes = supportedSchemes;
+        }
+
+        private DrmInfoImpl(Parcel parcel) {
+            Log.v(TAG, "DrmInfoImpl(" + parcel + ") size " + parcel.dataSize());
+
+            int psshsize = parcel.readInt();
+            byte[] pssh = new byte[psshsize];
+            parcel.readByteArray(pssh);
+
+            Log.v(TAG, "DrmInfoImpl() PSSH: " + arrToHex(pssh));
+            mMapPssh = parsePSSH(pssh, psshsize);
+            Log.v(TAG, "DrmInfoImpl() PSSH: " + mMapPssh);
+
+            int supportedDRMsCount = parcel.readInt();
+            mSupportedSchemes = new UUID[supportedDRMsCount];
+            for (int i = 0; i < supportedDRMsCount; i++) {
+                byte[] uuid = new byte[16];
+                parcel.readByteArray(uuid);
+
+                mSupportedSchemes[i] = bytesToUUID(uuid);
+
+                Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: "
+                        + mSupportedSchemes[i]);
+            }
+
+            Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize
+                    + " supportedDRMsCount: " + supportedDRMsCount);
+        }
+
+        private DrmInfoImpl makeCopy() {
+            return new DrmInfoImpl(this.mMapPssh, this.mSupportedSchemes);
+        }
+
+        private String arrToHex(byte[] bytes) {
+            String out = "0x";
+            for (int i = 0; i < bytes.length; i++) {
+                out += String.format("%02x", bytes[i]);
+            }
+
+            return out;
+        }
+
+        private UUID bytesToUUID(byte[] uuid) {
+            long msb = 0, lsb = 0;
+            for (int i = 0; i < 8; i++) {
+                msb |= (((long) uuid[i] & 0xff) << (8 * (7 - i)));
+                lsb |= (((long) uuid[i + 8] & 0xff) << (8 * (7 - i)));
+            }
+
+            return new UUID(msb, lsb);
+        }
+
+        private Map<UUID, byte[]> parsePSSH(byte[] pssh, int psshsize) {
+            Map<UUID, byte[]> result = new HashMap<UUID, byte[]>();
+
+            final int uuidSize = 16;
+            final int dataLenSize = 4;
+
+            int len = psshsize;
+            int numentries = 0;
+            int i = 0;
+
+            while (len > 0) {
+                if (len < uuidSize) {
+                    Log.w(TAG, String.format("parsePSSH: len is too short to parse "
+                            + "UUID: (%d < 16) pssh: %d", len, psshsize));
+                    return null;
+                }
+
+                byte[] subset = Arrays.copyOfRange(pssh, i, i + uuidSize);
+                UUID uuid = bytesToUUID(subset);
+                i += uuidSize;
+                len -= uuidSize;
+
+                // get data length
+                if (len < 4) {
+                    Log.w(TAG, String.format("parsePSSH: len is too short to parse "
+                            + "datalen: (%d < 4) pssh: %d", len, psshsize));
+                    return null;
+                }
+
+                subset = Arrays.copyOfRange(pssh, i, i + dataLenSize);
+                int datalen = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN)
+                        ? ((subset[3] & 0xff) << 24) | ((subset[2] & 0xff) << 16)
+                        | ((subset[1] & 0xff) << 8) | (subset[0] & 0xff)
+                        : ((subset[0] & 0xff) << 24) | ((subset[1] & 0xff) << 16)
+                                | ((subset[2] & 0xff) << 8) | (subset[3] & 0xff);
+                i += dataLenSize;
+                len -= dataLenSize;
+
+                if (len < datalen) {
+                    Log.w(TAG, String.format("parsePSSH: len is too short to parse "
+                            + "data: (%d < %d) pssh: %d", len, datalen, psshsize));
+                    return null;
+                }
+
+                byte[] data = Arrays.copyOfRange(pssh, i, i + datalen);
+
+                // skip the data
+                i += datalen;
+                len -= datalen;
+
+                Log.v(TAG, String.format("parsePSSH[%d]: <%s, %s> pssh: %d",
+                        numentries, uuid, arrToHex(data), psshsize));
+                numentries++;
+                result.put(uuid, data);
+            }
+
+            return result;
+        }
+
+    };  // DrmInfoImpl
+
+    /**
+     * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+     * Extends MediaDrm.MediaDrmException
+     */
+    public static final class NoDrmSchemeExceptionImpl extends NoDrmSchemeException {
+        public NoDrmSchemeExceptionImpl(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    /**
+     * Thrown when the device requires DRM provisioning but the provisioning attempt has
+     * failed due to a network error (Internet reachability, timeout, etc.).
+     * Extends MediaDrm.MediaDrmException
+     */
+    public static final class ProvisioningNetworkErrorExceptionImpl
+            extends ProvisioningNetworkErrorException {
+        public ProvisioningNetworkErrorExceptionImpl(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    /**
+     * Thrown when the device requires DRM provisioning but the provisioning attempt has
+     * failed due to the provisioning server denying the request.
+     * Extends MediaDrm.MediaDrmException
+     */
+    public static final class ProvisioningServerErrorExceptionImpl
+            extends ProvisioningServerErrorException {
+        public ProvisioningServerErrorExceptionImpl(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    private abstract class Task implements Runnable {
+        private final int mMediaCallType;
+        private final boolean mNeedToWaitForEventToComplete;
+        private DataSourceDesc mDSD;
+
+        Task(int mediaCallType, boolean needToWaitForEventToComplete) {
+            mMediaCallType = mediaCallType;
+            mNeedToWaitForEventToComplete = needToWaitForEventToComplete;
+        }
+
+        abstract void process() throws IOException, NoDrmSchemeException;
+
+        @Override
+        public void run() {
+            int status = CALL_STATUS_NO_ERROR;
+            try {
+                process();
+            } catch (IllegalStateException e) {
+                status = CALL_STATUS_INVALID_OPERATION;
+            } catch (IllegalArgumentException e) {
+                status = CALL_STATUS_BAD_VALUE;
+            } catch (SecurityException e) {
+                status = CALL_STATUS_PERMISSION_DENIED;
+            } catch (IOException e) {
+                status = CALL_STATUS_ERROR_IO;
+            } catch (NoDrmSchemeException e) {
+                status = CALL_STATUS_NO_DRM_SCHEME;
+            } catch (Exception e) {
+                status = CALL_STATUS_ERROR_UNKNOWN;
+            }
+            synchronized (mSrcLock) {
+                mDSD = mCurrentDSD;
+            }
+
+            if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
+
+                sendCompleteNotification(status);
+
+                synchronized (mTaskLock) {
+                    mCurrentTask = null;
+                    processPendingTask_l();
+                }
+            }
+        }
+
+        private void sendCompleteNotification(final int status) {
+            // In {@link #notifyWhenCommandLabelReached} case, a separate callback
+            // {#link #onCommandLabelReached} is already called in {@code process()}.
+            if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
+                return;
+            }
+            notifyMediaPlayer2Event(new Mp2EventNotifier() {
+                @Override
+                public void notify(MediaPlayer2EventCallback cb) {
+                    cb.onCallCompleted(
+                            MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
+                }
+            });
+        }
+    };
+}
diff --git a/androidx/media/MediaPlayer2Test.java b/androidx/media/MediaPlayer2Test.java
new file mode 100644
index 0000000..f565e8a
--- /dev/null
+++ b/androidx/media/MediaPlayer2Test.java
@@ -0,0 +1,2262 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.hardware.Camera;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaRecorder;
+import android.media.MediaTimestamp;
+import android.media.PlaybackParams;
+import android.media.SyncParams;
+import android.media.audiofx.AudioEffect;
+import android.media.audiofx.Visualizer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import androidx.media.test.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+public class MediaPlayer2Test extends MediaPlayer2TestBase {
+
+    private static final String LOG_TAG = "MediaPlayer2Test";
+
+    private static final int  RECORDED_VIDEO_WIDTH  = 176;
+    private static final int  RECORDED_VIDEO_HEIGHT = 144;
+    private static final long RECORDED_DURATION_MS  = 3000;
+    private static final float FLOAT_TOLERANCE = .0001f;
+
+    private String mRecordedFilePath;
+    private final Vector<Integer> mSubtitleTrackIndex = new Vector<>();
+    private final Monitor mOnSubtitleDataCalled = new Monitor();
+    private int mSelectedSubtitleIndex;
+
+    private File mOutFile;
+    private Camera mCamera;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mRecordedFilePath = new File(Environment.getExternalStorageDirectory(),
+                "mediaplayer_record.out").getAbsolutePath();
+        mOutFile = new File(mRecordedFilePath);
+    }
+
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        if (mOutFile != null && mOutFile.exists()) {
+            mOutFile.delete();
+        }
+    }
+
+    // Bug 13652927
+    public void testVorbisCrash() throws Exception {
+        MediaPlayer2 mp = mPlayer;
+        MediaPlayer2 mp2 = mPlayer2;
+        AssetFileDescriptor afd2 = mResources.openRawResourceFd(R.raw.testmp3_2);
+        mp2.setDataSource(new DataSourceDesc.Builder()
+                .setDataSource(afd2.getFileDescriptor(), afd2.getStartOffset(), afd2.getLength())
+                .build());
+        final Monitor onPrepareCalled = new Monitor();
+        final Monitor onErrorCalled = new Monitor();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    onPrepareCalled.signal();
+                }
+            }
+
+            @Override
+            public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                onErrorCalled.signal();
+            }
+        };
+        mp2.setMediaPlayer2EventCallback(mExecutor, ecb);
+        mp2.prepare();
+        onPrepareCalled.waitForSignal();
+        afd2.close();
+        mp2.clearMediaPlayer2EventCallback();
+
+        mp2.loopCurrent(true);
+        mp2.play();
+
+        for (int i = 0; i < 20; i++) {
+            try {
+                AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.bug13652927);
+                mp.setDataSource(new DataSourceDesc.Builder()
+                        .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
+                            afd.getLength())
+                        .build());
+                mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+                onPrepareCalled.reset();
+                mp.prepare();
+                onErrorCalled.waitForSignal();
+                afd.close();
+            } catch (Exception e) {
+                // expected to fail
+                Log.i("@@@", "failed: " + e);
+            }
+            Thread.sleep(500);
+            assertTrue("media player died",
+                    mp2.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            mp.reset();
+        }
+    }
+
+    public void testPlayNullSourcePath() throws Exception {
+        final Monitor onSetDataSourceCalled = new Monitor();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE) {
+                    assertTrue(status != MediaPlayer2.CALL_STATUS_NO_ERROR);
+                    onSetDataSourceCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        onSetDataSourceCalled.reset();
+        mPlayer.setDataSource((DataSourceDesc) null);
+        onSetDataSourceCalled.waitForSignal();
+    }
+
+    public void testPlayAudioFromDataURI() throws Exception {
+        final int mp3Duration = 34909;
+        final int tolerance = 70;
+        final int seekDuration = 100;
+
+        // This is "R.raw.testmp3_2", base64-encoded.
+        final int resid = R.raw.testmp3_3;
+
+        InputStream is = mContext.getResources().openRawResource(resid);
+        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+
+        StringBuilder builder = new StringBuilder();
+        builder.append("data:;base64,");
+        builder.append(reader.readLine());
+        Uri uri = Uri.parse(builder.toString());
+
+        MediaPlayer2 mp = createMediaPlayer2(mContext, uri);
+
+        final Monitor onPrepareCalled = new Monitor();
+        final Monitor onPlayCalled = new Monitor();
+        final Monitor onSeekToCalled = new Monitor();
+        final Monitor onLoopCurrentCalled = new Monitor();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    onPrepareCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    onPlayCalled.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+                    onLoopCurrentCalled.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    onSeekToCalled.signal();
+                }
+            }
+        };
+        mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        try {
+            AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+                    .build();
+            mp.setAudioAttributes(attributes);
+            /* FIXME: ensure screen is on while testing.
+            mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
+            */
+
+            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            onPlayCalled.reset();
+            mp.play();
+            onPlayCalled.waitForSignal();
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            /* FIXME: what's API for checking loop state?
+            assertFalse(mp.isLooping());
+            */
+            onLoopCurrentCalled.reset();
+            mp.loopCurrent(true);
+            onLoopCurrentCalled.waitForSignal();
+            /* FIXME: what's API for checking loop state?
+            assertTrue(mp.isLooping());
+            */
+
+            assertEquals(mp3Duration, mp.getDuration(), tolerance);
+            long pos = mp.getCurrentPosition();
+            assertTrue(pos >= 0);
+            assertTrue(pos < mp3Duration - seekDuration);
+
+            onSeekToCalled.reset();
+            mp.seekTo(pos + seekDuration, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+            onSeekToCalled.waitForSignal();
+            assertEquals(pos + seekDuration, mp.getCurrentPosition(), tolerance);
+
+            // test pause and restart
+            mp.pause();
+            Thread.sleep(SLEEP_TIME);
+            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            onPlayCalled.reset();
+            mp.play();
+            onPlayCalled.waitForSignal();
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            // test stop and restart
+            mp.reset();
+            mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+            mp.setDataSource(new DataSourceDesc.Builder()
+                    .setDataSource(mContext, uri)
+                    .build());
+            onPrepareCalled.reset();
+            mp.prepare();
+            onPrepareCalled.waitForSignal();
+
+            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            onPlayCalled.reset();
+            mp.play();
+            onPlayCalled.waitForSignal();
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            // waiting to complete
+            while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+                Thread.sleep(SLEEP_TIME);
+            }
+        } finally {
+            mp.close();
+        }
+    }
+
+    public void testPlayAudio() throws Exception {
+        final int resid = R.raw.testmp3_2;
+        final int mp3Duration = 34909;
+        final int tolerance = 70;
+        final int seekDuration = 100;
+
+        MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+
+        final Monitor onPrepareCalled = new Monitor();
+        final Monitor onPlayCalled = new Monitor();
+        final Monitor onSeekToCalled = new Monitor();
+        final Monitor onLoopCurrentCalled = new Monitor();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    onPrepareCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    onPlayCalled.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+                    onLoopCurrentCalled.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    onSeekToCalled.signal();
+                }
+            }
+        };
+        mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        try {
+            AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+                    .build();
+            mp.setAudioAttributes(attributes);
+
+            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            onPlayCalled.reset();
+            mp.play();
+            onPlayCalled.waitForSignal();
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            //assertFalse(mp.isLooping());
+            onLoopCurrentCalled.reset();
+            mp.loopCurrent(true);
+            onLoopCurrentCalled.waitForSignal();
+            //assertTrue(mp.isLooping());
+
+            assertEquals(mp3Duration, mp.getDuration(), tolerance);
+            long pos = mp.getCurrentPosition();
+            assertTrue(pos >= 0);
+            assertTrue(pos < mp3Duration - seekDuration);
+
+            onSeekToCalled.reset();
+            mp.seekTo(pos + seekDuration, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+            onSeekToCalled.waitForSignal();
+            assertEquals(pos + seekDuration, mp.getCurrentPosition(), tolerance);
+
+            // test pause and restart
+            mp.pause();
+            Thread.sleep(SLEEP_TIME);
+            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            onPlayCalled.reset();
+            mp.play();
+            onPlayCalled.waitForSignal();
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            // test stop and restart
+            mp.reset();
+            AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+            mp.setDataSource(new DataSourceDesc.Builder()
+                    .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+                    .build());
+
+            mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+            onPrepareCalled.reset();
+            mp.prepare();
+            onPrepareCalled.waitForSignal();
+            afd.close();
+
+            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            onPlayCalled.reset();
+            mp.play();
+            onPlayCalled.waitForSignal();
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            // waiting to complete
+            while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+                Thread.sleep(SLEEP_TIME);
+            }
+        } catch (Exception e) {
+            throw e;
+        } finally {
+            mp.close();
+        }
+    }
+
+    /*
+    public void testConcurentPlayAudio() throws Exception {
+        final int resid = R.raw.test1m1s; // MP3 longer than 1m are usualy offloaded
+        final int tolerance = 70;
+
+        List<MediaPlayer2> mps = Stream.generate(() -> createMediaPlayer2(mContext, resid))
+                                      .limit(5).collect(Collectors.toList());
+
+        try {
+            for (MediaPlayer2 mp : mps) {
+                Monitor onPlayCalled = new Monitor();
+                Monitor onLoopCurrentCalled = new Monitor();
+                MediaPlayer2.MediaPlayer2EventCallback ecb =
+                    new MediaPlayer2.MediaPlayer2EventCallback() {
+                        @Override
+                        public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+                                int what, int status) {
+                            if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                                onPlayCalled.signal();
+                            } else if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+                                onLoopCurrentCalled.signal();
+                            }
+                        }
+                    };
+                mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+                AudioAttributes attributes = new AudioAttributes.Builder()
+                        .setInternalLegacyStreamType(AudioManager.STREAM_MUSIC)
+                        .build();
+                mp.setAudioAttributes(attributes);
+                mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
+
+                assertFalse(mp.isPlaying());
+                onPlayCalled.reset();
+                mp.play();
+                onPlayCalled.waitForSignal();
+                assertTrue(mp.isPlaying());
+
+                assertFalse(mp.isLooping());
+                onLoopCurrentCalled.reset();
+                mp.loopCurrent(true);
+                onLoopCurrentCalled.waitForSignal();
+                assertTrue(mp.isLooping());
+
+                long pos = mp.getCurrentPosition();
+                assertTrue(pos >= 0);
+
+                Thread.sleep(SLEEP_TIME); // Delay each track to be able to ear them
+            }
+            // Check that all mp3 are playing concurrently here
+            for (MediaPlayer2 mp : mps) {
+                long pos = mp.getCurrentPosition();
+                Thread.sleep(SLEEP_TIME);
+                assertEquals(pos + SLEEP_TIME, mp.getCurrentPosition(), tolerance);
+            }
+        } finally {
+            mps.forEach(MediaPlayer2::close);
+        }
+    }
+    */
+
+    public void testPlayAudioLooping() throws Exception {
+        final int resid = R.raw.testmp3;
+
+        MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+        try {
+            AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+                    .build();
+            mp.setAudioAttributes(attributes);
+            mp.loopCurrent(true);
+            final Monitor onCompletionCalled = new Monitor();
+            final Monitor onPlayCalled = new Monitor();
+            MediaPlayer2.MediaPlayer2EventCallback ecb =
+                    new MediaPlayer2.MediaPlayer2EventCallback() {
+                        @Override
+                        public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd,
+                                int what, int extra) {
+                            Log.i("@@@", "got oncompletion");
+                            onCompletionCalled.signal();
+                        }
+
+                        @Override
+                        public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+                                int what, int status) {
+                            if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                                onPlayCalled.signal();
+                            }
+                        }
+                    };
+            mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+            assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            onPlayCalled.reset();
+            mp.play();
+            onPlayCalled.waitForSignal();
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            long duration = mp.getDuration();
+            Thread.sleep(duration * 4); // allow for several loops
+            assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertEquals("wrong number of completion signals", 0,
+                    onCompletionCalled.getNumSignal());
+            mp.loopCurrent(false);
+
+            // wait for playback to finish
+            while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+                Thread.sleep(SLEEP_TIME);
+            }
+            assertEquals("wrong number of completion signals", 1,
+                    onCompletionCalled.getNumSignal());
+        } finally {
+            mp.close();
+        }
+    }
+
+    public void testPlayMidi() throws Exception {
+        final int resid = R.raw.midi8sec;
+        final int midiDuration = 8000;
+        final int tolerance = 70;
+        final int seekDuration = 1000;
+
+        MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+
+        final Monitor onPrepareCalled = new Monitor();
+        final Monitor onSeekToCalled = new Monitor();
+        final Monitor onLoopCurrentCalled = new Monitor();
+        MediaPlayer2.MediaPlayer2EventCallback ecb =
+                new MediaPlayer2.MediaPlayer2EventCallback() {
+                    @Override
+                    public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                        if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                            onPrepareCalled.signal();
+                        }
+                    }
+
+                    @Override
+                    public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+                            int what, int status) {
+                        if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+                            onLoopCurrentCalled.signal();
+                        } else if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                            onSeekToCalled.signal();
+                        }
+                    }
+                };
+        mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        try {
+            AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+                    .build();
+            mp.setAudioAttributes(attributes);
+
+            mp.play();
+
+            /* FIXME: what's API for checking loop state?
+            assertFalse(mp.isLooping());
+            */
+            onLoopCurrentCalled.reset();
+            mp.loopCurrent(true);
+            onLoopCurrentCalled.waitForSignal();
+            /* FIXME: what's API for checking loop state?
+            assertTrue(mp.isLooping());
+            */
+
+            assertEquals(midiDuration, mp.getDuration(), tolerance);
+            long pos = mp.getCurrentPosition();
+            assertTrue(pos >= 0);
+            assertTrue(pos < midiDuration - seekDuration);
+
+            onSeekToCalled.reset();
+            mp.seekTo(pos + seekDuration, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+            onSeekToCalled.waitForSignal();
+            assertEquals(pos + seekDuration, mp.getCurrentPosition(), tolerance);
+
+            // test stop and restart
+            mp.reset();
+            AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+            mp.setDataSource(new DataSourceDesc.Builder()
+                    .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+                    .build());
+
+            mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+            onPrepareCalled.reset();
+            mp.prepare();
+            onPrepareCalled.waitForSignal();
+            afd.close();
+
+            mp.play();
+
+            Thread.sleep(SLEEP_TIME);
+        } finally {
+            mp.close();
+        }
+    }
+
+    static class OutputListener {
+        int mSession;
+        AudioEffect mVc;
+        Visualizer mVis;
+        byte [] mVisData;
+        boolean mSoundDetected;
+        OutputListener(int session) {
+            mSession = session;
+            /* FIXME: find out a public API for replacing AudioEffect contructor.
+            // creating a volume controller on output mix ensures that ro.audio.silent mutes
+            // audio after the effects and not before
+            mVc = new AudioEffect(
+                    AudioEffect.EFFECT_TYPE_NULL,
+                    UUID.fromString("119341a0-8469-11df-81f9-0002a5d5c51b"),
+                    0,
+                    session);
+            mVc.setEnabled(true);
+            */
+            mVis = new Visualizer(session);
+            int size = 256;
+            int[] range = Visualizer.getCaptureSizeRange();
+            if (size < range[0]) {
+                size = range[0];
+            }
+            if (size > range[1]) {
+                size = range[1];
+            }
+            assertTrue(mVis.setCaptureSize(size) == Visualizer.SUCCESS);
+
+            mVis.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
+                @Override
+                public void onWaveFormDataCapture(Visualizer visualizer,
+                        byte[] waveform, int samplingRate) {
+                    if (!mSoundDetected) {
+                        for (int i = 0; i < waveform.length; i++) {
+                            // 8 bit unsigned PCM, zero level is at 128, which is -128 when
+                            // seen as a signed byte
+                            if (waveform[i] != -128) {
+                                mSoundDetected = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                @Override
+                public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
+                }
+            }, 10000 /* milliHertz */, true /* PCM */, false /* FFT */);
+            assertTrue(mVis.setEnabled(true) == Visualizer.SUCCESS);
+        }
+
+        void reset() {
+            mSoundDetected = false;
+        }
+
+        boolean heardSound() {
+            return mSoundDetected;
+        }
+
+        void release() {
+            mVis.release();
+            /* FIXME: find out a public API for replacing AudioEffect contructor.
+            mVc.release();
+            */
+        }
+    }
+
+    public void testPlayAudioTwice() throws Exception {
+
+        final int resid = R.raw.camera_click;
+
+        MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+        try {
+            AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+                    .build();
+            mp.setAudioAttributes(attributes);
+
+            OutputListener listener = new OutputListener(mp.getAudioSessionId());
+
+            Thread.sleep(SLEEP_TIME);
+            assertFalse("noise heard before test started", listener.heardSound());
+
+            mp.play();
+            Thread.sleep(SLEEP_TIME);
+            assertFalse("player was still playing after " + SLEEP_TIME + " ms",
+                    mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+            assertTrue("nothing heard while test ran", listener.heardSound());
+            listener.reset();
+            mp.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+            mp.play();
+            Thread.sleep(SLEEP_TIME);
+            assertTrue("nothing heard when sound was replayed", listener.heardSound());
+            listener.release();
+        } finally {
+            mp.close();
+        }
+    }
+
+    @Test
+    @LargeTest
+    public void testPlayVideo() throws Exception {
+        playVideoTest(R.raw.testvideo, 352, 288);
+    }
+
+    /**
+     * Test for reseting a surface during video playback
+     * After reseting, the video should continue playing
+     * from the time setDisplay() was called
+     */
+    @Test
+    @LargeTest
+    public void testVideoSurfaceResetting() throws Exception {
+        final int tolerance = 150;
+        final int audioLatencyTolerance = 1000;  /* covers audio path latency variability */
+        final int seekPos = 4760;  // This is the I-frame position
+
+        final CountDownLatch seekDone = new CountDownLatch(1);
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    seekDone.countDown();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        if (!checkLoadResource(R.raw.testvideo)) {
+            return; // skip;
+        }
+        playLoadedVideo(352, 288, -1);
+
+        Thread.sleep(SLEEP_TIME);
+
+        long posBefore = mPlayer.getCurrentPosition();
+        mPlayer.setSurface(mActivity.getSurfaceHolder2().getSurface());
+        long posAfter = mPlayer.getCurrentPosition();
+
+        /* temporarily disable timestamp checking because MediaPlayer2 now seeks to I-frame
+         * position, instead of requested position. setDisplay invovles a seek operation
+         * internally.
+         */
+        // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
+        // assertEquals(posAfter, posBefore, tolerance);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        Thread.sleep(SLEEP_TIME);
+
+        mPlayer.seekTo(seekPos, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        seekDone.await();
+        posAfter = mPlayer.getCurrentPosition();
+        assertEquals(seekPos, posAfter, tolerance + audioLatencyTolerance);
+
+        Thread.sleep(SLEEP_TIME / 2);
+        posBefore = mPlayer.getCurrentPosition();
+        mPlayer.setSurface(null);
+        posAfter = mPlayer.getCurrentPosition();
+        // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
+        // assertEquals(posAfter, posBefore, tolerance);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        Thread.sleep(SLEEP_TIME);
+
+        posBefore = mPlayer.getCurrentPosition();
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+        posAfter = mPlayer.getCurrentPosition();
+
+        // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
+        // assertEquals(posAfter, posBefore, tolerance);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        Thread.sleep(SLEEP_TIME);
+    }
+
+    public void testRecordedVideoPlayback0() throws Exception {
+        testRecordedVideoPlaybackWithAngle(0);
+    }
+
+    public void testRecordedVideoPlayback90() throws Exception {
+        testRecordedVideoPlaybackWithAngle(90);
+    }
+
+    public void testRecordedVideoPlayback180() throws Exception {
+        testRecordedVideoPlaybackWithAngle(180);
+    }
+
+    public void testRecordedVideoPlayback270() throws Exception {
+        testRecordedVideoPlaybackWithAngle(270);
+    }
+
+    private boolean hasCamera() {
+        return mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
+    }
+
+    private void testRecordedVideoPlaybackWithAngle(int angle) throws Exception {
+        int width = RECORDED_VIDEO_WIDTH;
+        int height = RECORDED_VIDEO_HEIGHT;
+        final String file = mRecordedFilePath;
+        final long durationMs = RECORDED_DURATION_MS;
+
+        if (!hasCamera()) {
+            return;
+        }
+
+        boolean isSupported = false;
+        mCamera = Camera.open(0);
+        Camera.Parameters parameters = mCamera.getParameters();
+        List<Camera.Size> videoSizes = parameters.getSupportedVideoSizes();
+        // getSupportedVideoSizes returns null when separate video/preview size
+        // is not supported.
+        if (videoSizes == null) {
+            videoSizes = parameters.getSupportedPreviewSizes();
+        }
+        for (Camera.Size size : videoSizes) {
+            if (size.width == width && size.height == height) {
+                isSupported = true;
+                break;
+            }
+        }
+        mCamera.release();
+        mCamera = null;
+        if (!isSupported) {
+            width = videoSizes.get(0).width;
+            height = videoSizes.get(0).height;
+        }
+        checkOrientation(angle);
+        recordVideo(width, height, angle, file, durationMs);
+        checkDisplayedVideoSize(width, height, angle, file);
+        checkVideoRotationAngle(angle, file);
+    }
+
+    private void checkOrientation(int angle) throws Exception {
+        assertTrue(angle >= 0);
+        assertTrue(angle < 360);
+        assertTrue((angle % 90) == 0);
+    }
+
+    private void recordVideo(
+            int w, int h, int angle, String file, long durationMs) throws Exception {
+
+        MediaRecorder recorder = new MediaRecorder();
+        recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+        recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+        recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
+        recorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
+        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
+        recorder.setOutputFile(file);
+        recorder.setOrientationHint(angle);
+        recorder.setVideoSize(w, h);
+        recorder.setPreviewDisplay(mActivity.getSurfaceHolder2().getSurface());
+        recorder.prepare();
+        recorder.start();
+        Thread.sleep(durationMs);
+        recorder.stop();
+        recorder.release();
+        recorder = null;
+    }
+
+    private void checkDisplayedVideoSize(
+            int w, int h, int angle, String file) throws Exception {
+
+        int displayWidth  = w;
+        int displayHeight = h;
+        if ((angle % 180) != 0) {
+            displayWidth  = h;
+            displayHeight = w;
+        }
+        playVideoTest(file, displayWidth, displayHeight);
+    }
+
+    private void checkVideoRotationAngle(int angle, String file) {
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        retriever.setDataSource(file);
+        String rotation = retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+        retriever.release();
+        retriever = null;
+        assertNotNull(rotation);
+        assertEquals(Integer.parseInt(rotation), angle);
+    }
+
+    public void testPlaylist() throws Exception {
+        if (!checkLoadResource(
+                R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+            return; // skip
+        }
+        final DataSourceDesc dsd1 = createDataSourceDesc(
+                R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz);
+        final DataSourceDesc dsd2 = createDataSourceDesc(
+                R.raw.testvideo);
+        ArrayList<DataSourceDesc> nextDSDs = new ArrayList<DataSourceDesc>(2);
+        nextDSDs.add(dsd2);
+        nextDSDs.add(dsd1);
+
+        mPlayer.setNextDataSources(nextDSDs);
+
+        final Monitor onCompletion1Called = new Monitor();
+        final Monitor onCompletion2Called = new Monitor();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    Log.i(LOG_TAG, "testPlaylist: prepared dsd MediaId=" + dsd.getMediaId());
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+                    if (dsd == dsd1) {
+                        onCompletion1Called.signal();
+                    } else if (dsd == dsd2) {
+                        onCompletion2Called.signal();
+                    } else {
+                        mOnCompletionCalled.signal();
+                    }
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mOnCompletionCalled.reset();
+        onCompletion1Called.reset();
+        onCompletion2Called.reset();
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mPlayer.prepare();
+
+        mPlayer.play();
+
+        mOnCompletionCalled.waitForSignal();
+        onCompletion2Called.waitForSignal();
+        onCompletion1Called.waitForSignal();
+
+        mPlayer.reset();
+    }
+
+    // setPlaybackParams() with non-zero speed should NOT start playback.
+    // TODO: enable this test when MediaPlayer2.setPlaybackParams() is fixed
+    /*
+    public void testSetPlaybackParamsPositiveSpeed() throws Exception {
+        if (!checkLoadResource(
+                R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+            return; // skip
+        }
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+                    mOnCompletionCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    mOnSeekCompleteCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mOnCompletionCalled.reset();
+        mPlayer.setDisplay(mActivity.getSurfaceHolder());
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        mOnSeekCompleteCalled.reset();
+        mPlayer.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        mOnSeekCompleteCalled.waitForSignal();
+
+        final float playbackRate = 1.0f;
+
+        int playTime = 2000;  // The testing clip is about 10 second long.
+        mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+        assertTrue("MediaPlayer2 should be playing", mPlayer.isPlaying());
+        Thread.sleep(playTime);
+        assertTrue("MediaPlayer2 should still be playing",
+                mPlayer.getCurrentPosition() > 0);
+
+        long duration = mPlayer.getDuration();
+        mOnSeekCompleteCalled.reset();
+        mPlayer.seekTo(duration - 1000, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        mOnSeekCompleteCalled.waitForSignal();
+
+        mOnCompletionCalled.waitForSignal();
+        assertFalse("MediaPlayer2 should not be playing", mPlayer.isPlaying());
+        long eosPosition = mPlayer.getCurrentPosition();
+
+        mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+        assertTrue("MediaPlayer2 should be playing after EOS", mPlayer.isPlaying());
+        Thread.sleep(playTime);
+        long position = mPlayer.getCurrentPosition();
+        assertTrue("MediaPlayer2 should still be playing after EOS",
+                position > 0 && position < eosPosition);
+
+        mPlayer.reset();
+    }
+    */
+
+    @Test
+    @LargeTest
+    public void testPlaybackRate() throws Exception {
+        final int toleranceMs = 1000;
+        if (!checkLoadResource(
+                R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+            return; // skip
+        }
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        SyncParams sync = new SyncParams().allowDefaults();
+        mPlayer.setSyncParams(sync);
+        sync = mPlayer.getSyncParams();
+
+        float[] rates = { 0.25f, 0.5f, 1.0f, 2.0f };
+        for (float playbackRate : rates) {
+            mPlayer.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+            Thread.sleep(1000);
+            int playTime = 4000;  // The testing clip is about 10 second long.
+            mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+            mPlayer.play();
+            Thread.sleep(playTime);
+            PlaybackParams pbp = mPlayer.getPlaybackParams();
+            assertEquals(
+                    playbackRate, pbp.getSpeed(),
+                    FLOAT_TOLERANCE + playbackRate * sync.getTolerance());
+            assertTrue("MediaPlayer2 should still be playing",
+                    mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+            long playedMediaDurationMs = mPlayer.getCurrentPosition();
+            int diff = Math.abs((int) (playedMediaDurationMs / playbackRate) - playTime);
+            if (diff > toleranceMs) {
+                fail("Media player had error in playback rate " + playbackRate
+                        + ", play time is " + playTime + " vs expected " + playedMediaDurationMs);
+            }
+            mPlayer.pause();
+            pbp = mPlayer.getPlaybackParams();
+            // TODO: pause() should NOT change PlaybackParams.
+            // assertEquals(0.f, pbp.getSpeed(), FLOAT_TOLERANCE);
+        }
+        mPlayer.reset();
+    }
+
+    @Test
+    @LargeTest
+    public void testSeekModes() throws Exception {
+        // This clip has 2 I frames at 66687us and 4299687us.
+        if (!checkLoadResource(
+                R.raw.bbb_s1_320x240_mp4_h264_mp2_800kbps_30fps_aac_lc_5ch_240kbps_44100hz)) {
+            return; // skip
+        }
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    mOnSeekCompleteCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        mOnSeekCompleteCalled.reset();
+        mPlayer.play();
+
+        final long seekPosMs = 3000;
+        final long timeToleranceMs = 100;
+        final long syncTime1Ms = 67;
+        final long syncTime2Ms = 4300;
+
+        // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+        // seek to previous sync or next sync.
+        long cp = runSeekMode(MediaPlayer2.SEEK_CLOSEST, seekPosMs);
+        assertTrue("MediaPlayer2 did not seek to closest position",
+                cp > seekPosMs && cp < syncTime2Ms);
+
+        // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+        // seek to closest position or next sync.
+        cp = runSeekMode(MediaPlayer2.SEEK_PREVIOUS_SYNC, seekPosMs);
+        assertTrue("MediaPlayer2 did not seek to preivous sync position",
+                cp < seekPosMs - timeToleranceMs);
+
+        // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+        // seek to closest position or previous sync.
+        cp = runSeekMode(MediaPlayer2.SEEK_NEXT_SYNC, seekPosMs);
+        assertTrue("MediaPlayer2 did not seek to next sync position",
+                cp > syncTime2Ms - timeToleranceMs);
+
+        // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+        // seek to closest position or previous sync.
+        cp = runSeekMode(MediaPlayer2.SEEK_CLOSEST_SYNC, seekPosMs);
+        assertTrue("MediaPlayer2 did not seek to closest sync position",
+                cp > syncTime2Ms - timeToleranceMs);
+
+        mPlayer.reset();
+    }
+
+    private long runSeekMode(int seekMode, long seekPosMs) throws Exception {
+        final int sleepIntervalMs = 100;
+        int timeRemainedMs = 10000;  // total time for testing
+        final int timeToleranceMs = 100;
+
+        mPlayer.seekTo(seekPosMs, seekMode);
+        mOnSeekCompleteCalled.waitForSignal();
+        mOnSeekCompleteCalled.reset();
+        long cp = -seekPosMs;
+        while (timeRemainedMs > 0) {
+            cp = mPlayer.getCurrentPosition();
+            // Wait till MediaPlayer2 starts rendering since MediaPlayer2 caches
+            // seek position as current position.
+            if (cp < seekPosMs - timeToleranceMs || cp > seekPosMs + timeToleranceMs) {
+                break;
+            }
+            timeRemainedMs -= sleepIntervalMs;
+            Thread.sleep(sleepIntervalMs);
+        }
+        assertTrue("MediaPlayer2 did not finish seeking in time for mode " + seekMode,
+                timeRemainedMs > 0);
+        return cp;
+    }
+
+    @Test
+    @LargeTest
+    public void testGetTimestamp() throws Exception {
+        final int toleranceUs = 100000;
+        final float playbackRate = 1.0f;
+        if (!checkLoadResource(
+                R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+            return; // skip
+        }
+
+        final Monitor onPauseCalled = new Monitor();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PAUSE) {
+                    onPauseCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        mPlayer.play();
+        mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+        Thread.sleep(SLEEP_TIME);  // let player get into stable state.
+        long nt1 = System.nanoTime();
+        MediaTimestamp ts1 = mPlayer.getTimestamp();
+        long nt2 = System.nanoTime();
+        assertTrue("Media player should return a valid time stamp", ts1 != null);
+        assertEquals("MediaPlayer2 had error in clockRate " + ts1.getMediaClockRate(),
+                playbackRate, ts1.getMediaClockRate(), 0.001f);
+        assertTrue("The nanoTime of Media timestamp should be taken when getTimestamp is called.",
+                nt1 <= ts1.getAnchorSytemNanoTime() && ts1.getAnchorSytemNanoTime() <= nt2);
+
+        onPauseCalled.reset();
+        mPlayer.pause();
+        onPauseCalled.waitForSignal();
+        ts1 = mPlayer.getTimestamp();
+        assertTrue("Media player should return a valid time stamp", ts1 != null);
+        assertTrue("Media player should have play rate of 0.0f when paused",
+                ts1.getMediaClockRate() == 0.0f);
+
+        mPlayer.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        mPlayer.play();
+        Thread.sleep(SLEEP_TIME);  // let player get into stable state.
+        int playTime = 4000;  // The testing clip is about 10 second long.
+        ts1 = mPlayer.getTimestamp();
+        assertTrue("Media player should return a valid time stamp", ts1 != null);
+        Thread.sleep(playTime);
+        MediaTimestamp ts2 = mPlayer.getTimestamp();
+        assertTrue("Media player should return a valid time stamp", ts2 != null);
+        assertTrue("The clockRate should not be changed.",
+                ts1.getMediaClockRate() == ts2.getMediaClockRate());
+        assertEquals("MediaPlayer2 had error in timestamp.",
+                ts1.getAnchorMediaTimeUs() + (long) (playTime * ts1.getMediaClockRate() * 1000),
+                ts2.getAnchorMediaTimeUs(), toleranceUs);
+
+        mPlayer.reset();
+    }
+
+    public void testLocalVideo_MKV_H265_1280x720_500kbps_25fps_AAC_Stereo_128kbps_44100Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz, 1280, 720);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_500kbps_25fps_AAC_Stereo_128kbps_44110Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_500kbps_25fps_aac_stereo_128kbps_44100hz, 480, 360);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_500kbps_30fps_AAC_Stereo_128kbps_44110Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz, 480, 360);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_1000kbps_25fps_AAC_Stereo_128kbps_44110Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz, 480, 360);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_1000kbps_30fps_AAC_Stereo_128kbps_44110Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz, 480, 360);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_1350kbps_25fps_AAC_Stereo_128kbps_44110Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_1350kbps_25fps_aac_stereo_128kbps_44100hz, 480, 360);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_1350kbps_30fps_AAC_Stereo_128kbps_44110Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_128kbps_44100hz, 480, 360);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_1350kbps_30fps_AAC_Stereo_128kbps_44110Hz_frag()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_128kbps_44100hz_fragmented,
+                480, 360);
+    }
+
+    public void testLocalVideo_MP4_H264_480x360_1350kbps_30fps_AAC_Stereo_192kbps_44110Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz, 480, 360);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Mono_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_mono_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Mono_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_mono_24kbps_22050hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_128kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_128kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Mono_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_mono_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Mono_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_mono_24kbps_22050hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_128kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_128kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_128kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_128kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Mono_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Mono_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_22050hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_128kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_128kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Mono_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_mono_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Mono_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_mono_24kbps_22050hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_24kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_24kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_128kbps_11025Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz, 176, 144);
+    }
+
+    public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_128kbps_22050Hz()
+            throws Exception {
+        playVideoTest(
+                R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_22050hz, 176, 144);
+    }
+
+    private void readSubtitleTracks() throws Exception {
+        mSubtitleTrackIndex.clear();
+        List<MediaPlayer2.TrackInfo> trackInfos = mPlayer.getTrackInfo();
+        if (trackInfos == null || trackInfos.size() == 0) {
+            return;
+        }
+
+        Vector<Integer> subtitleTrackIndex = new Vector<>();
+        for (int i = 0; i < trackInfos.size(); ++i) {
+            assertTrue(trackInfos.get(i) != null);
+            if (trackInfos.get(i).getTrackType()
+                    == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+                subtitleTrackIndex.add(i);
+            }
+        }
+
+        mSubtitleTrackIndex.addAll(subtitleTrackIndex);
+    }
+
+    private void selectSubtitleTrack(int index) throws Exception {
+        int trackIndex = mSubtitleTrackIndex.get(index);
+        mPlayer.selectTrack(trackIndex);
+        mSelectedSubtitleIndex = index;
+    }
+
+    private void deselectSubtitleTrack(int index) throws Exception {
+        int trackIndex = mSubtitleTrackIndex.get(index);
+        mOnDeselectTrackCalled.reset();
+        mPlayer.deselectTrack(trackIndex);
+        mOnDeselectTrackCalled.waitForSignal();
+        if (mSelectedSubtitleIndex == index) {
+            mSelectedSubtitleIndex = -1;
+        }
+    }
+
+    public void testDeselectTrackForSubtitleTracks() throws Throwable {
+        if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
+            return; // skip;
+        }
+
+        /* FIXME: find out counter part of waitForIdleSync.
+        getInstrumentation().waitForIdleSync();
+        */
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+                    mOnInfoCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    mOnSeekCompleteCalled.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    mOnPlayCalled.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK) {
+                    mCallStatus = status;
+                    mOnDeselectTrackCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        /* TODO: uncomment once API is available in supportlib.
+        mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
+            @Override
+            public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+                if (data != null && data.getData() != null) {
+                    mOnSubtitleDataCalled.signal();
+                }
+            }
+        });
+        */
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        mOnPlayCalled.reset();
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        // Closed caption tracks are in-band.
+        // So, those tracks will be found after processing a number of frames.
+        mOnInfoCalled.waitForSignal(1500);
+
+        mOnInfoCalled.reset();
+        mOnInfoCalled.waitForSignal(1500);
+
+        readSubtitleTracks();
+
+        // Run twice to check if repeated selection-deselection on the same track works well.
+        for (int i = 0; i < 2; i++) {
+            // Waits until at least one subtitle is fired. Timeout is 2.5 seconds.
+            selectSubtitleTrack(i);
+            mOnSubtitleDataCalled.reset();
+            assertTrue(mOnSubtitleDataCalled.waitForSignal(2500));
+
+            // Try deselecting track.
+            deselectSubtitleTrack(i);
+            mOnSubtitleDataCalled.reset();
+            assertFalse(mOnSubtitleDataCalled.waitForSignal(1500));
+        }
+
+        // Deselecting unselected track: expected error status
+        mCallStatus = MediaPlayer2.CALL_STATUS_NO_ERROR;
+        deselectSubtitleTrack(0);
+        assertTrue(mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR);
+
+        mPlayer.reset();
+    }
+
+    public void testChangeSubtitleTrack() throws Throwable {
+        if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
+            return; // skip;
+        }
+
+        /* TODO: uncomment once API is available in supportlib.
+        mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
+            @Override
+            public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+                if (data != null && data.getData() != null) {
+                    mOnSubtitleDataCalled.signal();
+                }
+            }
+        });
+        */
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+                    mOnInfoCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    mOnPlayCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        mOnPlayCalled.reset();
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        // Closed caption tracks are in-band.
+        // So, those tracks will be found after processing a number of frames.
+        mOnInfoCalled.waitForSignal(1500);
+
+        mOnInfoCalled.reset();
+        mOnInfoCalled.waitForSignal(1500);
+
+        readSubtitleTracks();
+
+        // Waits until at least two captions are fired. Timeout is 2.5 sec.
+        selectSubtitleTrack(0);
+        assertTrue(mOnSubtitleDataCalled.waitForCountedSignals(2, 2500) >= 2);
+
+        mOnSubtitleDataCalled.reset();
+        selectSubtitleTrack(1);
+        assertTrue(mOnSubtitleDataCalled.waitForCountedSignals(2, 2500) >= 2);
+
+        mPlayer.reset();
+    }
+
+    @Test
+    @LargeTest
+    public void testGetTrackInfoForVideoWithSubtitleTracks() throws Throwable {
+        if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
+            return; // skip;
+        }
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+                    mOnInfoCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    mOnPlayCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        mOnPlayCalled.reset();
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        // The media metadata will be changed while playing since closed caption tracks are in-band
+        // and those tracks will be found after processing a number of frames. These tracks will be
+        // found within one second.
+        mOnInfoCalled.waitForSignal(1500);
+
+        mOnInfoCalled.reset();
+        mOnInfoCalled.waitForSignal(1500);
+
+        readSubtitleTracks();
+        assertEquals(2, mSubtitleTrackIndex.size());
+
+        mPlayer.reset();
+    }
+
+    /*
+     *  This test assumes the resources being tested are between 8 and 14 seconds long
+     *  The ones being used here are 10 seconds long.
+     */
+    public void testResumeAtEnd() throws Throwable {
+        int testsRun = testResumeAtEnd(R.raw.loudsoftmp3)
+                + testResumeAtEnd(R.raw.loudsoftwav)
+                + testResumeAtEnd(R.raw.loudsoftogg)
+                + testResumeAtEnd(R.raw.loudsoftitunes)
+                + testResumeAtEnd(R.raw.loudsoftfaac)
+                + testResumeAtEnd(R.raw.loudsoftaac);
+    }
+
+    // returns 1 if test was run, 0 otherwise
+    private int testResumeAtEnd(int res) throws Throwable {
+        if (!loadResource(res)) {
+            Log.i(LOG_TAG, "testResumeAtEnd: No decoder found for "
+                    + mContext.getResources().getResourceEntryName(res) + " --- skipping.");
+            return 0; // skip
+        }
+        mOnCompletionCalled.reset();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+                    mOnCompletionCalled.signal();
+                    mPlayer.play();
+                }
+            }
+        };
+        mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        // skip the first part of the file so we reach EOF sooner
+        mPlayer.seekTo(5000, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        mPlayer.play();
+        // sleep long enough that we restart playback at least once, but no more
+        Thread.sleep(10000);
+        assertTrue("MediaPlayer2 should still be playing",
+                mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        mPlayer.reset();
+        assertEquals("wrong number of repetitions", 1, mOnCompletionCalled.getNumSignal());
+        return 1;
+    }
+
+    @Test
+    @LargeTest
+    public void testPositionAtEnd() throws Throwable {
+        int testsRun = testPositionAtEnd(R.raw.test1m1shighstereo)
+                + testPositionAtEnd(R.raw.loudsoftmp3)
+                + testPositionAtEnd(R.raw.loudsoftwav)
+                + testPositionAtEnd(R.raw.loudsoftogg)
+                + testPositionAtEnd(R.raw.loudsoftitunes)
+                + testPositionAtEnd(R.raw.loudsoftfaac)
+                + testPositionAtEnd(R.raw.loudsoftaac);
+    }
+
+    private int testPositionAtEnd(int res) throws Throwable {
+        if (!loadResource(res)) {
+            Log.i(LOG_TAG, "testPositionAtEnd: No decoder found for "
+                    + mContext.getResources().getResourceEntryName(res) + " --- skipping.");
+            return 0; // skip
+        }
+        AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+                .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+                .build();
+        mPlayer.setAudioAttributes(attributes);
+
+        mOnCompletionCalled.reset();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+                    mOnCompletionCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    mOnPlayCalled.signal();
+                }
+            }
+        };
+        mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        long duration = mPlayer.getDuration();
+        assertTrue("resource too short", duration > 6000);
+        mPlayer.seekTo(duration - 5000, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        mOnPlayCalled.reset();
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+            Log.i("@@@@", "position: " + mPlayer.getCurrentPosition());
+            Thread.sleep(500);
+        }
+        Log.i("@@@@", "final position: " + mPlayer.getCurrentPosition());
+        assertTrue(mPlayer.getCurrentPosition() > duration - 1000);
+        mPlayer.reset();
+        return 1;
+    }
+
+    @Test
+    @LargeTest
+    public void testMediaPlayer2Callback() throws Throwable {
+        final int mp4Duration = 8484;
+
+        if (!checkLoadResource(R.raw.testvideo)) {
+            return; // skip;
+        }
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        mOnCompletionCalled.reset();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd,
+                    int width, int height) {
+                mOnVideoSizeChangedCalled.signal();
+            }
+
+            @Override
+            public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                mOnErrorCalled.signal();
+            }
+
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                mOnInfoCalled.signal();
+
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+                    mOnCompletionCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+                    mOnSeekCompleteCalled.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    mOnPlayCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        assertFalse(mOnPrepareCalled.isSignalled());
+        assertFalse(mOnVideoSizeChangedCalled.isSignalled());
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+        mOnVideoSizeChangedCalled.waitForSignal();
+
+        mOnSeekCompleteCalled.reset();
+        mPlayer.seekTo(mp4Duration >> 1, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        mOnSeekCompleteCalled.waitForSignal();
+
+        assertFalse(mOnCompletionCalled.isSignalled());
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+            Thread.sleep(SLEEP_TIME);
+        }
+        assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+        mOnCompletionCalled.waitForSignal();
+        assertFalse(mOnErrorCalled.isSignalled());
+        mPlayer.reset();
+    }
+
+    @Test
+    @LargeTest
+    public void testPlayerStates() throws Throwable {
+        final int mp4Duration = 8484;
+
+        if (!checkLoadResource(R.raw.testvideo)) {
+            return; // skip;
+        }
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        final Monitor prepareCompleted = new Monitor();
+        final Monitor playCompleted = new Monitor();
+        final Monitor pauseCompleted = new Monitor();
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PREPARE) {
+                    prepareCompleted.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    playCompleted.signal();
+                } else if (what == MediaPlayer2.CALL_COMPLETED_PAUSE) {
+                    pauseCompleted.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
+        assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        prepareCompleted.reset();
+        mPlayer.prepare();
+        prepareCompleted.waitForSignal();
+        assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+                mPlayer.getBufferingState());
+        assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+
+        playCompleted.reset();
+        mPlayer.play();
+        playCompleted.waitForSignal();
+        assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+                mPlayer.getBufferingState());
+        assertEquals(MediaPlayerBase.PLAYER_STATE_PLAYING, mPlayer.getPlayerState());
+
+        pauseCompleted.reset();
+        mPlayer.pause();
+        pauseCompleted.waitForSignal();
+        assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+                mPlayer.getBufferingState());
+        assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+
+        mPlayer.reset();
+        assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
+        assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+    }
+
+    @Test
+    @LargeTest
+    public void testPlayerEventCallback() throws Throwable {
+        final int mp4Duration = 8484;
+
+        if (!checkLoadResource(R.raw.testvideo)) {
+            return; // skip;
+        }
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+        final Monitor onPrepareCalled = new Monitor();
+        final Monitor onSeekCompleteCalled = new Monitor();
+        final Monitor onPlayerStateChangedCalled = new Monitor();
+        final AtomicInteger playerState = new AtomicInteger();
+        final Monitor onBufferingStateChangedCalled = new Monitor();
+        final AtomicInteger bufferingState = new AtomicInteger();
+        final Monitor onPlaybackSpeedChanged = new Monitor();
+        final AtomicReference<Float> playbackSpeed = new AtomicReference<>();
+
+        MediaPlayerBase.PlayerEventCallback callback = new MediaPlayerBase.PlayerEventCallback() {
+            // TODO: implement and add test case for onCurrentDataSourceChanged() callback.
+            @Override
+            public void onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd) {
+                onPrepareCalled.signal();
+            }
+
+            @Override
+            public void onPlayerStateChanged(MediaPlayerBase mpb, int state) {
+                playerState.set(state);
+                onPlayerStateChangedCalled.signal();
+            }
+
+            @Override
+            public void onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd,
+                    int state) {
+                bufferingState.set(state);
+                onBufferingStateChangedCalled.signal();
+            }
+
+            @Override
+            public void onPlaybackSpeedChanged(MediaPlayerBase mpb, float speed) {
+                playbackSpeed.set(speed);
+                onPlaybackSpeedChanged.signal();
+            }
+
+            @Override
+            public void onSeekCompleted(MediaPlayerBase mpb, long position) {
+                onSeekCompleteCalled.signal();
+            }
+        };
+        ExecutorService executor = Executors.newFixedThreadPool(1);
+        mPlayer.registerPlayerEventCallback(executor, callback);
+
+        onPrepareCalled.reset();
+        onPlayerStateChangedCalled.reset();
+        onBufferingStateChangedCalled.reset();
+        mPlayer.prepare();
+        do {
+            assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
+        } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED);
+
+        assertTrue(onPrepareCalled.waitForSignal(1000));
+        do {
+            assertTrue(onPlayerStateChangedCalled.waitForSignal(1000));
+        } while (playerState.get() != MediaPlayerBase.PLAYER_STATE_PAUSED);
+        do {
+            assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
+        } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+
+        onSeekCompleteCalled.reset();
+        mPlayer.seekTo(mp4Duration >> 1, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        onSeekCompleteCalled.waitForSignal();
+
+        onPlaybackSpeedChanged.reset();
+        mPlayer.setPlaybackSpeed(0.5f);
+        do {
+            assertTrue(onPlaybackSpeedChanged.waitForSignal(1000));
+        } while (Math.abs(playbackSpeed.get() - 0.5f) > FLOAT_TOLERANCE);
+
+        mPlayer.reset();
+
+        mPlayer.unregisterPlayerEventCallback(callback);
+        executor.shutdown();
+    }
+
+    public void testRecordAndPlay() throws Exception {
+        if (!hasMicrophone()) {
+            return;
+        }
+        /* FIXME: check the codec exists.
+        if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_AUDIO_AMR_NB)
+                || !MediaUtils.checkEncoder(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) {
+            return; // skip
+        }
+        */
+        File outputFile = new File(Environment.getExternalStorageDirectory(),
+                "record_and_play.3gp");
+        String outputFileLocation = outputFile.getAbsolutePath();
+        try {
+            recordMedia(outputFileLocation);
+
+            Uri uri = Uri.parse(outputFileLocation);
+            MediaPlayer2 mp = MediaPlayer2.create();
+            try {
+                mp.setDataSource(new DataSourceDesc.Builder()
+                        .setDataSource(mContext, uri)
+                        .build());
+                mp.prepare();
+                Thread.sleep(SLEEP_TIME);
+                playAndStop(mp);
+            } finally {
+                mp.close();
+            }
+
+            try {
+                mp = createMediaPlayer2(mContext, uri);
+                playAndStop(mp);
+            } finally {
+                if (mp != null) {
+                    mp.close();
+                }
+            }
+
+            try {
+                mp = createMediaPlayer2(mContext, uri, mActivity.getSurfaceHolder());
+                playAndStop(mp);
+            } finally {
+                if (mp != null) {
+                    mp.close();
+                }
+            }
+        } finally {
+            outputFile.delete();
+        }
+    }
+
+    private void playAndStop(MediaPlayer2 mp) throws Exception {
+        mp.play();
+        Thread.sleep(SLEEP_TIME);
+        mp.reset();
+    }
+
+    private void recordMedia(String outputFile) throws Exception {
+        MediaRecorder mr = new MediaRecorder();
+        try {
+            mr.setAudioSource(MediaRecorder.AudioSource.MIC);
+            mr.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
+            mr.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
+            mr.setOutputFile(outputFile);
+
+            mr.prepare();
+            mr.start();
+            Thread.sleep(SLEEP_TIME);
+            mr.stop();
+        } finally {
+            mr.release();
+        }
+    }
+
+    private boolean hasMicrophone() {
+        return mActivity.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_MICROPHONE);
+    }
+
+    // Smoke test playback from a Media2DataSource.
+    @Test
+    @LargeTest
+    public void testPlaybackFromAMedia2DataSource() throws Exception {
+        final int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
+        final int duration = 10000;
+
+        /* FIXME: check the codec exists.
+        if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+            return;
+        }
+        */
+
+        TestMedia2DataSource dataSource =
+                TestMedia2DataSource.fromAssetFd(mResources.openRawResourceFd(resid));
+        // Test returning -1 from getSize() to indicate unknown size.
+        dataSource.returnFromGetSize(-1);
+        mPlayer.setDataSource(new DataSourceDesc.Builder()
+                .setDataSource(dataSource)
+                .build());
+        playLoadedVideo(null, null, -1);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        // Test pause and restart.
+        mPlayer.pause();
+        Thread.sleep(SLEEP_TIME);
+        assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                    mOnPlayCalled.signal();
+                }
+            }
+        };
+        mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        mOnPlayCalled.reset();
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        // Test reset.
+        mPlayer.reset();
+        mPlayer.setDataSource(new DataSourceDesc.Builder()
+                .setDataSource(dataSource)
+                .build());
+
+        mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        mOnPlayCalled.reset();
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+        // Test seek. Note: the seek position is cached and returned as the
+        // current position so there's no point in comparing them.
+        mPlayer.seekTo(duration - SLEEP_TIME, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+        while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+            Thread.sleep(SLEEP_TIME);
+        }
+    }
+
+    @Test
+    @LargeTest
+    public void testNullMedia2DataSourceIsRejected() throws Exception {
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE) {
+                    mCallStatus = status;
+                    mOnPlayCalled.signal();
+                }
+            }
+        };
+        mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        mCallStatus = MediaPlayer2.CALL_STATUS_NO_ERROR;
+        mPlayer.setDataSource((DataSourceDesc) null);
+        mOnPlayCalled.waitForSignal();
+        assertTrue(mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR);
+    }
+
+    @Test
+    @LargeTest
+    public void testMedia2DataSourceIsClosedOnReset() throws Exception {
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                if (what == MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE) {
+                    mCallStatus = status;
+                    mOnPlayCalled.signal();
+                }
+            }
+        };
+        mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+        TestMedia2DataSource dataSource = new TestMedia2DataSource(new byte[0]);
+        mPlayer.setDataSource(new DataSourceDesc.Builder()
+                .setDataSource(dataSource)
+                .build());
+        mOnPlayCalled.waitForSignal();
+        mPlayer.reset();
+        assertTrue(dataSource.isClosed());
+    }
+
+    @Test
+    @LargeTest
+    public void testPlaybackFailsIfMedia2DataSourceThrows() throws Exception {
+        final int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
+        /* FIXME: check the codec exists.
+        if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+            return;
+        }
+        */
+
+        setOnErrorListener();
+        TestMedia2DataSource dataSource =
+                TestMedia2DataSource.fromAssetFd(mResources.openRawResourceFd(resid));
+        mPlayer.setDataSource(new DataSourceDesc.Builder()
+                .setDataSource(dataSource)
+                .build());
+
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        dataSource.throwFromReadAt();
+        mPlayer.play();
+        assertTrue(mOnErrorCalled.waitForSignal());
+    }
+
+    @Test
+    @LargeTest
+    public void testPlaybackFailsIfMedia2DataSourceReturnsAnError() throws Exception {
+        final int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
+        /* FIXME: check the codec exists.
+        if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+            return;
+        }
+        */
+
+        TestMedia2DataSource dataSource =
+                TestMedia2DataSource.fromAssetFd(mResources.openRawResourceFd(resid));
+        mPlayer.setDataSource(new DataSourceDesc.Builder()
+                .setDataSource(dataSource)
+                .build());
+
+        setOnErrorListener();
+        MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                    mOnPrepareCalled.signal();
+                }
+            }
+        };
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(ecb);
+        }
+
+        mOnPrepareCalled.reset();
+        mPlayer.prepare();
+        mOnPrepareCalled.waitForSignal();
+
+        dataSource.returnFromReadAt(-2);
+        mPlayer.play();
+        assertTrue(mOnErrorCalled.waitForSignal());
+    }
+}
diff --git a/androidx/media/MediaPlayer2TestBase.java b/androidx/media/MediaPlayer2TestBase.java
new file mode 100644
index 0000000..215993a
--- /dev/null
+++ b/androidx/media/MediaPlayer2TestBase.java
@@ -0,0 +1,584 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.MediaTimestamp;
+import android.media.TimedMetaData;
+import android.net.Uri;
+import android.support.test.rule.ActivityTestRule;
+import android.view.SurfaceHolder;
+
+import androidx.annotation.CallSuper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+
+import java.io.IOException;
+import java.net.HttpCookie;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * Base class for tests which use MediaPlayer2 to play audio or video.
+ */
+public class MediaPlayer2TestBase {
+    private static final Logger LOG = Logger.getLogger(MediaPlayer2TestBase.class.getName());
+
+    protected static final int SLEEP_TIME = 1000;
+    protected static final int LONG_SLEEP_TIME = 6000;
+    protected static final int STREAM_RETRIES = 20;
+
+    protected Monitor mOnVideoSizeChangedCalled = new Monitor();
+    protected Monitor mOnVideoRenderingStartCalled = new Monitor();
+    protected Monitor mOnBufferingUpdateCalled = new Monitor();
+    protected Monitor mOnPrepareCalled = new Monitor();
+    protected Monitor mOnPlayCalled = new Monitor();
+    protected Monitor mOnDeselectTrackCalled = new Monitor();
+    protected Monitor mOnSeekCompleteCalled = new Monitor();
+    protected Monitor mOnCompletionCalled = new Monitor();
+    protected Monitor mOnInfoCalled = new Monitor();
+    protected Monitor mOnErrorCalled = new Monitor();
+    protected int mCallStatus;
+
+    protected Context mContext;
+    protected Resources mResources;
+
+    protected ExecutorService mExecutor;
+
+    protected MediaPlayer2 mPlayer = null;
+    protected MediaPlayer2 mPlayer2 = null;
+    protected MediaStubActivity mActivity;
+
+    protected final Object mEventCbLock = new Object();
+    protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks =
+            new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+    protected final Object mEventCbLock2 = new Object();
+    protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 =
+            new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+
+    @Rule
+    public ActivityTestRule<MediaStubActivity> mActivityRule =
+            new ActivityTestRule<>(MediaStubActivity.class);
+
+    // convenience functions to create MediaPlayer2
+    protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
+        return createMediaPlayer2(context, uri, null);
+    }
+
+    protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
+            SurfaceHolder holder) {
+        AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        int s = am.generateAudioSessionId();
+        return createMediaPlayer2(context, uri, holder, null, s > 0 ? s : 0);
+    }
+
+    protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
+            AudioAttributesCompat audioAttributes, int audioSessionId) {
+        try {
+            MediaPlayer2 mp = MediaPlayer2.create();
+            final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
+                    new AudioAttributesCompat.Builder().build();
+            mp.setAudioAttributes(aa);
+            mp.setAudioSessionId(audioSessionId);
+            mp.setDataSource(new DataSourceDesc.Builder()
+                    .setDataSource(context, uri)
+                    .build());
+            if (holder != null) {
+                mp.setSurface(holder.getSurface());
+            }
+            final Monitor onPrepareCalled = new Monitor();
+            ExecutorService executor = Executors.newFixedThreadPool(1);
+            MediaPlayer2.MediaPlayer2EventCallback ecb =
+                    new MediaPlayer2.MediaPlayer2EventCallback() {
+                        @Override
+                        public void onInfo(
+                                MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                            if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                                onPrepareCalled.signal();
+                            }
+                        }
+                    };
+            mp.setMediaPlayer2EventCallback(executor, ecb);
+            mp.prepare();
+            onPrepareCalled.waitForSignal();
+            mp.clearMediaPlayer2EventCallback();
+            executor.shutdown();
+            return mp;
+        } catch (IllegalArgumentException ex) {
+            LOG.warning("create failed:" + ex);
+            // fall through
+        } catch (SecurityException ex) {
+            LOG.warning("create failed:" + ex);
+            // fall through
+        } catch (InterruptedException ex) {
+            LOG.warning("create failed:" + ex);
+            // fall through
+        }
+        return null;
+    }
+
+    protected static MediaPlayer2 createMediaPlayer2(Context context, int resid) {
+        AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        int s = am.generateAudioSessionId();
+        return createMediaPlayer2(context, resid, null, s > 0 ? s : 0);
+    }
+
+    protected static MediaPlayer2 createMediaPlayer2(Context context, int resid,
+            AudioAttributesCompat audioAttributes, int audioSessionId) {
+        try {
+            AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid);
+            if (afd == null) {
+                return null;
+            }
+
+            MediaPlayer2 mp = MediaPlayer2.create();
+
+            final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
+                    new AudioAttributesCompat.Builder().build();
+            mp.setAudioAttributes(aa);
+            mp.setAudioSessionId(audioSessionId);
+
+            mp.setDataSource(new DataSourceDesc.Builder()
+                    .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+                    .build());
+
+            final Monitor onPrepareCalled = new Monitor();
+            ExecutorService executor = Executors.newFixedThreadPool(1);
+            MediaPlayer2.MediaPlayer2EventCallback ecb =
+                    new MediaPlayer2.MediaPlayer2EventCallback() {
+                        @Override
+                        public void onInfo(
+                                MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                            if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                                onPrepareCalled.signal();
+                            }
+                        }
+                    };
+            mp.setMediaPlayer2EventCallback(executor, ecb);
+            mp.prepare();
+            onPrepareCalled.waitForSignal();
+            mp.clearMediaPlayer2EventCallback();
+            afd.close();
+            executor.shutdown();
+            return mp;
+        } catch (IOException ex) {
+            LOG.warning("create failed:" + ex);
+            // fall through
+        } catch (IllegalArgumentException ex) {
+            LOG.warning("create failed:" + ex);
+            // fall through
+        } catch (SecurityException ex) {
+            LOG.warning("create failed:" + ex);
+            // fall through
+        } catch (InterruptedException ex) {
+            LOG.warning("create failed:" + ex);
+            // fall through
+        }
+        return null;
+    }
+
+    public static class Monitor {
+        private int mNumSignal;
+
+        public synchronized void reset() {
+            mNumSignal = 0;
+        }
+
+        public synchronized void signal() {
+            mNumSignal++;
+            notifyAll();
+        }
+
+        public synchronized boolean waitForSignal() throws InterruptedException {
+            return waitForCountedSignals(1) > 0;
+        }
+
+        public synchronized int waitForCountedSignals(int targetCount) throws InterruptedException {
+            while (mNumSignal < targetCount) {
+                wait();
+            }
+            return mNumSignal;
+        }
+
+        public synchronized boolean waitForSignal(long timeoutMs) throws InterruptedException {
+            return waitForCountedSignals(1, timeoutMs) > 0;
+        }
+
+        public synchronized int waitForCountedSignals(int targetCount, long timeoutMs)
+                throws InterruptedException {
+            if (timeoutMs == 0) {
+                return waitForCountedSignals(targetCount);
+            }
+            long deadline = System.currentTimeMillis() + timeoutMs;
+            while (mNumSignal < targetCount) {
+                long delay = deadline - System.currentTimeMillis();
+                if (delay <= 0) {
+                    break;
+                }
+                wait(delay);
+            }
+            return mNumSignal;
+        }
+
+        public synchronized boolean isSignalled() {
+            return mNumSignal >= 1;
+        }
+
+        public synchronized int getNumSignal() {
+            return mNumSignal;
+        }
+    }
+
+    @Before
+    @CallSuper
+    public void setUp() throws Exception {
+        mActivity = mActivityRule.getActivity();
+        try {
+            mActivityRule.runOnUiThread(new Runnable() {
+                public void run() {
+                    mPlayer = MediaPlayer2.create();
+                    mPlayer2 = MediaPlayer2.create();
+                }
+            });
+        } catch (Throwable e) {
+            e.printStackTrace();
+            fail();
+        }
+        mContext = mActivityRule.getActivity();
+        mResources = mContext.getResources();
+        mExecutor = Executors.newFixedThreadPool(1);
+
+        setUpMP2ECb(mPlayer, mEventCbLock, mEventCallbacks);
+        setUpMP2ECb(mPlayer2, mEventCbLock2, mEventCallbacks2);
+    }
+
+    @After
+    @CallSuper
+    public void tearDown() throws Exception {
+        if (mPlayer != null) {
+            mPlayer.close();
+            mPlayer = null;
+        }
+        if (mPlayer2 != null) {
+            mPlayer2.close();
+            mPlayer2 = null;
+        }
+        mExecutor.shutdown();
+        mActivity = null;
+    }
+
+    protected void setUpMP2ECb(MediaPlayer2 mp, final Object cbLock,
+            final List<MediaPlayer2.MediaPlayer2EventCallback> ecbs) {
+        mp.setMediaPlayer2EventCallback(mExecutor, new MediaPlayer2.MediaPlayer2EventCallback() {
+            @Override
+            public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd, int w, int h) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onVideoSizeChanged(mp, dsd, w, h);
+                    }
+                }
+            }
+
+            @Override
+            public void onTimedMetaDataAvailable(MediaPlayer2 mp, DataSourceDesc dsd,
+                    TimedMetaData data) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onTimedMetaDataAvailable(mp, dsd, data);
+                    }
+                }
+            }
+
+            @Override
+            public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onError(mp, dsd, what, extra);
+                    }
+                }
+            }
+
+            @Override
+            public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onInfo(mp, dsd, what, extra);
+                    }
+                }
+            }
+
+            @Override
+            public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onCallCompleted(mp, dsd, what, status);
+                    }
+                }
+            }
+
+            @Override
+            public void onMediaTimeChanged(MediaPlayer2 mp, DataSourceDesc dsd,
+                    MediaTimestamp timestamp) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onMediaTimeChanged(mp, dsd, timestamp);
+                    }
+                }
+            }
+
+            @Override
+            public void onCommandLabelReached(MediaPlayer2 mp, Object label) {
+                synchronized (cbLock) {
+                    for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+                        ecb.onCommandLabelReached(mp, label);
+                    }
+                }
+            }
+        });
+    }
+
+    // returns true on success
+    protected boolean loadResource(int resid) throws Exception {
+        /* FIXME: ensure device has capability.
+        if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+            return false;
+        }
+        */
+
+        AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+        try {
+            mPlayer.setDataSource(new DataSourceDesc.Builder()
+                    .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+                    .build());
+        } finally {
+            // TODO: close afd only after setDataSource is confirmed.
+            // afd.close();
+        }
+        return true;
+    }
+
+    protected DataSourceDesc createDataSourceDesc(int resid) throws Exception {
+        /* FIXME: ensure device has capability.
+        if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+            return null;
+        }
+        */
+
+        AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+        return new DataSourceDesc.Builder()
+                .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+                .build();
+    }
+
+    protected boolean checkLoadResource(int resid) throws Exception {
+        return loadResource(resid);
+
+        /* FIXME: ensure device has capability.
+        return MediaUtils.check(loadResource(resid), "no decoder found");
+        */
+    }
+
+    protected void playLiveVideoTest(String path, int playTime) throws Exception {
+        playVideoWithRetries(path, null, null, playTime);
+    }
+
+    protected void playLiveAudioOnlyTest(String path, int playTime) throws Exception {
+        playVideoWithRetries(path, -1, -1, playTime);
+    }
+
+    protected void playVideoTest(String path, int width, int height) throws Exception {
+        playVideoWithRetries(path, width, height, 0);
+    }
+
+    protected void playVideoWithRetries(String path, Integer width, Integer height, int playTime)
+            throws Exception {
+        boolean playedSuccessfully = false;
+        final Uri uri = Uri.parse(path);
+        for (int i = 0; i < STREAM_RETRIES; i++) {
+            try {
+                mPlayer.setDataSource(new DataSourceDesc.Builder()
+                        .setDataSource(mContext, uri)
+                        .build());
+                playLoadedVideo(width, height, playTime);
+                playedSuccessfully = true;
+                break;
+            } catch (PrepareFailedException e) {
+                // prepare() can fail because of network issues, so try again
+                LOG.warning("prepare() failed on try " + i + ", trying playback again");
+            }
+        }
+        assertTrue("Stream did not play successfully after all attempts", playedSuccessfully);
+    }
+
+    protected void playVideoTest(int resid, int width, int height) throws Exception {
+        if (!checkLoadResource(resid)) {
+            return; // skip
+        }
+
+        playLoadedVideo(width, height, 0);
+    }
+
+    protected void playLiveVideoTest(
+            Uri uri, Map<String, String> headers, List<HttpCookie> cookies,
+            int playTime) throws Exception {
+        playVideoWithRetries(uri, headers, cookies, null /* width */, null /* height */, playTime);
+    }
+
+    protected void playVideoWithRetries(
+            Uri uri, Map<String, String> headers, List<HttpCookie> cookies,
+            Integer width, Integer height, int playTime) throws Exception {
+        boolean playedSuccessfully = false;
+        for (int i = 0; i < STREAM_RETRIES; i++) {
+            try {
+                mPlayer.setDataSource(new DataSourceDesc.Builder()
+                        .setDataSource(mContext,
+                            uri, headers, cookies)
+                        .build());
+                playLoadedVideo(width, height, playTime);
+                playedSuccessfully = true;
+                break;
+            } catch (PrepareFailedException e) {
+                // prepare() can fail because of network issues, so try again
+                // playLoadedVideo already has reset the player so we can try again safely.
+                LOG.warning("prepare() failed on try " + i + ", trying playback again");
+            }
+        }
+        assertTrue("Stream did not play successfully after all attempts", playedSuccessfully);
+    }
+
+    /**
+     * Play a video which has already been loaded with setDataSource().
+     *
+     * @param width width of the video to verify, or null to skip verification
+     * @param height height of the video to verify, or null to skip verification
+     * @param playTime length of time to play video, or 0 to play entire video.
+     * with a non-negative value, this method stops the playback after the length of
+     * time or the duration the video is elapsed. With a value of -1,
+     * this method simply starts the video and returns immediately without
+     * stoping the video playback.
+     */
+    protected void playLoadedVideo(final Integer width, final Integer height, int playTime)
+            throws Exception {
+        final float volume = 0.5f;
+
+        boolean audioOnly = (width != null && width.intValue() == -1)
+                || (height != null && height.intValue() == -1);
+
+        mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+        /* FIXME: ensure that screen is on in activity level.
+        mPlayer.setScreenOnWhilePlaying(true);
+        */
+
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(new MediaPlayer2.MediaPlayer2EventCallback() {
+                @Override
+                public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd, int w, int h) {
+                    if (w == 0 && h == 0) {
+                        // A size of 0x0 can be sent initially one time when using NuPlayer.
+                        assertFalse(mOnVideoSizeChangedCalled.isSignalled());
+                        return;
+                    }
+                    mOnVideoSizeChangedCalled.signal();
+                    if (width != null) {
+                        assertEquals(width.intValue(), w);
+                    }
+                    if (height != null) {
+                        assertEquals(height.intValue(), h);
+                    }
+                }
+
+                @Override
+                public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                    fail("Media player had error " + what + " playing video");
+                }
+
+                @Override
+                public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                    if (what == MediaPlayer2.MEDIA_INFO_VIDEO_RENDERING_START) {
+                        mOnVideoRenderingStartCalled.signal();
+                    } else if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+                        mOnPrepareCalled.signal();
+                    }
+                }
+
+                @Override
+                public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+                        int what, int status) {
+                    if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+                        mOnPlayCalled.signal();
+                    }
+                }
+            });
+        }
+        try {
+            mOnPrepareCalled.reset();
+            mPlayer.prepare();
+            mOnPrepareCalled.waitForSignal();
+        } catch (Exception e) {
+            mPlayer.reset();
+            throw new PrepareFailedException();
+        }
+
+        mOnPlayCalled.reset();
+        mPlayer.play();
+        mOnPlayCalled.waitForSignal();
+        if (!audioOnly) {
+            mOnVideoSizeChangedCalled.waitForSignal();
+            mOnVideoRenderingStartCalled.waitForSignal();
+        }
+        mPlayer.setPlayerVolume(volume);
+
+        // waiting to complete
+        if (playTime == -1) {
+            return;
+        } else if (playTime == 0) {
+            while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+                Thread.sleep(SLEEP_TIME);
+            }
+        } else {
+            Thread.sleep(playTime);
+        }
+
+        mPlayer.reset();
+    }
+
+    private static class PrepareFailedException extends Exception {}
+
+    protected void setOnErrorListener() {
+        synchronized (mEventCbLock) {
+            mEventCallbacks.add(new MediaPlayer2.MediaPlayer2EventCallback() {
+                @Override
+                public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+                    mOnErrorCalled.signal();
+                }
+            });
+        }
+    }
+}
diff --git a/androidx/media/MediaPlayerBase.java b/androidx/media/MediaPlayerBase.java
new file mode 100644
index 0000000..de0e128
--- /dev/null
+++ b/androidx/media/MediaPlayerBase.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base class for all media players that want media session.
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public abstract class MediaPlayerBase implements AutoCloseable {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({
+        PLAYER_STATE_IDLE,
+        PLAYER_STATE_PAUSED,
+        PLAYER_STATE_PLAYING,
+        PLAYER_STATE_ERROR })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PlayerState {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({
+        BUFFERING_STATE_UNKNOWN,
+        BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+        BUFFERING_STATE_BUFFERING_AND_STARVED,
+        BUFFERING_STATE_BUFFERING_COMPLETE })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BuffState {}
+
+    /**
+     * State when the player is idle, and needs configuration to start playback.
+     */
+    public static final int PLAYER_STATE_IDLE = 0;
+
+    /**
+     * State when the player's playback is paused
+     */
+    public static final int PLAYER_STATE_PAUSED = 1;
+
+    /**
+     * State when the player's playback is ongoing
+     */
+    public static final int PLAYER_STATE_PLAYING = 2;
+
+    /**
+     * State when the player is in error state and cannot be recovered self.
+     */
+    public static final int PLAYER_STATE_ERROR = 3;
+
+    /**
+     * Buffering state is unknown.
+     */
+    public static final int BUFFERING_STATE_UNKNOWN = 0;
+
+    /**
+     * Buffering state indicating the player is buffering but enough has been buffered
+     * for this player to be able to play the content.
+     * See {@link #getBufferedPosition()} for how far is buffered already.
+     */
+    public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1;
+
+    /**
+     * Buffering state indicating the player is buffering, but the player is currently starved
+     * for data, and cannot play.
+     */
+    public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2;
+
+    /**
+     * Buffering state indicating the player is done buffering, and the remainder of the content is
+     * available for playback.
+     */
+    public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3;
+
+    /**
+     * Starts or resumes playback.
+     */
+    public abstract void play();
+
+    /**
+     * Prepares the player for playback.
+     * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerBase, DataSourceDesc)} for being
+     * notified when the preparation phase completed. During this time, the player may allocate
+     * resources required to play, such as audio and video decoders.
+     */
+    public abstract void prepare();
+
+    /**
+     * Pauses playback.
+     */
+    public abstract void pause();
+
+    /**
+     * Resets the MediaPlayerBase to its uninitialized state.
+     */
+    public abstract void reset();
+
+    /**
+     *
+     */
+    public abstract void skipToNext();
+
+    /**
+     * Moves the playback head to the specified position
+     * @param pos the new playback position expressed in ms.
+     */
+    public abstract void seekTo(long pos);
+
+    public static final long UNKNOWN_TIME = -1;
+
+    /**
+     * Gets the current playback head position.
+     * @return the current playback position in ms, or {@link #UNKNOWN_TIME} if unknown.
+     */
+    public long getCurrentPosition() {
+        return UNKNOWN_TIME;
+    }
+
+    /**
+     * Returns the duration of the current data source, or {@link #UNKNOWN_TIME} if unknown.
+     * @return the duration in ms, or {@link #UNKNOWN_TIME}.
+     */
+    public long getDuration() {
+        return UNKNOWN_TIME;
+    }
+
+    /**
+     * Gets the buffered position of current playback, or {@link #UNKNOWN_TIME} if unknown.
+     * @return the buffered position in ms, or {@link #UNKNOWN_TIME}.
+     */
+    public long getBufferedPosition() {
+        return UNKNOWN_TIME;
+    }
+
+    /**
+     * Returns the current player state.
+     * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerBase, int)} for
+     * notification of changes.
+     * @return the current player state
+     */
+    public abstract @PlayerState int getPlayerState();
+
+    /**
+     * Returns the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     * @return the buffering state.
+     */
+    public abstract @BuffState int getBufferingState();
+
+    /**
+     * Sets the {@link AudioAttributesCompat} to be used during the playback of the media.
+     *
+     * @param attributes non-null <code>AudioAttributes</code>.
+     */
+    public abstract void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
+
+    /**
+     * Returns AudioAttributes that media player has.
+     */
+    public abstract @Nullable AudioAttributesCompat getAudioAttributes();
+
+    /**
+     * Sets the data source to be played.
+     * @param dsd
+     */
+    public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets the data source that will be played immediately after the current one is done playing.
+     * @param dsd
+     */
+    public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets the list of data sources that will be sequentially played after the current one. Each
+     * data source is played immediately after the previous one is done playing.
+     * @param dsds
+     */
+    public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+    /**
+     * Returns the current data source.
+     * @return the current data source, or null if none is set, or none available to play.
+     */
+    public abstract @Nullable DataSourceDesc getCurrentDataSource();
+
+    /**
+     * Configures the player to loop on the current data source.
+     * @param loop true if the current data source is meant to loop.
+     */
+    public abstract void loopCurrent(boolean loop);
+
+    /**
+     * Sets the playback speed.
+     * A value of 1.0f is the default playback value.
+     * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+     * before using negative values.<br>
+     * After changing the playback speed, it is recommended to query the actual speed supported
+     * by the player, see {@link #getPlaybackSpeed()}.
+     * @param speed
+     */
+    public abstract void setPlaybackSpeed(float speed);
+
+    /**
+     * Returns the actual playback speed to be used by the player when playing.
+     * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+     * @return the actual playback speed
+     */
+    public float getPlaybackSpeed() {
+        return 1.0f;
+    }
+
+    /**
+     * Indicates whether reverse playback is supported.
+     * Reverse playback is indicated by negative playback speeds, see
+     * {@link #setPlaybackSpeed(float)}.
+     * @return true if reverse playback is supported.
+     */
+    public boolean isReversePlaybackSupported() {
+        return false;
+    }
+
+    /**
+     * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+     * on the audio samples.
+     * Note that this volume is specific to the player, and is separate from stream volume
+     * used across the platform.<br>
+     * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+     * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+     * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+     */
+    public abstract void setPlayerVolume(float volume);
+
+    /**
+     * Returns the current volume of this player to this player.
+     * Note that it does not take into account the associated stream volume.
+     * @return the player volume.
+     */
+    public abstract float getPlayerVolume();
+
+    /**
+     * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+     */
+    public float getMaxPlayerVolume() {
+        return 1.0f;
+    }
+
+    /**
+     * Adds a callback to be notified of events for this player.
+     * @param e the {@link Executor} to be used for the events.
+     * @param cb the callback to receive the events.
+     */
+    public abstract void registerPlayerEventCallback(@NonNull Executor e,
+            @NonNull PlayerEventCallback cb);
+
+    /**
+     * Removes a previously registered callback for player events
+     * @param cb the callback to remove
+     */
+    public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
+
+    /**
+     * A callback class to receive notifications for events on the media player.
+     * See {@link MediaPlayerBase#registerPlayerEventCallback(Executor, PlayerEventCallback)} to
+     * register this callback.
+     */
+    public abstract static class PlayerEventCallback {
+        /**
+         * Called when the player's current data source has changed.
+         *
+         * @param mpb the player whose data source changed.
+         * @param dsd the new current data source. null, if no more data sources available.
+         */
+        public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+                @Nullable DataSourceDesc dsd) { }
+
+        /**
+         * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+         * referenced by the given data source.
+         * @param mpb the player that is prepared.
+         * @param dsd the data source that the player is prepared to play.
+         */
+        public void onMediaPrepared(@NonNull MediaPlayerBase mpb,
+                @NonNull DataSourceDesc dsd) { }
+
+        /**
+         * Called to indicate that the state of the player has changed.
+         * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+         * @param mpb the player whose state has changed.
+         * @param state the new state of the player.
+         */
+        public void onPlayerStateChanged(@NonNull MediaPlayerBase mpb, @PlayerState int state) { }
+
+        /**
+         * Called to report buffering events for a data source.
+         * @param mpb the player that is buffering
+         * @param dsd the data source for which buffering is happening.
+         * @param state the new buffering state.
+         */
+        public void onBufferingStateChanged(@NonNull MediaPlayerBase mpb,
+                @NonNull DataSourceDesc dsd, @BuffState int state) { }
+
+        /**
+         * Called to indicate that the playback speed has changed.
+         * @param mpb the player that has changed the playback speed.
+         * @param speed the new playback speed.
+         */
+        public void onPlaybackSpeedChanged(@NonNull MediaPlayerBase mpb, float speed) { }
+
+        /**
+         * Called to indicate that {@link #seekTo(long)} is completed.
+         *
+         * @param mpb the player that has completed seeking.
+         * @param position the previous seeking request.
+         * @see #seekTo(long)
+         */
+        public void onSeekCompleted(@NonNull MediaPlayerBase mpb, long position) { }
+    }
+}
diff --git a/androidx/media/MediaPlaylistAgent.java b/androidx/media/MediaPlaylistAgent.java
new file mode 100644
index 0000000..07838e8
--- /dev/null
+++ b/androidx/media/MediaPlaylistAgent.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.SimpleArrayMap;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * MediaPlaylistAgent is the abstract class an application needs to derive from to pass an object
+ * to a MediaSession2 that will override default playlist handling behaviors. It contains a set of
+ * notify methods to signal MediaSession2 that playlist-related state has changed.
+ * <p>
+ * Playlists are composed of one or multiple {@link MediaItem2} instances, which combine metadata
+ * and data sources (as {@link DataSourceDesc})
+ * Used by {@link MediaSession2} and {@link MediaController2}.
+ */
+// This class only includes methods that contain {@link MediaItem2}.
+public abstract class MediaPlaylistAgent {
+    private static final String TAG = "MediaPlaylistAgent";
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL,
+            REPEAT_MODE_GROUP})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RepeatMode {}
+
+    /**
+     * Playback will be stopped at the end of the playing media list.
+     */
+    public static final int REPEAT_MODE_NONE = 0;
+
+    /**
+     * Playback of the current playing media item will be repeated.
+     */
+    public static final int REPEAT_MODE_ONE = 1;
+
+    /**
+     * Playing media list will be repeated.
+     */
+    public static final int REPEAT_MODE_ALL = 2;
+
+    /**
+     * Playback of the playing media group will be repeated.
+     * A group is a logical block of media items which is specified in the section 5.7 of the
+     * Bluetooth AVRCP 1.6. An example of a group is the playlist.
+     */
+    public static final int REPEAT_MODE_GROUP = 3;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ShuffleMode {}
+
+    /**
+     * Media list will be played in order.
+     */
+    public static final int SHUFFLE_MODE_NONE = 0;
+
+    /**
+     * Media list will be played in shuffled order.
+     */
+    public static final int SHUFFLE_MODE_ALL = 1;
+
+    /**
+     * Media group will be played in shuffled order.
+     * A group is a logical block of media items which is specified in the section 5.7 of the
+     * Bluetooth AVRCP 1.6. An example of a group is the playlist.
+     */
+    public static final int SHUFFLE_MODE_GROUP = 2;
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final SimpleArrayMap<PlaylistEventCallback, Executor> mCallbacks =
+            new SimpleArrayMap<>();
+
+    /**
+     * Register {@link PlaylistEventCallback} to listen changes in the underlying
+     * {@link MediaPlaylistAgent}.
+     *
+     * @param executor a callback Executor
+     * @param callback a PlaylistEventCallback
+     * @throws IllegalArgumentException if executor or callback is {@code null}.
+     */
+    public final void registerPlaylistEventCallback(
+            @NonNull /*@CallbackExecutor*/ Executor executor,
+            @NonNull PlaylistEventCallback callback) {
+        if (executor == null) {
+            throw new IllegalArgumentException("executor shouldn't be null");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("callback shouldn't be null");
+        }
+
+        synchronized (mLock) {
+            if (mCallbacks.get(callback) != null) {
+                Log.w(TAG, "callback is already added. Ignoring.");
+                return;
+            }
+            mCallbacks.put(callback, executor);
+        }
+    }
+
+    /**
+     * Unregister the previously registered {@link PlaylistEventCallback}.
+     *
+     * @param callback the callback to be removed
+     * @throws IllegalArgumentException if the callback is {@code null}.
+     */
+    public final void unregisterPlaylistEventCallback(@NonNull PlaylistEventCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("callback shouldn't be null");
+        }
+        synchronized (mLock) {
+            mCallbacks.remove(callback);
+        }
+    }
+
+    /**
+     * Notifies the current playlist and playlist metadata. Call this API when the playlist is
+     * changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent, List, MediaMetadata2)}.
+     */
+    public final void notifyPlaylistChanged() {
+        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+        final List<MediaItem2> playlist = getPlaylist();
+        final MediaMetadata2 metadata = getPlaylistMetadata();
+        for (int i = 0; i < callbacks.size(); i++) {
+            final PlaylistEventCallback callback = callbacks.keyAt(i);
+            final Executor executor = callbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlaylistChanged(
+                            MediaPlaylistAgent.this, playlist, metadata);
+                }
+            });
+        }
+    }
+
+    /**
+     * Notifies the current playlist metadata. Call this API when the playlist metadata is changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onPlaylistMetadataChanged(MediaPlaylistAgent, MediaMetadata2)}.
+     */
+    public final void notifyPlaylistMetadataChanged() {
+        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+        for (int i = 0; i < callbacks.size(); i++) {
+            final PlaylistEventCallback callback = callbacks.keyAt(i);
+            final Executor executor = callbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlaylistMetadataChanged(
+                            MediaPlaylistAgent.this, MediaPlaylistAgent.this.getPlaylistMetadata());
+                }
+            });
+        }
+    }
+
+    /**
+     * Notifies the current shuffle mode. Call this API when the shuffle mode is changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onShuffleModeChanged(MediaPlaylistAgent, int)}.
+     */
+    public final void notifyShuffleModeChanged() {
+        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+        for (int i = 0; i < callbacks.size(); i++) {
+            final PlaylistEventCallback callback = callbacks.keyAt(i);
+            final Executor executor = callbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onShuffleModeChanged(
+                            MediaPlaylistAgent.this, MediaPlaylistAgent.this.getShuffleMode());
+                }
+            });
+        }
+    }
+
+    /**
+     * Notifies the current repeat mode. Call this API when the repeat mode is changed.
+     * <p>
+     * Registered {@link PlaylistEventCallback} would receive this event through the
+     * {@link PlaylistEventCallback#onRepeatModeChanged(MediaPlaylistAgent, int)}.
+     */
+    public final void notifyRepeatModeChanged() {
+        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+        for (int i = 0; i < callbacks.size(); i++) {
+            final PlaylistEventCallback callback = callbacks.keyAt(i);
+            final Executor executor = callbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onRepeatModeChanged(
+                            MediaPlaylistAgent.this, MediaPlaylistAgent.this.getRepeatMode());
+                }
+            });
+        }
+    }
+
+    /**
+     * Returns the playlist
+     *
+     * @return playlist, or null if none is set.
+     */
+    public abstract @Nullable List<MediaItem2> getPlaylist();
+
+    /**
+     * Sets the playlist with the metadata.
+     * <p>
+     * When the playlist is changed, call {@link #notifyPlaylistChanged()} to notify changes to the
+     * registered callbacks.
+     *
+     * @param list playlist
+     * @param metadata metadata of the playlist
+     * @see #notifyPlaylistChanged()
+     */
+    public abstract void setPlaylist(@NonNull List<MediaItem2> list,
+            @Nullable MediaMetadata2 metadata);
+
+    /**
+     * Returns the playlist metadata
+     *
+     * @return metadata metadata of the playlist, or null if none is set
+     */
+    public abstract @Nullable MediaMetadata2 getPlaylistMetadata();
+
+    /**
+     * Updates the playlist metadata.
+     * <p>
+     * When the playlist metadata is changed, call {@link #notifyPlaylistMetadataChanged()} to
+     * notify changes to the registered callbacks.
+     *
+     * @param metadata metadata of the playlist
+     * @see #notifyPlaylistMetadataChanged()
+     */
+    public abstract void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata);
+
+    /**
+     * Returns currently playing media item.
+     */
+    public abstract MediaItem2 getCurrentMediaItem();
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+     * the playlist.
+     * <p>
+     * This will not change the currently playing media item.
+     * If index is less than or equal to the current index of the playlist,
+     * the current index of the playlist will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    public abstract void addPlaylistItem(int index, @NonNull MediaItem2 item);
+
+    /**
+     * Removes the media item from the playlist
+     *
+     * @param item media item to remove
+     */
+    public abstract void removePlaylistItem(@NonNull MediaItem2 item);
+
+    /**
+     * Replace the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
+     *
+     * @param index the index of the item to replace
+     * @param item the new item
+     */
+    public abstract void replacePlaylistItem(int index, @NonNull MediaItem2 item);
+
+    /**
+     * Skips to the the media item, and plays from it.
+     *
+     * @param item media item to start playing from
+     */
+    public abstract void skipToPlaylistItem(@NonNull MediaItem2 item);
+
+    /**
+     * Skips to the previous item in the playlist.
+     */
+    public abstract void skipToPreviousItem();
+
+    /**
+     * Skips to the next item in the playlist.
+     */
+    public abstract void skipToNextItem();
+
+    /**
+     * Gets the repeat mode
+     *
+     * @return repeat mode
+     * @see #REPEAT_MODE_NONE
+     * @see #REPEAT_MODE_ONE
+     * @see #REPEAT_MODE_ALL
+     * @see #REPEAT_MODE_GROUP
+     */
+    public abstract @RepeatMode int getRepeatMode();
+
+    /**
+     * Sets the repeat mode.
+     * <p>
+     * When the repeat mode is changed, call {@link #notifyRepeatModeChanged()} to notify changes
+     * to the registered callbacks.
+     *
+     * @param repeatMode repeat mode
+     * @see #REPEAT_MODE_NONE
+     * @see #REPEAT_MODE_ONE
+     * @see #REPEAT_MODE_ALL
+     * @see #REPEAT_MODE_GROUP
+     * @see #notifyRepeatModeChanged()
+     */
+    public abstract void setRepeatMode(@RepeatMode int repeatMode);
+
+    /**
+     * Gets the shuffle mode
+     *
+     * @return The shuffle mode
+     * @see #SHUFFLE_MODE_NONE
+     * @see #SHUFFLE_MODE_ALL
+     * @see #SHUFFLE_MODE_GROUP
+     */
+    public abstract @ShuffleMode int getShuffleMode();
+
+    /**
+     * Sets the shuffle mode.
+     * <p>
+     * When the shuffle mode is changed, call {@link #notifyShuffleModeChanged()} to notify changes
+     * to the registered callbacks.
+     *
+     * @param shuffleMode The shuffle mode
+     * @see #SHUFFLE_MODE_NONE
+     * @see #SHUFFLE_MODE_ALL
+     * @see #SHUFFLE_MODE_GROUP
+     * @see #notifyShuffleModeChanged()
+     */
+    public abstract void setShuffleMode(@ShuffleMode int shuffleMode);
+
+    /**
+     * Called by {@link MediaSession2} when it wants to translate {@link DataSourceDesc} from the
+     * {@link MediaPlayerBase.PlayerEventCallback} to the {@link MediaItem2}. Override this method
+     * if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them with
+     * {@link #setPlaylist(List, MediaMetadata2)}.
+     * <p>
+     * Session would throw an exception if this returns {@code null} for the dsd from the
+     * {@link MediaPlayerBase.PlayerEventCallback}.
+     * <p>
+     * Default implementation calls the {@link #getPlaylist()} and searches the {@link MediaItem2}
+     * with the {@param dsd}.
+     *
+     * @param dsd The dsd to query
+     * @return A {@link MediaItem2} object in the playlist that matches given {@code dsd}.
+     * @throws IllegalArgumentException if {@code dsd} is null
+     */
+    public @Nullable MediaItem2 getMediaItem(@NonNull DataSourceDesc dsd) {
+        if (dsd == null) {
+            throw new IllegalArgumentException("dsd shouldn't be null");
+        }
+        List<MediaItem2> itemList = getPlaylist();
+        if (itemList == null) {
+            return null;
+        }
+        for (int i = 0; i < itemList.size(); i++) {
+            MediaItem2 item = itemList.get(i);
+            if (item != null && item.getDataSourceDesc().equals(dsd)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    private SimpleArrayMap<PlaylistEventCallback, Executor> getCallbacks() {
+        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = new SimpleArrayMap<>();
+        synchronized (mLock) {
+            callbacks.putAll(mCallbacks);
+        }
+        return callbacks;
+    }
+
+    /**
+     * A callback class to receive notifications for events on the media player. See
+     * {@link MediaPlaylistAgent#registerPlaylistEventCallback(Executor, PlaylistEventCallback)}
+     * to register this callback.
+     */
+    public abstract static class PlaylistEventCallback {
+        /**
+         * Called when a playlist is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param list new playlist
+         * @param metadata new metadata
+         */
+        public void onPlaylistChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when a playlist metadata is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param metadata new metadata
+         */
+        public void onPlaylistMetadataChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when the shuffle mode is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param shuffleMode repeat mode
+         * @see #SHUFFLE_MODE_NONE
+         * @see #SHUFFLE_MODE_ALL
+         * @see #SHUFFLE_MODE_GROUP
+         */
+        public void onShuffleModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @ShuffleMode int shuffleMode) { }
+
+        /**
+         * Called when the repeat mode is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param repeatMode repeat mode
+         * @see #REPEAT_MODE_NONE
+         * @see #REPEAT_MODE_ONE
+         * @see #REPEAT_MODE_ALL
+         * @see #REPEAT_MODE_GROUP
+         */
+        public void onRepeatModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @RepeatMode int repeatMode) { }
+    }
+}
diff --git a/androidx/media/MediaSession2.java b/androidx/media/MediaSession2.java
new file mode 100644
index 0000000..909e979
--- /dev/null
+++ b/androidx/media/MediaSession2.java
@@ -0,0 +1,1670 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioFocusRequest;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaPlayerBase.BuffState;
+import androidx.media.MediaPlayerBase.PlayerState;
+import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
+import androidx.media.MediaPlaylistAgent.RepeatMode;
+import androidx.media.MediaPlaylistAgent.ShuffleMode;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows a media app to expose its transport controls and playback information in a process to
+ * other processes including the Android framework and other apps. Common use cases are as follows.
+ * <ul>
+ *     <li>Bluetooth/wired headset key events support</li>
+ *     <li>Android Auto/Wearable support</li>
+ *     <li>Separating UI process and playback process</li>
+ * </ul>
+ * <p>
+ * A MediaSession2 should be created when an app wants to publish media playback information or
+ * handle media keys. In general an app only needs one session for all playback, though multiple
+ * sessions can be created to provide finer grain controls of media.
+ * <p>
+ * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
+ * to other processes to allow them to create a {@link MediaController2} to interact with the
+ * session.
+ * <p>
+ * When a session receive transport control commands, the session sends the commands directly to
+ * the the underlying media player set by {@link Builder} or
+ * {@link #updatePlayer}.
+ * <p>
+ * When an app is finished performing playback it must call {@link #close()} to clean up the session
+ * and notify any controllers.
+ * <p>
+ * {@link MediaSession2} objects should be used on the thread on the looper.
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class MediaSession2 extends MediaInterface2.SessionPlayer implements AutoCloseable {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, ERROR_CODE_NOT_SUPPORTED,
+            ERROR_CODE_AUTHENTICATION_EXPIRED, ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
+            ERROR_CODE_CONCURRENT_STREAM_LIMIT, ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
+            ERROR_CODE_NOT_AVAILABLE_IN_REGION, ERROR_CODE_CONTENT_ALREADY_PLAYING,
+            ERROR_CODE_SKIP_LIMIT_REACHED, ERROR_CODE_ACTION_ABORTED, ERROR_CODE_END_OF_QUEUE,
+            ERROR_CODE_SETUP_REQUIRED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorCode {}
+
+    /**
+     * This is the default error code and indicates that none of the other error codes applies.
+     */
+    public static final int ERROR_CODE_UNKNOWN_ERROR = 0;
+
+    /**
+     * Error code when the application state is invalid to fulfill the request.
+     */
+    public static final int ERROR_CODE_APP_ERROR = 1;
+
+    /**
+     * Error code when the request is not supported by the application.
+     */
+    public static final int ERROR_CODE_NOT_SUPPORTED = 2;
+
+    /**
+     * Error code when the request cannot be performed because authentication has expired.
+     */
+    public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3;
+
+    /**
+     * Error code when a premium account is required for the request to succeed.
+     */
+    public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4;
+
+    /**
+     * Error code when too many concurrent streams are detected.
+     */
+    public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5;
+
+    /**
+     * Error code when the content is blocked due to parental controls.
+     */
+    public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6;
+
+    /**
+     * Error code when the content is blocked due to being regionally unavailable.
+     */
+    public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7;
+
+    /**
+     * Error code when the requested content is already playing.
+     */
+    public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8;
+
+    /**
+     * Error code when the application cannot skip any more songs because skip limit is reached.
+     */
+    public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9;
+
+    /**
+     * Error code when the action is interrupted due to some external event.
+     */
+    public static final int ERROR_CODE_ACTION_ABORTED = 10;
+
+    /**
+     * Error code when the playback navigation (previous, next) is not possible because the queue
+     * was exhausted.
+     */
+    public static final int ERROR_CODE_END_OF_QUEUE = 11;
+
+    /**
+     * Error code when the session needs user's manual intervention.
+     */
+    public static final int ERROR_CODE_SETUP_REQUIRED = 12;
+
+    /**
+     * Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
+     * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
+     *
+     * #see #setOnDataSourceMissingHelper
+     */
+    public interface OnDataSourceMissingHelper {
+        /**
+         * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc}
+         * but it's needed now for preparing or playing it. Returned data source descriptor will be
+         * sent to the player directly to prepare or play the contents.
+         * <p>
+         * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the
+         * playlist, so items cannot be differentiated.
+         *
+         * @param session the session for this event
+         * @param item media item from the controller
+         * @return a data source descriptor if the media item. Can be {@code null} if the content
+         *        isn't available.
+         */
+        @Nullable DataSourceDesc onDataSourceMissing(@NonNull MediaSession2 session,
+                @NonNull MediaItem2 item);
+    }
+
+    /**
+     * Callback to be called for all incoming commands from {@link MediaController2}s.
+     * <p>
+     * If it's not set, the session will accept all controllers and all incoming commands by
+     * default.
+     */
+    public abstract static class SessionCallback {
+        /**
+         * Called when a controller is created for this session. Return allowed commands for
+         * controller. By default it allows all connection requests and commands.
+         * <p>
+         * You can reject the connection by return {@code null}. In that case, controller receives
+         * {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} and cannot
+         * be usable.
+         *
+         * @param session the session for this event
+         * @param controller controller information.
+         * @return allowed commands. Can be {@code null} to reject connection.
+         */
+        public @Nullable SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) {
+            SessionCommandGroup2 commands = new SessionCommandGroup2();
+            commands.addAllPredefinedCommands();
+            return commands;
+        }
+
+        /**
+         * Called when a controller is disconnected
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         */
+        public void onDisconnected(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) { }
+
+        /**
+         * Called when a controller sent a command which will be sent directly to one of the
+         * following:
+         * <ul>
+         *  <li> {@link MediaPlayerBase} </li>
+         *  <li> {@link MediaPlaylistAgent} </li>
+         *  <li> {@link android.media.AudioManager} or {@link VolumeProviderCompat} </li>
+         * </ul>
+         * Return {@code false} here to reject the request and stop sending command.
+         *
+         * @param session the session for this event
+         * @param controller controller information.
+         * @param command a command. This method will be called for every single command.
+         * @return {@code true} if you want to accept incoming command. {@code false} otherwise.
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PLAY
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PAUSE
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_RESET
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PREPARE
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_SEEK_TO
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REMOVE_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_LIST
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_LIST_METADATA
+         * @see SessionCommand2#COMMAND_CODE_VOLUME_SET_VOLUME
+         * @see SessionCommand2#COMMAND_CODE_VOLUME_ADJUST_VOLUME
+         */
+        public boolean onCommandRequest(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull SessionCommand2 command) {
+            return true;
+        }
+
+        /**
+         * Called when a controller set rating of a media item through
+         * {@link MediaController2#setRating(String, Rating2)}.
+         * <p>
+         * To allow setting user rating for a {@link MediaItem2}, the media item's metadata
+         * should have {@link Rating2} with the key {@link MediaMetadata2#METADATA_KEY_USER_RATING},
+         * in order to provide possible rating style for controller. Controller will follow the
+         * rating style.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param mediaId media id from the controller
+         * @param rating new rating from the controller
+         * @see SessionCommand2#COMMAND_CODE_SESSION_SET_RATING
+         */
+        public void onSetRating(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
+                @NonNull String mediaId, @NonNull Rating2 rating) { }
+
+        /**
+         * Called when a controller sent a custom command through
+         * {@link MediaController2#sendCustomCommand(SessionCommand2, Bundle, ResultReceiver)}.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param customCommand custom command.
+         * @param args optional arguments
+         * @param cb optional result receiver
+         * @see SessionCommand2#COMMAND_CODE_CUSTOM
+         */
+        public void onCustomCommand(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull SessionCommand2 customCommand,
+                @Nullable Bundle args, @Nullable ResultReceiver cb) { }
+
+        /**
+         * Called when a controller requested to play a specific mediaId through
+         * {@link MediaController2#playFromMediaId(String, Bundle)}.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param mediaId media id
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
+         */
+        public void onPlayFromMediaId(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String mediaId,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to begin playback from a search query through
+         * {@link MediaController2#playFromSearch(String, Bundle)}
+         * <p>
+         * An empty query indicates that the app may play any music. The implementation should
+         * attempt to make a smart choice about what to play.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param query query string. Can be empty to indicate any suggested media
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
+         */
+        public void onPlayFromSearch(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String query,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to play a specific media item represented by a URI
+         * through {@link MediaController2#playFromUri(Uri, Bundle)}
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param uri uri
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_URI
+         */
+        public void onPlayFromUri(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull Uri uri,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to prepare for playing a specific mediaId through
+         * {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+         * <p>
+         * During the preparation, a session should not hold audio focus in order to allow other
+         * sessions play seamlessly. The state of playback should be updated to
+         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+         * <p>
+         * The playback of the prepared content should start in the later calls of
+         * {@link MediaSession2#play()}.
+         * <p>
+         * Override {@link #onPlayFromMediaId} to handle requests for starting
+         * playback without preparation.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param mediaId media id to prepare
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
+         */
+        public void onPrepareFromMediaId(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String mediaId,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to prepare playback from a search query through
+         * {@link MediaController2#prepareFromSearch(String, Bundle)}.
+         * <p>
+         * An empty query indicates that the app may prepare any music. The implementation should
+         * attempt to make a smart choice about what to play.
+         * <p>
+         * The state of playback should be updated to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}
+         * after the preparation is done. The playback of the prepared content should start in the
+         * later calls of {@link MediaSession2#play()}.
+         * <p>
+         * Override {@link #onPlayFromSearch} to handle requests for starting playback without
+         * preparation.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param query query string. Can be empty to indicate any suggested media
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
+         */
+        public void onPrepareFromSearch(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String query,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to prepare a specific media item represented by a URI
+         * through {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+         * <p>
+         * During the preparation, a session should not hold audio focus in order to allow
+         * other sessions play seamlessly. The state of playback should be updated to
+         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+         * <p>
+         * The playback of the prepared content should start in the later calls of
+         * {@link MediaSession2#play()}.
+         * <p>
+         * Override {@link #onPlayFromUri} to handle requests for starting playback without
+         * preparation.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param uri uri
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_URI
+         */
+        public void onPrepareFromUri(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller called {@link MediaController2#fastForward()}
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @see SessionCommand2#COMMAND_CODE_SESSION_FAST_FORWARD
+         */
+        public void onFastForward(@NonNull MediaSession2 session, ControllerInfo controller) { }
+
+        /**
+         * Called when a controller called {@link MediaController2#rewind()}
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @see SessionCommand2#COMMAND_CODE_SESSION_REWIND
+         */
+        public void onRewind(@NonNull MediaSession2 session, ControllerInfo controller) { }
+
+        /**
+         * Called when a controller called {@link MediaController2#subscribeRoutesInfo()}
+         * Session app should notify the routes information by calling
+         * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List)}.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @see SessionCommand2#COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO
+         */
+        public void onSubscribeRoutesInfo(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) { }
+
+        /**
+         * Called when a controller called {@link MediaController2#unsubscribeRoutesInfo()}
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @see SessionCommand2#COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO
+         */
+        public void onUnsubscribeRoutesInfo(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) { }
+
+        /**
+         * Called when a controller called {@link MediaController2#selectRoute(Bundle)}.
+         * @param session the session for this event
+         * @param controller controller information
+         * @param route The route bundle which may be from MediaRouteDescritor.asBundle().
+         * @see SessionCommand2#COMMAND_CODE_SESSION_SELECT_ROUTE
+         */
+        public void onSelectRoute(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull Bundle route) { }
+        /**
+         * Called when the player's current playing item is changed
+         * <p>
+         * When it's called, you should invalidate previous playback information and wait for later
+         * callbacks.
+         *
+         * @param session the controller for this event
+         * @param player the player for this event
+         * @param item new item
+         */
+        public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
+
+        /**
+         * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+         * referenced by the given data source.
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param item the media item for which buffering is happening
+         */
+        public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
+                @NonNull MediaItem2 item) { }
+
+        /**
+         * Called to indicate that the state of the player has changed.
+         * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param state the new state of the player.
+         */
+        public void onPlayerStateChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, @PlayerState int state) { }
+
+        /**
+         * Called to report buffering events for a data source.
+         *
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param item the media item for which buffering is happening.
+         * @param state the new buffering state.
+         */
+        public void onBufferingStateChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, @NonNull MediaItem2 item, @BuffState int state) { }
+
+        /**
+         * Called to indicate that the playback speed has changed.
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param speed the new playback speed.
+         */
+        public void onPlaybackSpeedChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, float speed) { }
+
+        /**
+         * Called to indicate that {@link #seekTo(long)} is completed.
+         *
+         * @param session the session for this event.
+         * @param mpb the player that has completed seeking.
+         * @param position the previous seeking request.
+         * @see #seekTo(long)
+         */
+        public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb,
+                long position) { }
+
+        /**
+         * Called when a playlist is changed from the {@link MediaPlaylistAgent}.
+         * <p>
+         * This is called when the underlying agent has called
+         * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent,
+         * List, MediaMetadata2)}.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param list new playlist
+         * @param metadata new metadata
+         */
+        public void onPlaylistChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent, @NonNull List<MediaItem2> list,
+                @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when a playlist metadata is changed.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param metadata new metadata
+         */
+        public void onPlaylistMetadataChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent, @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when the shuffle mode is changed.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param shuffleMode repeat mode
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+         */
+        public void onShuffleModeChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent,
+                @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+        /**
+         * Called when the repeat mode is changed.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param repeatMode repeat mode
+         * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+         * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+         */
+        public void onRepeatModeChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent,
+                @MediaPlaylistAgent.RepeatMode int repeatMode) { }
+    }
+
+    /**
+     * Base builder class for MediaSession2 and its subclass. Any change in this class should be
+     * also applied to the subclasses {@link MediaSession2.Builder} and
+     * {@link MediaLibraryService2.MediaLibrarySession.Builder}.
+     * <p>
+     * APIs here should be package private, but should have documentations for developers.
+     * Otherwise, javadoc will generate documentation with the generic types such as follows.
+     * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre>
+     * <p>
+     * This class is hidden to prevent from generating test stub, which fails with
+     * 'unexpected bound' because it tries to auto generate stub class as follows.
+     * <pre>abstract static class BuilderBase<
+     *      T extends android.media.MediaSession2,
+     *      U extends android.media.MediaSession2.BuilderBase<
+     *              T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    abstract static class BuilderBase
+            <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
+        final Context mContext;
+        MediaSession2ImplBase.BuilderBase<T, C> mBaseImpl;
+        MediaPlayerBase mPlayer;
+        String mId;
+        Executor mCallbackExecutor;
+        C mCallback;
+        MediaPlaylistAgent mPlaylistAgent;
+        VolumeProviderCompat mVolumeProvider;
+        PendingIntent mSessionActivity;
+
+        BuilderBase(Context context) {
+            if (context == null) {
+                throw new IllegalArgumentException("context shouldn't be null");
+            }
+            mContext = context;
+            // Ensure non-null
+            mId = "";
+        }
+
+        /**
+         * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event
+         * to.
+         *
+         * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+         */
+        @NonNull U setPlayer(@NonNull MediaPlayerBase player) {
+            if (player == null) {
+                throw new IllegalArgumentException("player shouldn't be null");
+            }
+            mBaseImpl.setPlayer(player);
+            return (U) this;
+        }
+
+        /**
+         * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
+         * underlying {@link MediaPlayerBase}. The playlist agent should manage
+         * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
+         * <p>
+         * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
+         * agent.
+         *
+         * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
+         *                      {@code player}
+         */
+        U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+            if (playlistAgent == null) {
+                throw new IllegalArgumentException("playlistAgent shouldn't be null");
+            }
+            mBaseImpl.setPlaylistAgent(playlistAgent);
+            return (U) this;
+        }
+
+        /**
+         * Sets the {@link VolumeProviderCompat} for this session to handle volume events. If not
+         * set, system will adjust the appropriate stream volume for this session's player.
+         *
+         * @param volumeProvider The provider that will receive volume button events.
+         */
+        @NonNull U setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+            mBaseImpl.setVolumeProvider(volumeProvider);
+            return (U) this;
+        }
+
+        /**
+         * Set an intent for launching UI for this Session. This can be used as a
+         * quick link to an ongoing media screen. The intent should be for an
+         * activity that may be started using {@link Context#startActivity(Intent)}.
+         *
+         * @param pi The intent to launch to show UI for this session.
+         */
+        @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
+            mBaseImpl.setSessionActivity(pi);
+            return (U) this;
+        }
+
+        /**
+         * Set ID of the session. If it's not set, an empty string with used to create a session.
+         * <p>
+         * Use this if and only if your app supports multiple playback at the same time and also
+         * wants to provide external apps to have finer controls of them.
+         *
+         * @param id id of the session. Must be unique per package.
+         * @throws IllegalArgumentException if id is {@code null}
+         * @return
+         */
+        @NonNull U setId(@NonNull String id) {
+            if (id == null) {
+                throw new IllegalArgumentException("id shouldn't be null");
+            }
+            mBaseImpl.setId(id);
+            return (U) this;
+        }
+
+        /**
+         * Set callback for the session.
+         *
+         * @param executor callback executor
+         * @param callback session callback.
+         * @return
+         */
+        @NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
+            if (executor == null) {
+                throw new IllegalArgumentException("executor shouldn't be null");
+            }
+            if (callback == null) {
+                throw new IllegalArgumentException("callback shouldn't be null");
+            }
+            mBaseImpl.setSessionCallback(executor, callback);
+            return (U) this;
+        }
+
+        /**
+         * Build {@link MediaSession2}.
+         *
+         * @return a new session
+         * @throws IllegalStateException if the session with the same id is already exists for the
+         *      package.
+         */
+        @NonNull T build() {
+            return mBaseImpl.build();
+        }
+
+        void setImpl(MediaSession2ImplBase.BuilderBase<T, C> impl) {
+            mBaseImpl = impl;
+        }
+    }
+
+    /**
+     * Builder for {@link MediaSession2}.
+     * <p>
+     * Any incoming event from the {@link MediaController2} will be handled on the thread
+     * that created session with the {@link Builder#build()}.
+     */
+    public static final class Builder extends BuilderBase<MediaSession2, Builder, SessionCallback> {
+        private MediaSession2ImplBase.Builder mImpl;
+
+        public Builder(Context context) {
+            super(context);
+            mImpl = new MediaSession2ImplBase.Builder(context);
+            setImpl(mImpl);
+        }
+
+        @Override
+        public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+            return super.setPlayer(player);
+        }
+
+        @Override
+        public @NonNull Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+            return super.setPlaylistAgent(playlistAgent);
+        }
+
+        @Override
+        public @NonNull Builder setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+            return super.setVolumeProvider(volumeProvider);
+        }
+
+        @Override
+        public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) {
+            return super.setSessionActivity(pi);
+        }
+
+        @Override
+        public @NonNull Builder setId(@NonNull String id) {
+            return super.setId(id);
+        }
+
+        @Override
+        public @NonNull Builder setSessionCallback(@NonNull Executor executor,
+                @NonNull SessionCallback callback) {
+            return super.setSessionCallback(executor, callback);
+        }
+
+        @Override
+        public @NonNull MediaSession2 build() {
+            return super.build();
+        }
+    }
+
+    /**
+     * Information of a controller.
+     */
+    public static final class ControllerInfo {
+        private final int mUid;
+        private final String mPackageName;
+        // Note: IMediaControllerCallback should be used only for MediaSession2ImplBase
+        private final IMediaControllerCallback mIControllerCallback;
+        private final boolean mIsTrusted;
+
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public ControllerInfo(@NonNull Context context, int uid, int pid,
+                @NonNull String packageName, @NonNull IMediaControllerCallback callback) {
+            mUid = uid;
+            mPackageName = packageName;
+            mIControllerCallback = callback;
+            mIsTrusted = false;
+        }
+
+        /**
+         * @return package name of the controller
+         */
+        public @NonNull String getPackageName() {
+            return mPackageName;
+        }
+
+        /**
+         * @return uid of the controller
+         */
+        public int getUid() {
+            return mUid;
+        }
+
+        /**
+         * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
+         * has a enabled notification listener so can be trusted to accept connection and incoming
+         * command request.
+         *
+         * @return {@code true} if the controller is trusted.
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public boolean isTrusted() {
+            return mIsTrusted;
+        }
+
+        IBinder getId() {
+            return mIControllerCallback.asBinder();
+        }
+
+        @Override
+        public int hashCode() {
+            return mIControllerCallback.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof ControllerInfo)) {
+                return false;
+            }
+            ControllerInfo other = (ControllerInfo) obj;
+            return mIControllerCallback.asBinder().equals(other.mIControllerCallback.asBinder());
+        }
+
+        @Override
+        public String toString() {
+            return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + "})";
+        }
+
+        /**
+         * @hide
+         * @return Bundle
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public @NonNull Bundle toBundle() {
+            return new Bundle();
+        }
+
+        /**
+         * @hide
+         * @return Bundle
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public static @NonNull ControllerInfo fromBundle(@NonNull Context context, Bundle bundle) {
+            return new ControllerInfo(context, -1, -1, "TODO", null);
+        }
+
+        IMediaControllerCallback getControllerBinder() {
+            return mIControllerCallback;
+        }
+    }
+
+    /**
+     * Button for a {@link SessionCommand2} that will be shown by the controller.
+     * <p>
+     * It's up to the controller's decision to respect or ignore this customization request.
+     */
+    public static final class CommandButton {
+        private static final String KEY_COMMAND =
+                "android.media.media_session2.command_button.command";
+        private static final String KEY_ICON_RES_ID =
+                "android.media.media_session2.command_button.icon_res_id";
+        private static final String KEY_DISPLAY_NAME =
+                "android.media.media_session2.command_button.display_name";
+        private static final String KEY_EXTRAS =
+                "android.media.media_session2.command_button.extras";
+        private static final String KEY_ENABLED =
+                "android.media.media_session2.command_button.enabled";
+
+        private SessionCommand2 mCommand;
+        private int mIconResId;
+        private String mDisplayName;
+        private Bundle mExtras;
+        private boolean mEnabled;
+
+        private CommandButton(@Nullable SessionCommand2 command, int iconResId,
+                @Nullable String displayName, Bundle extras, boolean enabled) {
+            mCommand = command;
+            mIconResId = iconResId;
+            mDisplayName = displayName;
+            mExtras = extras;
+            mEnabled = enabled;
+        }
+
+        /**
+         * Get command associated with this button. Can be {@code null} if the button isn't enabled
+         * and only providing placeholder.
+         *
+         * @return command or {@code null}
+         */
+        public @Nullable SessionCommand2 getCommand() {
+            return mCommand;
+        }
+
+        /**
+         * Resource id of the button in this package. Can be {@code 0} if the command is predefined
+         * and custom icon isn't needed.
+         *
+         * @return resource id of the icon. Can be {@code 0}.
+         */
+        public int getIconResId() {
+            return mIconResId;
+        }
+
+        /**
+         * Display name of the button. Can be {@code null} or empty if the command is predefined
+         * and custom name isn't needed.
+         *
+         * @return custom display name. Can be {@code null} or empty.
+         */
+        public @Nullable String getDisplayName() {
+            return mDisplayName;
+        }
+
+        /**
+         * Extra information of the button. It's private information between session and controller.
+         *
+         * @return
+         */
+        public @Nullable Bundle getExtras() {
+            return mExtras;
+        }
+
+        /**
+         * Return whether it's enabled.
+         *
+         * @return {@code true} if enabled. {@code false} otherwise.
+         */
+        public boolean isEnabled() {
+            return mEnabled;
+        }
+
+        /**
+         * @hide
+         * @return Bundle
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public @NonNull Bundle toBundle() {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
+            bundle.putInt(KEY_ICON_RES_ID, mIconResId);
+            bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
+            bundle.putBundle(KEY_EXTRAS, mExtras);
+            bundle.putBoolean(KEY_ENABLED, mEnabled);
+            return bundle;
+        }
+
+        /**
+         * @hide
+         * @return CommandButton
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public static @Nullable CommandButton fromBundle(Bundle bundle) {
+            if (bundle == null) {
+                return null;
+            }
+            CommandButton.Builder builder = new CommandButton.Builder();
+            builder.setCommand(SessionCommand2.fromBundle(bundle.getBundle(KEY_COMMAND)));
+            builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
+            builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
+            builder.setExtras(bundle.getBundle(KEY_EXTRAS));
+            builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
+            try {
+                return builder.build();
+            } catch (IllegalStateException e) {
+                // Malformed or version mismatch. Return null for now.
+                return null;
+            }
+        }
+
+        /**
+         * Builder for {@link CommandButton}.
+         */
+        public static final class Builder {
+            private SessionCommand2 mCommand;
+            private int mIconResId;
+            private String mDisplayName;
+            private Bundle mExtras;
+            private boolean mEnabled;
+
+            /**
+             * Sets the {@link SessionCommand2} that would be sent to the session when the button
+             * is clicked.
+             *
+             * @param command session command
+             */
+            public @NonNull Builder setCommand(@Nullable SessionCommand2 command) {
+                mCommand = command;
+                return this;
+            }
+
+            /**
+             * Sets the bitmap-type (e.g. PNG) icon resource id of the button.
+             * <p>
+             * None bitmap type (e.g. VectorDrawabale) may cause unexpected behavior when it's sent
+             * to {@link MediaController2} app, so please avoid using it especially for the older
+             * platform (API < 21).
+             *
+             * @param resId resource id of the button
+             */
+            public @NonNull Builder setIconResId(int resId) {
+                mIconResId = resId;
+                return this;
+            }
+
+            /**
+             * Sets the display name of the button.
+             *
+             * @param displayName display name of the button
+             */
+            public @NonNull Builder setDisplayName(@Nullable String displayName) {
+                mDisplayName = displayName;
+                return this;
+            }
+
+            /**
+             * Sets whether the button is enabled. Can be {@code false} to indicate that the button
+             * should be shown but isn't clickable.
+             *
+             * @param enabled {@code true} if the button is enabled and ready.
+             *          {@code false} otherwise.
+             */
+            public @NonNull Builder setEnabled(boolean enabled) {
+                mEnabled = enabled;
+                return this;
+            }
+
+            /**
+             * Sets the extras of the button.
+             *
+             * @param extras extras information of the button
+             */
+            public @NonNull Builder setExtras(@Nullable Bundle extras) {
+                mExtras = extras;
+                return this;
+            }
+
+            /**
+             * Builds the {@link CommandButton}.
+             *
+             * @return a new {@link CommandButton}
+             */
+            public @NonNull CommandButton build() {
+                return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
+            }
+        }
+    }
+
+    abstract static class SupportLibraryImpl extends MediaInterface2.SessionPlayer
+            implements AutoCloseable {
+        abstract void updatePlayer(@NonNull MediaPlayerBase player,
+                @Nullable MediaPlaylistAgent playlistAgent,
+                @Nullable VolumeProviderCompat volumeProvider);
+        abstract @NonNull MediaPlayerBase getPlayer();
+        abstract @NonNull MediaPlaylistAgent getPlaylistAgent();
+        abstract @Nullable VolumeProviderCompat getVolumeProvider();
+        abstract @NonNull SessionToken2 getToken();
+        abstract @NonNull List<ControllerInfo> getConnectedControllers();
+
+        abstract void setAudioFocusRequest(@Nullable AudioFocusRequest afr);
+        abstract void setCustomLayout(@NonNull ControllerInfo controller,
+                @NonNull List<CommandButton> layout);
+        abstract void setAllowedCommands(@NonNull ControllerInfo controller,
+                @NonNull SessionCommandGroup2 commands);
+        abstract void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args);
+        abstract void sendCustomCommand(@NonNull ControllerInfo controller,
+                @NonNull SessionCommand2 command, @Nullable Bundle args,
+                @Nullable ResultReceiver receiver);
+        abstract void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+                @Nullable List<Bundle> routes);
+
+        // Internally used methods
+        abstract void setInstance(MediaSession2 session);
+        abstract MediaSession2 getInstance();
+        abstract Context getContext();
+        abstract Executor getCallbackExecutor();
+        abstract SessionCallback getCallback();
+        abstract boolean isClosed();
+        abstract PlaybackStateCompat getPlaybackStateCompat();
+        abstract PlaybackInfo getPlaybackInfo();
+    }
+
+    static final String TAG = "MediaSession2";
+
+    private final SupportLibraryImpl mImpl;
+
+    MediaSession2(SupportLibraryImpl impl) {
+        mImpl = impl;
+        mImpl.setInstance(this);
+    }
+
+    /**
+     * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session
+     * to dispatch incoming event to.
+     * <p>
+     * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
+     * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
+     * <p>
+     * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
+     * agent.
+     *
+     * @param player a {@link MediaPlayerBase} that handles actual media playback in your app
+     * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
+     * @param volumeProvider a {@link VolumeProviderCompat}. If {@code null}, system will adjust the
+     *                       appropriate stream volume for this session's player.
+     */
+    public void updatePlayer(@NonNull MediaPlayerBase player,
+            @Nullable MediaPlaylistAgent playlistAgent,
+            @Nullable VolumeProviderCompat volumeProvider) {
+        mImpl.updatePlayer(player, playlistAgent, volumeProvider);
+    }
+
+    @Override
+    public void close() {
+        try {
+            mImpl.close();
+        } catch (Exception e) {
+            // Should not be here.
+        }
+    }
+
+    /**
+     * @return player
+     */
+    public @NonNull MediaPlayerBase getPlayer() {
+        return mImpl.getPlayer();
+    }
+
+    /**
+     * @return playlist agent
+     */
+    public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+        return mImpl.getPlaylistAgent();
+    }
+
+    /**
+     * @return volume provider
+     */
+    public @Nullable VolumeProviderCompat getVolumeProvider() {
+        return mImpl.getVolumeProvider();
+    }
+
+    /**
+     * Returns the {@link SessionToken2} for creating {@link MediaController2}.
+     */
+    public @NonNull SessionToken2 getToken() {
+        return mImpl.getToken();
+    }
+
+    @NonNull Context getContext() {
+        return mImpl.getContext();
+    }
+
+    @NonNull Executor getCallbackExecutor() {
+        return mImpl.getCallbackExecutor();
+    }
+
+    @NonNull SessionCallback getCallback() {
+        return mImpl.getCallback();
+    }
+
+    /**
+     * Returns the list of connected controller.
+     *
+     * @return list of {@link ControllerInfo}
+     */
+    public @NonNull List<ControllerInfo> getConnectedControllers() {
+        return mImpl.getConnectedControllers();
+    }
+
+    /**
+     * Set the {@link AudioFocusRequest} to obtain the audio focus
+     *
+     * @param afr the full request parameters
+     */
+    public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+        mImpl.setAudioFocusRequest(afr);
+    }
+
+    /**
+     * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
+     * <p>
+     * It's up to controller's decision how to represent the layout in its own UI.
+     * Here's the same way
+     * (layout[i] means a CommandButton at index i in the given list)
+     * For 5 icons row
+     *      layout[3] layout[1] layout[0] layout[2] layout[4]
+     * For 3 icons row
+     *      layout[1] layout[0] layout[2]
+     * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
+     *      expanded row:   layout[5] layout[6] layout[7] layout[8] layout[9]
+     *      main row:       layout[3] layout[1] layout[0] layout[2] layout[4]
+     * <p>
+     * This API can be called in the
+     * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
+     *
+     * @param controller controller to specify layout.
+     * @param layout ordered list of layout.
+     */
+    public void setCustomLayout(@NonNull ControllerInfo controller,
+            @NonNull List<CommandButton> layout) {
+        mImpl.setCustomLayout(controller, layout);
+    }
+
+    /**
+     * Set the new allowed command group for the controller
+     *
+     * @param controller controller to change allowed commands
+     * @param commands new allowed commands
+     */
+    public void setAllowedCommands(@NonNull ControllerInfo controller,
+            @NonNull SessionCommandGroup2 commands) {
+        mImpl.setAllowedCommands(controller, commands);
+    }
+
+    /**
+     * Send custom command to all connected controllers.
+     *
+     * @param command a command
+     * @param args optional argument
+     */
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+        mImpl.sendCustomCommand(command, args);
+    }
+
+    /**
+     * Send custom command to a specific controller.
+     *
+     * @param command a command
+     * @param args optional argument
+     * @param receiver result receiver for the session
+     */
+    public void sendCustomCommand(@NonNull ControllerInfo controller,
+            @NonNull SessionCommand2 command, @Nullable Bundle args,
+            @Nullable ResultReceiver receiver) {
+        mImpl.sendCustomCommand(controller, command, args, receiver);
+    }
+
+    /**
+     * Play playback.
+     * <p>
+     * This calls {@link MediaPlayerBase#play()}.
+     */
+    @Override
+    public void play() {
+        mImpl.play();
+    }
+
+    /**
+     * Pause playback.
+     * <p>
+     * This calls {@link MediaPlayerBase#pause()}.
+     */
+    @Override
+    public void pause() {
+        mImpl.pause();
+    }
+
+    /**
+     * Stop playback, and reset the player to the initial state.
+     * <p>
+     * This calls {@link MediaPlayerBase#reset()}.
+     */
+    @Override
+    public void reset() {
+        mImpl.reset();
+    }
+
+    /**
+     * Request that the player prepare its playback. In other words, other sessions can continue
+     * to play during the preparation of this session. This method can be used to speed up the
+     * start of the playback. Once the preparation is done, the session will change its playback
+     * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
+     * to start playback.
+     * <p>
+     * This calls {@link MediaPlayerBase#reset()}.
+     */
+    @Override
+    public void prepare() {
+        mImpl.prepare();
+    }
+
+    /**
+     * Move to a new location in the media stream.
+     *
+     * @param pos Position to move to, in milliseconds.
+     */
+    @Override
+    public void seekTo(long pos) {
+        mImpl.seekTo(pos);
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void skipForward() {
+        mImpl.skipForward();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    public void skipBackward() {
+        mImpl.skipBackward();
+    }
+
+    /**
+     * Notify errors to the connected controllers
+     *
+     * @param errorCode error code
+     * @param extras extras
+     */
+    @Override
+    public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+        mImpl.notifyError(errorCode, extras);
+    }
+
+    /**
+     * Notify routes information to a connected controller
+     *
+     * @param controller controller information
+     * @param routes The routes information. Each bundle should be from
+     *              MediaRouteDescritor.asBundle().
+     */
+    public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+            @Nullable List<Bundle> routes) {
+        mImpl.notifyRoutesInfoChanged(controller, routes);
+    }
+
+    /**
+     * Gets the current player state.
+     *
+     * @return the current player state
+     */
+    @Override
+    public @PlayerState int getPlayerState() {
+        return mImpl.getPlayerState();
+    }
+
+    /**
+     * Gets the current position.
+     *
+     * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if
+     *         unknown.
+     */
+    @Override
+    public long getCurrentPosition() {
+        return mImpl.getCurrentPosition();
+    }
+
+    @Override
+    public long getDuration() {
+        return mImpl.getDuration();
+    }
+
+    /**
+     * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
+     *
+     * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
+     */
+    @Override
+    public long getBufferedPosition() {
+        return mImpl.getBufferedPosition();
+    }
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     *
+     * @return the buffering state.
+     */
+    @Override
+    public @BuffState int getBufferingState() {
+        return mImpl.getBufferingState();
+    }
+
+    /**
+     * Get the playback speed.
+     *
+     * @return speed
+     */
+    @Override
+    public float getPlaybackSpeed() {
+        return mImpl.getPlaybackSpeed();
+    }
+
+    /**
+     * Set the playback speed.
+     */
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        mImpl.setPlaybackSpeed(speed);
+    }
+
+    /**
+     * Sets the data source missing helper. Helper will be used to provide default implementation of
+     * {@link MediaPlaylistAgent} when it isn't set by developer.
+     * <p>
+     * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
+     * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
+     * when
+     * <ul>
+     *      <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
+     *          have {@link DataSourceDesc}</li>
+     *      <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
+     *          by {@link SessionCallback#onCommandRequest(
+     *          MediaSession2, ControllerInfo, SessionCommand2)}.
+     *          In that case, an item would be added automatically without the data source.</li>
+     * </ul>
+     * <p>
+     * If it's not set, playback wouldn't happen for the item without data source descriptor.
+     * <p>
+     * The helper will be run on the executor that was specified by
+     * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
+     *
+     * @param helper a data source missing helper.
+     * @throws IllegalStateException when the helper is set when the playlist agent is set
+     * @see #setPlaylist(List, MediaMetadata2)
+     * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+     */
+    @Override
+    public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
+        mImpl.setOnDataSourceMissingHelper(helper);
+    }
+
+    /**
+     * Clears the data source missing helper.
+     *
+     * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
+     */
+    @Override
+    public void clearOnDataSourceMissingHelper() {
+        mImpl.clearOnDataSourceMissingHelper();
+    }
+
+    /**
+     * Returns the playlist from the {@link MediaPlaylistAgent}.
+     * <p>
+     * This list may differ with the list that was specified with
+     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+     * implementation. Use media items returned here for other playlist agent APIs such as
+     * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+     *
+     * @return playlist
+     * @see MediaPlaylistAgent#getPlaylist()
+     * @see SessionCallback#onPlaylistChanged(
+     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+     */
+    @Override
+    public List<MediaItem2> getPlaylist() {
+        return mImpl.getPlaylist();
+    }
+
+    /**
+     * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
+     * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
+     * items.
+     * <p>
+     * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
+     * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
+     * List, MediaMetadata2)} to know the operation finishes.
+     * <p>
+     * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
+     * {@link MediaPlaylistAgent} has responsibility to dynamically query {link DataSourceDesc}
+     * when such media item is ready for preparation or play. Default implementation needs
+     * {@link OnDataSourceMissingHelper} for such case.
+     *
+     * @param list A list of {@link MediaItem2} objects to set as a play list.
+     * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
+     * items.
+     * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
+     * @see SessionCallback#onPlaylistChanged(
+     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+     * @see #setOnDataSourceMissingHelper
+     */
+    @Override
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        mImpl.setPlaylist(list, metadata);
+    }
+
+    /**
+     * Skips to the item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
+     * on the playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @param item The item in the playlist you want to play
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     */
+    @Override
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        mImpl.skipToPlaylistItem(item);
+    }
+
+    /**
+     * Skips to the previous item.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
+     * playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     **/
+    @Override
+    public void skipToPreviousItem() {
+        mImpl.skipToPreviousItem();
+    }
+
+    /**
+     * Skips to the next item.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
+     * playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     */
+    @Override
+    public void skipToNextItem() {
+        mImpl.skipToNextItem();
+    }
+
+    /**
+     * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
+     *
+     * @return the playlist metadata
+     */
+    @Override
+    public MediaMetadata2 getPlaylistMetadata() {
+        return mImpl.getPlaylistMetadata();
+    }
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+     * the playlist.
+     * <p>
+     * This will not change the currently playing media item.
+     * If index is less than or equal to the current index of the play list,
+     * the current index of the play list will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    @Override
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        mImpl.addPlaylistItem(index, item);
+    }
+
+    /**
+     * Removes the media item in the playlist.
+     * <p>
+     * If the item is the currently playing item of the playlist, current playback
+     * will be stopped and playback moves to next source in the list.
+     *
+     * @param item the media item you want to add
+     */
+    @Override
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        mImpl.removePlaylistItem(item);
+    }
+
+    /**
+     * Replaces the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
+     *
+     * @param index the index of the item to replace
+     * @param item the new item
+     */
+    @Override
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        mImpl.replacePlaylistItem(index, item);
+    }
+
+    /**
+     * Return currently playing media item.
+     *
+     * @return currently playing media item
+     */
+    @Override
+    public MediaItem2 getCurrentMediaItem() {
+        return mImpl.getCurrentMediaItem();
+    }
+
+    /**
+     * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
+     *
+     * @param metadata metadata of the playlist
+     */
+    @Override
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        mImpl.updatePlaylistMetadata(metadata);
+    }
+
+    /**
+     * Gets the repeat mode from the {@link MediaPlaylistAgent}.
+     *
+     * @return repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    @Override
+    public @RepeatMode int getRepeatMode() {
+        return mImpl.getRepeatMode();
+    }
+
+    /**
+     * Sets the repeat mode to the {@link MediaPlaylistAgent}.
+     *
+     * @param repeatMode repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    @Override
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        mImpl.setRepeatMode(repeatMode);
+    }
+
+    /**
+     * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
+     *
+     * @return The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    @Override
+    public @ShuffleMode int getShuffleMode() {
+        return mImpl.getShuffleMode();
+    }
+
+    /**
+     * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
+     *
+     * @param shuffleMode The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    @Override
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        mImpl.setShuffleMode(shuffleMode);
+    }
+}
diff --git a/androidx/media/MediaSession2ImplBase.java b/androidx/media/MediaSession2ImplBase.java
new file mode 100644
index 0000000..e474b45
--- /dev/null
+++ b/androidx/media/MediaSession2ImplBase.java
@@ -0,0 +1,1220 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaSession2.ControllerInfo;
+import static androidx.media.MediaSession2.ErrorCode;
+import static androidx.media.MediaSession2.OnDataSourceMissingHelper;
+import static androidx.media.MediaSession2.SessionCallback;
+import static androidx.media.SessionToken2.TYPE_LIBRARY_SERVICE;
+import static androidx.media.SessionToken2.TYPE_SESSION;
+import static androidx.media.SessionToken2.TYPE_SESSION_SERVICE;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaSession2ImplBase extends MediaSession2.SupportLibraryImpl {
+    static final String TAG = "MS2ImplBase";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final Object mLock = new Object();
+
+    private final Context mContext;
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+    private final MediaSessionCompat mSessionCompat;
+    private final MediaSession2StubImplBase mSession2Stub;
+    private final String mId;
+    private final Executor mCallbackExecutor;
+    private final SessionCallback mCallback;
+    private final SessionToken2 mSessionToken;
+    private final AudioManager mAudioManager;
+    private final MediaPlayerBase.PlayerEventCallback mPlayerEventCallback;
+    private final MediaPlaylistAgent.PlaylistEventCallback mPlaylistEventCallback;
+
+    private WeakReference<MediaSession2> mInstance;
+
+    @GuardedBy("mLock")
+    private MediaPlayerBase mPlayer;
+    @GuardedBy("mLock")
+    private MediaPlaylistAgent mPlaylistAgent;
+    @GuardedBy("mLock")
+    private SessionPlaylistAgentImplBase mSessionPlaylistAgent;
+    @GuardedBy("mLock")
+    private VolumeProviderCompat mVolumeProvider;
+    @GuardedBy("mLock")
+    private OnDataSourceMissingHelper mDsmHelper;
+    @GuardedBy("mLock")
+    private PlaybackStateCompat mPlaybackStateCompat;
+    @GuardedBy("mLock")
+    private PlaybackInfo mPlaybackInfo;
+
+    MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id,
+            MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+            VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
+            Executor callbackExecutor, SessionCallback callback) {
+        mContext = context;
+        mHandlerThread = new HandlerThread("MediaController2_Thread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        mSessionCompat = sessionCompat;
+        mSession2Stub = new MediaSession2StubImplBase(this);
+        mSessionCompat.setCallback(mSession2Stub, mHandler);
+        mSessionCompat.setSessionActivity(sessionActivity);
+
+        mId = id;
+        mCallback = callback;
+        mCallbackExecutor = callbackExecutor;
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+        // TODO: Set callback values properly
+        mPlayerEventCallback = new MyPlayerEventCallback(this);
+        mPlaylistEventCallback = new MyPlaylistEventCallback(this);
+
+        // Infer type from the id and package name.
+        String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id);
+        String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id);
+        if (sessionService != null && libraryService != null) {
+            throw new IllegalArgumentException("Ambiguous session type. Multiple"
+                    + " session services define the same id=" + id);
+        } else if (libraryService != null) {
+            mSessionToken = new SessionToken2(Process.myUid(), TYPE_LIBRARY_SERVICE,
+                    context.getPackageName(), libraryService, id, mSessionCompat.getSessionToken());
+        } else if (sessionService != null) {
+            mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION_SERVICE,
+                    context.getPackageName(), sessionService, id, mSessionCompat.getSessionToken());
+        } else {
+            mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION,
+                    context.getPackageName(), null, id, mSessionCompat.getSessionToken());
+        }
+        updatePlayer(player, playlistAgent, volumeProvider);
+    }
+
+    @Override
+    public void updatePlayer(@NonNull MediaPlayerBase player,
+            @Nullable MediaPlaylistAgent playlistAgent,
+            @Nullable VolumeProviderCompat volumeProvider) {
+        if (player == null) {
+            throw new IllegalArgumentException("player shouldn't be null");
+        }
+        final MediaPlayerBase oldPlayer;
+        final MediaPlaylistAgent oldAgent;
+        final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
+        synchronized (mLock) {
+            oldPlayer = mPlayer;
+            oldAgent = mPlaylistAgent;
+            mPlayer = player;
+            if (playlistAgent == null) {
+                mSessionPlaylistAgent = new SessionPlaylistAgentImplBase(this, mPlayer);
+                if (mDsmHelper != null) {
+                    mSessionPlaylistAgent.setOnDataSourceMissingHelper(mDsmHelper);
+                }
+                playlistAgent = mSessionPlaylistAgent;
+            }
+            mPlaylistAgent = playlistAgent;
+            mVolumeProvider = volumeProvider;
+            mPlaybackInfo = info;
+        }
+        if (player != oldPlayer) {
+            player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback);
+            if (oldPlayer != null) {
+                // Warning: Poorly implement player may ignore this
+                oldPlayer.unregisterPlayerEventCallback(mPlayerEventCallback);
+            }
+        }
+        if (playlistAgent != oldAgent) {
+            playlistAgent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback);
+            if (oldAgent != null) {
+                // Warning: Poorly implement player may ignore this
+                oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback);
+            }
+        }
+
+        if (oldPlayer != null) {
+            mSession2Stub.notifyPlaybackInfoChanged(info);
+            notifyPlayerUpdatedNotLocked(oldPlayer);
+        }
+        // TODO(jaewan): Repeat the same thing for the playlist agent.
+    }
+
+    private PlaybackInfo createPlaybackInfo(VolumeProviderCompat volumeProvider,
+            AudioAttributesCompat attrs) {
+        PlaybackInfo info;
+        if (volumeProvider == null) {
+            int stream;
+            if (attrs == null) {
+                stream = AudioManager.STREAM_MUSIC;
+            } else {
+                stream = attrs.getVolumeControlStream();
+                if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+                    // It may happen if the AudioAttributes doesn't have usage.
+                    // Change it to the STREAM_MUSIC because it's not supported by audio manager
+                    // for querying volume level.
+                    stream = AudioManager.STREAM_MUSIC;
+                }
+            }
+
+            int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+            if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+                controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+            }
+            info = PlaybackInfo.createPlaybackInfo(
+                    PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                    attrs,
+                    controlType,
+                    mAudioManager.getStreamMaxVolume(stream),
+                    mAudioManager.getStreamVolume(stream));
+        } else {
+            info = PlaybackInfo.createPlaybackInfo(
+                    PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    attrs,
+                    volumeProvider.getVolumeControl(),
+                    volumeProvider.getMaxVolume(),
+                    volumeProvider.getCurrentVolume());
+        }
+        return info;
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            if (mPlayer == null) {
+                return;
+            }
+            mPlayer.unregisterPlayerEventCallback(mPlayerEventCallback);
+            mPlayer = null;
+            mSessionCompat.release();
+            mHandler.removeCallbacksAndMessages(null);
+            if (mHandlerThread.isAlive()) {
+                mHandlerThread.quitSafely();
+            }
+        }
+    }
+
+    @Override
+    public @NonNull MediaPlayerBase getPlayer() {
+        synchronized (mLock) {
+            return mPlayer;
+        }
+    }
+
+    @Override
+    public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+        synchronized (mLock) {
+            return mPlaylistAgent;
+        }
+    }
+
+    @Override
+    public @Nullable VolumeProviderCompat getVolumeProvider() {
+        synchronized (mLock) {
+            return mVolumeProvider;
+        }
+    }
+
+    @Override
+    public @NonNull SessionToken2 getToken() {
+        return mSessionToken;
+    }
+
+    @Override
+    public @NonNull List<MediaSession2.ControllerInfo> getConnectedControllers() {
+        return mSession2Stub.getConnectedControllers();
+    }
+
+    @Override
+    public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+        // TODO(jaewan): implement this (b/72529899)
+        // mProvider.setAudioFocusRequest_impl(focusGain);
+    }
+
+    @Override
+    public void setCustomLayout(@NonNull ControllerInfo controller,
+            @NonNull List<MediaSession2.CommandButton> layout) {
+        if (controller == null) {
+            throw new IllegalArgumentException("controller shouldn't be null");
+        }
+        if (layout == null) {
+            throw new IllegalArgumentException("layout shouldn't be null");
+        }
+        mSession2Stub.notifyCustomLayout(controller, layout);
+    }
+
+    @Override
+    public void setAllowedCommands(@NonNull ControllerInfo controller,
+            @NonNull SessionCommandGroup2 commands) {
+        if (controller == null) {
+            throw new IllegalArgumentException("controller shouldn't be null");
+        }
+        if (commands == null) {
+            throw new IllegalArgumentException("commands shouldn't be null");
+        }
+        mSession2Stub.setAllowedCommands(controller, commands);
+    }
+
+    @Override
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        mSession2Stub.sendCustomCommand(command, args);
+    }
+
+    @Override
+    public void sendCustomCommand(@NonNull ControllerInfo controller,
+            @NonNull SessionCommand2 command, @Nullable Bundle args,
+            @Nullable ResultReceiver receiver) {
+        if (controller == null) {
+            throw new IllegalArgumentException("controller shouldn't be null");
+        }
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        mSession2Stub.sendCustomCommand(controller, command, args, receiver);
+    }
+
+    @Override
+    public void play() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            player.play();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void pause() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            player.pause();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void reset() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            player.reset();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void prepare() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            player.prepare();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            player.seekTo(pos);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void skipForward() {
+        // To match with KEYCODE_MEDIA_SKIP_FORWARD
+    }
+
+    @Override
+    public void skipBackward() {
+        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+    }
+
+    @Override
+    public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+        mSession2Stub.notifyError(errorCode, extras);
+    }
+
+    @Override
+    public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+            @Nullable List<Bundle> routes) {
+        mSession2Stub.notifyRoutesInfoChanged(controller, routes);
+    }
+
+    @Override
+    public @MediaPlayerBase.PlayerState int getPlayerState() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            return player.getPlayerState();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return MediaPlayerBase.PLAYER_STATE_ERROR;
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            return player.getCurrentPosition();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return MediaPlayerBase.UNKNOWN_TIME;
+    }
+
+    @Override
+    public long getDuration() {
+        // TODO: implement
+        return 0;
+    }
+
+    @Override
+    public long getBufferedPosition() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            return player.getBufferedPosition();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return MediaPlayerBase.UNKNOWN_TIME;
+    }
+
+    @Override
+    public @MediaPlayerBase.BuffState int getBufferingState() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            return player.getBufferingState();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return BUFFERING_STATE_UNKNOWN;
+    }
+
+    @Override
+    public float getPlaybackSpeed() {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            return player.getPlaybackSpeed();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return 1.0f;
+    }
+
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        if (player != null) {
+            player.setPlaybackSpeed(speed);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void setOnDataSourceMissingHelper(
+            @NonNull OnDataSourceMissingHelper helper) {
+        if (helper == null) {
+            throw new IllegalArgumentException("helper shouldn't be null");
+        }
+        synchronized (mLock) {
+            mDsmHelper = helper;
+            if (mSessionPlaylistAgent != null) {
+                mSessionPlaylistAgent.setOnDataSourceMissingHelper(helper);
+            }
+        }
+    }
+
+    @Override
+    public void clearOnDataSourceMissingHelper() {
+        synchronized (mLock) {
+            mDsmHelper = null;
+            if (mSessionPlaylistAgent != null) {
+                mSessionPlaylistAgent.clearOnDataSourceMissingHelper();
+            }
+        }
+    }
+
+    @Override
+    public List<MediaItem2> getPlaylist() {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            return agent.getPlaylist();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return null;
+    }
+
+    @Override
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        if (list == null) {
+            throw new IllegalArgumentException("list shouldn't be null");
+        }
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.setPlaylist(list, metadata);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.skipToPlaylistItem(item);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void skipToPreviousItem() {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.skipToPreviousItem();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void skipToNextItem() {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.skipToNextItem();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public MediaMetadata2 getPlaylistMetadata() {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            return agent.getPlaylistMetadata();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return null;
+    }
+
+    @Override
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        if (index < 0) {
+            throw new IllegalArgumentException("index shouldn't be negative");
+        }
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.addPlaylistItem(index, item);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.removePlaylistItem(item);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        if (index < 0) {
+            throw new IllegalArgumentException("index shouldn't be negative");
+        }
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.replacePlaylistItem(index, item);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public MediaItem2 getCurrentMediaItem() {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            return agent.getCurrentMediaItem();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return null;
+    }
+
+    @Override
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.updatePlaylistMetadata(metadata);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public @MediaPlaylistAgent.RepeatMode int getRepeatMode() {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            return agent.getRepeatMode();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return MediaPlaylistAgent.REPEAT_MODE_NONE;
+    }
+
+    @Override
+    public void setRepeatMode(@MediaPlaylistAgent.RepeatMode int repeatMode) {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.setRepeatMode(repeatMode);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    @Override
+    public @MediaPlaylistAgent.ShuffleMode int getShuffleMode() {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            return agent.getShuffleMode();
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+        return MediaPlaylistAgent.SHUFFLE_MODE_NONE;
+    }
+
+    @Override
+    public void setShuffleMode(int shuffleMode) {
+        MediaPlaylistAgent agent;
+        synchronized (mLock) {
+            agent = mPlaylistAgent;
+        }
+        if (agent != null) {
+            agent.setShuffleMode(shuffleMode);
+        } else if (DEBUG) {
+            Log.d(TAG, "API calls after the close()", new IllegalStateException());
+        }
+    }
+
+    ///////////////////////////////////////////////////
+    // package private and private methods
+    ///////////////////////////////////////////////////
+
+    @Override
+    void setInstance(MediaSession2 session) {
+        mInstance = new WeakReference<>(session);
+
+    }
+
+    @Override
+    MediaSession2 getInstance() {
+        return mInstance.get();
+    }
+
+    @Override
+    Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    Executor getCallbackExecutor() {
+        return mCallbackExecutor;
+    }
+
+    @Override
+    SessionCallback getCallback() {
+        return mCallback;
+    }
+
+    @Override
+    boolean isClosed() {
+        return !mHandlerThread.isAlive();
+    }
+
+    @Override
+    PlaybackStateCompat getPlaybackStateCompat() {
+        synchronized (mLock) {
+            int state = MediaUtils2.createPlaybackStateCompatState(getPlayerState(),
+                    getBufferingState());
+            // TODO: Consider following missing stuff
+            //       - setCustomAction(): Fill custom layout
+            //       - setErrorMessage(): Fill error message when notifyError() is called.
+            //       - setActiveQueueItemId(): Fill here with the current media item...
+            //       - setExtra(): No idea at this moment.
+            // TODO: generate actions from the allowed commands.
+            long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
+                    | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
+                    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+                    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+                    | PlaybackStateCompat.ACTION_FAST_FORWARD
+                    | PlaybackStateCompat.ACTION_SET_RATING
+                    | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE
+                    | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
+                    | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
+                    | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
+                    | PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE
+                    | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
+                    | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
+                    | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
+                    | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+                    | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
+                    | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
+            return new PlaybackStateCompat.Builder()
+                    .setState(state, getCurrentPosition(), getPlaybackSpeed())
+                    .setActions(allActions)
+                    .setBufferedPosition(getBufferedPosition())
+                    .build();
+        }
+    }
+
+    @Override
+    PlaybackInfo getPlaybackInfo() {
+        synchronized (mLock) {
+            return mPlaybackInfo;
+        }
+    }
+
+    MediaSession2StubImplBase getSession2Stub() {
+        return mSession2Stub;
+    }
+
+    private static String getServiceName(Context context, String serviceAction, String id) {
+        PackageManager manager = context.getPackageManager();
+        Intent serviceIntent = new Intent(serviceAction);
+        serviceIntent.setPackage(context.getPackageName());
+        List<ResolveInfo> services = manager.queryIntentServices(serviceIntent,
+                PackageManager.GET_META_DATA);
+        String serviceName = null;
+        if (services != null) {
+            for (int i = 0; i < services.size(); i++) {
+                String serviceId = SessionToken2.getSessionId(services.get(i));
+                if (serviceId != null && TextUtils.equals(id, serviceId)) {
+                    if (services.get(i).serviceInfo == null) {
+                        continue;
+                    }
+                    if (serviceName != null) {
+                        throw new IllegalArgumentException("Ambiguous session type. Multiple"
+                                + " session services define the same id=" + id);
+                    }
+                    serviceName = services.get(i).serviceInfo.name;
+                }
+            }
+        }
+        return serviceName;
+    }
+
+    private void notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer) {
+        MediaPlayerBase player;
+        synchronized (mLock) {
+            player = mPlayer;
+        }
+        // TODO(jaewan): (Can be post-P) Find better way for player.getPlayerState() //
+        //               In theory, Session.getXXX() may not be the same as Player.getXXX()
+        //               and we should notify information of the session.getXXX() instead of
+        //               player.getXXX()
+        // Notify to controllers as well.
+        final int state = player.getPlayerState();
+        if (state != oldPlayer.getPlayerState()) {
+            // TODO: implement
+            mSession2Stub.notifyPlayerStateChanged(state);
+        }
+
+        final long currentTimeMs = System.currentTimeMillis();
+        final long position = player.getCurrentPosition();
+        if (position != oldPlayer.getCurrentPosition()) {
+            // TODO: implement
+            //mSession2Stub.notifyPositionChangedNotLocked(currentTimeMs, position);
+        }
+
+        final float speed = player.getPlaybackSpeed();
+        if (speed != oldPlayer.getPlaybackSpeed()) {
+            // TODO: implement
+            //mSession2Stub.notifyPlaybackSpeedChangedNotLocked(speed);
+        }
+
+        final long bufferedPosition = player.getBufferedPosition();
+        if (bufferedPosition != oldPlayer.getBufferedPosition()) {
+            // TODO: implement
+            //mSession2Stub.notifyBufferedPositionChangedNotLocked(bufferedPosition);
+        }
+    }
+
+    private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+            List<MediaItem2> list, MediaMetadata2 metadata) {
+        synchronized (mLock) {
+            if (playlistAgent != mPlaylistAgent) {
+                // Ignore calls from the old agent.
+                return;
+            }
+        }
+        MediaSession2 session2 = mInstance.get();
+        if (session2 != null) {
+            mCallback.onPlaylistChanged(session2, playlistAgent, list, metadata);
+            mSession2Stub.notifyPlaylistChanged(list, metadata);
+        }
+    }
+
+    private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+            MediaMetadata2 metadata) {
+        synchronized (mLock) {
+            if (playlistAgent != mPlaylistAgent) {
+                // Ignore calls from the old agent.
+                return;
+            }
+        }
+        MediaSession2 session2 = mInstance.get();
+        if (session2 != null) {
+            mCallback.onPlaylistMetadataChanged(session2, playlistAgent, metadata);
+            mSession2Stub.notifyPlaylistMetadataChanged(metadata);
+        }
+    }
+
+    private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+            int repeatMode) {
+        synchronized (mLock) {
+            if (playlistAgent != mPlaylistAgent) {
+                // Ignore calls from the old agent.
+                return;
+            }
+        }
+        MediaSession2 session2 = mInstance.get();
+        if (session2 != null) {
+            mCallback.onRepeatModeChanged(session2, playlistAgent, repeatMode);
+            mSession2Stub.notifyRepeatModeChanged(repeatMode);
+        }
+    }
+
+    private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+            int shuffleMode) {
+        synchronized (mLock) {
+            if (playlistAgent != mPlaylistAgent) {
+                // Ignore calls from the old agent.
+                return;
+            }
+        }
+        MediaSession2 session2 = mInstance.get();
+        if (session2 != null) {
+            mCallback.onShuffleModeChanged(session2, playlistAgent, shuffleMode);
+            mSession2Stub.notifyShuffleModeChanged(shuffleMode);
+        }
+    }
+
+    ///////////////////////////////////////////////////
+    // Inner classes
+    ///////////////////////////////////////////////////
+
+    private static class MyPlayerEventCallback extends PlayerEventCallback {
+        private final WeakReference<MediaSession2ImplBase> mSession;
+
+        private MyPlayerEventCallback(MediaSession2ImplBase session) {
+            mSession = new WeakReference<>(session);
+        }
+
+        @Override
+        public void onCurrentDataSourceChanged(final MediaPlayerBase mpb,
+                final DataSourceDesc dsd) {
+            final MediaSession2ImplBase session = getSession();
+            // TODO: handle properly when dsd == null
+            if (session == null || dsd == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+                    if (item == null) {
+                        return;
+                    }
+                    session.getCallback().onCurrentMediaItemChanged(session.getInstance(), mpb,
+                            item);
+                    if (item.equals(session.getCurrentMediaItem())) {
+                        session.getSession2Stub().notifyCurrentMediaItemChanged(item);
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onMediaPrepared(final MediaPlayerBase mpb, final DataSourceDesc dsd) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null || dsd == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+                    if (item == null) {
+                        return;
+                    }
+                    session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
+                    // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936)
+                }
+            });
+        }
+
+        @Override
+        public void onPlayerStateChanged(final MediaPlayerBase mpb, final int state) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    session.getCallback().onPlayerStateChanged(session.getInstance(), mpb, state);
+                    session.getSession2Stub().notifyPlayerStateChanged(state);
+                }
+            });
+        }
+
+        @Override
+        public void onBufferingStateChanged(final MediaPlayerBase mpb, final DataSourceDesc dsd,
+                final int state) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null || dsd == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+                    if (item == null) {
+                        return;
+                    }
+                    session.getCallback().onBufferingStateChanged(
+                            session.getInstance(), mpb, item, state);
+                    session.getSession2Stub().notifyBufferingStateChanged(item, state);
+                }
+            });
+        }
+
+        @Override
+        public void onPlaybackSpeedChanged(final MediaPlayerBase mpb, final float speed) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed);
+                    session.getSession2Stub().notifyPlaybackSpeedChanged(speed);
+                }
+            });
+        }
+
+        private MediaSession2ImplBase getSession() {
+            final MediaSession2ImplBase session = mSession.get();
+            if (session == null && DEBUG) {
+                Log.d(TAG, "Session is closed", new IllegalStateException());
+            }
+            return session;
+        }
+
+        private MediaItem2 getMediaItem(MediaSession2ImplBase session, DataSourceDesc dsd) {
+            MediaPlaylistAgent agent = session.getPlaylistAgent();
+            if (agent == null) {
+                if (DEBUG) {
+                    Log.d(TAG, "Session is closed", new IllegalStateException());
+                }
+                return null;
+            }
+            MediaItem2 item = agent.getMediaItem(dsd);
+            if (item == null) {
+                if (DEBUG) {
+                    Log.d(TAG, "Could not find matching item for dsd=" + dsd,
+                            new NoSuchElementException());
+                }
+            }
+            return item;
+        }
+    }
+
+    private static class MyPlaylistEventCallback extends PlaylistEventCallback {
+        private final WeakReference<MediaSession2ImplBase> mSession;
+
+        private MyPlaylistEventCallback(MediaSession2ImplBase session) {
+            mSession = new WeakReference<>(session);
+        }
+
+        @Override
+        public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list,
+                MediaMetadata2 metadata) {
+            final MediaSession2ImplBase session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            session.notifyPlaylistChangedOnExecutor(playlistAgent, list, metadata);
+        }
+
+        @Override
+        public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent,
+                MediaMetadata2 metadata) {
+            final MediaSession2ImplBase session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            session.notifyPlaylistMetadataChangedOnExecutor(playlistAgent, metadata);
+        }
+
+        @Override
+        public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) {
+            final MediaSession2ImplBase session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            session.notifyRepeatModeChangedOnExecutor(playlistAgent, repeatMode);
+        }
+
+        @Override
+        public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) {
+            final MediaSession2ImplBase session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            session.notifyShuffleModeChangedOnExecutor(playlistAgent, shuffleMode);
+        }
+    }
+
+    abstract static class BuilderBase
+            <T extends MediaSession2, C extends SessionCallback> {
+        final Context mContext;
+        MediaPlayerBase mPlayer;
+        String mId;
+        Executor mCallbackExecutor;
+        C mCallback;
+        MediaPlaylistAgent mPlaylistAgent;
+        VolumeProviderCompat mVolumeProvider;
+        PendingIntent mSessionActivity;
+
+        BuilderBase(Context context) {
+            if (context == null) {
+                throw new IllegalArgumentException("context shouldn't be null");
+            }
+            mContext = context;
+            // Ensure MediaSessionCompat non-null or empty
+            mId = TAG;
+        }
+
+        void setPlayer(@NonNull MediaPlayerBase player) {
+            if (player == null) {
+                throw new IllegalArgumentException("player shouldn't be null");
+            }
+            mPlayer = player;
+        }
+
+        void setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+            if (playlistAgent == null) {
+                throw new IllegalArgumentException("playlistAgent shouldn't be null");
+            }
+            mPlaylistAgent = playlistAgent;
+        }
+
+        void setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+            mVolumeProvider = volumeProvider;
+        }
+
+        void setSessionActivity(@Nullable PendingIntent pi) {
+            mSessionActivity = pi;
+        }
+
+        void setId(@NonNull String id) {
+            if (id == null) {
+                throw new IllegalArgumentException("id shouldn't be null");
+            }
+            mId = id;
+        }
+
+        void setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
+            if (executor == null) {
+                throw new IllegalArgumentException("executor shouldn't be null");
+            }
+            if (callback == null) {
+                throw new IllegalArgumentException("callback shouldn't be null");
+            }
+            mCallbackExecutor = executor;
+            mCallback = callback;
+        }
+
+        abstract @NonNull T build();
+    }
+
+    static final class Builder extends
+            BuilderBase<MediaSession2, MediaSession2.SessionCallback> {
+        Builder(Context context) {
+            super(context);
+        }
+
+        @Override
+        public @NonNull MediaSession2 build() {
+            if (mCallbackExecutor == null) {
+                mCallbackExecutor = new MainHandlerExecutor(mContext);
+            }
+            if (mCallback == null) {
+                mCallback = new SessionCallback() {};
+            }
+            return new MediaSession2(new MediaSession2ImplBase(mContext,
+                    new MediaSessionCompat(mContext, mId), mId, mPlayer, mPlaylistAgent,
+                    mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback));
+        }
+    }
+
+    static class MainHandlerExecutor implements Executor {
+        private final Handler mHandler;
+
+        MainHandlerExecutor(Context context) {
+            mHandler = new Handler(context.getMainLooper());
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            if (!mHandler.post(command)) {
+                throw new RejectedExecutionException(mHandler + " is shutting down");
+            }
+        }
+    }
+}
diff --git a/androidx/media/MediaSession2StubImplBase.java b/androidx/media/MediaSession2StubImplBase.java
new file mode 100644
index 0000000..48e641e
--- /dev/null
+++ b/androidx/media/MediaSession2StubImplBase.java
@@ -0,0 +1,929 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_RATING;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.ARGUMENT_URI;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaSession2StubImplBase extends MediaSessionCompat.Callback {
+
+    private static final String TAG = "MS2StubImplBase";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
+            new SparseArray<>();
+
+    static {
+        SessionCommandGroup2 group = new SessionCommandGroup2();
+        group.addAllPlaybackCommands();
+        group.addAllPlaylistCommands();
+        group.addAllVolumeCommands();
+        Set<SessionCommand2> commands = group.getCommands();
+        for (SessionCommand2 command : commands) {
+            sCommandsForOnCommandRequest.append(command.getCommandCode(), command);
+        }
+    }
+
+    private final Object mLock = new Object();
+
+    final MediaSession2.SupportLibraryImpl mSession;
+    final Context mContext;
+
+    @GuardedBy("mLock")
+    private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
+    @GuardedBy("mLock")
+    private final Set<IBinder> mConnectingControllers = new HashSet<>();
+    @GuardedBy("mLock")
+    private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
+            new ArrayMap<>();
+
+    MediaSession2StubImplBase(MediaSession2.SupportLibraryImpl session) {
+        mSession = session;
+        mContext = mSession.getContext();
+    }
+
+    @Override
+    public void onPrepare() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.prepare();
+            }
+        });
+    }
+
+    @Override
+    public void onPlay() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.play();
+            }
+        });
+    }
+
+    @Override
+    public void onPause() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.pause();
+            }
+        });
+    }
+
+    @Override
+    public void onStop() {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.reset();
+            }
+        });
+    }
+
+    @Override
+    public void onSeekTo(final long pos) {
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.seekTo(pos);
+            }
+        });
+    }
+
+    @Override
+    public void onCommand(String command, final Bundle extras, final ResultReceiver cb) {
+        switch (command) {
+            case CONTROLLER_COMMAND_CONNECT:
+                connect(extras, cb);
+                break;
+            case CONTROLLER_COMMAND_DISCONNECT:
+                disconnect(extras);
+                break;
+            case CONTROLLER_COMMAND_BY_COMMAND_CODE: {
+                final int commandCode = extras.getInt(ARGUMENT_COMMAND_CODE);
+                IMediaControllerCallback caller =
+                        (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+                if (caller == null) {
+                    return;
+                }
+
+                onCommand2(caller.asBinder(), commandCode, new Session2Runnable() {
+                    @Override
+                    public void run(ControllerInfo controller) {
+                        switch (commandCode) {
+                            case COMMAND_CODE_PLAYBACK_PLAY:
+                                mSession.play();
+                                break;
+                            case COMMAND_CODE_PLAYBACK_PAUSE:
+                                mSession.pause();
+                                break;
+                            case COMMAND_CODE_PLAYBACK_RESET:
+                                mSession.reset();
+                                break;
+                            case COMMAND_CODE_PLAYBACK_PREPARE:
+                                mSession.prepare();
+                                break;
+                            case COMMAND_CODE_PLAYBACK_SEEK_TO: {
+                                long seekPos = extras.getLong(ARGUMENT_SEEK_POSITION);
+                                mSession.seekTo(seekPos);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE: {
+                                int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
+                                mSession.setRepeatMode(repeatMode);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE: {
+                                int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
+                                mSession.setShuffleMode(shuffleMode);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_SET_LIST: {
+                                List<MediaItem2> list = MediaUtils2.fromMediaItem2ParcelableArray(
+                                        extras.getParcelableArray(ARGUMENT_PLAYLIST));
+                                MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+                                        extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+                                mSession.setPlaylist(list, metadata);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_SET_LIST_METADATA: {
+                                MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+                                        extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+                                mSession.updatePlaylistMetadata(metadata);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_ADD_ITEM: {
+                                int index = extras.getInt(ARGUMENT_PLAYLIST_INDEX);
+                                MediaItem2 item = MediaItem2.fromBundle(
+                                        extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                                mSession.addPlaylistItem(index, item);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_REMOVE_ITEM: {
+                                MediaItem2 item = MediaItem2.fromBundle(
+                                        extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                                mSession.removePlaylistItem(item);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_REPLACE_ITEM: {
+                                int index = extras.getInt(ARGUMENT_PLAYLIST_INDEX);
+                                MediaItem2 item = MediaItem2.fromBundle(
+                                        extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                                mSession.replacePlaylistItem(index, item);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM: {
+                                mSession.skipToNextItem();
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM: {
+                                mSession.skipToPreviousItem();
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM: {
+                                MediaItem2 item = MediaItem2.fromBundle(
+                                        extras.getBundle(ARGUMENT_MEDIA_ITEM));
+                                mSession.skipToPlaylistItem(item);
+                                break;
+                            }
+                            case COMMAND_CODE_VOLUME_SET_VOLUME: {
+                                int value = extras.getInt(ARGUMENT_VOLUME);
+                                int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
+                                VolumeProviderCompat vp = mSession.getVolumeProvider();
+                                if (vp == null) {
+                                    // TODO: Revisit
+                                } else {
+                                    vp.onSetVolumeTo(value);
+                                }
+                                break;
+                            }
+                            case COMMAND_CODE_VOLUME_ADJUST_VOLUME: {
+                                int direction = extras.getInt(ARGUMENT_VOLUME_DIRECTION);
+                                int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
+                                VolumeProviderCompat vp = mSession.getVolumeProvider();
+                                if (vp == null) {
+                                    // TODO: Revisit
+                                } else {
+                                    vp.onAdjustVolume(direction);
+                                }
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_REWIND: {
+                                mSession.getCallback().onRewind(
+                                        mSession.getInstance(), controller);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_FAST_FORWARD: {
+                                mSession.getCallback().onFastForward(
+                                        mSession.getInstance(), controller);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID: {
+                                String mediaId = extras.getString(ARGUMENT_MEDIA_ID);
+                                Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+                                mSession.getCallback().onPlayFromMediaId(
+                                        mSession.getInstance(), controller, mediaId, extra);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_PLAY_FROM_SEARCH: {
+                                String query = extras.getString(ARGUMENT_QUERY);
+                                Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+                                mSession.getCallback().onPlayFromSearch(
+                                        mSession.getInstance(), controller, query, extra);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_PLAY_FROM_URI: {
+                                Uri uri = extras.getParcelable(ARGUMENT_URI);
+                                Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+                                mSession.getCallback().onPlayFromUri(
+                                        mSession.getInstance(), controller, uri, extra);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID: {
+                                String mediaId = extras.getString(ARGUMENT_MEDIA_ID);
+                                Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+                                mSession.getCallback().onPrepareFromMediaId(
+                                        mSession.getInstance(), controller, mediaId, extra);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH: {
+                                String query = extras.getString(ARGUMENT_QUERY);
+                                Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+                                mSession.getCallback().onPrepareFromSearch(
+                                        mSession.getInstance(), controller, query, extra);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_PREPARE_FROM_URI: {
+                                Uri uri = extras.getParcelable(ARGUMENT_URI);
+                                Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+                                mSession.getCallback().onPrepareFromUri(
+                                        mSession.getInstance(), controller, uri, extra);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_SET_RATING: {
+                                String mediaId = extras.getString(ARGUMENT_MEDIA_ID);
+                                Rating2 rating = Rating2.fromBundle(
+                                        extras.getBundle(ARGUMENT_RATING));
+                                mSession.getCallback().onSetRating(
+                                        mSession.getInstance(), controller, mediaId, rating);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO: {
+                                mSession.getCallback().onSubscribeRoutesInfo(
+                                        mSession.getInstance(), controller);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO: {
+                                mSession.getCallback().onUnsubscribeRoutesInfo(
+                                        mSession.getInstance(), controller);
+                                break;
+                            }
+                            case COMMAND_CODE_SESSION_SELECT_ROUTE: {
+                                Bundle route = extras.getBundle(ARGUMENT_ROUTE_BUNDLE);
+                                mSession.getCallback().onSelectRoute(
+                                        mSession.getInstance(), controller, route);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYBACK_SET_SPEED: {
+                                float speed = extras.getFloat(ARGUMENT_PLAYBACK_SPEED);
+                                mSession.setPlaybackSpeed(speed);
+                                break;
+                            }
+                        }
+                    }
+                });
+                break;
+            }
+            case CONTROLLER_COMMAND_BY_CUSTOM_COMMAND: {
+                final SessionCommand2 customCommand =
+                        SessionCommand2.fromBundle(extras.getBundle(ARGUMENT_CUSTOM_COMMAND));
+                IMediaControllerCallback caller =
+                        (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+                if (caller == null || customCommand == null) {
+                    return;
+                }
+
+                final Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
+                onCommand2(caller.asBinder(), customCommand, new Session2Runnable() {
+                    @Override
+                    public void run(ControllerInfo controller) throws RemoteException {
+                        mSession.getCallback().onCustomCommand(
+                                mSession.getInstance(), controller, customCommand, args, cb);
+                    }
+                });
+                break;
+            }
+        }
+    }
+
+    List<ControllerInfo> getConnectedControllers() {
+        ArrayList<ControllerInfo> controllers = new ArrayList<>();
+        synchronized (mLock) {
+            for (int i = 0; i < mControllers.size(); i++) {
+                controllers.add(mControllers.valueAt(i));
+            }
+        }
+        return controllers;
+    }
+
+    void notifyCustomLayout(ControllerInfo controller, final List<CommandButton> layout) {
+        notifyInternal(controller, new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
+                        MediaUtils2.toCommandButtonParcelableArray(layout));
+                controller.getControllerBinder().onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
+            }
+        });
+    }
+
+    void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) {
+        synchronized (mLock) {
+            mAllowedCommandGroupMap.put(controller, commands);
+        }
+        notifyInternal(controller, new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
+            }
+        });
+    }
+
+    public void sendCustomCommand(ControllerInfo controller, final SessionCommand2 command,
+            final Bundle args, final ResultReceiver receiver) {
+        if (receiver != null && controller == null) {
+            throw new IllegalArgumentException("Controller shouldn't be null if result receiver is"
+                    + " specified");
+        }
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        notifyInternal(controller, new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                // TODO: Send this event through MediaSessionCompat.XXX()
+                Bundle bundle = new Bundle();
+                bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+                bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+                bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
+                controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+            }
+        });
+    }
+
+    public void sendCustomCommand(final SessionCommand2 command, final Bundle args) {
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        final Bundle bundle = new Bundle();
+        bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+        bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+            }
+        });
+    }
+
+    void notifyCurrentMediaItemChanged(final MediaItem2 item) {
+        notifyAll(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM, new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyPlaybackInfoChanged(final PlaybackInfo info) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyPlayerStateChanged(final int state) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putInt(ARGUMENT_PLAYER_STATE, state);
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyPlaybackSpeedChanged(final float speed) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putParcelable(
+                        ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyBufferingStateChanged(final MediaItem2 item, final int bufferingState) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+                bundle.putInt(ARGUMENT_BUFFERING_STATE, bufferingState);
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED, bundle);
+            }
+        });
+    }
+
+    void notifyError(final int errorCode, final Bundle extras) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
+                bundle.putBundle(ARGUMENT_EXTRAS, extras);
+                controller.getControllerBinder().onEvent(SESSION_EVENT_ON_ERROR, bundle);
+            }
+        });
+    }
+
+    void notifyRoutesInfoChanged(@NonNull final ControllerInfo controller,
+            @Nullable final List<Bundle> routes) {
+        notifyInternal(controller, new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = null;
+                if (routes != null) {
+                    bundle = new Bundle();
+                    bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
+                }
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyPlaylistChanged(final List<MediaItem2> playlist,
+            final MediaMetadata2 metadata) {
+        notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putParcelableArray(ARGUMENT_PLAYLIST,
+                        MediaUtils2.toMediaItem2ParcelableArray(playlist));
+                bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                        metadata == null ? null : metadata.toBundle());
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyPlaylistMetadataChanged(final MediaMetadata2 metadata) {
+        notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA, new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                        metadata == null ? null : metadata.toBundle());
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyRepeatModeChanged(final int repeatMode) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
+            }
+        });
+    }
+
+    void notifyShuffleModeChanged(final int shuffleMode) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
+            }
+        });
+    }
+
+    private List<ControllerInfo> getControllers() {
+        ArrayList<ControllerInfo> controllers = new ArrayList<>();
+        synchronized (mLock) {
+            for (int i = 0; i < mControllers.size(); i++) {
+                controllers.add(mControllers.valueAt(i));
+            }
+        }
+        return controllers;
+    }
+
+    private void notifyAll(@NonNull Session2Runnable runnable) {
+        List<ControllerInfo> controllers = getControllers();
+        for (int i = 0; i < controllers.size(); i++) {
+            notifyInternal(controllers.get(i), runnable);
+        }
+    }
+
+    private void notifyAll(int commandCode, @NonNull Session2Runnable runnable) {
+        List<ControllerInfo> controllers = getControllers();
+        for (int i = 0; i < controllers.size(); i++) {
+            ControllerInfo controller = controllers.get(i);
+            if (isAllowedCommand(controller, commandCode)) {
+                notifyInternal(controller, runnable);
+            }
+        }
+    }
+
+    // TODO: Add a way to check permission from here.
+    private void notifyInternal(@NonNull ControllerInfo controller,
+            @NonNull Session2Runnable runnable) {
+        if (controller == null || controller.getControllerBinder() == null) {
+            return;
+        }
+        try {
+            runnable.run(controller);
+        } catch (DeadObjectException e) {
+            if (DEBUG) {
+                Log.d(TAG, controller.toString() + " is gone", e);
+            }
+            onControllerClosed(controller.getControllerBinder());
+        } catch (RemoteException e) {
+            // Currently it's TransactionTooLargeException or DeadSystemException.
+            // We'd better to leave log for those cases because
+            //   - TransactionTooLargeException means that we may need to fix our code.
+            //     (e.g. add pagination or special way to deliver Bitmap)
+            //   - DeadSystemException means that errors around it can be ignored.
+            Log.w(TAG, "Exception in " + controller.toString(), e);
+        }
+    }
+
+    private boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) {
+        SessionCommandGroup2 allowedCommands;
+        synchronized (mLock) {
+            allowedCommands = mAllowedCommandGroupMap.get(controller);
+        }
+        return allowedCommands != null && allowedCommands.hasCommand(command);
+    }
+
+    private boolean isAllowedCommand(ControllerInfo controller, int commandCode) {
+        SessionCommandGroup2 allowedCommands;
+        synchronized (mLock) {
+            allowedCommands = mAllowedCommandGroupMap.get(controller);
+        }
+        return allowedCommands != null && allowedCommands.hasCommand(commandCode);
+    }
+
+    private void onCommand2(@NonNull IBinder caller, final int commandCode,
+            @NonNull final Session2Runnable runnable) {
+        // TODO: Prevent instantiation of SessionCommand2
+        onCommand2(caller, new SessionCommand2(commandCode), runnable);
+    }
+
+    private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand,
+            @NonNull final Session2Runnable runnable) {
+        final ControllerInfo controller;
+        synchronized (mLock) {
+            controller = mControllers.get(caller);
+        }
+        if (mSession == null || controller == null) {
+            return;
+        }
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (!isAllowedCommand(controller, sessionCommand)) {
+                    return;
+                }
+                int commandCode = sessionCommand.getCommandCode();
+                SessionCommand2 command = sCommandsForOnCommandRequest.get(commandCode);
+                if (command != null) {
+                    boolean accepted = mSession.getCallback().onCommandRequest(
+                            mSession.getInstance(), controller, command);
+                    if (!accepted) {
+                        // Don't run rejected command.
+                        if (DEBUG) {
+                            Log.d(TAG, "Command (code=" + commandCode + ") from "
+                                    + controller + " was rejected by " + mSession);
+                        }
+                        return;
+                    }
+                }
+                try {
+                    runnable.run(controller);
+                } catch (RemoteException e) {
+                    // Currently it's TransactionTooLargeException or DeadSystemException.
+                    // We'd better to leave log for those cases because
+                    //   - TransactionTooLargeException means that we may need to fix our code.
+                    //     (e.g. add pagination or special way to deliver Bitmap)
+                    //   - DeadSystemException means that errors around it can be ignored.
+                    Log.w(TAG, "Exception in " + controller.toString(), e);
+                }
+            }
+        });
+    }
+
+    private void onControllerClosed(IMediaControllerCallback iController) {
+        ControllerInfo controller;
+        synchronized (mLock) {
+            controller = mControllers.remove(iController.asBinder());
+            if (DEBUG) {
+                Log.d(TAG, "releasing " + controller);
+            }
+        }
+        if (controller == null) {
+            return;
+        }
+        final ControllerInfo removedController = controller;
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                mSession.getCallback().onDisconnected(mSession.getInstance(), removedController);
+            }
+        });
+    }
+
+    private ControllerInfo createControllerInfo(Bundle extras) {
+        IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface(
+                extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK));
+        String packageName = extras.getString(ARGUMENT_PACKAGE_NAME);
+        int uid = extras.getInt(ARGUMENT_UID);
+        int pid = extras.getInt(ARGUMENT_PID);
+        // TODO: sanity check for packageName, uid, and pid.
+
+        return new ControllerInfo(mContext, uid, pid, packageName, callback);
+    }
+
+    private void connect(Bundle extras, final ResultReceiver cb) {
+        final ControllerInfo controllerInfo = createControllerInfo(extras);
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                synchronized (mLock) {
+                    // Keep connecting controllers.
+                    // This helps sessions to call APIs in the onConnect()
+                    // (e.g. setCustomLayout()) instead of pending them.
+                    mConnectingControllers.add(controllerInfo.getId());
+                }
+                SessionCommandGroup2 allowedCommands = mSession.getCallback().onConnect(
+                        mSession.getInstance(), controllerInfo);
+                // Don't reject connection for the request from trusted app.
+                // Otherwise server will fail to retrieve session's information to dispatch
+                // media keys to.
+                boolean accept = allowedCommands != null || controllerInfo.isTrusted();
+                if (accept) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo
+                                + " allowedCommands=" + allowedCommands);
+                    }
+                    if (allowedCommands == null) {
+                        // For trusted apps, send non-null allowed commands to keep
+                        // connection.
+                        allowedCommands = new SessionCommandGroup2();
+                    }
+                    synchronized (mLock) {
+                        mConnectingControllers.remove(controllerInfo.getId());
+                        mControllers.put(controllerInfo.getId(), controllerInfo);
+                        mAllowedCommandGroupMap.put(controllerInfo, allowedCommands);
+                    }
+                    // If connection is accepted, notify the current state to the
+                    // controller. It's needed because we cannot call synchronous calls
+                    // between session/controller.
+                    // Note: We're doing this after the onConnectionChanged(), but there's
+                    //       no guarantee that events here are notified after the
+                    //       onConnected() because IMediaController2 is oneway (i.e. async
+                    //       call) and Stub will use thread poll for incoming calls.
+                    final Bundle resultData = new Bundle();
+                    resultData.putBundle(ARGUMENT_ALLOWED_COMMANDS,
+                            allowedCommands.toBundle());
+                    resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState());
+                    resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState());
+                    resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+                            mSession.getPlaybackStateCompat());
+                    resultData.putInt(ARGUMENT_REPEAT_MODE, mSession.getRepeatMode());
+                    resultData.putInt(ARGUMENT_SHUFFLE_MODE, mSession.getShuffleMode());
+                    final List<MediaItem2> playlist = allowedCommands.hasCommand(
+                            COMMAND_CODE_PLAYLIST_GET_LIST) ? mSession.getPlaylist() : null;
+                    if (playlist != null) {
+                        resultData.putParcelableArray(ARGUMENT_PLAYLIST,
+                                MediaUtils2.toMediaItem2ParcelableArray(playlist));
+                    }
+                    final MediaItem2 currentMediaItem =
+                            allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM)
+                                    ? mSession.getCurrentMediaItem() : null;
+                    if (currentMediaItem != null) {
+                        resultData.putBundle(ARGUMENT_MEDIA_ITEM, currentMediaItem.toBundle());
+                    }
+                    resultData.putBundle(ARGUMENT_PLAYBACK_INFO,
+                            mSession.getPlaybackInfo().toBundle());
+                    final MediaMetadata2 playlistMetadata = mSession.getPlaylistMetadata();
+                    if (playlistMetadata != null) {
+                        resultData.putBundle(ARGUMENT_PLAYLIST_METADATA,
+                                playlistMetadata.toBundle());
+                    }
+                    // Double check if session is still there, because close() can be
+                    // called in another thread.
+                    if (mSession.isClosed()) {
+                        return;
+                    }
+                    cb.send(CONNECT_RESULT_CONNECTED, resultData);
+                } else {
+                    synchronized (mLock) {
+                        mConnectingControllers.remove(controllerInfo.getId());
+                    }
+                    if (DEBUG) {
+                        Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
+                    }
+                    cb.send(CONNECT_RESULT_DISCONNECTED, null);
+                }
+            }
+        });
+    }
+
+    private void disconnect(Bundle extras) {
+        final ControllerInfo controllerInfo = createControllerInfo(extras);
+        mSession.getCallbackExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (mSession.isClosed()) {
+                    return;
+                }
+                mSession.getCallback().onDisconnected(mSession.getInstance(), controllerInfo);
+            }
+        });
+    }
+
+    @FunctionalInterface
+    private interface Session2Runnable {
+        void run(ControllerInfo controller) throws RemoteException;
+    }
+}
diff --git a/androidx/media/MediaSession2Test.java b/androidx/media/MediaSession2Test.java
new file mode 100644
index 0000000..5e7ed0e
--- /dev/null
+++ b/androidx/media/MediaSession2Test.java
@@ -0,0 +1,1053 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+
+import static androidx.media.VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+import static androidx.media.VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaSession2}.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaSession2Test extends MediaSession2TestBase {
+    private static final String TAG = "MediaSession2Test";
+
+    private MediaSession2 mSession;
+    private MockPlayer mPlayer;
+    private MockPlaylistAgent mMockAgent;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mPlayer = new MockPlayer(0);
+        mMockAgent = new MockPlaylistAgent();
+
+        mSession = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public SessionCommandGroup2 onConnect(MediaSession2 session,
+                            ControllerInfo controller) {
+                        if (Process.myUid() == controller.getUid()) {
+                            return super.onConnect(session, controller);
+                        }
+                        return null;
+                    }
+                }).build();
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        mSession.close();
+    }
+
+    @Test
+    public void testBuilder() {
+        prepareLooper();
+        MediaSession2.Builder builder = new MediaSession2.Builder(mContext);
+        try {
+            builder.setPlayer(null);
+            fail("null player shouldn't be allowed");
+        } catch (IllegalArgumentException e) {
+            // expected. pass-through
+        }
+        try {
+            builder.setId(null);
+            fail("null id shouldn't be allowed");
+        } catch (IllegalArgumentException e) {
+            // expected. pass-through
+        }
+    }
+
+    @Test
+    public void testPlayerStateChange() throws Exception {
+        prepareLooper();
+        final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mSession = new MediaSession2.Builder(mContext)
+                        .setPlayer(mPlayer)
+                        .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                            @Override
+                            public void onPlayerStateChanged(MediaSession2 session,
+                                    MediaPlayerBase player, int state) {
+                                assertEquals(targetState, state);
+                                latchForSessionCallback.countDown();
+                            }
+                        }).build();
+            }
+        });
+
+        final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+        final MediaController2 controller =
+                createController(mSession.getToken(), true, new ControllerCallback() {
+                    @Override
+                    public void onPlayerStateChanged(MediaController2 controllerOut, int state) {
+                        assertEquals(targetState, state);
+                        latchForControllerCallback.countDown();
+                    }
+                });
+
+        mPlayer.notifyPlaybackState(targetState);
+        assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertEquals(targetState, controller.getPlayerState());
+    }
+
+    @Test
+    public void testBufferingStateChange() throws Exception {
+        prepareLooper();
+        final List<MediaItem2> playlist = TestUtils.createPlaylist(5);
+
+        final MediaItem2 targetItem = playlist.get(3);
+        final int targetBufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mMockAgent.setPlaylist(playlist, null);
+                mSession = new MediaSession2.Builder(mContext)
+                        .setPlayer(mPlayer)
+                        .setPlaylistAgent(mMockAgent)
+                        .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                            @Override
+                            public void onBufferingStateChanged(MediaSession2 session,
+                                    MediaPlayerBase player, MediaItem2 item, int state) {
+                                assertEquals(targetItem, item);
+                                assertEquals(targetBufferingState, state);
+                                latchForSessionCallback.countDown();
+                            }
+                        }).build();
+            }
+        });
+
+        final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+        final MediaController2 controller =
+                createController(mSession.getToken(), true, new ControllerCallback() {
+                    @Override
+                    public void onBufferingStateChanged(MediaController2 controller,
+                            MediaItem2 item, int state) {
+                        assertEquals(targetItem, item);
+                        assertEquals(targetBufferingState, state);
+                        latchForControllerCallback.countDown();
+                    }
+                });
+
+        mPlayer.notifyBufferingState(targetItem, targetBufferingState);
+        assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertEquals(targetBufferingState, controller.getBufferingState());
+    }
+
+    @Test
+    public void testCurrentDataSourceChanged() throws Exception {
+        prepareLooper();
+        final int listSize = 5;
+        final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+        mMockAgent.setPlaylist(list, null);
+
+        final MediaItem2 currentItem = list.get(3);
+        mMockAgent.mCurrentMediaItem = currentItem;
+
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testCurrentDataSourceChanged")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public void onCurrentMediaItemChanged(MediaSession2 session,
+                            MediaPlayerBase player, MediaItem2 itemOut) {
+                        assertSame(currentItem, itemOut);
+                        latchForSessionCallback.countDown();
+                    }
+                }).build()) {
+
+            final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+            final MediaController2 controller =
+                    createController(mSession.getToken(), true, new ControllerCallback() {
+                        @Override
+                        public void onCurrentMediaItemChanged(MediaController2 controller,
+                                MediaItem2 item) {
+                            assertEquals(currentItem, item);
+                            latchForControllerCallback.countDown();
+                        }
+                    });
+
+            mPlayer.notifyCurrentDataSourceChanged(currentItem.getDataSourceDesc());
+            assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            assertEquals(currentItem, controller.getCurrentMediaItem());
+        }
+    }
+
+    @Test
+    public void testMediaPrepared() throws Exception {
+        prepareLooper();
+        final int listSize = 5;
+        final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+        mMockAgent.setPlaylist(list, null);
+
+        final MediaItem2 currentItem = list.get(3);
+
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testMediaPrepared")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public void onMediaPrepared(MediaSession2 session, MediaPlayerBase player,
+                            MediaItem2 itemOut) {
+                        assertSame(currentItem, itemOut);
+                        latchForSessionCallback.countDown();
+                    }
+                }).build()) {
+
+            mPlayer.notifyMediaPrepared(currentItem.getDataSourceDesc());
+            assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            // TODO(jaewan): Test that controllers are also notified. (b/74505936)
+        }
+    }
+
+    @Test
+    public void testBufferingStateChanged() throws Exception {
+        prepareLooper();
+        final int listSize = 5;
+        final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+        mMockAgent.setPlaylist(list, null);
+
+        final MediaItem2 currentItem = list.get(3);
+        final int buffState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testBufferingStateChanged")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public void onBufferingStateChanged(MediaSession2 session,
+                            MediaPlayerBase player, MediaItem2 itemOut, int stateOut) {
+                        assertSame(currentItem, itemOut);
+                        assertEquals(buffState, stateOut);
+                        latchForSessionCallback.countDown();
+                    }
+                }).build()) {
+
+            mPlayer.notifyBufferingStateChanged(currentItem.getDataSourceDesc(), buffState);
+            assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            // TODO(jaewan): Test that controllers are also notified. (b/74505936)
+        }
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}
+     * and {@link MediaController2#getPlaybackSpeed()}.
+     */
+    @Test
+    public void testPlaybackSpeedChanged() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mPlayer.setPlaybackSpeed(speed);
+
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testPlaybackSpeedChanged")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public void onPlaybackSpeedChanged(MediaSession2 session,
+                            MediaPlayerBase player, float speedOut) {
+                        assertEquals(speed, speedOut, 0.0f);
+                        latchForSessionCallback.countDown();
+                    }
+                }).build()) {
+
+            final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+            final MediaController2 controller =
+                    createController(mSession.getToken(), true, new ControllerCallback() {
+                        @Override
+                        public void onPlaybackSpeedChanged(MediaController2 controller,
+                                float speedOut) {
+                            assertEquals(speed, speedOut, 0.0f);
+                            latchForControllerCallback.countDown();
+                        }
+                    });
+
+            mPlayer.notifyPlaybackSpeedChanged(speed);
+            assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
+        }
+    }
+
+    @Test
+    public void testUpdatePlayer() throws Exception {
+        prepareLooper();
+        final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+        final CountDownLatch latch = new CountDownLatch(1);
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                        .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                            @Override
+                            public void onPlayerStateChanged(MediaSession2 session,
+                                    MediaPlayerBase player, int state) {
+                                assertEquals(targetState, state);
+                                latch.countDown();
+                            }
+                        }).build();
+            }
+        });
+
+        MockPlayer player = new MockPlayer(0);
+
+        // Test if setPlayer doesn't crash with various situations.
+        mSession.updatePlayer(mPlayer, null, null);
+        assertEquals(mPlayer, mSession.getPlayer());
+        MediaPlaylistAgent agent = mSession.getPlaylistAgent();
+        assertNotNull(agent);
+
+        mSession.updatePlayer(player, null, null);
+        assertEquals(player, mSession.getPlayer());
+        assertNotNull(mSession.getPlaylistAgent());
+        assertNotEquals(agent, mSession.getPlaylistAgent());
+
+        player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PLAYING);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSetPlayer_playbackInfo() throws Exception {
+        prepareLooper();
+        MockPlayer player = new MockPlayer(0);
+        final AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+                .setContentType(CONTENT_TYPE_MUSIC)
+                .build();
+        player.setAudioAttributes(attrs);
+
+        final int maxVolume = 100;
+        final int currentVolume = 23;
+        final int volumeControlType = VOLUME_CONTROL_ABSOLUTE;
+        VolumeProviderCompat volumeProvider = new VolumeProviderCompat(
+                volumeControlType, maxVolume, currentVolume) { };
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaybackInfoChanged(MediaController2 controller, PlaybackInfo info) {
+                Assert.assertEquals(PlaybackInfo.PLAYBACK_TYPE_REMOTE, info.getPlaybackType());
+                assertEquals(attrs, info.getAudioAttributes());
+                assertEquals(volumeControlType, info.getPlaybackType());
+                assertEquals(maxVolume, info.getMaxVolume());
+                assertEquals(currentVolume, info.getCurrentVolume());
+                latch.countDown();
+            }
+        };
+
+        mSession.updatePlayer(player, null, null);
+
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        PlaybackInfo info = controller.getPlaybackInfo();
+        assertNotNull(info);
+        assertEquals(PlaybackInfo.PLAYBACK_TYPE_LOCAL, info.getPlaybackType());
+        assertEquals(attrs, info.getAudioAttributes());
+        AudioManager manager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+        int localVolumeControlType = VOLUME_CONTROL_ABSOLUTE;
+        if (Build.VERSION.SDK_INT >= 21 && manager.isVolumeFixed()) {
+            localVolumeControlType = VOLUME_CONTROL_FIXED;
+        }
+        assertEquals(localVolumeControlType, info.getControlType());
+        assertEquals(manager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), info.getMaxVolume());
+        assertEquals(manager.getStreamVolume(AudioManager.STREAM_MUSIC), info.getCurrentVolume());
+
+        mSession.updatePlayer(player, null, volumeProvider);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+
+        info = controller.getPlaybackInfo();
+        assertNotNull(info);
+        assertEquals(PlaybackInfo.PLAYBACK_TYPE_REMOTE, info.getPlaybackType());
+        assertEquals(attrs, info.getAudioAttributes());
+        assertEquals(volumeControlType, info.getControlType());
+        assertEquals(maxVolume, info.getMaxVolume());
+        assertEquals(currentVolume, info.getCurrentVolume());
+    }
+
+    @Test
+    public void testPlay() throws Exception {
+        prepareLooper();
+        mSession.play();
+        assertTrue(mPlayer.mPlayCalled);
+    }
+
+    @Test
+    public void testPause() throws Exception {
+        prepareLooper();
+        mSession.pause();
+        assertTrue(mPlayer.mPauseCalled);
+    }
+
+    @Test
+    public void testReset() throws Exception {
+        prepareLooper();
+        mSession.reset();
+        assertTrue(mPlayer.mResetCalled);
+    }
+
+    @Test
+    public void testPrepare() throws Exception {
+        prepareLooper();
+        mSession.prepare();
+        assertTrue(mPlayer.mPrepareCalled);
+    }
+
+    @Test
+    public void testSeekTo() throws Exception {
+        prepareLooper();
+        final long pos = 1004L;
+        mSession.seekTo(pos);
+        assertTrue(mPlayer.mSeekToCalled);
+        assertEquals(pos, mPlayer.mSeekPosition);
+    }
+
+    @Test
+    public void testSetPlaybackSpeed() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mSession.setPlaybackSpeed(speed);
+        assertTrue(mPlayer.mSetPlaybackSpeedCalled);
+        assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+    }
+
+    @Test
+    public void testGetPlaybackSpeed() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mPlayer.setPlaybackSpeed(speed);
+        assertEquals(speed, mSession.getPlaybackSpeed(), 0.0f);
+    }
+
+    @Test
+    public void testGetCurrentMediaItem() {
+        prepareLooper();
+        MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+        mMockAgent.mCurrentMediaItem = item;
+        assertEquals(item, mSession.getCurrentMediaItem());
+    }
+
+    @Test
+    public void testSkipToPreviousItem() {
+        prepareLooper();
+        mSession.skipToPreviousItem();
+        assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+    }
+
+    @Test
+    public void testSkipToNextItem() throws Exception {
+        prepareLooper();
+        mSession.skipToNextItem();
+        assertTrue(mMockAgent.mSkipToNextItemCalled);
+    }
+
+    @Test
+    public void testSkipToPlaylistItem() throws Exception {
+        prepareLooper();
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mSession.skipToPlaylistItem(testMediaItem);
+        assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+        assertSame(testMediaItem, mMockAgent.mItem);
+    }
+
+    @Test
+    public void testGetPlayerState() {
+        prepareLooper();
+        final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+        mPlayer.mLastPlayerState = state;
+        assertEquals(state, mSession.getPlayerState());
+    }
+
+    @Test
+    public void testGetBufferingState() {
+        prepareLooper();
+        final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+        mPlayer.mLastBufferingState = bufferingState;
+        assertEquals(bufferingState, mSession.getBufferingState());
+    }
+
+    @Test
+    public void testGetPosition() {
+        prepareLooper();
+        final long position = 150000;
+        mPlayer.mCurrentPosition = position;
+        assertEquals(position, mSession.getCurrentPosition());
+    }
+
+    @Test
+    public void testGetBufferedPosition() {
+        prepareLooper();
+        final long bufferedPosition = 900000;
+        mPlayer.mBufferedPosition = bufferedPosition;
+        assertEquals(bufferedPosition, mSession.getBufferedPosition());
+    }
+
+    @Test
+    public void testSetPlaylist() {
+        prepareLooper();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        mSession.setPlaylist(list, null);
+        assertTrue(mMockAgent.mSetPlaylistCalled);
+        assertSame(list, mMockAgent.mPlaylist);
+        assertNull(mMockAgent.mMetadata);
+    }
+
+    @Test
+    public void testGetPlaylist() {
+        prepareLooper();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        mMockAgent.mPlaylist = list;
+        assertEquals(list, mSession.getPlaylist());
+    }
+
+    @Test
+    public void testUpdatePlaylistMetadata() {
+        prepareLooper();
+        final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+        mSession.updatePlaylistMetadata(testMetadata);
+        assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+        assertSame(testMetadata, mMockAgent.mMetadata);
+    }
+
+    @Test
+    public void testGetPlaylistMetadata() {
+        prepareLooper();
+        final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+        mMockAgent.mMetadata = testMetadata;
+        assertEquals(testMetadata, mSession.getPlaylistMetadata());
+    }
+
+    @Test
+    public void testSessionCallback_onPlaylistChanged() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final CountDownLatch latch = new CountDownLatch(1);
+        mMockAgent.setPlaylist(list, null);
+
+        final SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public void onPlaylistChanged(MediaSession2 session, MediaPlaylistAgent playlistAgent,
+                    List<MediaItem2> playlist, MediaMetadata2 metadata) {
+                assertEquals(mMockAgent, playlistAgent);
+                assertEquals(list, playlist);
+                assertNull(metadata);
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testSessionCallback")
+                .setSessionCallback(sHandlerExecutor, sessionCallback)
+                .build()) {
+            mMockAgent.notifyPlaylistChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testAddPlaylistItem() {
+        prepareLooper();
+        final int testIndex = 12;
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mSession.addPlaylistItem(testIndex, testMediaItem);
+        assertTrue(mMockAgent.mAddPlaylistItemCalled);
+        assertEquals(testIndex, mMockAgent.mIndex);
+        assertSame(testMediaItem, mMockAgent.mItem);
+    }
+
+    @Test
+    public void testRemovePlaylistItem() {
+        prepareLooper();
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mSession.removePlaylistItem(testMediaItem);
+        assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+        assertSame(testMediaItem, mMockAgent.mItem);
+    }
+
+    @Test
+    public void testReplacePlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final int testIndex = 12;
+        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+        mSession.replacePlaylistItem(testIndex, testMediaItem);
+        assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+        assertEquals(testIndex, mMockAgent.mIndex);
+        assertSame(testMediaItem, mMockAgent.mItem);
+    }
+
+    /**
+     * This also tests {@link SessionCallback#onShuffleModeChanged}
+     */
+    @Test
+    public void testGetShuffleMode() throws InterruptedException {
+        prepareLooper();
+        final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+        mMockAgent.setShuffleMode(testShuffleMode);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public void onShuffleModeChanged(MediaSession2 session,
+                    MediaPlaylistAgent playlistAgent, int shuffleMode) {
+                assertEquals(mMockAgent, playlistAgent);
+                assertEquals(testShuffleMode, shuffleMode);
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testGetShuffleMode")
+                .setSessionCallback(sHandlerExecutor, sessionCallback)
+                .build()) {
+            mMockAgent.notifyShuffleModeChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testSetShuffleMode() {
+        prepareLooper();
+        final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+        mSession.setShuffleMode(testShuffleMode);
+        assertTrue(mMockAgent.mSetShuffleModeCalled);
+        assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+    }
+
+    /**
+     * This also tests {@link SessionCallback#onShuffleModeChanged}
+     */
+    @Test
+    public void testGetRepeatMode() throws InterruptedException {
+        prepareLooper();
+        final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+        mMockAgent.setRepeatMode(testRepeatMode);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public void onRepeatModeChanged(MediaSession2 session, MediaPlaylistAgent playlistAgent,
+                    int repeatMode) {
+                assertEquals(mMockAgent, playlistAgent);
+                assertEquals(testRepeatMode, repeatMode);
+                latch.countDown();
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setPlaylistAgent(mMockAgent)
+                .setId("testGetRepeatMode")
+                .setSessionCallback(sHandlerExecutor, sessionCallback)
+                .build()) {
+            mMockAgent.notifyRepeatModeChanged();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testSetRepeatMode() {
+        prepareLooper();
+        final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+        mSession.setRepeatMode(testRepeatMode);
+        assertTrue(mMockAgent.mSetRepeatModeCalled);
+        assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+    }
+
+    // TODO(jaewan): Revisit
+    @Ignore
+    @Test
+    public void testBadPlayer() throws InterruptedException {
+        prepareLooper();
+        // TODO(jaewan): Add equivalent tests again
+        final CountDownLatch latch = new CountDownLatch(4); // expected call + 1
+        final BadPlayer player = new BadPlayer(0);
+
+        mSession.updatePlayer(player, null, null);
+        mSession.updatePlayer(mPlayer, null, null);
+        player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PAUSED);
+        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    // This bad player will keep push events to the listener that is previously
+    // registered by session.setPlayer().
+    private static class BadPlayer extends MockPlayer {
+        BadPlayer(int count) {
+            super(count);
+        }
+
+        @Override
+        public void unregisterPlayerEventCallback(
+                @NonNull MediaPlayerBase.PlayerEventCallback listener) {
+            // No-op.
+        }
+    }
+
+    @Test
+    public void testOnCommandCallback() throws InterruptedException {
+        prepareLooper();
+        final MockOnCommandCallback callback = new MockOnCommandCallback();
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mPlayer = new MockPlayer(1);
+                mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                        .setSessionCallback(sHandlerExecutor, callback).build();
+            }
+        });
+        MediaController2 controller = createController(mSession.getToken());
+        controller.pause();
+        assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mPlayer.mPauseCalled);
+        assertEquals(1, callback.commands.size());
+        assertEquals(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE,
+                (long) callback.commands.get(0).getCommandCode());
+
+        controller.play();
+        assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mPlayer.mPlayCalled);
+        assertFalse(mPlayer.mPauseCalled);
+        assertEquals(2, callback.commands.size());
+        assertEquals(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY,
+                (long) callback.commands.get(1).getCommandCode());
+    }
+
+    @Test
+    public void testOnConnectCallback() throws InterruptedException {
+        prepareLooper();
+        final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession.close();
+                mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+                        .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+            }
+        });
+        MediaController2 controller = createController(mSession.getToken(), false, null);
+        assertNotNull(controller);
+        waitForConnect(controller, false);
+        waitForDisconnect(controller, true);
+    }
+
+    @Test
+    public void testOnDisconnectCallback() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testOnDisconnectCallback")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public void onDisconnected(MediaSession2 session,
+                            ControllerInfo controller) {
+                        assertEquals(Process.myUid(), controller.getUid());
+                        latch.countDown();
+                    }
+                }).build()) {
+            MediaController2 controller = createController(session.getToken());
+            controller.close();
+            assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testSetCustomLayout() throws InterruptedException {
+        prepareLooper();
+        final List<CommandButton> buttons = new ArrayList<>();
+        buttons.add(new CommandButton.Builder()
+                .setCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY))
+                .setDisplayName("button").build());
+        final CountDownLatch latch = new CountDownLatch(1);
+        final SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public SessionCommandGroup2 onConnect(MediaSession2 session,
+                    ControllerInfo controller) {
+                if (mContext.getPackageName().equals(controller.getPackageName())) {
+                    mSession.setCustomLayout(controller, buttons);
+                }
+                return super.onConnect(session, controller);
+            }
+        };
+
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testSetCustomLayout")
+                .setSessionCallback(sHandlerExecutor, sessionCallback)
+                .build()) {
+            if (mSession != null) {
+                mSession.close();
+                mSession = session;
+            }
+            final ControllerCallback callback = new ControllerCallback() {
+                @Override
+                public void onCustomLayoutChanged(MediaController2 controller2,
+                        List<CommandButton> layout) {
+                    assertEquals(layout.size(), buttons.size());
+                    for (int i = 0; i < layout.size(); i++) {
+                        assertEquals(layout.get(i).getCommand(), buttons.get(i).getCommand());
+                        assertEquals(layout.get(i).getDisplayName(),
+                                buttons.get(i).getDisplayName());
+                    }
+                    latch.countDown();
+                }
+            };
+            final MediaController2 controller =
+                    createController(session.getToken(), true, callback);
+            assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testSetAllowedCommands() throws InterruptedException {
+        prepareLooper();
+        final SessionCommandGroup2 commands = new SessionCommandGroup2();
+        commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY));
+        commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE));
+        commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_RESET));
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onAllowedCommandsChanged(MediaController2 controller,
+                    SessionCommandGroup2 commandsOut) {
+                assertNotNull(commandsOut);
+                Set<SessionCommand2> expected = commands.getCommands();
+                Set<SessionCommand2> actual = commandsOut.getCommands();
+
+                assertNotNull(actual);
+                assertEquals(expected.size(), actual.size());
+                for (SessionCommand2 command : expected) {
+                    assertTrue(actual.contains(command));
+                }
+                latch.countDown();
+            }
+        };
+
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        ControllerInfo controllerInfo = getTestControllerInfo();
+        assertNotNull(controllerInfo);
+
+        mSession.setAllowedCommands(controllerInfo, commands);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSendCustomCommand() throws InterruptedException {
+        prepareLooper();
+        final SessionCommand2 testCommand = new SessionCommand2(
+                SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+        final Bundle testArgs = new Bundle();
+        testArgs.putString("args", "testSendCustomAction");
+
+        final CountDownLatch latch = new CountDownLatch(2);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+                    Bundle args, ResultReceiver receiver) {
+                assertEquals(testCommand, command);
+                assertTrue(TestUtils.equals(testArgs, args));
+                assertNull(receiver);
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller =
+                createController(mSession.getToken(), true, callback);
+        // TODO(jaewan): Test with multiple controllers
+        mSession.sendCustomCommand(testCommand, testArgs);
+
+        ControllerInfo controllerInfo = getTestControllerInfo();
+        assertNotNull(controllerInfo);
+        // TODO(jaewan): Test receivers as well.
+        mSession.sendCustomCommand(controllerInfo, testCommand, testArgs, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testNotifyError() throws InterruptedException {
+        prepareLooper();
+        final int errorCode = MediaSession2.ERROR_CODE_NOT_AVAILABLE_IN_REGION;
+        final Bundle extras = new Bundle();
+        extras.putString("args", "testNotifyError");
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onError(MediaController2 controller, int errorCodeOut, Bundle extrasOut) {
+                assertEquals(errorCode, errorCodeOut);
+                assertTrue(TestUtils.equals(extras, extrasOut));
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        // TODO(jaewan): Test with multiple controllers
+        mSession.notifyError(errorCode, extras);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testNotifyRoutesInfoChanged() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+                    @Nullable List<Bundle> routes) {
+                assertNull(routes);
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        ControllerInfo controllerInfo = getTestControllerInfo();
+        mSession.notifyRoutesInfoChanged(controllerInfo, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    private ControllerInfo getTestControllerInfo() {
+        List<ControllerInfo> controllers = mSession.getConnectedControllers();
+        assertNotNull(controllers);
+        for (int i = 0; i < controllers.size(); i++) {
+            if (Process.myUid() == controllers.get(i).getUid()) {
+                return controllers.get(i);
+            }
+        }
+        fail("Failed to get test controller info");
+        return null;
+    }
+
+    public class MockOnConnectCallback extends SessionCallback {
+        @Override
+        public SessionCommandGroup2 onConnect(MediaSession2 session,
+                ControllerInfo controllerInfo) {
+            if (Process.myUid() != controllerInfo.getUid()) {
+                return null;
+            }
+            assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+            assertEquals(Process.myUid(), controllerInfo.getUid());
+            assertFalse(controllerInfo.isTrusted());
+            // Reject all
+            return null;
+        }
+    }
+
+    public class MockOnCommandCallback extends SessionCallback {
+        public final ArrayList<SessionCommand2> commands = new ArrayList<>();
+
+        @Override
+        public boolean onCommandRequest(MediaSession2 session, ControllerInfo controllerInfo,
+                SessionCommand2 command) {
+            assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+            assertEquals(Process.myUid(), controllerInfo.getUid());
+            assertFalse(controllerInfo.isTrusted());
+            commands.add(command);
+            if (command.getCommandCode() == SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    private static void assertMediaItemListEquals(List<MediaItem2> a, List<MediaItem2> b) {
+        if (a == null || b == null) {
+            assertEquals(a, b);
+        }
+        assertEquals(a.size(), b.size());
+
+        for (int i = 0; i < a.size(); i++) {
+            MediaItem2 aItem = a.get(i);
+            MediaItem2 bItem = b.get(i);
+
+            if (aItem == null || bItem == null) {
+                assertEquals(aItem, bItem);
+                continue;
+            }
+
+            assertEquals(aItem.getMediaId(), bItem.getMediaId());
+            assertEquals(aItem.getFlags(), bItem.getFlags());
+            TestUtils.equals(aItem.getMetadata().toBundle(), bItem.getMetadata().toBundle());
+
+            // Note: Here it does not check whether DataSourceDesc are equal,
+            // since there DataSourceDec is not comparable.
+        }
+    }
+}
diff --git a/androidx/media/MediaSession2TestBase.java b/androidx/media/MediaSession2TestBase.java
new file mode 100644
index 0000000..745ef3a
--- /dev/null
+++ b/androidx/media/MediaSession2TestBase.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.InstrumentationRegistry;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.TestUtils.SyncHandler;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Base class for session test.
+ * <p>
+ * For all subclasses, all individual tests should begin with the {@link #prepareLooper()}. See
+ * {@link #prepareLooper} for details.
+ */
+abstract class MediaSession2TestBase {
+    // Expected success
+    static final int WAIT_TIME_MS = 1000;
+
+    // Expected timeout
+    static final int TIMEOUT_MS = 500;
+
+    static SyncHandler sHandler;
+    static Executor sHandlerExecutor;
+
+    Context mContext;
+    private List<MediaController2> mControllers = new ArrayList<>();
+
+    interface TestControllerInterface {
+        ControllerCallback getCallback();
+    }
+
+    interface TestControllerCallbackInterface {
+        void waitForConnect(boolean expect) throws InterruptedException;
+        void waitForDisconnect(boolean expect) throws InterruptedException;
+        void setRunnableForOnCustomCommand(Runnable runnable);
+    }
+
+    /**
+     * All tests methods should start with this.
+     * <p>
+     * MediaControllerCompat, which is wrapped by the MediaSession2, can be only created by the
+     * thread whose Looper is prepared. However, when the presubmit tests runs on the server,
+     * test runs with the {@link org.junit.internal.runners.statements.FailOnTimeout} which creates
+     * dedicated thread for running test methods while methods annotated with @After or @Before
+     * runs on the different thread. This ensures that the current Looper is prepared.
+     * <p>
+     * To address the issue .
+     */
+    public static void prepareLooper() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+    }
+
+    @BeforeClass
+    public static void setUpThread() {
+        synchronized (MediaSession2TestBase.class) {
+            if (sHandler != null) {
+                return;
+            }
+            prepareLooper();
+            HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+            handlerThread.start();
+            sHandler = new SyncHandler(handlerThread.getLooper());
+            sHandlerExecutor = new Executor() {
+                @Override
+                public void execute(Runnable runnable) {
+                    SyncHandler handler;
+                    synchronized (MediaSession2TestBase.class) {
+                        handler = sHandler;
+                    }
+                    if (handler != null) {
+                        handler.post(runnable);
+                    }
+                }
+            };
+        }
+    }
+
+    @AfterClass
+    public static void cleanUpThread() {
+        synchronized (MediaSession2TestBase.class) {
+            if (sHandler == null) {
+                return;
+            }
+            sHandler.getLooper().quitSafely();
+            sHandler = null;
+            sHandlerExecutor = null;
+        }
+    }
+
+    @CallSuper
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @CallSuper
+    public void cleanUp() throws Exception {
+        for (int i = 0; i < mControllers.size(); i++) {
+            mControllers.get(i).close();
+        }
+    }
+
+    final MediaController2 createController(SessionToken2 token) throws InterruptedException {
+        return createController(token, true, null);
+    }
+
+    final MediaController2 createController(@NonNull SessionToken2 token,
+            boolean waitForConnect, @Nullable ControllerCallback callback)
+            throws InterruptedException {
+        TestControllerInterface instance = onCreateController(token, callback);
+        if (!(instance instanceof MediaController2)) {
+            throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
+                    + instance);
+        }
+        MediaController2 controller = (MediaController2) instance;
+        mControllers.add(controller);
+        if (waitForConnect) {
+            waitForConnect(controller, true);
+        }
+        return controller;
+    }
+
+    private static TestControllerCallbackInterface getTestControllerCallbackInterface(
+            MediaController2 controller) {
+        if (!(controller instanceof TestControllerInterface)) {
+            throw new RuntimeException("Test has a bug. Expected controller implemented"
+                    + " TestControllerInterface but got " + controller);
+        }
+        ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
+        if (!(callback instanceof TestControllerCallbackInterface)) {
+            throw new RuntimeException("Test has a bug. Expected controller with callback "
+                    + " implemented TestControllerCallbackInterface but got " + controller);
+        }
+        return (TestControllerCallbackInterface) callback;
+    }
+
+    public static void waitForConnect(MediaController2 controller, boolean expected)
+            throws InterruptedException {
+        getTestControllerCallbackInterface(controller).waitForConnect(expected);
+    }
+
+    public static void waitForDisconnect(MediaController2 controller, boolean expected)
+            throws InterruptedException {
+        getTestControllerCallbackInterface(controller).waitForDisconnect(expected);
+    }
+
+    public static void setRunnableForOnCustomCommand(MediaController2 controller,
+            Runnable runnable) {
+        getTestControllerCallbackInterface(controller).setRunnableForOnCustomCommand(runnable);
+    }
+
+    TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
+            @Nullable ControllerCallback callback) throws InterruptedException {
+        final ControllerCallback controllerCallback =
+                callback != null ? callback : new ControllerCallback() {};
+        final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
+        sHandler.postAndSync(new Runnable() {
+            @Override
+            public void run() {
+                // Create controller on the test handler, for changing MediaBrowserCompat's Handler
+                // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
+                // and commands wouldn't be run if tests codes waits on the test handler.
+                controller.set(new TestMediaController(
+                        mContext, token, new TestControllerCallback(controllerCallback)));
+            }
+        });
+        return controller.get();
+    }
+
+    // TODO(jaewan): (Can be Post-P): Deprecate this
+    public static class TestControllerCallback extends MediaController2.ControllerCallback
+            implements TestControllerCallbackInterface {
+        public final ControllerCallback mCallbackProxy;
+        public final CountDownLatch connectLatch = new CountDownLatch(1);
+        public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+        @GuardedBy("this")
+        private Runnable mOnCustomCommandRunnable;
+
+        TestControllerCallback(@NonNull ControllerCallback callbackProxy) {
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+            }
+            mCallbackProxy = callbackProxy;
+        }
+
+        @CallSuper
+        @Override
+        public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+            connectLatch.countDown();
+        }
+
+        @CallSuper
+        @Override
+        public void onDisconnected(MediaController2 controller) {
+            disconnectLatch.countDown();
+        }
+
+        @Override
+        public void waitForConnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+
+        @Override
+        public void waitForDisconnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+
+        @Override
+        public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+                Bundle args, ResultReceiver receiver) {
+            mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+            synchronized (this) {
+                if (mOnCustomCommandRunnable != null) {
+                    mOnCustomCommandRunnable.run();
+                }
+            }
+        }
+
+        @Override
+        public void onPlaybackInfoChanged(MediaController2 controller,
+                MediaController2.PlaybackInfo info) {
+            mCallbackProxy.onPlaybackInfoChanged(controller, info);
+        }
+
+        @Override
+        public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+            mCallbackProxy.onCustomLayoutChanged(controller, layout);
+        }
+
+        @Override
+        public void onAllowedCommandsChanged(MediaController2 controller,
+                SessionCommandGroup2 commands) {
+            mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+        }
+
+        @Override
+        public void onPlayerStateChanged(MediaController2 controller, int state) {
+            mCallbackProxy.onPlayerStateChanged(controller, state);
+        }
+
+        @Override
+        public void onSeekCompleted(MediaController2 controller, long position) {
+            mCallbackProxy.onSeekCompleted(controller, position);
+        }
+
+        @Override
+        public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+            mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+        }
+
+        @Override
+        public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+                int state) {
+            mCallbackProxy.onBufferingStateChanged(controller, item, state);
+        }
+
+        @Override
+        public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+            mCallbackProxy.onError(controller, errorCode, extras);
+        }
+
+        @Override
+        public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+            mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+        }
+
+        @Override
+        public void onPlaylistChanged(MediaController2 controller,
+                List<MediaItem2> list, MediaMetadata2 metadata) {
+            mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+        }
+
+        @Override
+        public void onPlaylistMetadataChanged(MediaController2 controller,
+                MediaMetadata2 metadata) {
+            mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+        }
+
+        @Override
+        public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+            mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+        }
+
+        @Override
+        public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+            mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+        }
+
+        @Override
+        public void setRunnableForOnCustomCommand(Runnable runnable) {
+            synchronized (this) {
+                mOnCustomCommandRunnable = runnable;
+            }
+        }
+
+        @Override
+        public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+                @Nullable List<Bundle> routes) {
+            mCallbackProxy.onRoutesInfoChanged(controller, routes);
+        }
+    }
+
+    public class TestMediaController extends MediaController2 implements TestControllerInterface {
+        private final ControllerCallback mCallback;
+
+        TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
+                @NonNull ControllerCallback callback) {
+            super(context, token, sHandlerExecutor, callback);
+            mCallback = callback;
+        }
+
+        @Override
+        public ControllerCallback getCallback() {
+            return mCallback;
+        }
+    }
+}
diff --git a/androidx/media/MediaSession2_PermissionTest.java b/androidx/media/MediaSession2_PermissionTest.java
new file mode 100644
index 0000000..3895ea5
--- /dev/null
+++ b/androidx/media/MediaSession2_PermissionTest.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Process;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests whether {@link MediaSession2} receives commands that hasn't allowed.
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MediaSession2_PermissionTest extends MediaSession2TestBase {
+    private static final String SESSION_ID = "MediaSession2Test_permission";
+
+    private MockPlayer mPlayer;
+    private MediaSession2 mSession;
+    private MySessionCallback mCallback;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        if (mSession != null) {
+            mSession.close();
+            mSession = null;
+        }
+        mPlayer = null;
+        mCallback = null;
+    }
+
+    private MediaSession2 createSessionWithAllowedActions(final SessionCommandGroup2 commands) {
+        mPlayer = new MockPlayer(0);
+        mCallback = new MySessionCallback() {
+            @Override
+            public SessionCommandGroup2 onConnect(MediaSession2 session,
+                    ControllerInfo controller) {
+                if (Process.myUid() != controller.getUid()) {
+                    return null;
+                }
+                return commands == null ? new SessionCommandGroup2() : commands;
+            }
+        };
+        if (mSession != null) {
+            mSession.close();
+        }
+        mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer).setId(SESSION_ID)
+                .setSessionCallback(sHandlerExecutor, mCallback).build();
+        return mSession;
+    }
+
+    private SessionCommandGroup2 createCommandGroupWith(int commandCode) {
+        SessionCommandGroup2 commands = new SessionCommandGroup2();
+        commands.addCommand(new SessionCommand2(commandCode));
+        return commands;
+    }
+
+    private SessionCommandGroup2 createCommandGroupWithout(int commandCode) {
+        SessionCommandGroup2 commands = new SessionCommandGroup2();
+        commands.addAllPredefinedCommands();
+        commands.removeCommand(new SessionCommand2(commandCode));
+        return commands;
+    }
+
+    private void testOnCommandRequest(int commandCode, PermissionTestRunnable runnable)
+            throws InterruptedException {
+        createSessionWithAllowedActions(createCommandGroupWith(commandCode));
+        runnable.run(createController(mSession.getToken()));
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnCommandRequestCalled);
+        assertEquals(commandCode, mCallback.mCommand.getCommandCode());
+
+        createSessionWithAllowedActions(createCommandGroupWithout(commandCode));
+        runnable.run(createController(mSession.getToken()));
+
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnCommandRequestCalled);
+    }
+
+    @Test
+    public void testPlay() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_PLAYBACK_PLAY, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.play();
+            }
+        });
+    }
+
+    @Test
+    public void testPause() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_PLAYBACK_PAUSE, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.pause();
+            }
+        });
+    }
+
+    @Test
+    public void testReset() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_PLAYBACK_RESET, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.reset();
+            }
+        });
+    }
+
+    @Test
+    public void testSeekTo() throws InterruptedException {
+        prepareLooper();
+        final long position = 10;
+        testOnCommandRequest(COMMAND_CODE_PLAYBACK_SEEK_TO, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.seekTo(position);
+            }
+        });
+    }
+
+    @Test
+    public void testSkipToNext() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.skipToNextItem();
+            }
+        });
+    }
+
+    @Test
+    public void testSkipToPrevious() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.skipToPreviousItem();
+            }
+        });
+    }
+
+    @Test
+    public void testSkipToPlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+        testOnCommandRequest(
+                COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM,
+                new PermissionTestRunnable() {
+                    @Override
+                    public void run(MediaController2 controller) {
+                        controller.skipToPlaylistItem(testItem);
+                    }
+                });
+    }
+
+    @Test
+    public void testSetPlaylist() throws InterruptedException {
+        prepareLooper();
+        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        testOnCommandRequest(COMMAND_CODE_PLAYLIST_SET_LIST, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.setPlaylist(list, null);
+            }
+        });
+    }
+
+    @Test
+    public void testUpdatePlaylistMetadata() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.updatePlaylistMetadata(null);
+            }
+        });
+    }
+
+    @Test
+    public void testAddPlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+        testOnCommandRequest(COMMAND_CODE_PLAYLIST_ADD_ITEM, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.addPlaylistItem(0, testItem);
+            }
+        });
+    }
+
+    @Test
+    public void testRemovePlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+        testOnCommandRequest(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.removePlaylistItem(testItem);
+            }
+        });
+    }
+
+    @Test
+    public void testReplacePlaylistItem() throws InterruptedException {
+        prepareLooper();
+        final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+        testOnCommandRequest(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.replacePlaylistItem(0, testItem);
+            }
+        });
+    }
+
+    @Test
+    public void testSetVolume() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_VOLUME_SET_VOLUME, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.setVolumeTo(0, 0);
+            }
+        });
+    }
+
+    @Test
+    public void testAdjustVolume() throws InterruptedException {
+        prepareLooper();
+        testOnCommandRequest(COMMAND_CODE_VOLUME_ADJUST_VOLUME, new PermissionTestRunnable() {
+            @Override
+            public void run(MediaController2 controller) {
+                controller.adjustVolume(0, 0);
+            }
+        });
+    }
+
+    @Test
+    public void testFastForward() throws InterruptedException {
+        prepareLooper();
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_FAST_FORWARD));
+        createController(mSession.getToken()).fastForward();
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnFastForwardCalled);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_FAST_FORWARD));
+        createController(mSession.getToken()).fastForward();
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnFastForwardCalled);
+    }
+
+    @Test
+    public void testRewind() throws InterruptedException {
+        prepareLooper();
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_REWIND));
+        createController(mSession.getToken()).rewind();
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnRewindCalled);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_REWIND));
+        createController(mSession.getToken()).rewind();
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnRewindCalled);
+    }
+
+    @Test
+    public void testPlayFromMediaId() throws InterruptedException {
+        prepareLooper();
+        final String mediaId = "testPlayFromMediaId";
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID));
+        createController(mSession.getToken()).playFromMediaId(mediaId, null);
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+        assertEquals(mediaId, mCallback.mMediaId);
+        assertNull(mCallback.mExtras);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID));
+        createController(mSession.getToken()).playFromMediaId(mediaId, null);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnPlayFromMediaIdCalled);
+    }
+
+    @Test
+    public void testPlayFromUri() throws InterruptedException {
+        prepareLooper();
+        final Uri uri = Uri.parse("play://from.uri");
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_URI));
+        createController(mSession.getToken()).playFromUri(uri, null);
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnPlayFromUriCalled);
+        assertEquals(uri, mCallback.mUri);
+        assertNull(mCallback.mExtras);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_URI));
+        createController(mSession.getToken()).playFromUri(uri, null);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnPlayFromUriCalled);
+    }
+
+    @Test
+    public void testPlayFromSearch() throws InterruptedException {
+        prepareLooper();
+        final String query = "testPlayFromSearch";
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH));
+        createController(mSession.getToken()).playFromSearch(query, null);
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnPlayFromSearchCalled);
+        assertEquals(query, mCallback.mQuery);
+        assertNull(mCallback.mExtras);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH));
+        createController(mSession.getToken()).playFromSearch(query, null);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnPlayFromSearchCalled);
+    }
+
+    @Test
+    public void testPrepareFromMediaId() throws InterruptedException {
+        prepareLooper();
+        final String mediaId = "testPrepareFromMediaId";
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+        createController(mSession.getToken()).prepareFromMediaId(mediaId, null);
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+        assertEquals(mediaId, mCallback.mMediaId);
+        assertNull(mCallback.mExtras);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+        createController(mSession.getToken()).prepareFromMediaId(mediaId, null);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnPrepareFromMediaIdCalled);
+    }
+
+    @Test
+    public void testPrepareFromUri() throws InterruptedException {
+        prepareLooper();
+        final Uri uri = Uri.parse("prepare://from.uri");
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+        createController(mSession.getToken()).prepareFromUri(uri, null);
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnPrepareFromUriCalled);
+        assertEquals(uri, mCallback.mUri);
+        assertNull(mCallback.mExtras);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+        createController(mSession.getToken()).prepareFromUri(uri, null);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnPrepareFromUriCalled);
+    }
+
+    @Test
+    public void testPrepareFromSearch() throws InterruptedException {
+        prepareLooper();
+        final String query = "testPrepareFromSearch";
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+        createController(mSession.getToken()).prepareFromSearch(query, null);
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnPrepareFromSearchCalled);
+        assertEquals(query, mCallback.mQuery);
+        assertNull(mCallback.mExtras);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+        createController(mSession.getToken()).prepareFromSearch(query, null);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnPrepareFromSearchCalled);
+    }
+
+    @Test
+    public void testSetRating() throws InterruptedException {
+        prepareLooper();
+        final String mediaId = "testSetRating";
+        final Rating2 rating = Rating2.newStarRating(Rating2.RATING_5_STARS, 3.5f);
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_SET_RATING));
+        createController(mSession.getToken()).setRating(mediaId, rating);
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnSetRatingCalled);
+        assertEquals(mediaId, mCallback.mMediaId);
+        assertEquals(rating, mCallback.mRating);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_SET_RATING));
+        createController(mSession.getToken()).setRating(mediaId, rating);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnSetRatingCalled);
+    }
+
+    @Test
+    public void testChangingPermissionWithSetAllowedCommands() throws InterruptedException {
+        prepareLooper();
+        final String query = "testChangingPermissionWithSetAllowedCommands";
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+
+        ControllerCallbackForPermissionChange controllerCallback =
+                new ControllerCallbackForPermissionChange();
+        MediaController2 controller =
+                createController(mSession.getToken(), true, controllerCallback);
+
+        controller.prepareFromSearch(query, null);
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnPrepareFromSearchCalled);
+        assertEquals(query, mCallback.mQuery);
+        assertNull(mCallback.mExtras);
+        mCallback.reset();
+
+        // Change allowed commands.
+        mSession.setAllowedCommands(getTestControllerInfo(),
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+        assertTrue(controllerCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        controller.prepareFromSearch(query, null);
+        assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    private ControllerInfo getTestControllerInfo() {
+        List<ControllerInfo> controllers = mSession.getConnectedControllers();
+        assertNotNull(controllers);
+        for (int i = 0; i < controllers.size(); i++) {
+            if (Process.myUid() == controllers.get(i).getUid()) {
+                return controllers.get(i);
+            }
+        }
+        fail("Failed to get test controller info");
+        return null;
+    }
+
+    @FunctionalInterface
+    private interface PermissionTestRunnable {
+        void run(@NonNull MediaController2 controller);
+    }
+
+    public class MySessionCallback extends MediaSession2.SessionCallback {
+        public CountDownLatch mCountDownLatch;
+
+        public SessionCommand2 mCommand;
+        public String mMediaId;
+        public String mQuery;
+        public Uri mUri;
+        public Bundle mExtras;
+        public Rating2 mRating;
+
+        public boolean mOnCommandRequestCalled;
+        public boolean mOnPlayFromMediaIdCalled;
+        public boolean mOnPlayFromSearchCalled;
+        public boolean mOnPlayFromUriCalled;
+        public boolean mOnPrepareFromMediaIdCalled;
+        public boolean mOnPrepareFromSearchCalled;
+        public boolean mOnPrepareFromUriCalled;
+        public boolean mOnFastForwardCalled;
+        public boolean mOnRewindCalled;
+        public boolean mOnSetRatingCalled;
+
+
+        public MySessionCallback() {
+            mCountDownLatch = new CountDownLatch(1);
+        }
+
+        public void reset() {
+            mCountDownLatch = new CountDownLatch(1);
+
+            mCommand = null;
+            mMediaId = null;
+            mQuery = null;
+            mUri = null;
+            mExtras = null;
+
+            mOnCommandRequestCalled = false;
+            mOnPlayFromMediaIdCalled = false;
+            mOnPlayFromSearchCalled = false;
+            mOnPlayFromUriCalled = false;
+            mOnPrepareFromMediaIdCalled = false;
+            mOnPrepareFromSearchCalled = false;
+            mOnPrepareFromUriCalled = false;
+            mOnFastForwardCalled = false;
+            mOnRewindCalled = false;
+            mOnSetRatingCalled = false;
+        }
+
+        @Override
+        public boolean onCommandRequest(MediaSession2 session, ControllerInfo controller,
+                SessionCommand2 command) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnCommandRequestCalled = true;
+            mCommand = command;
+            mCountDownLatch.countDown();
+            return super.onCommandRequest(session, controller, command);
+        }
+
+        @Override
+        public void onFastForward(MediaSession2 session, ControllerInfo controller) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnFastForwardCalled = true;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onRewind(MediaSession2 session, ControllerInfo controller) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnRewindCalled = true;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+                String mediaId, Bundle extras) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnPlayFromMediaIdCalled = true;
+            mMediaId = mediaId;
+            mExtras = extras;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+                String query, Bundle extras) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnPlayFromSearchCalled = true;
+            mQuery = query;
+            mExtras = extras;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onPlayFromUri(MediaSession2 session, ControllerInfo controller,
+                Uri uri, Bundle extras) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnPlayFromUriCalled = true;
+            mUri = uri;
+            mExtras = extras;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+                String mediaId, Bundle extras) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnPrepareFromMediaIdCalled = true;
+            mMediaId = mediaId;
+            mExtras = extras;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+                String query, Bundle extras) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnPrepareFromSearchCalled = true;
+            mQuery = query;
+            mExtras = extras;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller,
+                Uri uri, Bundle extras) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnPrepareFromUriCalled = true;
+            mUri = uri;
+            mExtras = extras;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onSetRating(MediaSession2 session, ControllerInfo controller,
+                String mediaId, Rating2 rating) {
+            assertEquals(Process.myUid(), controller.getUid());
+            mOnSetRatingCalled = true;
+            mMediaId = mediaId;
+            mRating = rating;
+            mCountDownLatch.countDown();
+        }
+    }
+
+    public class ControllerCallbackForPermissionChange extends MediaController2.ControllerCallback {
+        public CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+        @Override
+        public void onAllowedCommandsChanged(MediaController2 controller,
+                SessionCommandGroup2 commands) {
+            mCountDownLatch.countDown();
+        }
+    }
+}
diff --git a/androidx/media/MediaSessionManager_MediaSession2Test.java b/androidx/media/MediaSessionManager_MediaSession2Test.java
new file mode 100644
index 0000000..904b768
--- /dev/null
+++ b/androidx/media/MediaSessionManager_MediaSession2Test.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+public class MediaSessionManager_MediaSession2Test extends MediaSession2TestBase {
+    private static final String TAG = "MediaSessionManager_MediaSession2Test";
+
+    private MediaSessionManager mManager;
+    private MediaSession2 mSession;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+
+        // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
+        // per test thread differs across the {@link MediaSession2} with the same TAG.
+        final MockPlayer player = new MockPlayer(1);
+        mSession = new MediaSession2.Builder(mContext)
+                .setPlayer(player)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() { })
+                .setId(TAG)
+                .build();
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        sHandler.removeCallbacksAndMessages(null);
+        mSession.close();
+    }
+
+    // TODO(jaewan): Make this host-side test to see per-user behavior.
+    @Ignore
+    @Test
+    public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
+        prepareLooper();
+        final MockPlayer player = (MockPlayer) mSession.getPlayer();
+        player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_IDLE);
+
+        MediaController2 controller = null;
+//        List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+//        assertNotNull(tokens);
+//        for (int i = 0; i < tokens.size(); i++) {
+//            SessionToken2 token = tokens.get(i);
+//            if (mContext.getPackageName().equals(token.getPackageName())
+//                    && TAG.equals(token.getId())) {
+//                assertNull(controller);
+//                controller = createController(token);
+//            }
+//        }
+//        assertNotNull(controller);
+//
+//        // Test if the found controller is correct one.
+//        assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, controller.getPlayerState());
+//        controller.play();
+//
+//        assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+//        assertTrue(player.mPlayCalled);
+    }
+
+    /**
+     * Test if server recognizes a session even if the session refuses the connection from server.
+     *
+     * @throws InterruptedException
+     */
+    @Test
+    public void testGetSessionTokens_sessionRejected() throws InterruptedException {
+        prepareLooper();
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext).setPlayer(new MockPlayer(0))
+                .setId(TAG).setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public SessionCommandGroup2 onConnect(
+                            MediaSession2 session, ControllerInfo controller) {
+                        // Reject all connection request.
+                        return null;
+                    }
+                }).build();
+
+        boolean foundSession = false;
+//        List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+//        assertNotNull(tokens);
+//        for (int i = 0; i < tokens.size(); i++) {
+//            SessionToken2 token = tokens.get(i);
+//            if (mContext.getPackageName().equals(token.getPackageName())
+//                    && TAG.equals(token.getId())) {
+//                assertFalse(foundSession);
+//                foundSession = true;
+//            }
+//        }
+//        assertTrue(foundSession);
+    }
+
+    @Test
+    public void testGetMediaSession2Tokens_sessionClosed() throws InterruptedException {
+        prepareLooper();
+        mSession.close();
+
+        // When a session is closed, it should lose binder connection between server immediately.
+        // So server will forget the session.
+//        List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+//        for (int i = 0; i < tokens.size(); i++) {
+//            SessionToken2 token = tokens.get(i);
+//            assertFalse(mContext.getPackageName().equals(token.getPackageName())
+//                    && TAG.equals(token.getId()));
+//        }
+    }
+
+    @Test
+    public void testGetMediaSessionService2Token() throws InterruptedException {
+        prepareLooper();
+        boolean foundTestSessionService = false;
+        boolean foundTestLibraryService = false;
+//        List<SessionToken2> tokens = mManager.getSessionServiceTokens();
+//        for (int i = 0; i < tokens.size(); i++) {
+//            SessionToken2 token = tokens.get(i);
+//            if (mContext.getPackageName().equals(token.getPackageName())
+//                    && MockMediaSessionService2.ID.equals(token.getId())) {
+//                assertFalse(foundTestSessionService);
+//                assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+//                foundTestSessionService = true;
+//            } else if (mContext.getPackageName().equals(token.getPackageName())
+//                    && MockMediaLibraryService2.ID.equals(token.getId())) {
+//                assertFalse(foundTestLibraryService);
+//                assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+//                foundTestLibraryService = true;
+//            }
+//        }
+//        assertTrue(foundTestSessionService);
+//        assertTrue(foundTestLibraryService);
+    }
+
+    @Test
+    public void testGetAllSessionTokens() throws InterruptedException {
+        prepareLooper();
+        boolean foundTestSession = false;
+        boolean foundTestSessionService = false;
+        boolean foundTestLibraryService = false;
+//        List<SessionToken2> tokens = mManager.getAllSessionTokens();
+//        for (int i = 0; i < tokens.size(); i++) {
+//            SessionToken2 token = tokens.get(i);
+//            if (!mContext.getPackageName().equals(token.getPackageName())) {
+//                continue;
+//            }
+//            switch (token.getId()) {
+//                case TAG:
+//                    assertFalse(foundTestSession);
+//                    foundTestSession = true;
+//                    break;
+//                case MockMediaSessionService2.ID:
+//                    assertFalse(foundTestSessionService);
+//                    foundTestSessionService = true;
+//                    assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+//                    break;
+//                case MockMediaLibraryService2.ID:
+//                    assertFalse(foundTestLibraryService);
+//                    assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+//                    foundTestLibraryService = true;
+//                    break;
+//                default:
+//                    fail("Unexpected session " + token + " exists in the package");
+//            }
+//        }
+//        assertTrue(foundTestSession);
+//        assertTrue(foundTestSessionService);
+//        assertTrue(foundTestLibraryService);
+    }
+
+    @Test
+    public void testAddOnSessionTokensChangedListener() throws InterruptedException {
+//        prepareLooper();
+//        TokensChangedListener listener = new TokensChangedListener();
+//        mManager.addOnSessionTokensChangedListener(sHandlerExecutor, listener);
+//
+//        listener.reset();
+//        MediaSession2 session1 = new MediaSession2.Builder(mContext)
+//                .setPlayer(new MockPlayer(0))
+//                .setId(UUID.randomUUID().toString())
+//                .build();
+//        assertTrue(listener.await());
+//        assertTrue(listener.findToken(session1.getToken()));
+//
+//        listener.reset();
+//        session1.close();
+//        assertTrue(listener.await());
+//        assertFalse(listener.findToken(session1.getToken()));
+//
+//        listener.reset();
+//        MediaSession2 session2 = new MediaSession2.Builder(mContext)
+//                .setPlayer(new MockPlayer(0))
+//                .setId(UUID.randomUUID().toString())
+//                .build();
+//        assertTrue(listener.await());
+//        assertFalse(listener.findToken(session1.getToken()));
+//        assertTrue(listener.findToken(session2.getToken()));
+//
+//        listener.reset();
+//        MediaSession2 session3 = new MediaSession2.Builder(mContext)
+//                .setPlayer(new MockPlayer(0))
+//                .setId(UUID.randomUUID().toString())
+//                .build();
+//        assertTrue(listener.await());
+//        assertFalse(listener.findToken(session1.getToken()));
+//        assertTrue(listener.findToken(session2.getToken()));
+//        assertTrue(listener.findToken(session3.getToken()));
+//
+//        listener.reset();
+//        session2.close();
+//        assertTrue(listener.await());
+//        assertFalse(listener.findToken(session1.getToken()));
+//        assertFalse(listener.findToken(session2.getToken()));
+//        assertTrue(listener.findToken(session3.getToken()));
+//
+//        listener.reset();
+//        session3.close();
+//        assertTrue(listener.await());
+//        assertFalse(listener.findToken(session1.getToken()));
+//        assertFalse(listener.findToken(session2.getToken()));
+//        assertFalse(listener.findToken(session3.getToken()));
+//
+//        mManager.removeOnSessionTokensChangedListener(listener);
+    }
+
+    @Test
+    public void testRemoveOnSessionTokensChangedListener() throws InterruptedException {
+//        prepareLooper();
+//        TokensChangedListener listener = new TokensChangedListener();
+//        mManager.addOnSessionTokensChangedListener(sHandlerExecutor, listener);
+//
+//        listener.reset();
+//        MediaSession2 session1 = new MediaSession2.Builder(mContext)
+//                .setPlayer(new MockPlayer(0))
+//                .setId(UUID.randomUUID().toString())
+//                .build();
+//        assertTrue(listener.await());
+//
+//        mManager.removeOnSessionTokensChangedListener(listener);
+//
+//        listener.reset();
+//        session1.close();
+//        assertFalse(listener.await());
+//
+//        listener.reset();
+//        MediaSession2 session2 = new MediaSession2.Builder(mContext)
+//                .setPlayer(new MockPlayer(0))
+//                .setId(UUID.randomUUID().toString())
+//                .build();
+//        assertFalse(listener.await());
+//
+//        listener.reset();
+//        MediaSession2 session3 = new MediaSession2.Builder(mContext)
+//                .setPlayer(new MockPlayer(0))
+//                .setId(UUID.randomUUID().toString())
+//                .build();
+//        assertFalse(listener.await());
+//
+//        listener.reset();
+//        session2.close();
+//        assertFalse(listener.await());
+//
+//        listener.reset();
+//        session3.close();
+//        assertFalse(listener.await());
+    }
+
+//    private class TokensChangedListener implements OnSessionTokensChangedListener {
+//        private CountDownLatch mLatch;
+//        private List<SessionToken2> mTokens;
+//
+//        private void reset() {
+//            mLatch = new CountDownLatch(1);
+//            mTokens = null;
+//        }
+//
+//        private boolean await() throws InterruptedException {
+//            return mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+//        }
+//
+//        private boolean findToken(SessionToken2 token) {
+//            return mTokens.contains(token);
+//        }
+//
+//        @Override
+//        public void onSessionTokensChanged(List<SessionToken2> tokens) {
+//            mTokens = tokens;
+//            mLatch.countDown();
+//        }
+//    }
+}
diff --git a/androidx/media/MediaSessionService2.java b/androidx/media/MediaSessionService2.java
new file mode 100644
index 0000000..7bad65c
--- /dev/null
+++ b/androidx/media/MediaSessionService2.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaBrowserServiceCompat.BrowserRoot;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.SessionToken2.TokenType;
+
+import java.util.List;
+
+/**
+ * @hide
+ * Base class for media session services, which is the service version of the {@link MediaSession2}.
+ * <p>
+ * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
+ * to keep media playback in the background.
+ * <p>
+ * Here's the benefits of using {@link MediaSessionService2} instead of
+ * {@link MediaSession2}.
+ * <ul>
+ * <li>Another app can know that your app supports {@link MediaSession2} even when your app
+ * isn't running.
+ * <li>Another app can start playback of your app even when your app isn't running.
+ * </ul>
+ * For example, user's voice command can start playback of your app even when it's not running.
+ * <p>
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ *   &lt;intent-filter&gt;
+ *     &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ *   &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * A {@link MediaSessionService2} is another form of {@link MediaSession2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ *   &lt;intent-filter&gt;
+ *     &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ *   &lt;/intent-filter&gt;
+ *   &lt;meta-data android:name="android.media.session"
+ *       android:value="session_id"/&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * It's recommended for an app to have a single {@link MediaSessionService2} declared in the
+ * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another
+ * app fails to pick the right session service when it wants to start the playback this app.
+ * <p>
+ * If there's conflicts with the session ID among the services, services wouldn't be available for
+ * any controllers.
+ * <p>
+ * Topic covered here:
+ * <ol>
+ * <li><a href="#ServiceLifecycle">Service Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * </ol>
+ * <div class="special reference">
+ * <a name="ServiceLifecycle"></a>
+ * <h3>Service Lifecycle</h3>
+ * <p>
+ * Session service is bounded service. When a {@link MediaController2} is created for the
+ * session service, the controller binds to the session service. {@link #onCreateSession(String)}
+ * may be called after the {@link #onCreate} if the service hasn't created yet.
+ * <p>
+ * After the binding, session's
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}
+ *
+ * will be called to accept or reject connection request from a controller. If the connection is
+ * rejected, the controller will unbind. If it's accepted, the controller will be available to use
+ * and keep binding.
+ * <p>
+ * When playback is started for this session service, {@link #onUpdateNotification()}
+ * is called and service would become a foreground service. It's needed to keep playback after the
+ * controller is destroyed. The session service becomes background service when the playback is
+ * stopped.
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>
+ * Any app can bind to the session service with controller, but the controller can be used only if
+ * the session service accepted the connection request through
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MediaSessionService2 extends Service {
+    //private final MediaSessionService2Provider mProvider;
+
+    /**
+     * This is the interface name that a service implementing a session service should say that it
+     * support -- that is, this is the action it uses for its intent filter.
+     */
+    public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2";
+
+    /**
+     * Name under which a MediaSessionService2 component publishes information about itself.
+     * This meta-data must provide a string value for the ID.
+     */
+    public static final String SERVICE_META_DATA = "android.media.session";
+
+    // Stub BrowserRoot for accepting any connction here.
+    // See MyBrowserService#onGetRoot() for detail.
+    static final BrowserRoot sDefaultBrowserRoot = new BrowserRoot(SERVICE_INTERFACE, null);
+
+    private final MediaBrowserServiceCompat mBrowserServiceCompat;
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private NotificationManager mNotificationManager;
+    @GuardedBy("mLock")
+    private Intent mStartSelfIntent;
+    @GuardedBy("mLock")
+    private boolean mIsRunningForeground;
+    @GuardedBy("mLock")
+    private MediaSession2 mSession;
+
+    public MediaSessionService2() {
+        super();
+        mBrowserServiceCompat = createBrowserServiceCompat();
+    }
+
+    MediaBrowserServiceCompat createBrowserServiceCompat() {
+        return new MyBrowserService();
+    }
+
+    /**
+     * Default implementation for {@link MediaSessionService2} to initialize session service.
+     * <p>
+     * Override this method if you need your own initialization. Derived classes MUST call through
+     * to the super class's implementation of this method.
+     */
+    @CallSuper
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mBrowserServiceCompat.attachToBaseContext(this);
+        mBrowserServiceCompat.onCreate();
+        SessionToken2 token = new SessionToken2(this,
+                new ComponentName(getPackageName(), getClass().getName()));
+        if (token.getType() != getSessionType()) {
+            throw new RuntimeException("Expected session type " + getSessionType()
+                    + " but was " + token.getType());
+        }
+        MediaSession2 session = onCreateSession(token.getId());
+        synchronized (mLock) {
+            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+            mStartSelfIntent = new Intent(this, getClass());
+            mSession = session;
+            if (mSession == null || !token.getId().equals(mSession.getToken().getId())) {
+                throw new RuntimeException("Expected session with id " + token.getId()
+                        + ", but got " + mSession);
+            }
+            mBrowserServiceCompat.setSessionToken(mSession.getToken().getSessionCompatToken());
+        }
+    }
+
+    @TokenType int getSessionType() {
+        return SessionToken2.TYPE_SESSION_SERVICE;
+    }
+
+    /**
+     * Called when another app requested to start this service to get {@link MediaSession2}.
+     * <p>
+     * Session service will accept or reject the connection with the
+     * {@link MediaSession2.SessionCallback} in the created session.
+     * <p>
+     * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+     * expected ID that you've specified through the AndroidManifest.xml.
+     * <p>
+     * This method will be called on the main thread.
+     *
+     * @param sessionId session id written in the AndroidManifest.xml.
+     * @return a new session
+     * @see MediaSession2.Builder
+     * @see #getSession()
+     */
+    public @NonNull abstract MediaSession2 onCreateSession(String sessionId);
+
+    /**
+     * Called when the playback state of this session is changed so notification needs update.
+     * Override this method to show or cancel your own notification UI.
+     * <p>
+     * With the notification returned here, the service become foreground service when the playback
+     * is started. It becomes background service after the playback is stopped.
+     *
+     * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown.
+     */
+    public @Nullable MediaNotification onUpdateNotification() {
+        return null;
+    }
+
+    /**
+     * Get instance of the {@link MediaSession2} that you've previously created with the
+     * {@link #onCreateSession} for this service.
+     * <p>
+     * This may be {@code null} before the {@link #onCreate()} is finished.
+     *
+     * @return created session
+     */
+    public final @Nullable MediaSession2 getSession() {
+        synchronized (mLock) {
+            return mSession;
+        }
+    }
+
+    /**
+     * Default implementation for {@link MediaSessionService2} to handle incoming binding
+     * request. If the request is for getting the session, the intent will have action
+     * {@link #SERVICE_INTERFACE}.
+     * <p>
+     * Override this method if this service also needs to handle binder requests other than
+     * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's
+     * implementation of this method.
+     *
+     * @param intent
+     * @return Binder
+     */
+    @CallSuper
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (MediaSessionService2.SERVICE_INTERFACE.equals(intent.getAction())
+                || MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) {
+            // Change the intent action for browser service.
+            Intent browserServiceIntent = new Intent(intent);
+            browserServiceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE);
+            return mBrowserServiceCompat.onBind(intent);
+        }
+        return null;
+    }
+
+    MediaBrowserServiceCompat getServiceCompat() {
+        return mBrowserServiceCompat;
+    }
+
+    /**
+     * Returned by {@link #onUpdateNotification()} for making session service foreground service
+     * to keep playback running in the background. It's highly recommended to show media style
+     * notification here.
+     */
+    public static class MediaNotification {
+        private final int mNotificationId;
+        private final Notification mNotification;
+
+        /**
+         * Default constructor
+         *
+         * @param notificationId notification id to be used for
+         *      {@link NotificationManager#notify(int, Notification)}.
+         * @param notification a notification to make session service foreground service. Media
+         *      style notification is recommended here.
+         */
+        public MediaNotification(int notificationId, @NonNull Notification notification) {
+            if (notification == null) {
+                throw new IllegalArgumentException("notification shouldn't be null");
+            }
+            mNotificationId = notificationId;
+            mNotification = notification;
+        }
+
+        /**
+         * Gets the id of the id.
+         *
+         * @return the notification id
+         */
+        public int getNotificationId() {
+            return mNotificationId;
+        }
+
+        /**
+         * Gets the notification.
+         *
+         * @return the notification
+         */
+        public @NonNull Notification getNotification() {
+            return mNotification;
+        }
+    }
+
+    private static class MyBrowserService extends MediaBrowserServiceCompat {
+        @Override
+        public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+            // Returns *stub* root here. Here's the reason.
+            //   1. A non-null BrowserRoot should be returned here to keep the binding
+            //   2. MediaSessionService2 is defined as the simplified version of the library
+            //      service with no browsing feature, so shouldn't allow MediaBrowserServiceCompat
+            //      specific operations.
+            // TODO: Revisit here API not to return stub root here. The fake media ID here may be
+            //       used by the browser service for real.
+            return sDefaultBrowserRoot;
+        }
+
+        @Override
+        public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
+            // Disallow loading children.
+        }
+    }
+}
diff --git a/androidx/media/MediaStubActivity.java b/androidx/media/MediaStubActivity.java
new file mode 100644
index 0000000..44d8ad9
--- /dev/null
+++ b/androidx/media/MediaStubActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import androidx.media.test.R;
+
+public class MediaStubActivity extends Activity {
+    private static final String TAG = "MediaStubActivity";
+    private SurfaceHolder mHolder;
+    private SurfaceHolder mHolder2;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.mediaplayer);
+
+        SurfaceView surfaceV = (SurfaceView) findViewById(R.id.surface);
+        mHolder = surfaceV.getHolder();
+
+        SurfaceView surfaceV2 = (SurfaceView) findViewById(R.id.surface2);
+        mHolder2 = surfaceV2.getHolder();
+    }
+
+    @Override
+    protected void onResume() {
+        Log.i(TAG, "onResume");
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        Log.i(TAG, "onPause");
+        super.onPause();
+    }
+    public SurfaceHolder getSurfaceHolder() {
+        return mHolder;
+    }
+
+    public SurfaceHolder getSurfaceHolder2() {
+        return mHolder2;
+    }
+}
diff --git a/androidx/media/MediaUtils2.java b/androidx/media/MediaUtils2.java
new file mode 100644
index 0000000..657e24d
--- /dev/null
+++ b/androidx/media/MediaUtils2.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.media.AudioAttributesCompat.CONTENT_TYPE_UNKNOWN;
+import static androidx.media.AudioAttributesCompat.USAGE_UNKNOWN;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_DESCRIPTION;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON_URI;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_SUBTITLE;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_TITLE;
+import static androidx.media.MediaMetadata2.METADATA_KEY_EXTRAS;
+import static androidx.media.MediaMetadata2.METADATA_KEY_MEDIA_ID;
+import static androidx.media.MediaMetadata2.METADATA_KEY_MEDIA_URI;
+import static androidx.media.MediaMetadata2.METADATA_KEY_TITLE;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import androidx.media.MediaSession2.CommandButton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class MediaUtils2 {
+    static final String AUDIO_ATTRIBUTES_USAGE = "androidx.media.audio_attrs.USAGE";
+    static final String AUDIO_ATTRIBUTES_CONTENT_TYPE = "androidx.media.audio_attrs.CONTENT_TYPE";
+    static final String AUDIO_ATTRIBUTES_FLAGS = "androidx.media.audio_attrs.FLAGS";
+
+    private MediaUtils2() {
+    }
+
+    /**
+     * Creates a {@link MediaItem} from the {@link MediaItem2}.
+     *
+     * @param item2 an item.
+     * @return The newly created media item.
+     */
+    static MediaItem createMediaItem(MediaItem2 item2) {
+        if (item2 == null) {
+            return null;
+        }
+        MediaDescriptionCompat descCompat;
+
+        MediaMetadata2 metadata = item2.getMetadata();
+        if (metadata == null) {
+            descCompat = new MediaDescriptionCompat.Builder()
+                    .setMediaId(item2.getMediaId())
+                    .build();
+        } else {
+            MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder()
+                    .setMediaId(item2.getMediaId())
+                    .setSubtitle(metadata.getText(METADATA_KEY_DISPLAY_SUBTITLE))
+                    .setDescription(metadata.getText(METADATA_KEY_DISPLAY_DESCRIPTION))
+                    .setIconBitmap(metadata.getBitmap(METADATA_KEY_DISPLAY_ICON))
+                    .setExtras(metadata.getExtras());
+
+            String title = metadata.getString(METADATA_KEY_TITLE);
+            if (title != null) {
+                builder.setTitle(title);
+            } else {
+                builder.setTitle(metadata.getString(METADATA_KEY_DISPLAY_TITLE));
+            }
+
+            String displayIconUri = metadata.getString(METADATA_KEY_DISPLAY_ICON_URI);
+            if (displayIconUri != null) {
+                builder.setIconUri(Uri.parse(displayIconUri));
+            }
+
+            String mediaUri = metadata.getString(METADATA_KEY_MEDIA_URI);
+            if (mediaUri != null) {
+                builder.setMediaUri(Uri.parse(mediaUri));
+            }
+
+            descCompat = builder.build();
+        }
+        return new MediaItem(descCompat, item2.getFlags());
+    }
+
+    /**
+     * Creates a {@link MediaItem2} from the {@link MediaItem}.
+     *
+     * @param item an item.
+     * @return The newly created media item.
+     */
+    static MediaItem2 createMediaItem2(MediaItem item) {
+        if (item == null || item.getMediaId() == null) {
+            return null;
+        }
+
+        MediaMetadata2 metadata2 = createMediaMetadata2(item.getDescription());
+        return new MediaItem2.Builder(item.getFlags())
+                .setMediaId(item.getMediaId())
+                .setMetadata(metadata2)
+                .build();
+    }
+
+    /**
+     * Creates a {@link MediaMetadata2} from the {@link MediaDescriptionCompat}.
+     *
+     * @param descCompat A {@link MediaDescriptionCompat} object.
+     * @return The newly created {@link MediaMetadata2} object.
+     */
+    static MediaMetadata2 createMediaMetadata2(MediaDescriptionCompat descCompat) {
+        if (descCompat == null) {
+            return null;
+        }
+
+        MediaMetadata2.Builder metadata2Builder = new MediaMetadata2.Builder();
+        metadata2Builder.putString(METADATA_KEY_MEDIA_ID, descCompat.getMediaId());
+
+        CharSequence title = descCompat.getTitle();
+        if (title != null) {
+            metadata2Builder.putText(METADATA_KEY_DISPLAY_TITLE, title);
+        }
+
+        CharSequence description = descCompat.getDescription();
+        if (description != null) {
+            metadata2Builder.putText(METADATA_KEY_DISPLAY_DESCRIPTION, descCompat.getDescription());
+        }
+
+        CharSequence subtitle = descCompat.getSubtitle();
+        if (subtitle != null) {
+            metadata2Builder.putText(METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
+        }
+
+        Bitmap icon = descCompat.getIconBitmap();
+        if (icon != null) {
+            metadata2Builder.putBitmap(METADATA_KEY_DISPLAY_ICON, icon);
+        }
+
+        Uri iconUri = descCompat.getIconUri();
+        if (iconUri != null) {
+            metadata2Builder.putText(METADATA_KEY_DISPLAY_ICON_URI, iconUri.toString());
+        }
+
+        Bundle bundle = descCompat.getExtras();
+        if (bundle != null) {
+            metadata2Builder.setExtras(descCompat.getExtras());
+        }
+
+        Uri mediaUri = descCompat.getMediaUri();
+        if (mediaUri != null) {
+            metadata2Builder.putText(METADATA_KEY_MEDIA_URI, mediaUri.toString());
+        }
+
+        return metadata2Builder.build();
+    }
+
+    /**
+     * Creates a {@link MediaMetadata2} from the {@link MediaMetadataCompat}.
+     *
+     * @param metadataCompat A {@link MediaMetadataCompat} object.
+     * @return The newly created {@link MediaMetadata2} object.
+     */
+    MediaMetadata2 createMediaMetadata2(MediaMetadataCompat metadataCompat) {
+        if (metadataCompat == null) {
+            return null;
+        }
+        return new MediaMetadata2(metadataCompat.getBundle());
+    }
+
+    /**
+     * Creates a {@link MediaMetadataCompat} from the {@link MediaMetadata2}.
+     *
+     * @param metadata2 A {@link MediaMetadata2} object.
+     * @return The newly created {@link MediaMetadataCompat} object.
+     */
+    MediaMetadataCompat createMediaMetadataCompat(MediaMetadata2 metadata2) {
+        if (metadata2 == null) {
+            return null;
+        }
+
+        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+        List<String> skippedKeys = new ArrayList<>();
+        Bundle bundle = metadata2.toBundle();
+        for (String key : bundle.keySet()) {
+            Object value = bundle.get(key);
+            if (value instanceof CharSequence) {
+                builder.putText(key, (CharSequence) value);
+            } else if (value instanceof Rating2) {
+                builder.putRating(key, createRatingCompat((Rating2) value));
+            } else if (value instanceof Bitmap) {
+                builder.putBitmap(key, (Bitmap) value);
+            } else if (value instanceof Long) {
+                builder.putLong(key, (Long) value);
+            } else {
+                // There is no 'float' or 'bundle' type in MediaMetadataCompat.
+                skippedKeys.add(key);
+            }
+        }
+
+        MediaMetadataCompat result = builder.build();
+        for (String key : skippedKeys) {
+            Object value = bundle.get(key);
+            if (value instanceof Float) {
+                // Compatibility for MediaMetadata2.Builder.putFloat()
+                result.getBundle().putFloat(key, (Float) value);
+            } else if (METADATA_KEY_EXTRAS.equals(value)) {
+                // Compatibility for MediaMetadata2.Builder.setExtras()
+                result.getBundle().putBundle(key, (Bundle) value);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Creates a {@link Rating2} from the {@link RatingCompat}.
+     *
+     * @param ratingCompat A {@link RatingCompat} object.
+     * @return The newly created {@link Rating2} object.
+     */
+    Rating2 createRating2(RatingCompat ratingCompat) {
+        if (ratingCompat == null) {
+            return null;
+        }
+        if (!ratingCompat.isRated()) {
+            return Rating2.newUnratedRating(ratingCompat.getRatingStyle());
+        }
+
+        switch (ratingCompat.getRatingStyle()) {
+            case RatingCompat.RATING_3_STARS:
+            case RatingCompat.RATING_4_STARS:
+            case RatingCompat.RATING_5_STARS:
+                return Rating2.newStarRating(
+                        ratingCompat.getRatingStyle(), ratingCompat.getStarRating());
+            case RatingCompat.RATING_HEART:
+                return Rating2.newHeartRating(ratingCompat.hasHeart());
+            case RatingCompat.RATING_THUMB_UP_DOWN:
+                return Rating2.newThumbRating(ratingCompat.isThumbUp());
+            case RatingCompat.RATING_PERCENTAGE:
+                return Rating2.newPercentageRating(ratingCompat.getPercentRating());
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * Creates a {@link RatingCompat} from the {@link Rating2}.
+     *
+     * @param rating2 A {@link Rating2} object.
+     * @return The newly created {@link RatingCompat} object.
+     */
+    RatingCompat createRatingCompat(Rating2 rating2) {
+        if (rating2 == null) {
+            return null;
+        }
+        if (!rating2.isRated()) {
+            return RatingCompat.newUnratedRating(rating2.getRatingStyle());
+        }
+
+        switch (rating2.getRatingStyle()) {
+            case Rating2.RATING_3_STARS:
+            case Rating2.RATING_4_STARS:
+            case Rating2.RATING_5_STARS:
+                return RatingCompat.newStarRating(
+                        rating2.getRatingStyle(), rating2.getStarRating());
+            case Rating2.RATING_HEART:
+                return RatingCompat.newHeartRating(rating2.hasHeart());
+            case Rating2.RATING_THUMB_UP_DOWN:
+                return RatingCompat.newThumbRating(rating2.isThumbUp());
+            case Rating2.RATING_PERCENTAGE:
+                return RatingCompat.newPercentageRating(rating2.getPercentRating());
+            default:
+                return null;
+        }
+    }
+
+    static Parcelable[] toMediaItem2ParcelableArray(List<MediaItem2> playlist) {
+        if (playlist == null) {
+            return null;
+        }
+        List<Parcelable> parcelableList = new ArrayList<>();
+        for (int i = 0; i < playlist.size(); i++) {
+            final MediaItem2 item = playlist.get(i);
+            if (item != null) {
+                final Parcelable itemBundle = item.toBundle();
+                if (itemBundle != null) {
+                    parcelableList.add(itemBundle);
+                }
+            }
+        }
+        return parcelableList.toArray(new Parcelable[0]);
+    }
+
+    static List<MediaItem2> fromMediaItem2ParcelableArray(Parcelable[] itemParcelableList) {
+        List<MediaItem2> playlist = new ArrayList<>();
+        if (itemParcelableList != null) {
+            for (int i = 0; i < itemParcelableList.length; i++) {
+                if (!(itemParcelableList[i] instanceof Bundle)) {
+                    continue;
+                }
+                MediaItem2 item = MediaItem2.fromBundle((Bundle) itemParcelableList[i]);
+                if (item != null) {
+                    playlist.add(item);
+                }
+            }
+        }
+        return playlist;
+    }
+
+    static Parcelable[] toCommandButtonParcelableArray(List<CommandButton> layout) {
+        if (layout == null) {
+            return null;
+        }
+        List<Bundle> layoutBundles = new ArrayList<>();
+        for (int i = 0; i < layout.size(); i++) {
+            Bundle bundle = layout.get(i).toBundle();
+            if (bundle != null) {
+                layoutBundles.add(bundle);
+            }
+        }
+        return layoutBundles.toArray(new Parcelable[0]);
+    }
+
+    static List<CommandButton> fromCommandButtonParcelableArray(Parcelable[] list) {
+        List<CommandButton> layout = new ArrayList<>();
+        if (layout != null) {
+            for (int i = 0; i < list.length; i++) {
+                if (!(list[i] instanceof Bundle)) {
+                    continue;
+                }
+                CommandButton button = CommandButton.fromBundle((Bundle) list[i]);
+                if (button != null) {
+                    layout.add(button);
+                }
+            }
+        }
+        return layout;
+    }
+
+    static Bundle toAudioAttributesBundle(AudioAttributesCompat attrs) {
+        if (attrs == null) {
+            return null;
+        }
+        Bundle bundle = new Bundle();
+        bundle.putInt(AUDIO_ATTRIBUTES_USAGE, attrs.getUsage());
+        bundle.putInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, attrs.getContentType());
+        bundle.putInt(AUDIO_ATTRIBUTES_FLAGS, attrs.getFlags());
+        return bundle;
+    }
+
+    static AudioAttributesCompat fromAudioAttributesBundle(Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        return new AudioAttributesCompat.Builder()
+                .setUsage(bundle.getInt(AUDIO_ATTRIBUTES_USAGE, USAGE_UNKNOWN))
+                .setContentType(bundle.getInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, CONTENT_TYPE_UNKNOWN))
+                .setFlags(bundle.getInt(AUDIO_ATTRIBUTES_FLAGS, 0))
+                .build();
+    }
+
+    static List<Bundle> toBundleList(Parcelable[] array) {
+        if (array == null) {
+            return null;
+        }
+        List<Bundle> bundleList = new ArrayList<>();
+        for (Parcelable p : array) {
+            bundleList.add((Bundle) p);
+        }
+        return bundleList;
+    }
+
+    static int createPlaybackStateCompatState(int playerState, int bufferingState) {
+        switch (playerState) {
+            case MediaPlayerBase.PLAYER_STATE_PLAYING:
+                switch (bufferingState) {
+                    case MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED:
+                        return PlaybackStateCompat.STATE_BUFFERING;
+                }
+                return PlaybackStateCompat.STATE_PLAYING;
+            case MediaPlayerBase.PLAYER_STATE_PAUSED:
+                return PlaybackStateCompat.STATE_PAUSED;
+            case MediaPlayerBase.PLAYER_STATE_IDLE:
+                return PlaybackStateCompat.STATE_NONE;
+            case MediaPlayerBase.PLAYER_STATE_ERROR:
+                return PlaybackStateCompat.STATE_ERROR;
+        }
+        // For unknown value
+        return PlaybackStateCompat.STATE_ERROR;
+    }
+
+    static int toPlayerState(int playbackStateCompatState) {
+        switch (playbackStateCompatState) {
+            case PlaybackStateCompat.STATE_ERROR:
+                return MediaPlayerBase.PLAYER_STATE_ERROR;
+            case PlaybackStateCompat.STATE_NONE:
+                return MediaPlayerBase.PLAYER_STATE_IDLE;
+            case PlaybackStateCompat.STATE_PAUSED:
+            case PlaybackStateCompat.STATE_STOPPED:
+            case PlaybackStateCompat.STATE_BUFFERING: // means paused for buffering.
+                return MediaPlayerBase.PLAYER_STATE_PAUSED;
+            case PlaybackStateCompat.STATE_FAST_FORWARDING:
+            case PlaybackStateCompat.STATE_PLAYING:
+            case PlaybackStateCompat.STATE_REWINDING:
+            case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT:
+            case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS:
+            case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM:
+            case PlaybackStateCompat.STATE_CONNECTING: // Note: there's no perfect match for this.
+                return MediaPlayerBase.PLAYER_STATE_PLAYING;
+        }
+        return MediaPlayerBase.PLAYER_STATE_ERROR;
+    }
+
+    static boolean isDefaultLibraryRootHint(Bundle bundle) {
+        return bundle != null && bundle.getBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, false);
+    }
+}
diff --git a/android/security/keystore/SessionExpiredException.java b/androidx/media/MockActivity.java
similarity index 61%
copy from android/security/keystore/SessionExpiredException.java
copy to androidx/media/MockActivity.java
index 7c8d5e4..df0af16 100644
--- a/android/security/keystore/SessionExpiredException.java
+++ b/androidx/media/MockActivity.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright 2018 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.
@@ -14,14 +14,9 @@
  * limitations under the License.
  */
 
-package android.security.keystore;
+package androidx.media;
 
-/**
- * @deprecated Use {@link android.security.keystore.recovery.SessionExpiredException}.
- * @hide
- */
-public class SessionExpiredException extends RecoveryControllerException {
-    public SessionExpiredException(String msg) {
-        super(msg);
-    }
+import android.app.Activity;
+
+public class MockActivity extends Activity {
 }
diff --git a/androidx/media/MockMediaLibraryService2.java b/androidx/media/MockMediaLibraryService2.java
new file mode 100644
index 0000000..8f5d5bb
--- /dev/null
+++ b/androidx/media/MockMediaLibraryService2.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Mock implementation of {@link MediaLibraryService2} for testing.
+ */
+public class MockMediaLibraryService2 extends MediaLibraryService2 {
+    // Keep in sync with the AndroidManifest.xml
+    public static final String ID = "TestLibrary";
+
+    public static final String ROOT_ID = "rootId";
+    public static final Bundle EXTRAS = new Bundle();
+
+    public static final String MEDIA_ID_GET_ITEM = "media_id_get_item";
+
+    public static final String PARENT_ID = "parent_id";
+    public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children";
+    public static final String PARENT_ID_ERROR = "parent_id_error";
+
+    public static final List<MediaItem2> GET_CHILDREN_RESULT = new ArrayList<>();
+    public static final int CHILDREN_COUNT = 100;
+
+    public static final String SEARCH_QUERY = "search_query";
+    public static final String SEARCH_QUERY_TAKES_TIME = "search_query_takes_time";
+    public static final int SEARCH_TIME_IN_MS = 5000;
+    public static final String SEARCH_QUERY_EMPTY_RESULT = "search_query_empty_result";
+
+    public static final List<MediaItem2> SEARCH_RESULT = new ArrayList<>();
+    public static final int SEARCH_RESULT_COUNT = 50;
+
+    // TODO(jaewan): Uncomment here after DataSourceDesc.builder is ready.
+//    private static final DataSourceDesc DATA_SOURCE_DESC =
+//            new DataSourceDesc.Builder().setDataSource(new FileDescriptor()).build();
+    private static final DataSourceDesc DATA_SOURCE_DESC = null;
+
+    private static final String TAG = "MockMediaLibrarySvc2";
+
+    static {
+        EXTRAS.putString(ROOT_ID, ROOT_ID);
+    }
+    @GuardedBy("MockMediaLibraryService2.class")
+    private static SessionToken2 sToken;
+
+    private MediaLibrarySession mSession;
+
+    public MockMediaLibraryService2() {
+        super();
+        GET_CHILDREN_RESULT.clear();
+        String getChildrenMediaIdPrefix = "get_children_media_id_";
+        for (int i = 0; i < CHILDREN_COUNT; i++) {
+            GET_CHILDREN_RESULT.add(createMediaItem(getChildrenMediaIdPrefix + i));
+        }
+
+        SEARCH_RESULT.clear();
+        String getSearchResultMediaIdPrefix = "get_search_result_media_id_";
+        for (int i = 0; i < SEARCH_RESULT_COUNT; i++) {
+            SEARCH_RESULT.add(createMediaItem(getSearchResultMediaIdPrefix + i));
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        TestServiceRegistry.getInstance().setServiceInstance(this);
+        super.onCreate();
+    }
+
+    @Override
+    public MediaLibrarySession onCreateSession(String sessionId) {
+        final MockPlayer player = new MockPlayer(1);
+        final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+        final Executor executor = new Executor() {
+            @Override
+            public void execute(Runnable runnable) {
+                handler.post(runnable);
+            }
+        };
+        SessionCallback callback = TestServiceRegistry.getInstance().getSessionCallback();
+        MediaLibrarySessionCallback librarySessionCallback;
+        if (callback instanceof MediaLibrarySessionCallback) {
+            librarySessionCallback = (MediaLibrarySessionCallback) callback;
+        } else {
+            // Callback hasn't set. Use default callback
+            librarySessionCallback = new TestLibrarySessionCallback();
+        }
+        mSession = new MediaLibrarySession.Builder(MockMediaLibraryService2.this, executor,
+                librarySessionCallback).setPlayer(player).setId(sessionId).build();
+        return mSession;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        TestServiceRegistry.getInstance().cleanUp();
+    }
+
+    public static SessionToken2 getToken(Context context) {
+        synchronized (MockMediaLibraryService2.class) {
+            if (sToken == null) {
+                sToken = new SessionToken2(context, new ComponentName(
+                        context.getPackageName(), MockMediaLibraryService2.class.getName()));
+                assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, sToken.getType());
+            }
+            return sToken;
+        }
+    }
+
+    private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
+        @Override
+        public LibraryRoot onGetLibraryRoot(MediaLibrarySession session, ControllerInfo controller,
+                Bundle rootHints) {
+            return new LibraryRoot(ROOT_ID, EXTRAS);
+        }
+
+        @Override
+        public MediaItem2 onGetItem(MediaLibrarySession session, ControllerInfo controller,
+                String mediaId) {
+            if (MEDIA_ID_GET_ITEM.equals(mediaId)) {
+                return createMediaItem(mediaId);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+                ControllerInfo controller, String parentId, int page, int pageSize, Bundle extras) {
+            if (PARENT_ID.equals(parentId)) {
+                return getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize);
+            } else if (PARENT_ID_ERROR.equals(parentId)) {
+                return null;
+            }
+            // Includes the case of PARENT_ID_NO_CHILDREN.
+            return new ArrayList<>();
+        }
+
+        @Override
+        public void onSearch(MediaLibrarySession session, final ControllerInfo controllerInfo,
+                final String query, final Bundle extras) {
+            if (SEARCH_QUERY.equals(query)) {
+                mSession.notifySearchResultChanged(controllerInfo, query, SEARCH_RESULT_COUNT,
+                        extras);
+            } else if (SEARCH_QUERY_TAKES_TIME.equals(query)) {
+                // Searching takes some time. Notify after 5 seconds.
+                Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
+                    @Override
+                    public void run() {
+                        mSession.notifySearchResultChanged(
+                                controllerInfo, query, SEARCH_RESULT_COUNT, extras);
+                    }
+                }, SEARCH_TIME_IN_MS, TimeUnit.MILLISECONDS);
+            } else if (SEARCH_QUERY_EMPTY_RESULT.equals(query)) {
+                mSession.notifySearchResultChanged(controllerInfo, query, 0, extras);
+            } else {
+                // TODO: For the error case, how should we notify the browser?
+            }
+        }
+
+        @Override
+        public List<MediaItem2> onGetSearchResult(MediaLibrarySession session,
+                ControllerInfo controllerInfo, String query, int page, int pageSize,
+                Bundle extras) {
+            if (SEARCH_QUERY.equals(query)) {
+                return getPaginatedResult(SEARCH_RESULT, page, pageSize);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    private List<MediaItem2> getPaginatedResult(List<MediaItem2> items, int page, int pageSize) {
+        if (items == null) {
+            return null;
+        } else if (items.size() == 0) {
+            return new ArrayList<>();
+        }
+
+        final int totalItemCount = items.size();
+        int fromIndex = (page - 1) * pageSize;
+        int toIndex = Math.min(page * pageSize, totalItemCount);
+
+        List<MediaItem2> paginatedResult = new ArrayList<>();
+        try {
+            // The case of (fromIndex >= totalItemCount) will throw exception below.
+            paginatedResult = items.subList(fromIndex, toIndex);
+        } catch (IndexOutOfBoundsException | IllegalArgumentException ex) {
+            Log.d(TAG, "Result is empty for given pagination arguments: totalItemCount="
+                    + totalItemCount + ", page=" + page + ", pageSize=" + pageSize, ex);
+        }
+        return paginatedResult;
+    }
+
+    private MediaItem2 createMediaItem(String mediaId) {
+        Context context = MockMediaLibraryService2.this;
+        return new MediaItem2.Builder(0 /* Flags */)
+                .setMediaId(mediaId)
+                .setDataSourceDesc(DATA_SOURCE_DESC)
+                .setMetadata(new MediaMetadata2.Builder()
+                                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+                                .build())
+                .build();
+    }
+}
diff --git a/androidx/media/MockMediaSessionService2.java b/androidx/media/MockMediaSessionService2.java
new file mode 100644
index 0000000..bee5357
--- /dev/null
+++ b/androidx/media/MockMediaSessionService2.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Mock implementation of {@link MediaSessionService2} for testing.
+ */
+public class MockMediaSessionService2 extends MediaSessionService2 {
+    // Keep in sync with the AndroidManifest.xml
+    public static final String ID = "TestSession";
+
+    private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
+    private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
+
+    private NotificationChannel mDefaultNotificationChannel;
+    private MediaSession2 mSession;
+    private NotificationManager mNotificationManager;
+
+    @Override
+    public void onCreate() {
+        TestServiceRegistry.getInstance().setServiceInstance(this);
+        super.onCreate();
+        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    @Override
+    public MediaSession2 onCreateSession(String sessionId) {
+        final MockPlayer player = new MockPlayer(1);
+        final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+        final Executor executor = new Executor() {
+            @Override
+            public void execute(Runnable runnable) {
+                handler.post(runnable);
+            }
+        };
+        SessionCallback sessionCallback = TestServiceRegistry.getInstance().getSessionCallback();
+        if (sessionCallback == null) {
+            // Ensures non-null
+            sessionCallback = new SessionCallback() {};
+        }
+        mSession = new MediaSession2.Builder(this)
+                .setPlayer(player)
+                .setSessionCallback(executor, sessionCallback)
+                .setId(sessionId).build();
+        return mSession;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        TestServiceRegistry.getInstance().cleanUp();
+    }
+
+    @Override
+    public MediaNotification onUpdateNotification() {
+        if (mDefaultNotificationChannel == null) {
+            mDefaultNotificationChannel = new NotificationChannel(
+                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+                    NotificationManager.IMPORTANCE_DEFAULT);
+            mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
+        }
+        Notification notification = new Notification.Builder(
+                this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
+                .setContentTitle(getPackageName())
+                .setContentText("Dummt test notification")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+        return new MediaNotification(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
+    }
+}
diff --git a/androidx/media/MockPlayer.java b/androidx/media/MockPlayer.java
new file mode 100644
index 0000000..49b1a19
--- /dev/null
+++ b/androidx/media/MockPlayer.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.util.ArrayMap;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerBase} for testing.
+ */
+public class MockPlayer extends MediaPlayerBase {
+    public final CountDownLatch mCountDownLatch;
+
+    public boolean mPlayCalled;
+    public boolean mPauseCalled;
+    public boolean mResetCalled;
+    public boolean mPrepareCalled;
+    public boolean mSeekToCalled;
+    public boolean mSetPlaybackSpeedCalled;
+    public long mSeekPosition;
+    public long mCurrentPosition;
+    public long mBufferedPosition;
+    public float mPlaybackSpeed = 1.0f;
+    public @PlayerState int mLastPlayerState;
+    public @BuffState int mLastBufferingState;
+
+    public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
+
+    private AudioAttributesCompat mAudioAttributes;
+
+    public MockPlayer(int count) {
+        mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+    }
+
+    @Override
+    public void close() {
+        // no-op
+    }
+
+    @Override
+    public void reset() {
+        mResetCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void play() {
+        mPlayCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void pause() {
+        mPauseCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void prepare() {
+        mPrepareCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        mSeekToCalled = true;
+        mSeekPosition = pos;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void skipToNext() {
+        // No-op. This skipToNext() means 'skip to next item in the setNextDataSources()'
+    }
+
+    @Override
+    public int getPlayerState() {
+        return mLastPlayerState;
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        return mCurrentPosition;
+    }
+
+    @Override
+    public long getBufferedPosition() {
+        return mBufferedPosition;
+    }
+
+    @Override
+    public float getPlaybackSpeed() {
+        return mPlaybackSpeed;
+    }
+
+    @Override
+    public int getBufferingState() {
+        return mLastBufferingState;
+    }
+
+    @Override
+    public void registerPlayerEventCallback(@NonNull Executor executor,
+            @NonNull PlayerEventCallback callback) {
+        if (callback == null || executor == null) {
+            throw new IllegalArgumentException("callback=" + callback + " executor=" + executor);
+        }
+        mCallbacks.put(callback, executor);
+    }
+
+    @Override
+    public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public void notifyPlaybackState(final int state) {
+        mLastPlayerState = state;
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlayerStateChanged(MockPlayer.this, state);
+                }
+            });
+        }
+    }
+
+    public void notifyBufferingState(final MediaItem2 item, final int bufferingState) {
+        mLastBufferingState = bufferingState;
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onBufferingStateChanged(
+                            MockPlayer.this, item.getDataSourceDesc(), bufferingState);
+                }
+            });
+        }
+    }
+
+    public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onCurrentDataSourceChanged(MockPlayer.this, dsd);
+                }
+            });
+        }
+    }
+
+    public void notifyMediaPrepared(final DataSourceDesc dsd) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onMediaPrepared(MockPlayer.this, dsd);
+                }
+            });
+        }
+    }
+
+    public void notifyBufferingStateChanged(final DataSourceDesc dsd,
+            final @BuffState int buffState) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onBufferingStateChanged(MockPlayer.this, dsd, buffState);
+                }
+            });
+        }
+    }
+
+    public void notifyPlaybackSpeedChanged(final float speed) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlaybackSpeedChanged(MockPlayer.this, speed);
+                }
+            });
+        }
+    }
+
+    public void notifyError(int what) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            // TODO: Uncomment or remove
+            //executor.execute(() -> callback.onError(null, what, 0));
+        }
+    }
+
+    @Override
+    public void setAudioAttributes(AudioAttributesCompat attributes) {
+        mAudioAttributes = attributes;
+    }
+
+    @Override
+    public AudioAttributesCompat getAudioAttributes() {
+        return mAudioAttributes;
+    }
+
+    @Override
+    public void setDataSource(@NonNull DataSourceDesc dsd) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+        // TODO: Implement this
+    }
+
+    @Override
+    public DataSourceDesc getCurrentDataSource() {
+        // TODO: Implement this
+        return null;
+    }
+
+    @Override
+    public void loopCurrent(boolean loop) {
+        // TODO: implement this
+    }
+
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        mSetPlaybackSpeedCalled = true;
+        mPlaybackSpeed = speed;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void setPlayerVolume(float volume) {
+        // TODO: implement this
+    }
+
+    @Override
+    public float getPlayerVolume() {
+        // TODO: implement this
+        return -1;
+    }
+}
diff --git a/androidx/media/MockPlaylistAgent.java b/androidx/media/MockPlaylistAgent.java
new file mode 100644
index 0000000..2f9d70a
--- /dev/null
+++ b/androidx/media/MockPlaylistAgent.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A mock implementation of {@link MediaPlaylistAgent} for testing.
+ * <p>
+ * Do not use mockito for {@link MediaPlaylistAgent}. Instead, use this.
+ * Mocks created from mockito should not be shared across different threads.
+ */
+public class MockPlaylistAgent extends MediaPlaylistAgent {
+    public final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+    public List<MediaItem2> mPlaylist;
+    public MediaMetadata2 mMetadata;
+    public MediaItem2 mCurrentMediaItem;
+    public MediaItem2 mItem;
+    public int mIndex = -1;
+    public @RepeatMode int mRepeatMode = -1;
+    public @ShuffleMode int mShuffleMode = -1;
+
+    public boolean mSetPlaylistCalled;
+    public boolean mUpdatePlaylistMetadataCalled;
+    public boolean mAddPlaylistItemCalled;
+    public boolean mRemovePlaylistItemCalled;
+    public boolean mReplacePlaylistItemCalled;
+    public boolean mSkipToPlaylistItemCalled;
+    public boolean mSkipToPreviousItemCalled;
+    public boolean mSkipToNextItemCalled;
+    public boolean mSetRepeatModeCalled;
+    public boolean mSetShuffleModeCalled;
+
+    @Override
+    public List<MediaItem2> getPlaylist() {
+        return mPlaylist;
+    }
+
+    @Override
+    public void setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata) {
+        mSetPlaylistCalled = true;
+        mPlaylist = list;
+        mMetadata = metadata;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public MediaMetadata2 getPlaylistMetadata() {
+        return mMetadata;
+    }
+
+    @Override
+    public void updatePlaylistMetadata(MediaMetadata2 metadata) {
+        mUpdatePlaylistMetadataCalled = true;
+        mMetadata = metadata;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public MediaItem2 getCurrentMediaItem() {
+        return mCurrentMediaItem;
+    }
+
+    @Override
+    public void addPlaylistItem(int index, MediaItem2 item) {
+        mAddPlaylistItemCalled = true;
+        mIndex = index;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void removePlaylistItem(MediaItem2 item) {
+        mRemovePlaylistItemCalled = true;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void replacePlaylistItem(int index, MediaItem2 item) {
+        mReplacePlaylistItemCalled = true;
+        mIndex = index;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void skipToPlaylistItem(MediaItem2 item) {
+        mSkipToPlaylistItemCalled = true;
+        mItem = item;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void skipToPreviousItem() {
+        mSkipToPreviousItemCalled = true;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public void skipToNextItem() {
+        mSkipToNextItemCalled = true;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public int getRepeatMode() {
+        return mRepeatMode;
+    }
+
+    @Override
+    public void setRepeatMode(int repeatMode) {
+        mSetRepeatModeCalled = true;
+        mRepeatMode = repeatMode;
+        mCountDownLatch.countDown();
+    }
+
+    @Override
+    public int getShuffleMode() {
+        return mShuffleMode;
+    }
+
+    @Override
+    public void setShuffleMode(int shuffleMode) {
+        mSetShuffleModeCalled = true;
+        mShuffleMode = shuffleMode;
+        mCountDownLatch.countDown();
+    }
+}
diff --git a/androidx/media/Rating2.java b/androidx/media/Rating2.java
new file mode 100644
index 0000000..8c81331
--- /dev/null
+++ b/androidx/media/Rating2.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.ObjectsCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class to encapsulate rating information used as content metadata.
+ * A rating is defined by its rating style (see {@link #RATING_HEART},
+ * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+ * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may
+ * be defined as "unrated"), both of which are defined when the rating instance is constructed
+ * through one of the factory methods.
+ */
+// New version of Rating with following change
+//   - Don't implement Parcelable for updatable support.
+public final class Rating2 {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({RATING_NONE, RATING_HEART, RATING_THUMB_UP_DOWN, RATING_3_STARS, RATING_4_STARS,
+            RATING_5_STARS, RATING_PERCENTAGE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Style {}
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({RATING_3_STARS, RATING_4_STARS, RATING_5_STARS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StarStyle {}
+
+    /**
+     * Indicates a rating style is not supported. A Rating2 will never have this
+     * type, but can be used by other classes to indicate they do not support
+     * Rating2.
+     */
+    public static final int RATING_NONE = 0;
+
+    /**
+     * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to
+     * indicate the content referred to is a favorite (or not).
+     */
+    public static final int RATING_HEART = 1;
+
+    /**
+     * A rating style for "thumb up" vs "thumb down".
+     */
+    public static final int RATING_THUMB_UP_DOWN = 2;
+
+    /**
+     * A rating style with 0 to 3 stars.
+     */
+    public static final int RATING_3_STARS = 3;
+
+    /**
+     * A rating style with 0 to 4 stars.
+     */
+    public static final int RATING_4_STARS = 4;
+
+    /**
+     * A rating style with 0 to 5 stars.
+     */
+    public static final int RATING_5_STARS = 5;
+
+    /**
+     * A rating style expressed as a percentage.
+     */
+    public static final int RATING_PERCENTAGE = 6;
+
+    private static final String TAG = "Rating2";
+
+    private static final float RATING_NOT_RATED = -1.0f;
+    private static final String KEY_STYLE = "android.media.rating2.style";
+    private static final String KEY_VALUE = "android.media.rating2.value";
+
+    private final int mRatingStyle;
+    private final float mRatingValue;
+
+    private Rating2(@Style int ratingStyle, float rating) {
+        mRatingStyle = ratingStyle;
+        mRatingValue = rating;
+    }
+
+    @Override
+    public String toString() {
+        return "Rating2:style=" + mRatingStyle + " rating="
+                + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof Rating2)) {
+            return false;
+        }
+        Rating2 other = (Rating2) obj;
+        return mRatingStyle == other.mRatingStyle && mRatingValue == other.mRatingValue;
+    }
+
+    @Override
+    public int hashCode() {
+        return ObjectsCompat.hash(mRatingStyle, mRatingValue);
+    }
+
+    /**
+     * Create an instance from bundle object, previously created by {@link #toBundle()}
+     *
+     * @param bundle bundle
+     * @return new Rating2 instance or {@code null} for error
+     */
+    public static Rating2 fromBundle(@Nullable Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        return new Rating2(bundle.getInt(KEY_STYLE), bundle.getFloat(KEY_VALUE));
+    }
+
+    /**
+     * Return bundle for this object to share across the process.
+     * @return bundle of this object
+     */
+    public Bundle toBundle() {
+        Bundle bundle = new Bundle();
+        bundle.putInt(KEY_STYLE, mRatingStyle);
+        bundle.putFloat(KEY_VALUE, mRatingValue);
+        return bundle;
+    }
+
+    /**
+     * Return a Rating2 instance with no rating.
+     * Create and return a new Rating2 instance with no rating known for the given
+     * rating style.
+     *
+     * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+     *    {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+     *    or {@link #RATING_PERCENTAGE}.
+     * @return null if an invalid rating style is passed, a new Rating2 instance otherwise.
+     */
+    public static @Nullable Rating2 newUnratedRating(@Style int ratingStyle) {
+        switch(ratingStyle) {
+            case RATING_HEART:
+            case RATING_THUMB_UP_DOWN:
+            case RATING_3_STARS:
+            case RATING_4_STARS:
+            case RATING_5_STARS:
+            case RATING_PERCENTAGE:
+                return new Rating2(ratingStyle, RATING_NOT_RATED);
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * Return a Rating2 instance with a heart-based rating.
+     * Create and return a new Rating2 instance with a rating style of {@link #RATING_HEART},
+     * and a heart-based rating.
+     *
+     * @param hasHeart true for a "heart selected" rating, false for "heart unselected".
+     * @return a new Rating2 instance.
+     */
+    public static @Nullable Rating2 newHeartRating(boolean hasHeart) {
+        return new Rating2(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+    }
+
+    /**
+     * Return a Rating2 instance with a thumb-based rating.
+     * Create and return a new Rating2 instance with a {@link #RATING_THUMB_UP_DOWN}
+     * rating style, and a "thumb up" or "thumb down" rating.
+     *
+     * @param thumbIsUp true for a "thumb up" rating, false for "thumb down".
+     * @return a new Rating2 instance.
+     */
+    public static @Nullable Rating2 newThumbRating(boolean thumbIsUp) {
+        return new Rating2(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+    }
+
+    /**
+     * Return a Rating2 instance with a star-based rating.
+     * Create and return a new Rating2 instance with one of the star-base rating styles
+     * and the given integer or fractional number of stars. Non integer values can for instance
+     * be used to represent an average rating value, which might not be an integer number of stars.
+     *
+     * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+     *     {@link #RATING_5_STARS}.
+     * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to
+     *     the rating style.
+     * @return null if the rating style is invalid, or the rating is out of range,
+     *     a new Rating2 instance otherwise.
+     */
+    public static @Nullable Rating2 newStarRating(@StarStyle int starRatingStyle,
+            float starRating) {
+        float maxRating;
+        switch(starRatingStyle) {
+            case RATING_3_STARS:
+                maxRating = 3.0f;
+                break;
+            case RATING_4_STARS:
+                maxRating = 4.0f;
+                break;
+            case RATING_5_STARS:
+                maxRating = 5.0f;
+                break;
+            default:
+                Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating");
+                return null;
+        }
+        if ((starRating < 0.0f) || (starRating > maxRating)) {
+            Log.e(TAG, "Trying to set out of range star-based rating");
+            return null;
+        }
+        return new Rating2(starRatingStyle, starRating);
+    }
+
+    /**
+     * Return a Rating2 instance with a percentage-based rating.
+     * Create and return a new Rating2 instance with a {@link #RATING_PERCENTAGE}
+     * rating style, and a rating of the given percentage.
+     *
+     * @param percent the value of the rating
+     * @return null if the rating is out of range, a new Rating2 instance otherwise.
+     */
+    public static @Nullable Rating2 newPercentageRating(float percent) {
+        if ((percent < 0.0f) || (percent > 100.0f)) {
+            Log.e(TAG, "Invalid percentage-based rating value");
+            return null;
+        } else {
+            return new Rating2(RATING_PERCENTAGE, percent);
+        }
+    }
+
+    /**
+     * Return whether there is a rating value available.
+     * @return true if the instance was not created with {@link #newUnratedRating(int)}.
+     */
+    public boolean isRated() {
+        return mRatingValue >= 0.0f;
+    }
+
+    /**
+     * Return the rating style.
+     * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+     *    {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+     *    or {@link #RATING_PERCENTAGE}.
+     */
+    public @Style int getRatingStyle() {
+        return mRatingStyle;
+    }
+
+    /**
+     * Return whether the rating is "heart selected".
+     * @return true if the rating is "heart selected", false if the rating is "heart unselected",
+     *    if the rating style is not {@link #RATING_HEART} or if it is unrated.
+     */
+    public boolean hasHeart() {
+        return mRatingStyle == RATING_HEART && mRatingValue == 1.0f;
+    }
+
+    /**
+     * Return whether the rating is "thumb up".
+     * @return true if the rating is "thumb up", false if the rating is "thumb down",
+     *    if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated.
+     */
+    public boolean isThumbUp() {
+        return mRatingStyle == RATING_THUMB_UP_DOWN && mRatingValue == 1.0f;
+    }
+
+    /**
+     * Return the star-based rating value.
+     * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+     *    not star-based, or if it is unrated.
+     */
+    public float getStarRating() {
+        switch (mRatingStyle) {
+            case RATING_3_STARS:
+            case RATING_4_STARS:
+            case RATING_5_STARS:
+                if (isRated()) {
+                    return mRatingValue;
+                }
+            // Fall through
+            default:
+                return -1.0f;
+        }
+    }
+
+    /**
+     * Return the percentage-based rating value.
+     * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+     *    not percentage-based, or if it is unrated.
+     */
+    public float getPercentRating() {
+        if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) {
+            return -1.0f;
+        } else {
+            return mRatingValue;
+        }
+    }
+}
diff --git a/androidx/media/SessionCommand2.java b/androidx/media/SessionCommand2.java
new file mode 100644
index 0000000..f017941
--- /dev/null
+++ b/androidx/media/SessionCommand2.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+
+import java.util.List;
+
+/**
+ * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
+ * <p>
+ * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
+ * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
+ * {@link #getCustomCommand()} shouldn't be {@code null}.
+ */
+public final class SessionCommand2 {
+    /**
+     * Command code for the custom command which can be defined by string action in the
+     * {@link SessionCommand2}.
+     */
+    public static final int COMMAND_CODE_CUSTOM = 0;
+
+    /**
+     * Command code for {@link MediaController2#play()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_PLAY = 1;
+
+    /**
+     * Command code for {@link MediaController2#pause()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
+
+    /**
+     * Command code for {@link MediaController2#reset()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_RESET = 3;
+
+    /**
+     * Command code for {@link MediaController2#skipToNextItem()}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the {@link SessionCallback#onCommandRequest(
+     * MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM = 4;
+
+    /**
+     * Command code for {@link MediaController2#skipToPreviousItem()}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the {@link SessionCallback#onCommandRequest(
+     * MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM = 5;
+
+    /**
+     * Command code for {@link MediaController2#prepare()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
+
+    /**
+     * Command code for {@link MediaController2#fastForward()}.
+     */
+    public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 7;
+
+    /**
+     * Command code for {@link MediaController2#rewind()}.
+     */
+    public static final int COMMAND_CODE_SESSION_REWIND = 8;
+
+    /**
+     * Command code for {@link MediaController2#seekTo(long)}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
+
+    /**
+     * Command code for both {@link MediaController2#setVolumeTo(int, int)}.
+     * <p>
+     * Command would set the device volume or send to the volume provider directly if the session
+     * doesn't reject the request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_VOLUME_SET_VOLUME = 10;
+
+    /**
+     * Command code for both {@link MediaController2#adjustVolume(int, int)}.
+     * <p>
+     * Command would adjust the device volume or send to the volume provider directly if the session
+     * doesn't reject the request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_VOLUME_ADJUST_VOLUME = 11;
+
+    /**
+     * Command code for {@link MediaController2#skipToPlaylistItem(MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM = 12;
+
+    /**
+     * Command code for {@link MediaController2#setShuffleMode(int)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE = 13;
+
+    /**
+     * Command code for {@link MediaController2#setRepeatMode(int)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE = 14;
+
+    /**
+     * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_ADD_ITEM = 15;
+
+    /**
+     * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_REMOVE_ITEM = 16;
+
+    /**
+     * Command code for {@link MediaController2#replacePlaylistItem(int, MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_REPLACE_ITEM = 17;
+
+    /**
+     * Command code for {@link MediaController2#getPlaylist()}. This will expose metadata
+     * information to the controller.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18;
+
+    /**
+     * Command code for {@link MediaController2#setPlaylist(List, MediaMetadata2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_LIST = 19;
+
+    /**
+     * Command code for {@link MediaController2#getPlaylistMetadata()}. This will expose
+     * metadata information to the controller.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20;
+
+    /**
+     * Command code for {@link MediaController2#updatePlaylistMetadata(MediaMetadata2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21;
+
+    /**
+     * Command code for {@link MediaController2#getCurrentMediaItem()}. This will expose
+     * metadata information to the controller.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM = 20;
+
+    /**
+     * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22;
+
+    /**
+     * Command code for {@link MediaController2#playFromUri(Uri, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 23;
+
+    /**
+     * Command code for {@link MediaController2#playFromSearch(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 24;
+
+    /**
+     * Command code for {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID = 25;
+
+    /**
+     * Command code for {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_URI = 26;
+
+    /**
+     * Command code for {@link MediaController2#prepareFromSearch(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH = 27;
+
+    /**
+     * Command code for {@link MediaController2#setRating(String, Rating2)}.
+     */
+    public static final int COMMAND_CODE_SESSION_SET_RATING = 28;
+
+    /**
+     * Command code for {@link MediaController2#subscribeRoutesInfo()}
+     */
+    public static final int COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO = 36;
+
+    /**
+     * Command code for {@link MediaController2#unsubscribeRoutesInfo()}
+     */
+    public static final int COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO = 37;
+
+    /**
+     * Command code for {@link MediaController2#selectRoute(Bundle)}}
+     */
+    public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 38;
+
+    /**
+     * @hide
+     * Command code for {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29;
+
+    /**
+     * @hide
+     * Command code for {@link MediaBrowser2#getItem(String)}.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30;
+
+    /**
+     * @hide
+     * Command code for {@link MediaBrowser2#getLibraryRoot(Bundle)}.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31;
+
+    /**
+     * @hide
+     * Command code for {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32;
+
+    /**
+     * @hide
+     * Command code for {@link MediaBrowser2#search(String, Bundle)}.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_LIBRARY_SEARCH = 33;
+
+    /**
+     * @hide
+     * Command code for {@link MediaBrowser2#subscribe(String, Bundle)}.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34;
+
+    /**
+     * @hide
+     * Command code for {@link MediaBrowser2#unsubscribe(String)}.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
+
+    /**
+     * Command code for {@link MediaController2#setPlaybackSpeed(float)}}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_SET_SPEED = 39;
+
+    private static final String KEY_COMMAND_CODE =
+            "android.media.media_session2.command.command_code";
+    private static final String KEY_COMMAND_CUSTOM_COMMAND =
+            "android.media.media_session2.command.custom_command";
+    private static final String KEY_COMMAND_EXTRAS =
+            "android.media.media_session2.command.extras";
+
+    private final int mCommandCode;
+    // Nonnull if it's custom command
+    private final String mCustomCommand;
+    private final Bundle mExtras;
+
+    /**
+     * Constructor for creating a predefined command.
+     *
+     * @param commandCode A command code for predefined command.
+     */
+    public SessionCommand2(int commandCode) {
+        if (commandCode == COMMAND_CODE_CUSTOM) {
+            throw new IllegalArgumentException("commandCode shouldn't be COMMAND_CODE_CUSTOM");
+        }
+        mCommandCode = commandCode;
+        mCustomCommand = null;
+        mExtras = null;
+    }
+
+    /**
+     * Constructor for creating a custom command.
+     *
+     * @param action The action of this custom command.
+     * @param extras An extra bundle for this custom command.
+     */
+    public SessionCommand2(@NonNull String action, @Nullable Bundle extras) {
+        if (action == null) {
+            throw new IllegalArgumentException("action shouldn't be null");
+        }
+        mCommandCode = COMMAND_CODE_CUSTOM;
+        mCustomCommand = action;
+        mExtras = extras;
+    }
+
+    /**
+     * Gets the command code of a predefined command.
+     * This will return {@link #COMMAND_CODE_CUSTOM} for a custom command.
+     */
+    public int getCommandCode() {
+        return mCommandCode;
+    }
+
+    /**
+     * Gets the action of a custom command.
+     * This will return {@code null} for a predefined command.
+     */
+    public @Nullable String getCustomCommand() {
+        return mCustomCommand;
+    }
+
+    /**
+     * Gets the extra bundle of a custom command.
+     * This will return {@code null} for a predefined command.
+     */
+    public @Nullable Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * @return a new {@link Bundle} instance from the command
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public Bundle toBundle() {
+        Bundle bundle = new Bundle();
+        bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
+        bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
+        bundle.putBundle(KEY_COMMAND_EXTRAS, mExtras);
+        return bundle;
+    }
+
+    /**
+     * @return a new {@link SessionCommand2} instance from the Bundle
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static SessionCommand2 fromBundle(@NonNull Bundle command) {
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        int code = command.getInt(KEY_COMMAND_CODE);
+        if (code != COMMAND_CODE_CUSTOM) {
+            return new SessionCommand2(code);
+        } else {
+            String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
+            if (customCommand == null) {
+                return null;
+            }
+            return new SessionCommand2(customCommand, command.getBundle(KEY_COMMAND_EXTRAS));
+        }
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof SessionCommand2)) {
+            return false;
+        }
+        SessionCommand2 other = (SessionCommand2) obj;
+        return mCommandCode == other.mCommandCode
+                && TextUtils.equals(mCustomCommand, other.mCustomCommand);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        return ((mCustomCommand != null) ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
+    }
+}
diff --git a/androidx/media/SessionCommandGroup2.java b/androidx/media/SessionCommandGroup2.java
new file mode 100644
index 0000000..691eb70
--- /dev/null
+++ b/androidx/media/SessionCommandGroup2.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.SessionCommand2.COMMAND_CODE_CUSTOM;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A set of {@link SessionCommand2} which represents a command group.
+ */
+public final class SessionCommandGroup2 {
+
+    private static final String TAG = "SessionCommandGroup2";
+    private static final String KEY_COMMANDS = "android.media.mediasession2.commandgroup.commands";
+    // Prefix for all command codes
+    private static final String PREFIX_COMMAND_CODE = "COMMAND_CODE_";
+    // Prefix for command codes that will be sent directly to the MediaPlayerBase
+    private static final String PREFIX_COMMAND_CODE_PLAYBACK = "COMMAND_CODE_PLAYBACK_";
+    // Prefix for command codes that will be sent directly to the MediaPlaylistAgent
+    private static final String PREFIX_COMMAND_CODE_PLAYLIST = "COMMAND_CODE_PLAYLIST_";
+    // Prefix for command codes that will be sent directly to AudioManager or VolumeProvider.
+    private static final String PREFIX_COMMAND_CODE_VOLUME = "COMMAND_CODE_VOLUME_";
+
+    private Set<SessionCommand2> mCommands = new HashSet<>();
+
+    /**
+     * Default Constructor.
+     */
+    public SessionCommandGroup2() { }
+
+    /**
+     * Creates a new SessionCommandGroup2 with commands copied from another object.
+     *
+     * @param other The SessionCommandGroup2 instance to copy.
+     */
+    public SessionCommandGroup2(@Nullable SessionCommandGroup2 other) {
+        if (other != null) {
+            mCommands.addAll(other.mCommands);
+        }
+    }
+
+    /**
+     * Adds a command to this command group.
+     *
+     * @param command A command to add. Shouldn't be {@code null}.
+     */
+    public void addCommand(@NonNull SessionCommand2 command) {
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        mCommands.add(command);
+    }
+
+    /**
+     * Adds a predefined command with given {@code commandCode} to this command group.
+     *
+     * @param commandCode A command code to add.
+     *                    Shouldn't be {@link SessionCommand2#COMMAND_CODE_CUSTOM}.
+     */
+    public void addCommand(int commandCode) {
+        if (commandCode == COMMAND_CODE_CUSTOM) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        mCommands.add(new SessionCommand2(commandCode));
+    }
+
+    /**
+     * Adds all predefined commands to this command group.
+     */
+    public void addAllPredefinedCommands() {
+        addCommandsWithPrefix(PREFIX_COMMAND_CODE);
+    }
+
+    void addAllPlaybackCommands() {
+        addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYBACK);
+    }
+
+    void addAllPlaylistCommands() {
+        addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYLIST);
+    }
+
+    void addAllVolumeCommands() {
+        addCommandsWithPrefix(PREFIX_COMMAND_CODE_VOLUME);
+    }
+
+    private void addCommandsWithPrefix(String prefix) {
+        final Field[] fields = SessionCommand2.class.getFields();
+        if (fields != null) {
+            for (int i = 0; i < fields.length; i++) {
+                if (fields[i].getName().startsWith(prefix)
+                        && !fields[i].getName().equals("COMMAND_CODE_CUSTOM")) {
+                    try {
+                        mCommands.add(new SessionCommand2(fields[i].getInt(null)));
+                    } catch (IllegalAccessException e) {
+                        Log.w(TAG, "Unexpected " + fields[i] + " in MediaSession2");
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Removes a command from this group which matches given {@code command}.
+     *
+     * @param command A command to find. Shouldn't be {@code null}.
+     */
+    public void removeCommand(@NonNull SessionCommand2 command) {
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        mCommands.remove(command);
+    }
+
+    /**
+     * Removes a command from this group which matches given {@code commandCode}.
+     *
+     * @param commandCode A command code to find.
+     *                    Shouldn't be {@link SessionCommand2#COMMAND_CODE_CUSTOM}.
+     */
+    public void removeCommand(int commandCode) {
+        if (commandCode == COMMAND_CODE_CUSTOM) {
+            throw new IllegalArgumentException("commandCode shouldn't be COMMAND_CODE_CUSTOM");
+        }
+        mCommands.remove(new SessionCommand2(commandCode));
+    }
+
+    /**
+     * Checks whether this command group has a command that matches given {@code command}.
+     *
+     * @param command A command to find. Shouldn't be {@code null}.
+     */
+    public boolean hasCommand(@NonNull SessionCommand2 command) {
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        return mCommands.contains(command);
+    }
+
+    /**
+     * Checks whether this command group has a command that matches given {@code commandCode}.
+     *
+     * @param commandCode A command code to find.
+     *                    Shouldn't be {@link SessionCommand2#COMMAND_CODE_CUSTOM}.
+     */
+    public boolean hasCommand(int commandCode) {
+        if (commandCode == COMMAND_CODE_CUSTOM) {
+            throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
+        }
+        for (SessionCommand2 command : mCommands) {
+            if (command.getCommandCode() == commandCode) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets all commands of this command group.
+     */
+    public @NonNull Set<SessionCommand2> getCommands() {
+        return new HashSet<>(mCommands);
+    }
+
+    /**
+     * @return A new {@link Bundle} instance from the SessionCommandGroup2.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public @NonNull Bundle toBundle() {
+        ArrayList<Bundle> list = new ArrayList<>();
+        for (SessionCommand2 command : mCommands) {
+            list.add(command.toBundle());
+        }
+        Bundle bundle = new Bundle();
+        bundle.putParcelableArrayList(KEY_COMMANDS, list);
+        return bundle;
+    }
+
+    /**
+     * @return A new {@link SessionCommandGroup2} instance from the bundle.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static @Nullable SessionCommandGroup2 fromBundle(Bundle commands) {
+        if (commands == null) {
+            return null;
+        }
+        List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS);
+        if (list == null) {
+            return null;
+        }
+        SessionCommandGroup2 commandGroup = new SessionCommandGroup2();
+        for (int i = 0; i < list.size(); i++) {
+            Parcelable parcelable = list.get(i);
+            if (!(parcelable instanceof Bundle)) {
+                continue;
+            }
+            Bundle commandBundle = (Bundle) parcelable;
+            SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
+            if (command != null) {
+                commandGroup.addCommand(command);
+            }
+        }
+        return commandGroup;
+    }
+}
diff --git a/androidx/media/SessionPlaylistAgentImplBase.java b/androidx/media/SessionPlaylistAgentImplBase.java
new file mode 100644
index 0000000..431b188
--- /dev/null
+++ b/androidx/media/SessionPlaylistAgentImplBase.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.util.ArrayMap;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.media.MediaSession2.OnDataSourceMissingHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class SessionPlaylistAgentImplBase extends MediaPlaylistAgent {
+    @VisibleForTesting
+    static final int END_OF_PLAYLIST = -1;
+    @VisibleForTesting
+    static final int NO_VALID_ITEMS = -2;
+
+    private final PlayItem mEopPlayItem = new PlayItem(END_OF_PLAYLIST, null);
+
+    private final Object mLock = new Object();
+    private final MediaSession2ImplBase mSession;
+    private final MyPlayerEventCallback mPlayerCallback;
+
+    @GuardedBy("mLock")
+    private MediaPlayerBase mPlayer;
+    @GuardedBy("mLock")
+    private OnDataSourceMissingHelper mDsmHelper;
+    // TODO: Check if having the same item is okay (b/74090741)
+    @GuardedBy("mLock")
+    private ArrayList<MediaItem2> mPlaylist = new ArrayList<>();
+    @GuardedBy("mLock")
+    private ArrayList<MediaItem2> mShuffledList = new ArrayList<>();
+    @GuardedBy("mLock")
+    private Map<MediaItem2, DataSourceDesc> mItemDsdMap = new ArrayMap<>();
+    @GuardedBy("mLock")
+    private MediaMetadata2 mMetadata;
+    @GuardedBy("mLock")
+    private int mRepeatMode;
+    @GuardedBy("mLock")
+    private int mShuffleMode;
+    @GuardedBy("mLock")
+    private PlayItem mCurrent;
+
+    // Called on session callback executor.
+    private class MyPlayerEventCallback extends PlayerEventCallback {
+        @Override
+        public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+                @Nullable DataSourceDesc dsd) {
+            synchronized (mLock) {
+                if (mPlayer != mpb) {
+                    return;
+                }
+                if (dsd == null && mCurrent != null) {
+                    mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+                    updateCurrentIfNeededLocked();
+                }
+            }
+        }
+    }
+
+    private class PlayItem {
+        public int shuffledIdx;
+        public DataSourceDesc dsd;
+        public MediaItem2 mediaItem;
+
+        PlayItem(int shuffledIdx) {
+            this(shuffledIdx, null);
+        }
+
+        PlayItem(int shuffledIdx, DataSourceDesc dsd) {
+            this.shuffledIdx = shuffledIdx;
+            if (shuffledIdx >= 0) {
+                this.mediaItem = mShuffledList.get(shuffledIdx);
+                if (dsd == null) {
+                    synchronized (mLock) {
+                        this.dsd = retrieveDataSourceDescLocked(this.mediaItem);
+                    }
+                } else {
+                    this.dsd = dsd;
+                }
+            }
+        }
+
+        @SuppressWarnings("ReferenceEquality")
+        boolean isValid() {
+            if (this == mEopPlayItem) {
+                return true;
+            }
+            if (mediaItem == null) {
+                return false;
+            }
+            if (dsd == null) {
+                return false;
+            }
+            if (mediaItem.getDataSourceDesc() != null
+                    && !mediaItem.getDataSourceDesc().equals(dsd)) {
+                return false;
+            }
+            synchronized (mLock) {
+                if (shuffledIdx >= mShuffledList.size()) {
+                    return false;
+                }
+                if (mediaItem != mShuffledList.get(shuffledIdx)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    SessionPlaylistAgentImplBase(@NonNull MediaSession2ImplBase session,
+            @NonNull MediaPlayerBase player) {
+        super();
+        if (session == null) {
+            throw new IllegalArgumentException("sessionImpl shouldn't be null");
+        }
+        if (player == null) {
+            throw new IllegalArgumentException("player shouldn't be null");
+        }
+        mSession = session;
+        mPlayer = player;
+        mPlayerCallback = new MyPlayerEventCallback();
+        mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback);
+    }
+
+    public void setPlayer(@NonNull MediaPlayerBase player) {
+        if (player == null) {
+            throw new IllegalArgumentException("player shouldn't be null");
+        }
+        synchronized (mLock) {
+            if (player == mPlayer) {
+                return;
+            }
+            mPlayer.unregisterPlayerEventCallback(mPlayerCallback);
+            mPlayer = player;
+            mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback);
+            updatePlayerDataSourceLocked();
+        }
+    }
+
+    public void setOnDataSourceMissingHelper(OnDataSourceMissingHelper helper) {
+        synchronized (mLock) {
+            mDsmHelper = helper;
+        }
+    }
+
+    public void clearOnDataSourceMissingHelper() {
+        synchronized (mLock) {
+            mDsmHelper = null;
+        }
+    }
+
+    @Override
+    public @Nullable List<MediaItem2> getPlaylist() {
+        synchronized (mLock) {
+            return Collections.unmodifiableList(mPlaylist);
+        }
+    }
+
+    @Override
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        if (list == null) {
+            throw new IllegalArgumentException("list shouldn't be null");
+        }
+
+        synchronized (mLock) {
+            mItemDsdMap.clear();
+
+            mPlaylist.clear();
+            mPlaylist.addAll(list);
+            applyShuffleModeLocked();
+
+            mMetadata = metadata;
+            mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+            updatePlayerDataSourceLocked();
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+        synchronized (mLock) {
+            return mMetadata;
+        }
+    }
+
+    @Override
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        synchronized (mLock) {
+            if (metadata == mMetadata) {
+                return;
+            }
+            mMetadata = metadata;
+        }
+        notifyPlaylistMetadataChanged();
+    }
+
+    @Override
+    public MediaItem2 getCurrentMediaItem() {
+        synchronized (mLock) {
+            return mCurrent == null ? null : mCurrent.mediaItem;
+        }
+    }
+
+    @Override
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            index = clamp(index, mPlaylist.size());
+            int shuffledIdx = index;
+            mPlaylist.add(index, item);
+            if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_NONE) {
+                mShuffledList.add(index, item);
+            } else {
+                // Add the item in random position of mShuffledList.
+                shuffledIdx = (int) (Math.random() * (mShuffledList.size() + 1));
+                mShuffledList.add(shuffledIdx, item);
+            }
+            if (!hasValidItem()) {
+                mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+                updatePlayerDataSourceLocked();
+            } else {
+                updateCurrentIfNeededLocked();
+            }
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            if (!mPlaylist.remove(item)) {
+                return;
+            }
+            mShuffledList.remove(item);
+            mItemDsdMap.remove(item);
+            updateCurrentIfNeededLocked();
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            if (mPlaylist.size() <= 0) {
+                return;
+            }
+            index = clamp(index, mPlaylist.size() - 1);
+            int shuffledIdx = mShuffledList.indexOf(mPlaylist.get(index));
+            mItemDsdMap.remove(mShuffledList.get(shuffledIdx));
+            mShuffledList.set(shuffledIdx, item);
+            mPlaylist.set(index, item);
+            if (!hasValidItem()) {
+                mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+                updatePlayerDataSourceLocked();
+            } else {
+                updateCurrentIfNeededLocked();
+            }
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            if (!hasValidItem() || item.equals(mCurrent.mediaItem)) {
+                return;
+            }
+            int shuffledIdx = mShuffledList.indexOf(item);
+            if (shuffledIdx < 0) {
+                return;
+            }
+            mCurrent = new PlayItem(shuffledIdx);
+            updateCurrentIfNeededLocked();
+        }
+    }
+
+    @Override
+    public void skipToPreviousItem() {
+        synchronized (mLock) {
+            if (!hasValidItem()) {
+                return;
+            }
+            PlayItem prev = getNextValidPlayItemLocked(mCurrent.shuffledIdx, -1);
+            if (prev != mEopPlayItem) {
+                mCurrent = prev;
+            }
+            updateCurrentIfNeededLocked();
+        }
+    }
+
+    @Override
+    public void skipToNextItem() {
+        synchronized (mLock) {
+            if (!hasValidItem() || mCurrent == mEopPlayItem) {
+                return;
+            }
+            PlayItem next = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+            if (next != mEopPlayItem) {
+                mCurrent = next;
+            }
+            updateCurrentIfNeededLocked();
+        }
+    }
+
+    @Override
+    public int getRepeatMode() {
+        synchronized (mLock) {
+            return mRepeatMode;
+        }
+    }
+
+    @Override
+    @SuppressWarnings("FallThrough")
+    public void setRepeatMode(int repeatMode) {
+        if (repeatMode < MediaPlaylistAgent.REPEAT_MODE_NONE
+                || repeatMode > MediaPlaylistAgent.REPEAT_MODE_GROUP) {
+            return;
+        }
+        synchronized (mLock) {
+            if (mRepeatMode == repeatMode) {
+                return;
+            }
+            mRepeatMode = repeatMode;
+            switch (repeatMode) {
+                case MediaPlaylistAgent.REPEAT_MODE_ONE:
+                    if (mCurrent != null && mCurrent != mEopPlayItem) {
+                        mPlayer.loopCurrent(true);
+                    }
+                    break;
+                case MediaPlaylistAgent.REPEAT_MODE_ALL:
+                case MediaPlaylistAgent.REPEAT_MODE_GROUP:
+                    if (mCurrent == mEopPlayItem) {
+                        mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+                        updatePlayerDataSourceLocked();
+                    }
+                    // Fall through
+                case MediaPlaylistAgent.REPEAT_MODE_NONE:
+                    mPlayer.loopCurrent(false);
+                    break;
+            }
+        }
+        notifyRepeatModeChanged();
+    }
+
+    @Override
+    public int getShuffleMode() {
+        synchronized (mLock) {
+            return mShuffleMode;
+        }
+    }
+
+    @Override
+    public void setShuffleMode(int shuffleMode) {
+        if (shuffleMode < MediaPlaylistAgent.SHUFFLE_MODE_NONE
+                || shuffleMode > MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+            return;
+        }
+        synchronized (mLock) {
+            if (mShuffleMode == shuffleMode) {
+                return;
+            }
+            mShuffleMode = shuffleMode;
+            applyShuffleModeLocked();
+            updateCurrentIfNeededLocked();
+        }
+        notifyShuffleModeChanged();
+    }
+
+    @Override
+    public MediaItem2 getMediaItem(DataSourceDesc dsd) {
+        // TODO: implement this
+        return null;
+    }
+
+    @VisibleForTesting
+    int getCurShuffledIndex() {
+        synchronized (mLock) {
+            return hasValidItem() ? mCurrent.shuffledIdx : NO_VALID_ITEMS;
+        }
+    }
+
+    private boolean hasValidItem() {
+        synchronized (mLock) {
+            return mCurrent != null;
+        }
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private DataSourceDesc retrieveDataSourceDescLocked(MediaItem2 item) {
+        DataSourceDesc dsd = item.getDataSourceDesc();
+        if (dsd != null) {
+            mItemDsdMap.put(item, dsd);
+            return dsd;
+        }
+        dsd = mItemDsdMap.get(item);
+        if (dsd != null) {
+            return dsd;
+        }
+        OnDataSourceMissingHelper helper = mDsmHelper;
+        if (helper != null) {
+            // TODO: Do not call onDataSourceMissing with the lock (b/74090741).
+            dsd = helper.onDataSourceMissing(mSession.getInstance(), item);
+            if (dsd != null) {
+                mItemDsdMap.put(item, dsd);
+            }
+        }
+        return dsd;
+    }
+
+    // TODO: consider to call updateCurrentIfNeededLocked inside (b/74090741)
+    @SuppressWarnings("GuardedBy")
+    private PlayItem getNextValidPlayItemLocked(int curShuffledIdx, int direction) {
+        int size = mPlaylist.size();
+        if (curShuffledIdx == END_OF_PLAYLIST) {
+            curShuffledIdx = (direction > 0) ? -1 : size;
+        }
+        for (int i = 0; i < size; i++) {
+            curShuffledIdx += direction;
+            if (curShuffledIdx < 0 || curShuffledIdx >= mPlaylist.size()) {
+                if (mRepeatMode == REPEAT_MODE_NONE) {
+                    return (i == size - 1) ? null : mEopPlayItem;
+                } else {
+                    curShuffledIdx = curShuffledIdx < 0 ? mPlaylist.size() - 1 : 0;
+                }
+            }
+            DataSourceDesc dsd = retrieveDataSourceDescLocked(mShuffledList.get(curShuffledIdx));
+            if (dsd != null) {
+                return new PlayItem(curShuffledIdx, dsd);
+            }
+        }
+        return null;
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private void updateCurrentIfNeededLocked() {
+        if (!hasValidItem() || mCurrent.isValid()) {
+            return;
+        }
+        int shuffledIdx = mShuffledList.indexOf(mCurrent.mediaItem);
+        if (shuffledIdx >= 0) {
+            // Added an item.
+            mCurrent.shuffledIdx = shuffledIdx;
+            return;
+        }
+
+        if (mCurrent.shuffledIdx >= mShuffledList.size()) {
+            mCurrent = getNextValidPlayItemLocked(mShuffledList.size() - 1, 1);
+        } else {
+            mCurrent.mediaItem = mShuffledList.get(mCurrent.shuffledIdx);
+            if (retrieveDataSourceDescLocked(mCurrent.mediaItem) == null) {
+                mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+            }
+        }
+        updatePlayerDataSourceLocked();
+        return;
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private void updatePlayerDataSourceLocked() {
+        if (mCurrent == null || mCurrent == mEopPlayItem) {
+            return;
+        }
+        if (mPlayer.getCurrentDataSource() != mCurrent.dsd) {
+            mPlayer.setDataSource(mCurrent.dsd);
+            mPlayer.loopCurrent(mRepeatMode == MediaPlaylistAgent.REPEAT_MODE_ONE);
+        }
+        // TODO: Call setNextDataSource (b/74090741)
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private void applyShuffleModeLocked() {
+        mShuffledList.clear();
+        mShuffledList.addAll(mPlaylist);
+        if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_ALL
+                || mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+            Collections.shuffle(mShuffledList);
+        }
+    }
+
+    // Clamps value to [0, size]
+    private static int clamp(int value, int size) {
+        if (value < 0) {
+            return 0;
+        }
+        return (value > size) ? size : value;
+    }
+}
diff --git a/androidx/media/SessionToken2.java b/androidx/media/SessionToken2.java
new file mode 100644
index 0000000..eb42297
--- /dev/null
+++ b/androidx/media/SessionToken2.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Represents an ongoing {@link MediaSession2}.
+ * <p>
+ * This may be passed to apps by the session owner to allow them to create a
+ * {@link MediaController2} to communicate with the session.
+ * <p>
+ * It can be also obtained by {@link MediaSessionManager}.
+ */
+// New version of MediaSession.Token for following reasons
+//   - Stop implementing Parcelable for updatable support
+//   - Represent session and library service (formerly browser service) in one class.
+//     Previously MediaSession.Token was for session and ComponentName was for service.
+public final class SessionToken2 {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
+    public @interface TokenType {
+    }
+
+    /**
+     * Type for {@link MediaSession2}.
+     */
+    public static final int TYPE_SESSION = 0;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int TYPE_SESSION_SERVICE = 1;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int TYPE_LIBRARY_SERVICE = 2;
+
+    //private final SessionToken2Provider mProvider;
+
+    // From the return value of android.os.Process.getUidForName(String) when error
+    private static final int UID_UNKNOWN = -1;
+
+    private static final String KEY_UID = "android.media.token.uid";
+    private static final String KEY_TYPE = "android.media.token.type";
+    private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
+    private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
+    private static final String KEY_ID = "android.media.token.id";
+    private static final String KEY_SESSION_TOKEN = "android.media.token.session_token";
+
+    private final int mUid;
+    private final @TokenType int mType;
+    private final String mPackageName;
+    private final String mServiceName;
+    private final String mId;
+    private final MediaSessionCompat.Token mSessionCompatToken;
+    private final ComponentName mComponentName;
+
+    /**
+     * @hide
+     * Constructor for the token. You can only create token for session service or library service
+     * to use by {@link MediaController2} or {@link MediaBrowser2}.
+     *
+     * @param context The context.
+     * @param serviceComponent The component name of the media browser service.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent) {
+        this(context, serviceComponent, UID_UNKNOWN);
+    }
+
+    /**
+     * Constructor for the token. You can only create token for session service or library service
+     * to use by {@link MediaController2} or {@link MediaBrowser2}.
+     *
+     * @param context The context.
+     * @param serviceComponent The component name of the media browser service.
+     * @param uid uid of the app.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent,
+            int uid) {
+        if (serviceComponent == null) {
+            throw new IllegalArgumentException("serviceComponent shouldn't be null");
+        }
+        mComponentName = serviceComponent;
+        mPackageName = serviceComponent.getPackageName();
+        mServiceName = serviceComponent.getClassName();
+        // Calculate uid if it's not specified.
+        final PackageManager manager = context.getPackageManager();
+        if (uid < 0) {
+            try {
+                uid = manager.getApplicationInfo(mPackageName, 0).uid;
+            } catch (PackageManager.NameNotFoundException e) {
+                throw new IllegalArgumentException("Cannot find package " + mPackageName);
+            }
+        }
+        mUid = uid;
+
+        // Infer id and type from package name and service name
+        String id = getSessionIdFromService(manager, MediaLibraryService2.SERVICE_INTERFACE,
+                serviceComponent);
+        if (id != null) {
+            mId = id;
+            mType = TYPE_LIBRARY_SERVICE;
+        } else {
+            // retry with session service
+            mId = getSessionIdFromService(manager, MediaSessionService2.SERVICE_INTERFACE,
+                    serviceComponent);
+            mType = TYPE_SESSION_SERVICE;
+        }
+        if (mId == null) {
+            throw new IllegalArgumentException("service " + mServiceName + " doesn't implement"
+                    + " session service nor library service. Use service's full name.");
+        }
+        mSessionCompatToken = null;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    SessionToken2(int uid, int type, String packageName, String serviceName,
+            String id, MediaSessionCompat.Token sessionCompatToken) {
+        mUid = uid;
+        mType = type;
+        mPackageName = packageName;
+        mServiceName = serviceName;
+        mComponentName = (mType == TYPE_SESSION) ? null
+                : new ComponentName(packageName, serviceName);
+        mId = id;
+        mSessionCompatToken = sessionCompatToken;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        return mType
+                + prime * (mUid
+                + prime * (mPackageName.hashCode()
+                + prime * (mId.hashCode()
+                + prime * (mServiceName != null ? mServiceName.hashCode() : 0))));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof SessionToken2)) {
+            return false;
+        }
+        SessionToken2 other = (SessionToken2) obj;
+        return mUid == other.mUid
+                && TextUtils.equals(mPackageName, other.mPackageName)
+                && TextUtils.equals(mServiceName, other.mServiceName)
+                && TextUtils.equals(mId, other.mId)
+                && mType == other.mType;
+    }
+
+    @Override
+    public String toString() {
+        return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
+                + " service=" + mServiceName + " sessionCompatToken=" + mSessionCompatToken + "}";
+    }
+
+    /**
+     * @return uid of the session
+     */
+    public int getUid() {
+        return mUid;
+    }
+
+    /**
+     * @return package name
+     */
+    public @NonNull String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * @return service name. Can be {@code null} for TYPE_SESSION.
+     */
+    public @Nullable String getServiceName() {
+        return mServiceName;
+    }
+
+    /**
+     * @hide
+     * @return component name of this session token. Can be null for TYPE_SESSION.
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    /**
+     * @return id
+     */
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * @return type of the token
+     * @see #TYPE_SESSION
+     */
+    public @TokenType int getType() {
+        return mType;
+    }
+
+    /**
+     * Create a token from the bundle, exported by {@link #toBundle()}.
+     *
+     * @param bundle
+     * @return
+     */
+    public static SessionToken2 fromBundle(@NonNull Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        final int uid = bundle.getInt(KEY_UID);
+        final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
+        final String packageName = bundle.getString(KEY_PACKAGE_NAME);
+        final String serviceName = bundle.getString(KEY_SERVICE_NAME);
+        final String id = bundle.getString(KEY_ID);
+        final MediaSessionCompat.Token token = bundle.getParcelable(KEY_SESSION_TOKEN);
+
+        // Sanity check.
+        switch (type) {
+            case TYPE_SESSION:
+                if (token == null) {
+                    throw new IllegalArgumentException("Unexpected token for session,"
+                            + " SessionCompat.Token=" + token);
+                }
+                break;
+            case TYPE_SESSION_SERVICE:
+            case TYPE_LIBRARY_SERVICE:
+                if (TextUtils.isEmpty(serviceName)) {
+                    throw new IllegalArgumentException("Session service needs service name");
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid type");
+        }
+        if (TextUtils.isEmpty(packageName) || id == null) {
+            throw new IllegalArgumentException("Package name nor ID cannot be null.");
+        }
+        return new SessionToken2(uid, type, packageName, serviceName, id, token);
+    }
+
+    /**
+     * Create a {@link Bundle} from this token to share it across processes.
+     * @return Bundle
+     */
+    public Bundle toBundle() {
+        Bundle bundle = new Bundle();
+        bundle.putInt(KEY_UID, mUid);
+        bundle.putString(KEY_PACKAGE_NAME, mPackageName);
+        bundle.putString(KEY_SERVICE_NAME, mServiceName);
+        bundle.putString(KEY_ID, mId);
+        bundle.putInt(KEY_TYPE, mType);
+        bundle.putParcelable(KEY_SESSION_TOKEN, mSessionCompatToken);
+        return bundle;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static String getSessionId(ResolveInfo resolveInfo) {
+        if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+            return null;
+        } else if (resolveInfo.serviceInfo.metaData == null) {
+            return "";
+        } else {
+            return resolveInfo.serviceInfo.metaData.getString(
+                    MediaSessionService2.SERVICE_META_DATA, "");
+        }
+    }
+
+    MediaSessionCompat.Token getSessionCompatToken() {
+        return mSessionCompatToken;
+    }
+
+    private static String getSessionIdFromService(PackageManager manager, String serviceInterface,
+            ComponentName serviceComponent) {
+        Intent serviceIntent = new Intent(serviceInterface);
+        // Use queryIntentServices to find services with MediaLibraryService2.SERVICE_INTERFACE.
+        // We cannot use resolveService with intent specified class name, because resolveService
+        // ignores actions if Intent.setClassName() is specified.
+        serviceIntent.setPackage(serviceComponent.getPackageName());
+
+        List<ResolveInfo> list = manager.queryIntentServices(
+                serviceIntent, PackageManager.GET_META_DATA);
+        if (list != null) {
+            for (int i = 0; i < list.size(); i++) {
+                ResolveInfo resolveInfo = list.get(i);
+                if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+                    continue;
+                }
+                if (TextUtils.equals(
+                        resolveInfo.serviceInfo.name, serviceComponent.getClassName())) {
+                    return getSessionId(resolveInfo);
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/androidx/media/SessionToken2Test.java b/androidx/media/SessionToken2Test.java
new file mode 100644
index 0000000..22881a8
--- /dev/null
+++ b/androidx/media/SessionToken2Test.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Process;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests {@link SessionToken2}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SessionToken2Test {
+    private Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    public void testConstructor_sessionService() {
+        SessionToken2 token = new SessionToken2(mContext, new ComponentName(
+                mContext.getPackageName(),
+                MockMediaSessionService2.class.getCanonicalName()));
+        assertEquals(MockMediaSessionService2.ID, token.getId());
+        assertEquals(mContext.getPackageName(), token.getPackageName());
+        assertEquals(Process.myUid(), token.getUid());
+        assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+    }
+
+    @Test
+    public void testConstructor_libraryService() {
+        SessionToken2 token = new SessionToken2(mContext, new ComponentName(
+                mContext.getPackageName(),
+                MockMediaLibraryService2.class.getCanonicalName()));
+        assertEquals(MockMediaLibraryService2.ID, token.getId());
+        assertEquals(mContext.getPackageName(), token.getPackageName());
+        assertEquals(Process.myUid(), token.getUid());
+        assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+    }
+}
diff --git a/androidx/media/TestMedia2DataSource.java b/androidx/media/TestMedia2DataSource.java
new file mode 100644
index 0000000..c578bef
--- /dev/null
+++ b/androidx/media/TestMedia2DataSource.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import android.content.res.AssetFileDescriptor;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A Media2DataSource that reads from a byte array for use in tests.
+ */
+public class TestMedia2DataSource extends Media2DataSource {
+    private static final String TAG = "TestMedia2DataSource";
+
+    private byte[] mData;
+
+    private boolean mThrowFromReadAt;
+    private boolean mThrowFromGetSize;
+    private Integer mReturnFromReadAt;
+    private Long mReturnFromGetSize;
+    private boolean mIsClosed;
+
+    // Read an asset fd into a new byte array data source. Closes afd.
+    public static TestMedia2DataSource fromAssetFd(AssetFileDescriptor afd) throws IOException {
+        try {
+            InputStream in = afd.createInputStream();
+            final int size = (int) afd.getDeclaredLength();
+            byte[] data = new byte[(int) size];
+            int writeIndex = 0;
+            int numRead = 0;
+            do {
+                numRead = in.read(data, writeIndex, size - writeIndex);
+                writeIndex += numRead;
+            } while (numRead >= 0);
+            return new TestMedia2DataSource(data);
+        } finally {
+            afd.close();
+        }
+    }
+
+    public TestMedia2DataSource(byte[] data) {
+        mData = data;
+    }
+
+    @Override
+    public synchronized int readAt(long position, byte[] buffer, int offset, int size)
+            throws IOException {
+        if (mThrowFromReadAt) {
+            throw new IOException("Test exception from readAt()");
+        }
+        if (mReturnFromReadAt != null) {
+            return mReturnFromReadAt;
+        }
+
+        // Clamp reads past the end of the source.
+        if (position >= mData.length) {
+            return -1; // -1 indicates EOF
+        }
+        if (position + size > mData.length) {
+            size -= (position + size) - mData.length;
+        }
+        System.arraycopy(mData, (int) position, buffer, offset, size);
+        return size;
+    }
+
+    @Override
+    public synchronized long getSize() throws IOException {
+        if (mThrowFromGetSize) {
+            throw new IOException("Test exception from getSize()");
+        }
+        if (mReturnFromGetSize != null) {
+            return mReturnFromGetSize;
+        }
+
+        Log.v(TAG, "getSize: " + mData.length);
+        return mData.length;
+    }
+
+    // Note: it's fine to keep using this data source after closing it.
+    @Override
+    public synchronized void close() {
+        Log.v(TAG, "close()");
+        mIsClosed = true;
+    }
+
+    // Whether close() has been called.
+    public synchronized boolean isClosed() {
+        return mIsClosed;
+    }
+
+    public void throwFromReadAt() {
+        mThrowFromReadAt = true;
+    }
+
+    public void throwFromGetSize() {
+        mThrowFromGetSize = true;
+    }
+
+    public void returnFromReadAt(int numRead) {
+        mReturnFromReadAt = numRead;
+    }
+
+    public void returnFromGetSize(long size) {
+        mReturnFromGetSize = size;
+    }
+}
+
diff --git a/androidx/media/TestServiceRegistry.java b/androidx/media/TestServiceRegistry.java
new file mode 100644
index 0000000..38286aa
--- /dev/null
+++ b/androidx/media/TestServiceRegistry.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+
+import androidx.annotation.GuardedBy;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+    @GuardedBy("TestServiceRegistry.class")
+    private static TestServiceRegistry sInstance;
+    @GuardedBy("TestServiceRegistry.class")
+    private MediaSessionService2 mService;
+    @GuardedBy("TestServiceRegistry.class")
+    private SyncHandler mHandler;
+    @GuardedBy("TestServiceRegistry.class")
+    private MediaLibrarySessionCallback mSessionCallback;
+    @GuardedBy("TestServiceRegistry.class")
+    private SessionServiceCallback mSessionServiceCallback;
+
+    /**
+     * Callback for session service's lifecyle (onCreate() / onDestroy())
+     */
+    public interface SessionServiceCallback {
+        void onCreated();
+        void onDestroyed();
+    }
+
+    public static TestServiceRegistry getInstance() {
+        synchronized (TestServiceRegistry.class) {
+            if (sInstance == null) {
+                sInstance = new TestServiceRegistry();
+            }
+            return sInstance;
+        }
+    }
+
+    public void setHandler(Handler handler) {
+        synchronized (TestServiceRegistry.class) {
+            mHandler = new SyncHandler(handler.getLooper());
+        }
+    }
+
+    public Handler getHandler() {
+        synchronized (TestServiceRegistry.class) {
+            return mHandler;
+        }
+    }
+
+    public void setSessionServiceCallback(SessionServiceCallback sessionServiceCallback) {
+        synchronized (TestServiceRegistry.class) {
+            mSessionServiceCallback = sessionServiceCallback;
+        }
+    }
+
+    public void setSessionCallback(MediaLibrarySessionCallback sessionCallback) {
+        synchronized (TestServiceRegistry.class) {
+            mSessionCallback = sessionCallback;
+        }
+    }
+
+    public MediaLibrarySessionCallback getSessionCallback() {
+        synchronized (TestServiceRegistry.class) {
+            return mSessionCallback;
+        }
+    }
+
+    public void setServiceInstance(MediaSessionService2 service) {
+        synchronized (TestServiceRegistry.class) {
+            if (mService != null) {
+                fail("Previous service instance is still running. Clean up manually to ensure"
+                        + " previoulsy running service doesn't break current test");
+            }
+            mService = service;
+            if (mSessionServiceCallback != null) {
+                mSessionServiceCallback.onCreated();
+            }
+        }
+    }
+
+    public MediaSessionService2 getServiceInstance() {
+        synchronized (TestServiceRegistry.class) {
+            return mService;
+        }
+    }
+
+    public void cleanUp() {
+        synchronized (TestServiceRegistry.class) {
+            if (mService != null) {
+                // TODO(jaewan): Remove this, and override SessionService#onDestroy() to do this
+                mService.getSession().close();
+                // stopSelf() would not kill service while the binder connection established by
+                // bindService() exists, and close() above will do the job instead.
+                // So stopSelf() isn't really needed, but just for sure.
+                mService.stopSelf();
+                mService = null;
+            }
+            if (mHandler != null) {
+                mHandler.removeCallbacksAndMessages(null);
+            }
+            mSessionCallback = null;
+            if (mSessionServiceCallback != null) {
+                mSessionServiceCallback.onDestroyed();
+                mSessionServiceCallback = null;
+            }
+        }
+    }
+}
diff --git a/androidx/media/TestUtils.java b/androidx/media/TestUtils.java
new file mode 100644
index 0000000..1e3ba9b
--- /dev/null
+++ b/androidx/media/TestUtils.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2018 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 androidx.media;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtils {
+    private static final int WAIT_TIME_MS = 1000;
+    private static final int WAIT_SERVICE_TIME_MS = 5000;
+
+    /**
+     * Finds the session with id in this test package.
+     *
+     * @param context
+     * @param id
+     * @return
+     */
+    public static SessionToken2 getServiceToken(Context context, String id) {
+        switch (id) {
+            case MockMediaSessionService2.ID:
+                return new SessionToken2(context, new ComponentName(
+                        context.getPackageName(), MockMediaSessionService2.class.getName()));
+            case MockMediaLibraryService2.ID:
+                return new SessionToken2(context, new ComponentName(
+                        context.getPackageName(), MockMediaLibraryService2.class.getName()));
+        }
+        fail("Unknown id=" + id);
+        return null;
+    }
+
+    /**
+     * Compares contents of two bundles.
+     *
+     * @param a a bundle
+     * @param b another bundle
+     * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
+     *     incorrect if any bundle contains a bundle.
+     */
+    public static boolean equals(Bundle a, Bundle b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null || b == null) {
+            return false;
+        }
+        if (!a.keySet().containsAll(b.keySet())
+                || !b.keySet().containsAll(a.keySet())) {
+            return false;
+        }
+        for (String key : a.keySet()) {
+            if (!Objects.equals(a.get(key), b.get(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Create a playlist for testing purpose
+     * <p>
+     * Caller's method name will be used for prefix of each media item's media id.
+     *
+     * @param size lits size
+     * @return the newly created playlist
+     */
+    public static List<MediaItem2> createPlaylist(int size) {
+        final List<MediaItem2> list = new ArrayList<>();
+        String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
+        for (int i = 0; i < size; i++) {
+            list.add(new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+                    .setMediaId(caller + "_item_" + (size + 1))
+                    .setDataSourceDesc(
+                            new DataSourceDesc.Builder()
+                                    .setDataSource(new FileDescriptor())
+                                    .build())
+                    .build());
+        }
+        return list;
+    }
+
+    /**
+     * Create a media item with the metadata for testing purpose.
+     *
+     * @return the newly created media item
+     * @see #createMetadata()
+     */
+    public static MediaItem2 createMediaItemWithMetadata() {
+        return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+                .setMetadata(createMetadata()).build();
+    }
+
+    /**
+     * Create a media metadata for testing purpose.
+     * <p>
+     * Caller's method name will be used for the media id.
+     *
+     * @return the newly created media item
+     */
+    public static MediaMetadata2 createMetadata() {
+        String mediaId = Thread.currentThread().getStackTrace()[1].getMethodName();
+        return new MediaMetadata2.Builder()
+                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
+    }
+
+    /**
+     * Handler that always waits until the Runnable finishes.
+     */
+    public static class SyncHandler extends Handler {
+        public SyncHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void postAndSync(final Runnable runnable) throws InterruptedException {
+            if (getLooper() == Looper.myLooper()) {
+                runnable.run();
+            } else {
+                final CountDownLatch latch = new CountDownLatch(1);
+                post(new Runnable() {
+                    @Override
+                    public void run() {
+                        runnable.run();
+                        latch.countDown();
+                    }
+                });
+                assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+    }
+}
diff --git a/androidx/paging/DataSource.java b/androidx/paging/DataSource.java
index 55cd0a9..157750a 100644
--- a/androidx/paging/DataSource.java
+++ b/androidx/paging/DataSource.java
@@ -16,391 +16,7 @@
 
 package androidx.paging;
 
-import androidx.annotation.AnyThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.util.Function;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Base class for loading pages of snapshot data into a {@link PagedList}.
- * <p>
- * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as
- * it loads more data, but the data loaded cannot be updated. If the underlying data set is
- * modified, a new PagedList / DataSource pair must be created to represent the new data.
- * <h4>Loading Pages</h4>
- * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter}
- * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView.
- * <p>
- * To control how and when a PagedList queries data from its DataSource, see
- * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance.
- * <h4>Updating Paged Data</h4>
- * A PagedList / DataSource pair are a snapshot of the data set. A new pair of
- * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or
- * content update occurs. A DataSource must detect that it cannot continue loading its
- * snapshot (for instance, when Database query notices a table being invalidated), and call
- * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from
- * the new state of the Database query.
- * <p>
- * To page in data that doesn't update, you can create a single DataSource, and pass it to a single
- * PagedList. For example, loading from network when the network's paging API doesn't provide
- * updates.
- * <p>
- * To page in data from a source that does provide updates, you can create a
- * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the
- * data set occurs that makes the current snapshot invalid. For example, when paging a query from
- * the Database, and the table being queried inserts or removes items. You can also use a
- * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content
- * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data,
- * you can connect an explicit refresh signal to call {@link #invalidate()} on the current
- * DataSource.
- * <p>
- * If you have more granular update signals, such as a network API signaling an update to a single
- * item in the list, it's recommended to load data from network into memory. Then present that
- * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory
- * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the
- * snapshot can be created.
- * <h4>Implementing a DataSource</h4>
- * To implement, extend one of the subclasses: {@link PageKeyedDataSource},
- * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
- * <p>
- * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
- * example a network response that returns some items, and a next/previous page links.
- * <p>
- * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
- * {@code N}. For example, if requesting the backend for the next comments in the list
- * requires the ID or timestamp of the most recent loaded comment, or if querying the next users
- * from a name-sorted database query requires the name and unique ID of the previous.
- * <p>
- * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
- * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
- * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
- * PositionalDataSource is required to respect page size for efficient tiling. If you want to
- * override page size (e.g. when network page size constraints are only known at runtime), use one
- * of the other DataSource classes.
- * <p>
- * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
- * return {@code null} items in lists that it loads. This is so that users of the PagedList
- * can differentiate unloaded placeholder items from content that has been paged in.
- *
- * @param <Key> Input used to trigger initial load from the DataSource. Often an Integer position.
- * @param <Value> Value type loaded by the DataSource.
- */
-@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
-public abstract class DataSource<Key, Value> {
-    /**
-     * Factory for DataSources.
-     * <p>
-     * Data-loading systems of an application or library can implement this interface to allow
-     * {@code LiveData<PagedList>}s to be created. For example, Room can provide a
-     * DataSource.Factory for a given SQL query:
-     *
-     * <pre>
-     * {@literal @}Dao
-     * interface UserDao {
-     *    {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
-     *    public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
-     * }
-     * </pre>
-     * In the above sample, {@code Integer} is used because it is the {@code Key} type of
-     * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to
-     * page a large query with a PositionalDataSource.
-     *
-     * @param <Key> Key identifying items in DataSource.
-     * @param <Value> Type of items in the list loaded by the DataSources.
-     */
-    public abstract static class Factory<Key, Value> {
-        /**
-         * Create a DataSource.
-         * <p>
-         * The DataSource should invalidate itself if the snapshot is no longer valid. If a
-         * DataSource becomes invalid, the only way to query more data is to create a new DataSource
-         * from the Factory.
-         * <p>
-         * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
-         * when the current DataSource is invalidated, and pass the new PagedList through the
-         * {@code LiveData<PagedList>} to observers.
-         *
-         * @return the new DataSource.
-         */
-        public abstract DataSource<Key, Value> create();
-
-        /**
-         * Applies the given function to each value emitted by DataSources produced by this Factory.
-         * <p>
-         * Same as {@link #mapByPage(Function)}, but operates on individual items.
-         *
-         * @param function Function that runs on each loaded item, returning items of a potentially
-         *                  new type.
-         * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
-         *
-         * @return A new DataSource.Factory, which transforms items using the given function.
-         *
-         * @see #mapByPage(Function)
-         * @see DataSource#map(Function)
-         * @see DataSource#mapByPage(Function)
-         */
-        @NonNull
-        public <ToValue> DataSource.Factory<Key, ToValue> map(
-                @NonNull Function<Value, ToValue> function) {
-            return mapByPage(createListFunction(function));
-        }
-
-        /**
-         * Applies the given function to each value emitted by DataSources produced by this Factory.
-         * <p>
-         * Same as {@link #map(Function)}, but allows for batch conversions.
-         *
-         * @param function Function that runs on each loaded page, returning items of a potentially
-         *                  new type.
-         * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
-         *
-         * @return A new DataSource.Factory, which transforms items using the given function.
-         *
-         * @see #map(Function)
-         * @see DataSource#map(Function)
-         * @see DataSource#mapByPage(Function)
-         */
-        @NonNull
-        public <ToValue> DataSource.Factory<Key, ToValue> mapByPage(
-                @NonNull final Function<List<Value>, List<ToValue>> function) {
-            return new Factory<Key, ToValue>() {
-                @Override
-                public DataSource<Key, ToValue> create() {
-                    return Factory.this.create().mapByPage(function);
-                }
-            };
-        }
-    }
-
-    @NonNull
-    static <X, Y> Function<List<X>, List<Y>> createListFunction(
-            final @NonNull Function<X, Y> innerFunc) {
-        return new Function<List<X>, List<Y>>() {
-            @Override
-            public List<Y> apply(@NonNull List<X> source) {
-                List<Y> out = new ArrayList<>(source.size());
-                for (int i = 0; i < source.size(); i++) {
-                    out.add(innerFunc.apply(source.get(i)));
-                }
-                return out;
-            }
-        };
-    }
-
-    static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
-        List<B> dest = function.apply(source);
-        if (dest.size() != source.size()) {
-            throw new IllegalStateException("Invalid Function " + function
-                    + " changed return size. This is not supported.");
-        }
-        return dest;
-    }
-
-    // Since we currently rely on implementation details of two implementations,
-    // prevent external subclassing, except through exposed subclasses
-    DataSource() {
-    }
-
-    /**
-     * Applies the given function to each value emitted by the DataSource.
-     * <p>
-     * Same as {@link #map(Function)}, but allows for batch conversions.
-     *
-     * @param function Function that runs on each loaded page, returning items of a potentially
-     *                  new type.
-     * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
-     *
-     * @return A new DataSource, which transforms items using the given function.
-     *
-     * @see #map(Function)
-     * @see DataSource.Factory#map(Function)
-     * @see DataSource.Factory#mapByPage(Function)
-     */
-    @NonNull
-    public abstract <ToValue> DataSource<Key, ToValue> mapByPage(
-            @NonNull Function<List<Value>, List<ToValue>> function);
-
-    /**
-     * Applies the given function to each value emitted by the DataSource.
-     * <p>
-     * Same as {@link #mapByPage(Function)}, but operates on individual items.
-     *
-     * @param function Function that runs on each loaded item, returning items of a potentially
-     *                  new type.
-     * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
-     *
-     * @return A new DataSource, which transforms items using the given function.
-     *
-     * @see #mapByPage(Function)
-     * @see DataSource.Factory#map(Function)
-     * @see DataSource.Factory#mapByPage(Function)
-     */
-    @NonNull
-    public abstract <ToValue> DataSource<Key, ToValue> map(
-            @NonNull Function<Value, ToValue> function);
-
-    /**
-     * Returns true if the data source guaranteed to produce a contiguous set of items,
-     * never producing gaps.
-     */
-    abstract boolean isContiguous();
-
-    static class LoadCallbackHelper<T> {
-        static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
-            if (position < 0) {
-                throw new IllegalArgumentException("Position must be non-negative");
-            }
-            if (data.size() + position > totalCount) {
-                throw new IllegalArgumentException(
-                        "List size + position too large, last item in list beyond totalCount.");
-            }
-            if (data.size() == 0 && totalCount > 0) {
-                throw new IllegalArgumentException(
-                        "Initial result cannot be empty if items are present in data set.");
-            }
-        }
-
-        @PageResult.ResultType
-        final int mResultType;
-        private final DataSource mDataSource;
-        private final PageResult.Receiver<T> mReceiver;
-
-        // mSignalLock protects mPostExecutor, and mHasSignalled
-        private final Object mSignalLock = new Object();
-        private Executor mPostExecutor = null;
-        private boolean mHasSignalled = false;
-
-        LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
-                @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
-            mDataSource = dataSource;
-            mResultType = resultType;
-            mPostExecutor = mainThreadExecutor;
-            mReceiver = receiver;
-        }
-
-        void setPostExecutor(Executor postExecutor) {
-            synchronized (mSignalLock) {
-                mPostExecutor = postExecutor;
-            }
-        }
-
-        /**
-         * Call before verifying args, or dispatching actul results
-         *
-         * @return true if DataSource was invalid, and invalid result dispatched
-         */
-        boolean dispatchInvalidResultIfInvalid() {
-            if (mDataSource.isInvalid()) {
-                dispatchResultToReceiver(PageResult.<T>getInvalidResult());
-                return true;
-            }
-            return false;
-        }
-
-        void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
-            Executor executor;
-            synchronized (mSignalLock) {
-                if (mHasSignalled) {
-                    throw new IllegalStateException(
-                            "callback.onResult already called, cannot call again.");
-                }
-                mHasSignalled = true;
-                executor = mPostExecutor;
-            }
-
-            if (executor != null) {
-                executor.execute(new Runnable() {
-                    @Override
-                    public void run() {
-                        mReceiver.onPageResult(mResultType, result);
-                    }
-                });
-            } else {
-                mReceiver.onPageResult(mResultType, result);
-            }
-        }
-    }
-
-    /**
-     * Invalidation callback for DataSource.
-     * <p>
-     * Used to signal when a DataSource a data source has become invalid, and that a new data source
-     * is needed to continue loading data.
-     */
-    public interface InvalidatedCallback {
-        /**
-         * Called when the data backing the list has become invalid. This callback is typically used
-         * to signal that a new data source is needed.
-         * <p>
-         * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
-         * for the data source to invalidate itself during its load methods, or for an outside
-         * source to invalidate it.
-         */
-        @AnyThread
-        void onInvalidated();
-    }
-
-    private AtomicBoolean mInvalid = new AtomicBoolean(false);
-
-    private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
-            new CopyOnWriteArrayList<>();
-
-    /**
-     * Add a callback to invoke when the DataSource is first invalidated.
-     * <p>
-     * Once invalidated, a data source will not become valid again.
-     * <p>
-     * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
-     * is called, on that thread.
-     *
-     * @param onInvalidatedCallback The callback, will be invoked on thread that
-     *                              {@link #invalidate()} is called on.
-     */
-    @AnyThread
-    @SuppressWarnings("WeakerAccess")
-    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
-        mOnInvalidatedCallbacks.add(onInvalidatedCallback);
-    }
-
-    /**
-     * Remove a previously added invalidate callback.
-     *
-     * @param onInvalidatedCallback The previously added callback.
-     */
-    @AnyThread
-    @SuppressWarnings("WeakerAccess")
-    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
-        mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
-    }
-
-    /**
-     * Signal the data source to stop loading, and notify its callback.
-     * <p>
-     * If invalidate has already been called, this method does nothing.
-     */
-    @AnyThread
-    public void invalidate() {
-        if (mInvalid.compareAndSet(false, true)) {
-            for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
-                callback.onInvalidated();
-            }
-        }
-    }
-
-    /**
-     * Returns true if the data source is invalid, and can no longer be queried for data.
-     *
-     * @return True if the data source is invalid, and can no longer return data.
-     */
-    @WorkerThread
-    public boolean isInvalid() {
-        return mInvalid.get();
+abstract public class DataSource<K, T> {
+    public interface Factory<Key, Value> {
     }
 }
diff --git a/androidx/paging/LivePagedListBuilder.java b/androidx/paging/LivePagedListBuilder.java
index f8af819..672e40f 100644
--- a/androidx/paging/LivePagedListBuilder.java
+++ b/androidx/paging/LivePagedListBuilder.java
@@ -30,7 +30,7 @@
  * {@link PagedList.Config}.
  * <p>
  * The required parameters are in the constructor, so you can simply construct and build, or
- * optionally enable extra features (such as initial load key, or BoundaryCallback.
+ * optionally enable extra features (such as initial load key, or BoundaryCallback).
  *
  * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
  *             you're using PositionalDataSource.
diff --git a/androidx/paging/PositionalDataSource.java b/androidx/paging/PositionalDataSource.java
index 4f7cd3f..ff5338a 100644
--- a/androidx/paging/PositionalDataSource.java
+++ b/androidx/paging/PositionalDataSource.java
@@ -1,546 +1,5 @@
-/*
- * Copyright 2018 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 androidx.paging;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.util.Function;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
- * arbitrary page positions.
- * <p>
- * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
- * positions, and provide a fixed item count. If your data source can't support loading arbitrary
- * requested page sizes (e.g. when network page size constraints are only known at runtime), use
- * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
- * <p>
- * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
- * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
- * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
- * If placeholders are disabled, initialize with the two parameter
- * {@link LoadInitialCallback#onResult(List, int)}.
- * <p>
- * Room can generate a Factory of PositionalDataSources for you:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- *     {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
- *     public abstract DataSource.Factory&lt;Integer, User> loadUsersByAgeDesc();
- * }</pre>
- *
- * @param <T> Type of items being loaded by the PositionalDataSource.
- */
 public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
 
-    /**
-     * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
-     */
-    @SuppressWarnings("WeakerAccess")
-    public static class LoadInitialParams {
-        /**
-         * Initial load position requested.
-         * <p>
-         * Note that this may not be within the bounds of your data set, it may need to be adjusted
-         * before you execute your load.
-         */
-        public final int requestedStartPosition;
-
-        /**
-         * Requested number of items to load.
-         * <p>
-         * Note that this may be larger than available data.
-         */
-        public final int requestedLoadSize;
-
-        /**
-         * Defines page size acceptable for return values.
-         * <p>
-         * List of items passed to the callback must be an integer multiple of page size.
-         */
-        public final int pageSize;
-
-        /**
-         * Defines whether placeholders are enabled, and whether the total count passed to
-         * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
-         */
-        public final boolean placeholdersEnabled;
-
-        public LoadInitialParams(
-                int requestedStartPosition,
-                int requestedLoadSize,
-                int pageSize,
-                boolean placeholdersEnabled) {
-            this.requestedStartPosition = requestedStartPosition;
-            this.requestedLoadSize = requestedLoadSize;
-            this.pageSize = pageSize;
-            this.placeholdersEnabled = placeholdersEnabled;
-        }
-    }
-
-    /**
-     * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
-     */
-    @SuppressWarnings("WeakerAccess")
-    public static class LoadRangeParams {
-        /**
-         * Start position of data to load.
-         * <p>
-         * Returned data must start at this position.
-         */
-        public final int startPosition;
-        /**
-         * Number of items to load.
-         * <p>
-         * Returned data must be of this size, unless at end of the list.
-         */
-        public final int loadSize;
-
-        public LoadRangeParams(int startPosition, int loadSize) {
-            this.startPosition = startPosition;
-            this.loadSize = loadSize;
-        }
-    }
-
-    /**
-     * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
-     * to return data, position, and count.
-     * <p>
-     * A callback should be called only once, and may throw if called again.
-     * <p>
-     * It is always valid for a DataSource loading method that takes a callback to stash the
-     * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
-     * temporary, recoverable error states (such as a network error that can be retried).
-     *
-     * @param <T> Type of items being loaded.
-     */
-    public abstract static class LoadInitialCallback<T> {
-        /**
-         * Called to pass initial load state from a DataSource.
-         * <p>
-         * Call this method from your DataSource's {@code loadInitial} function to return data,
-         * and inform how many placeholders should be shown before and after. If counting is cheap
-         * to compute (for example, if a network load returns the information regardless), it's
-         * recommended to pass the total size to the totalCount parameter. If placeholders are not
-         * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
-         * call {@link #onResult(List, int)}.
-         *
-         * @param data List of items loaded from the DataSource. If this is empty, the DataSource
-         *             is treated as empty, and no further loads will occur.
-         * @param position Position of the item at the front of the list. If there are {@code N}
-         *                 items before the items in data that can be loaded from this DataSource,
-         *                 pass {@code N}.
-         * @param totalCount Total number of items that may be returned from this DataSource.
-         *                   Includes the number in the initial {@code data} parameter
-         *                   as well as any items that can be loaded in front or behind of
-         *                   {@code data}.
-         */
-        public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
-
-        /**
-         * Called to pass initial load state from a DataSource without total count,
-         * when placeholders aren't requested.
-         * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
-         * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
-         * <p>
-         * Call this method from your DataSource's {@code loadInitial} function to return data,
-         * if position is known but total size is not. If placeholders are requested, call the three
-         * parameter variant: {@link #onResult(List, int, int)}.
-         *
-         * @param data List of items loaded from the DataSource. If this is empty, the DataSource
-         *             is treated as empty, and no further loads will occur.
-         * @param position Position of the item at the front of the list. If there are {@code N}
-         *                 items before the items in data that can be provided by this DataSource,
-         *                 pass {@code N}.
-         */
-        public abstract void onResult(@NonNull List<T> data, int position);
-    }
-
-    /**
-     * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
-     * to return data.
-     * <p>
-     * A callback should be called only once, and may throw if called again.
-     * <p>
-     * It is always valid for a DataSource loading method that takes a callback to stash the
-     * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
-     * temporary, recoverable error states (such as a network error that can be retried).
-     *
-     * @param <T> Type of items being loaded.
-     */
-    public abstract static class LoadRangeCallback<T> {
-        /**
-         * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
-         *
-         * @param data List of items loaded from the DataSource. Must be same size as requested,
-         *             unless at end of list.
-         */
-        public abstract void onResult(@NonNull List<T> data);
-    }
-
-    static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
-        final LoadCallbackHelper<T> mCallbackHelper;
-        private final boolean mCountingEnabled;
-        private final int mPageSize;
-
-        LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
-                int pageSize, PageResult.Receiver<T> receiver) {
-            mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
-            mCountingEnabled = countingEnabled;
-            mPageSize = pageSize;
-            if (mPageSize < 1) {
-                throw new IllegalArgumentException("Page size must be non-negative");
-            }
-        }
-
-        @Override
-        public void onResult(@NonNull List<T> data, int position, int totalCount) {
-            if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
-                LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
-                if (position + data.size() != totalCount
-                        && data.size() % mPageSize != 0) {
-                    throw new IllegalArgumentException("PositionalDataSource requires initial load"
-                            + " size to be a multiple of page size to support internal tiling."
-                            + " loadSize " + data.size() + ", position " + position
-                            + ", totalCount " + totalCount + ", pageSize " + mPageSize);
-                }
-
-                if (mCountingEnabled) {
-                    int trailingUnloadedCount = totalCount - position - data.size();
-                    mCallbackHelper.dispatchResultToReceiver(
-                            new PageResult<>(data, position, trailingUnloadedCount, 0));
-                } else {
-                    // Only occurs when wrapped as contiguous
-                    mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
-                }
-            }
-        }
-
-        @Override
-        public void onResult(@NonNull List<T> data, int position) {
-            if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
-                if (position < 0) {
-                    throw new IllegalArgumentException("Position must be non-negative");
-                }
-                if (data.isEmpty() && position != 0) {
-                    throw new IllegalArgumentException(
-                            "Initial result cannot be empty if items are present in data set.");
-                }
-                if (mCountingEnabled) {
-                    throw new IllegalStateException("Placeholders requested, but totalCount not"
-                            + " provided. Please call the three-parameter onResult method, or"
-                            + " disable placeholders in the PagedList.Config");
-                }
-                mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
-            }
-        }
-    }
-
-    static class LoadRangeCallbackImpl<T> extends LoadRangeCallback<T> {
-        private LoadCallbackHelper<T> mCallbackHelper;
-        private final int mPositionOffset;
-        LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource,
-                @PageResult.ResultType int resultType, int positionOffset,
-                Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
-            mCallbackHelper = new LoadCallbackHelper<>(
-                    dataSource, resultType, mainThreadExecutor, receiver);
-            mPositionOffset = positionOffset;
-        }
-
-        @Override
-        public void onResult(@NonNull List<T> data) {
-            if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
-                mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
-                        data, 0, 0, mPositionOffset));
-            }
-        }
-    }
-
-    final void dispatchLoadInitial(boolean acceptCount,
-            int requestedStartPosition, int requestedLoadSize, int pageSize,
-            @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
-        LoadInitialCallbackImpl<T> callback =
-                new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
-
-        LoadInitialParams params = new LoadInitialParams(
-                requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
-        loadInitial(params, callback);
-
-        // If initialLoad's callback is not called within the body, we force any following calls
-        // to post to the UI thread. This constructor may be run on a background thread, but
-        // after constructor, mutation must happen on UI thread.
-        callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
-    }
-
-    final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
-            int count, @NonNull Executor mainThreadExecutor,
-            @NonNull PageResult.Receiver<T> receiver) {
-        LoadRangeCallback<T> callback = new LoadRangeCallbackImpl<>(
-                this, resultType, startPosition, mainThreadExecutor, receiver);
-        if (count == 0) {
-            callback.onResult(Collections.<T>emptyList());
-        } else {
-            loadRange(new LoadRangeParams(startPosition, count), callback);
-        }
-    }
-
-    /**
-     * Load initial list data.
-     * <p>
-     * This method is called to load the initial page(s) from the DataSource.
-     * <p>
-     * Result list must be a multiple of pageSize to enable efficient tiling.
-     *
-     * @param params Parameters for initial load, including requested start position, load size, and
-     *               page size.
-     * @param callback Callback that receives initial load data, including
-     *                 position and total data set size.
-     */
-    @WorkerThread
-    public abstract void loadInitial(
-            @NonNull LoadInitialParams params,
-            @NonNull LoadInitialCallback<T> callback);
-
-    /**
-     * Called to load a range of data from the DataSource.
-     * <p>
-     * This method is called to load additional pages from the DataSource after the
-     * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
-     * <p>
-     * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return
-     * the number of items requested, at the position requested.
-     *
-     * @param params Parameters for load, including start position and load size.
-     * @param callback Callback that receives loaded data.
-     */
-    @WorkerThread
-    public abstract void loadRange(@NonNull LoadRangeParams params,
-            @NonNull LoadRangeCallback<T> callback);
-
-    @Override
-    boolean isContiguous() {
-        return false;
-    }
-
-    @NonNull
-    ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
-        return new ContiguousWithoutPlaceholdersWrapper<>(this);
-    }
-
-    /**
-     * Helper for computing an initial position in
-     * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
-     * computed ahead of loading.
-     * <p>
-     * The value computed by this function will do bounds checking, page alignment, and positioning
-     * based on initial load size requested.
-     * <p>
-     * Example usage in a PositionalDataSource subclass:
-     * <pre>
-     * class ItemDataSource extends PositionalDataSource&lt;Item> {
-     *     private int computeCount() {
-     *         // actual count code here
-     *     }
-     *
-     *     private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
-     *         // actual load code here
-     *     }
-     *
-     *     {@literal @}Override
-     *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
-     *             {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
-     *         int totalCount = computeCount();
-     *         int position = computeInitialLoadPosition(params, totalCount);
-     *         int loadSize = computeInitialLoadSize(params, position, totalCount);
-     *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
-     *     }
-     *
-     *     {@literal @}Override
-     *     public void loadRange({@literal @}NonNull LoadRangeParams params,
-     *             {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
-     *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
-     *     }
-     * }</pre>
-     *
-     * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
-     *               including page size, and requested start/loadSize.
-     * @param totalCount Total size of the data set.
-     * @return Position to start loading at.
-     *
-     * @see #computeInitialLoadSize(LoadInitialParams, int, int)
-     */
-    public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
-            int totalCount) {
-        int position = params.requestedStartPosition;
-        int initialLoadSize = params.requestedLoadSize;
-        int pageSize = params.pageSize;
-
-        int roundedPageStart = Math.round(position / pageSize) * pageSize;
-
-        // maximum start pos is that which will encompass end of list
-        int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize;
-        roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
-
-        // minimum start position is 0
-        roundedPageStart = Math.max(0, roundedPageStart);
-
-        return roundedPageStart;
-    }
-
-    /**
-     * Helper for computing an initial load size in
-     * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
-     * computed ahead of loading.
-     * <p>
-     * This function takes the requested load size, and bounds checks it against the value returned
-     * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
-     * <p>
-     * Example usage in a PositionalDataSource subclass:
-     * <pre>
-     * class ItemDataSource extends PositionalDataSource&lt;Item> {
-     *     private int computeCount() {
-     *         // actual count code here
-     *     }
-     *
-     *     private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
-     *         // actual load code here
-     *     }
-     *
-     *     {@literal @}Override
-     *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
-     *             {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
-     *         int totalCount = computeCount();
-     *         int position = computeInitialLoadPosition(params, totalCount);
-     *         int loadSize = computeInitialLoadSize(params, position, totalCount);
-     *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
-     *     }
-     *
-     *     {@literal @}Override
-     *     public void loadRange({@literal @}NonNull LoadRangeParams params,
-     *             {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
-     *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
-     *     }
-     * }</pre>
-     *
-     * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
-     *               including page size, and requested start/loadSize.
-     * @param initialLoadPosition Value returned by
-     *                          {@link #computeInitialLoadPosition(LoadInitialParams, int)}
-     * @param totalCount Total size of the data set.
-     * @return Number of items to load.
-     *
-     * @see #computeInitialLoadPosition(LoadInitialParams, int)
-     */
-    @SuppressWarnings("WeakerAccess")
-    public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
-            int initialLoadPosition, int totalCount) {
-        return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize);
-    }
-
-    @SuppressWarnings("deprecation")
-    static class ContiguousWithoutPlaceholdersWrapper<Value>
-            extends ContiguousDataSource<Integer, Value> {
-
-        @NonNull
-        final PositionalDataSource<Value> mPositionalDataSource;
-
-        ContiguousWithoutPlaceholdersWrapper(
-                @NonNull PositionalDataSource<Value> positionalDataSource) {
-            mPositionalDataSource = positionalDataSource;
-        }
-
-        @NonNull
-        @Override
-        public <ToValue> DataSource<Integer, ToValue> mapByPage(
-                @NonNull Function<List<Value>, List<ToValue>> function) {
-            throw new UnsupportedOperationException(
-                    "Inaccessible inner type doesn't support map op");
-        }
-
-        @NonNull
-        @Override
-        public <ToValue> DataSource<Integer, ToValue> map(
-                @NonNull Function<Value, ToValue> function) {
-            throw new UnsupportedOperationException(
-                    "Inaccessible inner type doesn't support map op");
-        }
-
-        @Override
-        void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
-                boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
-                @NonNull PageResult.Receiver<Value> receiver) {
-            final int convertPosition = position == null ? 0 : position;
-
-            // Note enablePlaceholders will be false here, but we don't have a way to communicate
-            // this to PositionalDataSource. This is fine, because only the list and its position
-            // offset will be consumed by the LoadInitialCallback.
-            mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
-                    pageSize, mainThreadExecutor, receiver);
-        }
-
-        @Override
-        void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
-                @NonNull Executor mainThreadExecutor,
-                @NonNull PageResult.Receiver<Value> receiver) {
-            int startIndex = currentEndIndex + 1;
-            mPositionalDataSource.dispatchLoadRange(
-                    PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
-        }
-
-        @Override
-        void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
-                int pageSize, @NonNull Executor mainThreadExecutor,
-                @NonNull PageResult.Receiver<Value> receiver) {
-
-            int startIndex = currentBeginIndex - 1;
-            if (startIndex < 0) {
-                // trigger empty list load
-                mPositionalDataSource.dispatchLoadRange(
-                        PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
-            } else {
-                int loadSize = Math.min(pageSize, startIndex + 1);
-                startIndex = startIndex - loadSize + 1;
-                mPositionalDataSource.dispatchLoadRange(
-                        PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
-            }
-        }
-
-        @Override
-        Integer getKey(int position, Value item) {
-            return position;
-        }
-
-    }
-
-    @NonNull
-    @Override
-    public final <V> PositionalDataSource<V> mapByPage(
-            @NonNull Function<List<T>, List<V>> function) {
-        return new WrapperPositionalDataSource<>(this, function);
-    }
-
-    @NonNull
-    @Override
-    public final <V> PositionalDataSource<V> map(@NonNull Function<T, V> function) {
-        return mapByPage(createListFunction(function));
-    }
-}
+}
\ No newline at end of file
diff --git a/androidx/paging/RxPagedListBuilder.java b/androidx/paging/RxPagedListBuilder.java
new file mode 100644
index 0000000..f98bc1e
--- /dev/null
+++ b/androidx/paging/RxPagedListBuilder.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+import java.util.concurrent.Executor;
+
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.Scheduler;
+import io.reactivex.functions.Cancellable;
+import io.reactivex.schedulers.Schedulers;
+
+/**
+ * Builder for {@code Observable<PagedList>} or {@code Flowable<PagedList>}, given a
+ * {@link DataSource.Factory} and a {@link PagedList.Config}.
+ * <p>
+ * The required parameters are in the constructor, so you can simply construct and build, or
+ * optionally enable extra features (such as initial load key, or BoundaryCallback).
+ * <p>
+ * The returned observable/flowable will already be subscribed on the
+ * {@link #setFetchScheduler(Scheduler)}, and will perform all loading on that scheduler. It will
+ * already be observed on {@link #setNotifyScheduler(Scheduler)}, and will dispatch new PagedLists,
+ * as well as their updates to that scheduler.
+ *
+ * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
+ *             you're using PositionalDataSource.
+ * @param <Value> Item type being presented.
+ */
+public final class RxPagedListBuilder<Key, Value> {
+    private Key mInitialLoadKey;
+    private PagedList.Config mConfig;
+    private DataSource.Factory<Key, Value> mDataSourceFactory;
+    private PagedList.BoundaryCallback mBoundaryCallback;
+    private Executor mNotifyExecutor;
+    private Executor mFetchExecutor;
+    private Scheduler mFetchScheduler;
+    private Scheduler mNotifyScheduler;
+
+    /**
+     * Creates a RxPagedListBuilder with required parameters.
+     *
+     * @param dataSourceFactory DataSource factory providing DataSource generations.
+     * @param config Paging configuration.
+     */
+    public RxPagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+            @NonNull PagedList.Config config) {
+        //noinspection ConstantConditions
+        if (config == null) {
+            throw new IllegalArgumentException("PagedList.Config must be provided");
+        }
+        //noinspection ConstantConditions
+        if (dataSourceFactory == null) {
+            throw new IllegalArgumentException("DataSource.Factory must be provided");
+        }
+        mDataSourceFactory = dataSourceFactory;
+        mConfig = config;
+    }
+
+    /**
+     * Creates a RxPagedListBuilder with required parameters.
+     * <p>
+     * This method is a convenience for:
+     * <pre>
+     * RxPagedListBuilder(dataSourceFactory,
+     *         new PagedList.Config.Builder().setPageSize(pageSize).build())
+     * </pre>
+     *
+     * @param dataSourceFactory DataSource.Factory providing DataSource generations.
+     * @param pageSize Size of pages to load.
+     */
+    @SuppressWarnings("unused")
+    public RxPagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+            int pageSize) {
+        this(dataSourceFactory, new PagedList.Config.Builder().setPageSize(pageSize).build());
+    }
+
+    /**
+     * First loading key passed to the first PagedList/DataSource.
+     * <p>
+     * When a new PagedList/DataSource pair is created after the first, it acquires a load key from
+     * the previous generation so that data is loaded around the position already being observed.
+     *
+     * @param key Initial load key passed to the first PagedList/DataSource.
+     * @return this
+     */
+    @SuppressWarnings("unused")
+    @NonNull
+    public RxPagedListBuilder<Key, Value> setInitialLoadKey(@Nullable Key key) {
+        mInitialLoadKey = key;
+        return this;
+    }
+
+    /**
+     * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load
+     * additional data from network when paging from local storage.
+     * <p>
+     * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this
+     * method is not called, or {@code null} is passed, you will not be notified when each
+     * DataSource runs out of data to provide to its PagedList.
+     * <p>
+     * If you are paging from a DataSource.Factory backed by local storage, you can set a
+     * BoundaryCallback to know when there is no more information to page from local storage.
+     * This is useful to page from the network when local storage is a cache of network data.
+     * <p>
+     * Note that when using a BoundaryCallback with a {@code Observable<PagedList>}, method calls
+     * on the callback may be dispatched multiple times - one for each PagedList/DataSource
+     * pair. If loading network data from a BoundaryCallback, you should prevent multiple
+     * dispatches of the same method from triggering multiple simultaneous network loads.
+     *
+     * @param boundaryCallback The boundary callback for listening to PagedList load state.
+     * @return this
+     */
+    @SuppressWarnings("unused")
+    @NonNull
+    public RxPagedListBuilder<Key, Value> setBoundaryCallback(
+            @Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
+        mBoundaryCallback = boundaryCallback;
+        return this;
+    }
+
+    /**
+     * Sets scheduler which will be used for observing new PagedLists, as well as loading updates
+     * within the PagedLists.
+     * <p>
+     * The built observable will be {@link Observable#observeOn(Scheduler) observed on} this
+     * scheduler, so that the thread receiving PagedLists will also receive the internal updates to
+     * the PagedList.
+     *
+     * @param scheduler Scheduler for background DataSource loading.
+     * @return this
+     */
+    public RxPagedListBuilder<Key, Value> setNotifyScheduler(
+            final @NonNull Scheduler scheduler) {
+        mNotifyScheduler = scheduler;
+        final Scheduler.Worker worker = scheduler.createWorker();
+        mNotifyExecutor = new Executor() {
+            @Override
+            public void execute(@NonNull Runnable command) {
+                // We use a worker here since the page load notifications
+                // should not be dispatched in parallel
+                worker.schedule(command);
+            }
+        };
+        return this;
+    }
+
+    /**
+     * Sets scheduler which will be used for background fetching of PagedLists, as well as on-demand
+     * fetching of pages inside.
+     *
+     * @param scheduler Scheduler for background DataSource loading.
+     * @return this
+     */
+    @SuppressWarnings({"unused", "WeakerAccess"})
+    @NonNull
+    public RxPagedListBuilder<Key, Value> setFetchScheduler(
+            final @NonNull Scheduler scheduler) {
+        mFetchExecutor = new Executor() {
+            @Override
+            public void execute(@NonNull Runnable command) {
+                // We use scheduleDirect since the page loads that use
+                // executor are intentionally parallel.
+                scheduler.scheduleDirect(command);
+            }
+        };
+        mFetchScheduler = scheduler;
+        return this;
+    }
+
+    /**
+     * Constructs a {@code Observable<PagedList>}.
+     * <p>
+     * The returned Observable will already be observed on the
+     * {@link #setNotifyScheduler(Scheduler) notify scheduler}, and subscribed on the
+     * {@link #setFetchScheduler(Scheduler) fetch scheduler}.
+     *
+     * @return The Observable of PagedLists
+     */
+    @NonNull
+    public Observable<PagedList<Value>> buildObservable() {
+        if (mNotifyExecutor == null) {
+            mNotifyExecutor = ArchTaskExecutor.getMainThreadExecutor();
+            mNotifyScheduler = Schedulers.from(mNotifyExecutor);
+        }
+        if (mFetchExecutor == null) {
+            mFetchExecutor = ArchTaskExecutor.getIOThreadExecutor();
+            mFetchScheduler = Schedulers.from(mFetchExecutor);
+        }
+        return Observable.create(new PagingObservableOnSubscribe<>(
+                mInitialLoadKey,
+                mConfig,
+                mBoundaryCallback,
+                mDataSourceFactory,
+                mNotifyExecutor,
+                mFetchExecutor))
+                        .observeOn(mNotifyScheduler)
+                        .subscribeOn(mFetchScheduler);
+    }
+
+    /**
+     * Constructs a {@code Flowable<PagedList>}.
+     *
+     * The returned Observable will already be observed on the
+     * {@link #setNotifyScheduler(Scheduler) notify scheduler}, and subscribed on the
+     * {@link #setFetchScheduler(Scheduler) fetch scheduler}.
+     *
+     * @param backpressureStrategy BackpressureStrategy for the Flowable to use.
+     * @return The Flowable of PagedLists
+     */
+    @NonNull
+    public Flowable<PagedList<Value>> buildFlowable(BackpressureStrategy backpressureStrategy) {
+        return buildObservable()
+                .toFlowable(backpressureStrategy);
+    }
+
+    static class PagingObservableOnSubscribe<Key, Value>
+            implements ObservableOnSubscribe<PagedList<Value>>, DataSource.InvalidatedCallback,
+            Cancellable,
+            Runnable {
+
+        @Nullable
+        private final Key mInitialLoadKey;
+        @NonNull
+        private final PagedList.Config mConfig;
+        @Nullable
+        private final PagedList.BoundaryCallback mBoundaryCallback;
+        @NonNull
+        private final DataSource.Factory<Key, Value> mDataSourceFactory;
+        @NonNull
+        private final Executor mNotifyExecutor;
+        @NonNull
+        private final Executor mFetchExecutor;
+
+        @Nullable
+        private PagedList<Value> mList;
+        @Nullable
+        private DataSource<Key, Value> mDataSource;
+
+        private ObservableEmitter<PagedList<Value>> mEmitter;
+
+        private PagingObservableOnSubscribe(@Nullable Key initialLoadKey,
+                @NonNull PagedList.Config config,
+                @Nullable PagedList.BoundaryCallback boundaryCallback,
+                @NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+                @NonNull Executor notifyExecutor,
+                @NonNull Executor fetchExecutor) {
+            mInitialLoadKey = initialLoadKey;
+            mConfig = config;
+            mBoundaryCallback = boundaryCallback;
+            mDataSourceFactory = dataSourceFactory;
+            mNotifyExecutor = notifyExecutor;
+            mFetchExecutor = fetchExecutor;
+        }
+
+        @Override
+        public void subscribe(ObservableEmitter<PagedList<Value>> emitter)
+                throws Exception {
+            mEmitter = emitter;
+            mEmitter.setCancellable(this);
+
+            // known that subscribe is already on fetchScheduler
+            mEmitter.onNext(createPagedList());
+        }
+
+        @Override
+        public void cancel() throws Exception {
+            if (mDataSource != null) {
+                mDataSource.removeInvalidatedCallback(this);
+            }
+        }
+
+        @Override
+        public void run() {
+            // fetch data, run on fetchExecutor
+            mEmitter.onNext(createPagedList());
+        }
+
+        @Override
+        public void onInvalidated() {
+            if (!mEmitter.isDisposed()) {
+                mFetchExecutor.execute(this);
+            }
+        }
+
+        private PagedList<Value> createPagedList() {
+            @Nullable Key initializeKey = mInitialLoadKey;
+            if (mList != null) {
+                //noinspection unchecked
+                initializeKey = (Key) mList.getLastKey();
+            }
+
+            do {
+                if (mDataSource != null) {
+                    mDataSource.removeInvalidatedCallback(this);
+                }
+                mDataSource = mDataSourceFactory.create();
+                mDataSource.addInvalidatedCallback(this);
+
+                mList = new PagedList.Builder<>(mDataSource, mConfig)
+                        .setNotifyExecutor(mNotifyExecutor)
+                        .setFetchExecutor(mFetchExecutor)
+                        .setBoundaryCallback(mBoundaryCallback)
+                        .setInitialKey(initializeKey)
+                        .build();
+            } while (mList.isDetached());
+            return mList;
+        }
+    }
+}
diff --git a/androidx/paging/WrapperItemKeyedDataSource.java b/androidx/paging/WrapperItemKeyedDataSource.java
index 72a6d5c..1583f9d 100644
--- a/androidx/paging/WrapperItemKeyedDataSource.java
+++ b/androidx/paging/WrapperItemKeyedDataSource.java
@@ -25,13 +25,6 @@
 class WrapperItemKeyedDataSource<K, A, B> extends ItemKeyedDataSource<K, B> {
     private final ItemKeyedDataSource<K, A> mSource;
     private final Function<List<A>, List<B>> mListFunction;
-    private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
-        @Override
-        public void onInvalidated() {
-            invalidate();
-            removeCallback();
-        }
-    };
 
     private final IdentityHashMap<B, K> mKeyMap = new IdentityHashMap<>();
 
@@ -39,11 +32,26 @@
             Function<List<A>, List<B>> listFunction) {
         mSource = source;
         mListFunction = listFunction;
-        mSource.addInvalidatedCallback(mInvalidatedCallback);
     }
 
-    private void removeCallback() {
-        mSource.removeInvalidatedCallback(mInvalidatedCallback);
+    @Override
+    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.addInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.removeInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void invalidate() {
+        mSource.invalidate();
+    }
+
+    @Override
+    public boolean isInvalid() {
+        return mSource.isInvalid();
     }
 
     private List<B> convertWithStashedKeys(List<A> source) {
diff --git a/androidx/paging/WrapperPageKeyedDataSource.java b/androidx/paging/WrapperPageKeyedDataSource.java
index 7675026..4c947d2 100644
--- a/androidx/paging/WrapperPageKeyedDataSource.java
+++ b/androidx/paging/WrapperPageKeyedDataSource.java
@@ -25,23 +25,31 @@
 class WrapperPageKeyedDataSource<K, A, B> extends PageKeyedDataSource<K, B> {
     private final PageKeyedDataSource<K, A> mSource;
     private final Function<List<A>, List<B>> mListFunction;
-    private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
-        @Override
-        public void onInvalidated() {
-            invalidate();
-            removeCallback();
-        }
-    };
 
     WrapperPageKeyedDataSource(PageKeyedDataSource<K, A> source,
             Function<List<A>, List<B>> listFunction) {
         mSource = source;
         mListFunction = listFunction;
-        mSource.addInvalidatedCallback(mInvalidatedCallback);
     }
 
-    private void removeCallback() {
-        mSource.removeInvalidatedCallback(mInvalidatedCallback);
+    @Override
+    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.addInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.removeInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void invalidate() {
+        mSource.invalidate();
+    }
+
+    @Override
+    public boolean isInvalid() {
+        return mSource.isInvalid();
     }
 
     @Override
diff --git a/androidx/paging/WrapperPositionalDataSource.java b/androidx/paging/WrapperPositionalDataSource.java
index 257f6c7..3b739ea 100644
--- a/androidx/paging/WrapperPositionalDataSource.java
+++ b/androidx/paging/WrapperPositionalDataSource.java
@@ -25,23 +25,30 @@
     private final PositionalDataSource<A> mSource;
     private final Function<List<A>, List<B>> mListFunction;
 
-    private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
-        @Override
-        public void onInvalidated() {
-            invalidate();
-            removeCallback();
-        }
-    };
-
     WrapperPositionalDataSource(PositionalDataSource<A> source,
             Function<List<A>, List<B>> listFunction) {
         mSource = source;
         mListFunction = listFunction;
-        mSource.addInvalidatedCallback(mInvalidatedCallback);
     }
 
-    private void removeCallback() {
-        mSource.removeInvalidatedCallback(mInvalidatedCallback);
+    @Override
+    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.addInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.removeInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void invalidate() {
+        mSource.invalidate();
+    }
+
+    @Override
+    public boolean isInvalid() {
+        return mSource.isInvalid();
     }
 
     @Override
diff --git a/androidx/preference/CollapsiblePreferenceGroupController.java b/androidx/preference/CollapsiblePreferenceGroupController.java
index bcc71ce..3e1d792 100644
--- a/androidx/preference/CollapsiblePreferenceGroupController.java
+++ b/androidx/preference/CollapsiblePreferenceGroupController.java
@@ -133,7 +133,8 @@
 
     private ExpandButton createExpandButton(final PreferenceGroup group,
             List<Preference> collapsedPreferences) {
-        final ExpandButton preference = new ExpandButton(mContext, collapsedPreferences);
+        final ExpandButton preference = new ExpandButton(mContext, collapsedPreferences,
+                group.getId());
         preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
             @Override
             public boolean onPreferenceClick(Preference preference) {
@@ -150,10 +151,16 @@
      * {@link PreferenceGroup}.
      */
     static class ExpandButton extends Preference {
-        ExpandButton(Context context, List<Preference> collapsedPreferences) {
+        private long mId;
+
+        ExpandButton(Context context, List<Preference> collapsedPreferences, long parentId) {
             super(context);
             initLayout();
             setSummary(collapsedPreferences);
+            // Since IDs are unique, using the parentId as a reference ensures that this expand
+            // button will have a unique ID and hence transitions will be correctly animated by
+            // RecyclerView when there are multiple ExpandButtons.
+            mId = parentId + 1000000;
         }
 
         private void initLayout() {
@@ -201,6 +208,11 @@
             super.onBindViewHolder(holder);
             holder.setDividerAllowedAbove(false);
         }
+
+        @Override
+        public long getId() {
+            return mId;
+        }
     }
 
 }
\ No newline at end of file
diff --git a/androidx/recyclerview/selection/BandSelectionHelper.java b/androidx/recyclerview/selection/BandSelectionHelper.java
index 883fcbf..6aabd45 100644
--- a/androidx/recyclerview/selection/BandSelectionHelper.java
+++ b/androidx/recyclerview/selection/BandSelectionHelper.java
@@ -48,7 +48,7 @@
  * the user interacts with items using their pointer (and the band). Selectable items that intersect
  * with the band, both on and off screen, are selected on pointer up.
  *
- * @see SelectionTracker.Builder#withBandTooltypes(int...) for details on the specific
+ * @see SelectionTracker.Builder#withPointerTooltypes(int...) for details on the specific
  *     tooltypes routed to this helper.
  *
  * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
diff --git a/androidx/recyclerview/selection/DefaultSelectionTracker.java b/androidx/recyclerview/selection/DefaultSelectionTracker.java
index c7be1b8..4e65be8 100644
--- a/androidx/recyclerview/selection/DefaultSelectionTracker.java
+++ b/androidx/recyclerview/selection/DefaultSelectionTracker.java
@@ -30,6 +30,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.selection.Range.RangeType;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -60,6 +61,7 @@
     private final SelectionPredicate<K> mSelectionPredicate;
     private final StorageStrategy<K> mStorage;
     private final RangeCallbacks mRangeCallbacks;
+    private final AdapterObserver mAdapterObserver;
     private final boolean mSingleSelect;
     private final String mSelectionId;
 
@@ -94,6 +96,8 @@
         mRangeCallbacks = new RangeCallbacks();
 
         mSingleSelect = !selectionPredicate.canSelectMultiple();
+
+        mAdapterObserver = new AdapterObserver(this);
     }
 
     @Override
@@ -344,7 +348,11 @@
     }
 
     @Override
-    void onDataSetChanged() {
+    AdapterDataObserver getAdapterDataObserver() {
+        return mAdapterObserver;
+    }
+
+    private void onDataSetChanged() {
         mSelection.clearProvisionalSelection();
 
         notifySelectionRefresh();
@@ -524,4 +532,41 @@
             }
         }
     }
+
+    private static final class AdapterObserver extends AdapterDataObserver {
+
+        private final DefaultSelectionTracker<?> mSelectionTracker;
+
+        AdapterObserver(@NonNull DefaultSelectionTracker<?> selectionTracker) {
+            checkArgument(selectionTracker != null);
+            mSelectionTracker = selectionTracker;
+        }
+
+        @Override
+        public void onChanged() {
+            mSelectionTracker.onDataSetChanged();
+        }
+
+        @Override
+        public void onItemRangeChanged(int startPosition, int itemCount, @Nullable Object payload) {
+            if (!SelectionTracker.SELECTION_CHANGED_MARKER.equals(payload)) {
+                mSelectionTracker.onDataSetChanged();
+            }
+        }
+
+        @Override
+        public void onItemRangeInserted(int startPosition, int itemCount) {
+            mSelectionTracker.endRange();
+        }
+
+        @Override
+        public void onItemRangeRemoved(int startPosition, int itemCount) {
+            mSelectionTracker.endRange();
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            mSelectionTracker.endRange();
+        }
+    }
 }
diff --git a/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java b/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java
index 525c264..f7512ca 100644
--- a/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java
+++ b/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java
@@ -303,7 +303,7 @@
 
     @Test
     public void testProvisionalSelection() {
-        Selection s = mTracker.getSelection();
+        Selection<String> s = mTracker.getSelection();
         mSelection.assertNoSelection();
 
         // Mimicking band selection case -- BandController notifies item callback by itself.
@@ -319,7 +319,7 @@
 
     @Test
     public void testProvisionalSelection_Replace() {
-        Selection s = mTracker.getSelection();
+        Selection<String> s = mTracker.getSelection();
 
         // Mimicking band selection case -- BandController notifies item callback by itself.
         mListener.onItemStateChanged(mItems.get(1), true);
@@ -343,7 +343,7 @@
 
     @Test
     public void testProvisionalSelection_IntersectsExistingProvisionalSelection() {
-        Selection s = mTracker.getSelection();
+        Selection<String> s = mTracker.getSelection();
 
         // Mimicking band selection case -- BandController notifies item callback by itself.
         mListener.onItemStateChanged(mItems.get(1), true);
@@ -365,7 +365,7 @@
 
     @Test
     public void testProvisionalSelection_Apply() {
-        Selection s = mTracker.getSelection();
+        Selection<String> s = mTracker.getSelection();
 
         // Mimicking band selection case -- BandController notifies item callback by itself.
         mListener.onItemStateChanged(mItems.get(1), true);
@@ -383,7 +383,7 @@
     public void testProvisionalSelection_Cancel() {
         mTracker.select(mItems.get(1));
         mTracker.select(mItems.get(2));
-        Selection s = mTracker.getSelection();
+        Selection<String> s = mTracker.getSelection();
 
         SparseBooleanArray provisional = new SparseBooleanArray();
         provisional.append(3, true);
@@ -399,7 +399,7 @@
     public void testProvisionalSelection_IntersectsAppliedSelection() {
         mTracker.select(mItems.get(1));
         mTracker.select(mItems.get(2));
-        Selection s = mTracker.getSelection();
+        Selection<String> s = mTracker.getSelection();
 
         // Mimicking band selection case -- BandController notifies item callback by itself.
         mListener.onItemStateChanged(mItems.get(3), true);
diff --git a/androidx/recyclerview/selection/EventBridge.java b/androidx/recyclerview/selection/EventBridge.java
index e117944..6c43f7e 100644
--- a/androidx/recyclerview/selection/EventBridge.java
+++ b/androidx/recyclerview/selection/EventBridge.java
@@ -17,13 +17,13 @@
 package androidx.recyclerview.selection;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
 import static androidx.core.util.Preconditions.checkArgument;
 import static androidx.recyclerview.selection.Shared.VERBOSE;
 
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.RecyclerView;
@@ -39,7 +39,7 @@
  * @hide
  */
 @RestrictTo(LIBRARY_GROUP)
-@VisibleForTesting
+@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
 public class EventBridge {
 
     private static final String TAG = "EventsRelays";
@@ -58,49 +58,9 @@
             @NonNull SelectionTracker<K> selectionTracker,
             @NonNull ItemKeyProvider<K> keyProvider) {
 
-        // setup bridges to relay selection events.
-        new AdapterToTrackerBridge(adapter, selectionTracker);
+        // setup bridges to relay selection and adapter events
         new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter);
-    }
-
-    private static final class AdapterToTrackerBridge extends RecyclerView.AdapterDataObserver {
-
-        private final SelectionTracker<?> mSelectionTracker;
-
-        AdapterToTrackerBridge(
-                @NonNull RecyclerView.Adapter<?> adapter,
-                @NonNull SelectionTracker<?> selectionTracker) {
-            adapter.registerAdapterDataObserver(this);
-
-            checkArgument(selectionTracker != null);
-            mSelectionTracker = selectionTracker;
-        }
-
-        @Override
-        public void onChanged() {
-            mSelectionTracker.onDataSetChanged();
-        }
-
-        @Override
-        public void onItemRangeChanged(int startPosition, int itemCount, @Nullable Object payload) {
-            // No change in position. Ignore.
-            // TODO(b/72393576): Properties of items could change. Should reevaluate selected status
-        }
-
-        @Override
-        public void onItemRangeInserted(int startPosition, int itemCount) {
-            // Uninteresting to us since selection is stable ID based.
-        }
-
-        @Override
-        public void onItemRangeRemoved(int startPosition, int itemCount) {
-            // Uninteresting to us since selection is stable ID based.
-        }
-
-        @Override
-        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
-            // Uninteresting to us since selection is stable ID based.
-        }
+        adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver());
     }
 
     private static final class TrackerToAdapterBridge<K>
diff --git a/androidx/recyclerview/selection/GestureSelectionHelper.java b/androidx/recyclerview/selection/GestureSelectionHelper.java
index a780bda..ea7ec16 100644
--- a/androidx/recyclerview/selection/GestureSelectionHelper.java
+++ b/androidx/recyclerview/selection/GestureSelectionHelper.java
@@ -75,7 +75,13 @@
      */
     void start() {
         checkState(!mStarted);
-        checkState(mLastStartedItemPos > -1);
+        // See: b/70518185. It appears start() is being called via onLongPress
+        // even though we never received an intial handleInterceptedDownEvent
+        // where we would usually initialize mLastStartedItemPos.
+        if (mLastStartedItemPos < 0) {
+            Log.w(TAG, "Illegal state. Can't start without valid mLastStartedItemPos.");
+            return;
+        }
 
         // Partner code in MotionInputHandler ensures items
         // are selected and range established prior to
diff --git a/androidx/recyclerview/selection/GestureSelectionHelperTest.java b/androidx/recyclerview/selection/GestureSelectionHelperTest.java
index 4c10930..9fd494d 100644
--- a/androidx/recyclerview/selection/GestureSelectionHelperTest.java
+++ b/androidx/recyclerview/selection/GestureSelectionHelperTest.java
@@ -18,7 +18,6 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -56,17 +55,17 @@
     private GestureSelectionHelper mHelper;
     private SelectionTracker<String> mSelectionTracker;
     private SelectionProbe mSelection;
-    private OperationMonitor mLock;
+    private OperationMonitor mMonitor;
     private TestViewDelegate mView;
 
     @Before
     public void setUp() {
         mSelectionTracker = SelectionTrackers.createStringTracker("gesture-selection-test", 100);
         mSelection = new SelectionProbe(mSelectionTracker);
-        mLock = new OperationMonitor();
+        mMonitor = new OperationMonitor();
         mView = new TestViewDelegate();
         mHelper = new GestureSelectionHelper(
-                mSelectionTracker, mView, new TestAutoScroller(), mLock);
+                mSelectionTracker, mView, new TestAutoScroller(), mMonitor);
     }
 
     @Test
@@ -78,12 +77,10 @@
 
     @Test
     public void testNoStartOnIllegalPosition() {
+        mView.mNextPosition = RecyclerView.NO_POSITION;
         mHelper.onInterceptTouchEvent(null, DOWN);
-        try {
-            mHelper.start();
-            fail("Should have thrown.");
-        } catch (Exception expected) {
-        }
+        mHelper.start();
+        assertFalse(mMonitor.isStarted());
     }
 
     @Test
diff --git a/androidx/recyclerview/selection/GridModel.java b/androidx/recyclerview/selection/GridModel.java
index f496b0c..abc2144 100644
--- a/androidx/recyclerview/selection/GridModel.java
+++ b/androidx/recyclerview/selection/GridModel.java
@@ -27,7 +27,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
@@ -169,7 +168,6 @@
      *                        though its
      *                        absolute point has a higher y-value.
      */
-    @VisibleForTesting
     void resizeSelection(Point relativePointer) {
         mPointer = mHost.createAbsolutePoint(relativePointer);
         updateModel();
diff --git a/androidx/recyclerview/selection/MotionEvents.java b/androidx/recyclerview/selection/MotionEvents.java
index 90d79e6..bc47a76 100644
--- a/androidx/recyclerview/selection/MotionEvents.java
+++ b/androidx/recyclerview/selection/MotionEvents.java
@@ -104,6 +104,16 @@
         return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
     }
 
+    /**
+     * Returns true if the event is a drag event (which is presumbaly, but not
+     * explicitly required to be a mouse event).
+     * @param e
+     */
+    static boolean isPointerDragEvent(MotionEvent e) {
+        return isPrimaryMouseButtonPressed(e)
+                && isActionMove(e);
+    }
+
     private static boolean hasBit(int metaState, int bit) {
         return (metaState & bit) != 0;
     }
diff --git a/androidx/recyclerview/selection/OnDragInitiatedListener.java b/androidx/recyclerview/selection/OnDragInitiatedListener.java
index a479872..50d8f1b 100644
--- a/androidx/recyclerview/selection/OnDragInitiatedListener.java
+++ b/androidx/recyclerview/selection/OnDragInitiatedListener.java
@@ -16,31 +16,58 @@
 
 package androidx.recyclerview.selection;
 
+import static androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+import android.content.ClipData;
 import android.view.MotionEvent;
+import android.view.View;
 
 import androidx.annotation.NonNull;
 
 /**
- * Register an OnDragInitiatedListener to be notified of potential drag operations,
- * and to handle them.
+ * Register an OnDragInitiatedListener to be notified when user intent to perform drag and drop
+ * operations on an item or items has been detected. Handle these events using {@link View}
+ * support for Drag and drop.
+ *
+ * <p>
+ * See {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
+ * for details.
  */
 public interface OnDragInitiatedListener {
 
     /**
-     * Called when a drag is initiated. Touch input handler only considers
-     * a drag to be initiated on long press on an existing selection,
-     * as normal touch and drag events are strongly associated with scrolling of the view.
+     * Called when user intent to perform a drag and drop operation has been detected.
      *
      * <p>
-     * Drag will only be initiated when the item under the event is already selected.
+     * The following circumstances are considered to be expressing drag and drop intent:
+     *
+     * <ol>
+     *     <li>Long press on selected item.</li>
+     *     <li>Click and drag in the {@link ItemDetails#inDragRegion(MotionEvent) drag region}
+     *     of selected item with a pointer device.</li>
+     *     <li>Click and drag in drag region of un-selected item with a pointer device.</li>
+     * </ol>
      *
      * <p>
      * The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
-     * to this method as there may be multiple items selected. Clients can obtain the current
-     * list of selected items from {@link SelectionTracker#copySelection(MutableSelection)}.
+     * to this method as there may be multiple items selected or no items selected (as may be
+     * the case in pointer drive drag and drop.)
+     *
+     * <p>
+     * Obtain the current list of selected items from
+     * {@link SelectionTracker#copySelection(MutableSelection)}. If there is no selection
+     * get the item under the event using {@link ItemDetailsLookup#getItemDetails(MotionEvent)}.
+     *
+     * <p>
+     * Drag region used with pointer devices is specified by
+     * {@link ItemDetails#inDragRegion(MotionEvent)}
+     *
+     * <p>
+     * See {@link android.view.View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
+     * for details on drag and drop implementation.
      *
      * @param e the event associated with the drag.
-     * @return true if the event was handled.
+     * @return true if drag and drop was initiated.
      */
     boolean onDragInitiated(@NonNull MotionEvent e);
 }
diff --git a/androidx/recyclerview/selection/PointerDragEventInterceptor.java b/androidx/recyclerview/selection/PointerDragEventInterceptor.java
new file mode 100644
index 0000000..46ec5dd
--- /dev/null
+++ b/androidx/recyclerview/selection/PointerDragEventInterceptor.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 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 androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+
+/**
+ * OnItemTouchListener that delegates drag events to a drag listener,
+ * else sends event to fallback {@link OnItemTouchListener}.
+ *
+ * <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop.
+ */
+final class PointerDragEventInterceptor implements OnItemTouchListener {
+
+    private final ItemDetailsLookup mEventDetailsLookup;
+    private final OnDragInitiatedListener mDragListener;
+    private @Nullable OnItemTouchListener mDelegate;
+
+    PointerDragEventInterceptor(
+            ItemDetailsLookup eventDetailsLookup,
+            OnDragInitiatedListener dragListener,
+            @Nullable OnItemTouchListener delegate) {
+
+        checkArgument(eventDetailsLookup != null);
+        checkArgument(dragListener != null);
+
+        mEventDetailsLookup = eventDetailsLookup;
+        mDragListener = dragListener;
+        mDelegate = delegate;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+        if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) {
+            return mDragListener.onDragInitiated(e);
+        } else if (mDelegate != null) {
+            return mDelegate.onInterceptTouchEvent(rv, e);
+        }
+        return false;
+    }
+
+    @Override
+    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+        if (mDelegate != null) {
+            mDelegate.onTouchEvent(rv, e);
+        }
+    }
+
+    @Override
+    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        if (mDelegate != null) {
+            mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept);
+        }
+    }
+}
diff --git a/androidx/recyclerview/selection/SelectionTest.java b/androidx/recyclerview/selection/SelectionTest.java
index eb2402b..b8c6583 100644
--- a/androidx/recyclerview/selection/SelectionTest.java
+++ b/androidx/recyclerview/selection/SelectionTest.java
@@ -41,11 +41,11 @@
             "auth|id=@53di*/f3#d"
     };
 
-    private Selection mSelection;
+    private Selection<String> mSelection;
 
     @Before
     public void setUp() throws Exception {
-        mSelection = new Selection();
+        mSelection = new Selection<>();
         mSelection.add(mIds[0]);
         mSelection.add(mIds[1]);
         mSelection.add(mIds[2]);
@@ -96,14 +96,14 @@
 
     @Test
     public void testIsEmpty() {
-        assertTrue(new Selection().isEmpty());
+        assertTrue(new Selection<>().isEmpty());
         mSelection.clear();
         assertTrue(mSelection.isEmpty());
     }
 
     @Test
     public void testSize() {
-        Selection other = new Selection();
+        Selection<String> other = new Selection<>();
         for (int i = 0; i < mSelection.size(); i++) {
             other.add(mIds[i]);
         }
@@ -117,7 +117,7 @@
 
     @Test
     public void testEqualsOther() {
-        Selection other = new Selection();
+        Selection<String> other = new Selection<>();
         other.add(mIds[0]);
         other.add(mIds[1]);
         other.add(mIds[2]);
@@ -127,7 +127,7 @@
 
     @Test
     public void testEqualsCopy() {
-        Selection other = new Selection();
+        Selection<String> other = new Selection<>();
         other.copyFrom(mSelection);
         assertEquals(mSelection, other);
         assertEquals(mSelection.hashCode(), other.hashCode());
@@ -135,7 +135,7 @@
 
     @Test
     public void testNotEquals() {
-        Selection other = new Selection();
+        Selection<String> other = new Selection<>();
         other.add("foobar");
         assertFalse(mSelection.equals(other));
     }
diff --git a/androidx/recyclerview/selection/SelectionTracker.java b/androidx/recyclerview/selection/SelectionTracker.java
index 2b35f5d..283426f 100644
--- a/androidx/recyclerview/selection/SelectionTracker.java
+++ b/androidx/recyclerview/selection/SelectionTracker.java
@@ -29,6 +29,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 
 import java.util.Set;
 
@@ -119,7 +121,7 @@
      * of the selection that will not reflect future changes
      * to selection.
      */
-    public abstract Selection getSelection();
+    public abstract Selection<K> getSelection();
 
     /**
      * Updates {@code dest} to reflect the current selection.
@@ -175,7 +177,7 @@
      */
     public abstract boolean deselect(@NonNull K key);
 
-    abstract void onDataSetChanged();
+    abstract AdapterDataObserver getAdapterDataObserver();
 
     /**
      * Attempts to establish a range selection at {@code position}, selecting the item
@@ -471,7 +473,7 @@
                 MotionEvent.TOOL_TYPE_UNKNOWN
         };
 
-        private int[] mBandToolTypes = new int[] {
+        private int[] mPointerToolTypes = new int[] {
                 MotionEvent.TOOL_TYPE_MOUSE
         };
 
@@ -637,14 +639,16 @@
         }
 
         /**
-         * Replaces default band selection tool-types. Defaults are:
+         * Replaces default pointer tool-types. Pointer tools
+         * are associated with band selection, and certain
+         * drag and drop behaviors. Defaults are:
          * {@link MotionEvent#TOOL_TYPE_MOUSE}.
          *
          * @param toolTypes the tool types to be used
          * @return this
          */
-        public Builder<K> withBandTooltypes(int... toolTypes) {
-            mBandToolTypes = toolTypes;
+        public Builder<K> withPointerTooltypes(int... toolTypes) {
+            mPointerToolTypes = toolTypes;
             return this;
         }
 
@@ -769,10 +773,12 @@
                     mOnItemActivatedListener,
                     mFocusDelegate);
 
-            for (int toolType : mBandToolTypes) {
+            for (int toolType : mPointerToolTypes) {
                 gestureRouter.register(toolType, mouseHandler);
             }
 
+            @Nullable BandSelectionHelper bandHelper = null;
+
             // Band selection not supported in single select mode, or when key access
             // is limited to anything less than the entire corpus.
             if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
@@ -782,7 +788,7 @@
                 // necessarily models and caches list/grid information as the user's pointer
                 // interacts with the item in the RecyclerView. Selectable items that intersect
                 // with the band, both on and off screen, are selected.
-                BandSelectionHelper bandHelper = BandSelectionHelper.create(
+                bandHelper = BandSelectionHelper.create(
                         mRecyclerView,
                         scroller,
                         mBandOverlayId,
@@ -792,10 +798,13 @@
                         mBandPredicate,
                         mFocusDelegate,
                         mMonitor);
+            }
 
-                for (int toolType : mBandToolTypes) {
-                    eventRouter.register(toolType, bandHelper);
-                }
+            OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
+                    mDetailsLookup, mOnDragInitiatedListener, bandHelper);
+
+            for (int toolType : mPointerToolTypes) {
+                eventRouter.register(toolType, pointerEventHandler);
             }
 
             return tracker;
diff --git a/androidx/recyclerview/selection/testing/SelectionProbe.java b/androidx/recyclerview/selection/testing/SelectionProbe.java
index e8ae607..8ac4baf 100644
--- a/androidx/recyclerview/selection/testing/SelectionProbe.java
+++ b/androidx/recyclerview/selection/testing/SelectionProbe.java
@@ -64,7 +64,7 @@
     }
 
     public void assertSelectionSize(int expected) {
-        Selection selection = mMgr.getSelection();
+        Selection<String> selection = mMgr.getSelection();
         assertEquals(selection.toString(), expected, selection.size());
 
         mSelectionListener.assertSelectionSize(expected);
diff --git a/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index 25aa00d..60eb6d2 100644
--- a/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -1180,7 +1180,16 @@
             adapter.addAndNotify(5 + (i % 3) * 3, 1);
             Thread.sleep(25);
         }
-        smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
+
+        final AtomicInteger lastVisiblePosition = new AtomicInteger();
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                lastVisiblePosition.set(mLayoutManager.findLastVisibleItemPosition());
+            }
+        });
+
+        smoothScrollToPosition(lastVisiblePosition.get() + 20);
         waitForAnimations(2);
         getInstrumentation().waitForIdleSync();
         assertEquals("Children count should add up", childCount.get(),
diff --git a/androidx/room/ForeignKey.java b/androidx/room/ForeignKey.java
index 847941a..54b9252 100644
--- a/androidx/room/ForeignKey.java
+++ b/androidx/room/ForeignKey.java
@@ -13,11 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.room;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import androidx.annotation.IntDef;
 
+import java.lang.annotation.Retention;
+
 /**
  * Declares a foreign key on another {@link Entity}.
  * <p>
@@ -161,6 +164,7 @@
      * {@link #onUpdate()}.
      */
     @IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
+    @Retention(SOURCE)
     @interface Action {
     }
 }
diff --git a/androidx/room/integration/testapp/CustomerViewModel.java b/androidx/room/integration/testapp/CustomerViewModel.java
index eca2552..d0ac7d8 100644
--- a/androidx/room/integration/testapp/CustomerViewModel.java
+++ b/androidx/room/integration/testapp/CustomerViewModel.java
@@ -25,6 +25,7 @@
 import androidx.paging.DataSource;
 import androidx.paging.LivePagedListBuilder;
 import androidx.paging.PagedList;
+import androidx.paging.RxPagedListBuilder;
 import androidx.room.Room;
 import androidx.room.integration.testapp.database.Customer;
 import androidx.room.integration.testapp.database.LastNameAscCustomerDataSource;
@@ -32,6 +33,9 @@
 
 import java.util.UUID;
 
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+
 /**
  * Sample database-backed view model of Customers
  */
@@ -48,20 +52,17 @@
         mDatabase = Room.databaseBuilder(this.getApplication(),
                 SampleDatabase.class, "customerDatabase").build();
 
-        ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
-            @Override
-            public void run() {
-                // fill with some simple data
-                int customerCount = mDatabase.getCustomerDao().countCustomers();
-                if (customerCount == 0) {
-                    Customer[] initialCustomers = new Customer[10];
-                    for (int i = 0; i < 10; i++) {
-                        initialCustomers[i] = createCustomer();
-                    }
-                    mDatabase.getCustomerDao().insertAll(initialCustomers);
+        ArchTaskExecutor.getInstance().executeOnDiskIO(() -> {
+            // fill with some simple data
+            int customerCount = mDatabase.getCustomerDao().countCustomers();
+            if (customerCount == 0) {
+                Customer[] initialCustomers = new Customer[10];
+                for (int i = 0; i < 10; i++) {
+                    initialCustomers[i] = createCustomer();
                 }
-
+                mDatabase.getCustomerDao().insertAll(initialCustomers);
             }
+
         });
     }
 
@@ -74,12 +75,13 @@
     }
 
     void insertCustomer() {
-        ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
-            @Override
-            public void run() {
-                mDatabase.getCustomerDao().insert(createCustomer());
-            }
-        });
+        ArchTaskExecutor.getInstance().executeOnDiskIO(
+                () -> mDatabase.getCustomerDao().insert(createCustomer()));
+    }
+
+    void clearAllCustomers() {
+        ArchTaskExecutor.getInstance().executeOnDiskIO(
+                () -> mDatabase.getCustomerDao().removeAll());
     }
 
     private static <K> LiveData<PagedList<Customer>> getLivePagedList(
@@ -93,6 +95,20 @@
                 .build();
     }
 
+    private static <K> Flowable<PagedList<Customer>> getPagedListFlowable(
+            DataSource.Factory<K, Customer> dataSourceFactory) {
+        PagedList.Config config = new PagedList.Config.Builder()
+                .setPageSize(10)
+                .setEnablePlaceholders(false)
+                .build();
+        return new RxPagedListBuilder<>(dataSourceFactory, config)
+                .buildFlowable(BackpressureStrategy.LATEST);
+    }
+
+    Flowable<PagedList<Customer>> getPagedListFlowable() {
+        return getPagedListFlowable(mDatabase.getCustomerDao().loadPagedAgeOrder());
+    }
+
     LiveData<PagedList<Customer>> getLivePagedList(int position) {
         if (mLiveCustomerList == null) {
             mLiveCustomerList =
diff --git a/androidx/room/integration/testapp/RoomPagedListRxActivity.java b/androidx/room/integration/testapp/RoomPagedListRxActivity.java
new file mode 100644
index 0000000..16d24a5
--- /dev/null
+++ b/androidx/room/integration/testapp/RoomPagedListRxActivity.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp;
+
+import android.os.Bundle;
+import android.widget.Button;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.RecyclerView;
+
+import io.reactivex.disposables.CompositeDisposable;
+
+/**
+ * Sample {@code Flowable<PagedList>} activity which uses Room.
+ */
+public class RoomPagedListRxActivity extends AppCompatActivity {
+
+    private PagedListCustomerAdapter mAdapter;
+
+    private final CompositeDisposable mDisposable = new CompositeDisposable();
+    private CustomerViewModel mViewModel;
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_recycler_view);
+        mViewModel = ViewModelProviders.of(this)
+                .get(CustomerViewModel.class);
+
+        RecyclerView recyclerView = findViewById(R.id.recyclerview);
+        mAdapter = new PagedListCustomerAdapter();
+        recyclerView.setAdapter(mAdapter);
+
+        final Button addButton = findViewById(R.id.addButton);
+        addButton.setOnClickListener(v -> mViewModel.insertCustomer());
+
+        final Button clearButton = findViewById(R.id.clearButton);
+        clearButton.setOnClickListener(v -> mViewModel.clearAllCustomers());
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        mDisposable.add(mViewModel.getPagedListFlowable()
+                .subscribe(list -> mAdapter.submitList(list)));
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        mDisposable.clear();
+    }
+}
diff --git a/androidx/room/integration/testapp/database/CustomerDao.java b/androidx/room/integration/testapp/database/CustomerDao.java
index 311a593..1aa0163 100644
--- a/androidx/room/integration/testapp/database/CustomerDao.java
+++ b/androidx/room/integration/testapp/database/CustomerDao.java
@@ -44,6 +44,12 @@
     void insertAll(Customer[] customers);
 
     /**
+     * Delete all customers
+     */
+    @Query("DELETE FROM customer")
+    void removeAll();
+
+    /**
      * @return DataSource.Factory of customers, ordered by last name. Use
      * {@link androidx.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
      */
diff --git a/androidx/room/integration/testapp/test/ClearAllTablesTest.java b/androidx/room/integration/testapp/test/ClearAllTablesTest.java
index 8bffdef..76cc06a 100644
--- a/androidx/room/integration/testapp/test/ClearAllTablesTest.java
+++ b/androidx/room/integration/testapp/test/ClearAllTablesTest.java
@@ -16,11 +16,13 @@
 
 package androidx.room.integration.testapp.test;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
 
 import android.content.Context;
 import android.database.Cursor;
@@ -133,6 +135,29 @@
 
     @Test
     @SmallTest
+    public void inTransaction() {
+        mDao.insertParent(new Parent(1, "A"));
+        assertThat(mDao.countParent(), is(1));
+        // Running clearAllTables in a transaction is not recommended, but we should not crash.
+        mDatabase.runInTransaction(() -> mDatabase.clearAllTables());
+        assertThat(mDao.countParent(), is(0));
+    }
+
+    @Test
+    @SmallTest
+    public void inMainThread() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            try {
+                mDatabase.clearAllTables();
+                fail("Was expecting an exception");
+            } catch (IllegalStateException e) {
+                assertThat(e.getMessage(), containsString("main thread"));
+            }
+        });
+    }
+
+    @Test
+    @SmallTest
     public void foreignKey() {
         mDao.insertParent(new Parent(1, "A"));
         mDao.insertChild(new Child(1, "a", 1));
@@ -187,12 +212,12 @@
         db.dao().insertParent(new Parent(1, uuid));
         assertThat(queryEncoding(db), is(equalTo("UTF-8")));
         db.close();
-        assertThat(containsString(file, uuid), is(true));
+        assertThat(fileContainsString(file, uuid), is(true));
         db = Room.databaseBuilder(context, ClearAllTablesDatabase.class, dbName)
                 .setJournalMode(journalMode).build();
         db.clearAllTables();
         db.close();
-        assertThat(containsString(file, uuid), is(false));
+        assertThat(fileContainsString(file, uuid), is(false));
     }
 
     private String queryEncoding(RoomDatabase db) {
@@ -208,7 +233,7 @@
         }
     }
 
-    private boolean containsString(File file, String s) throws IOException {
+    private boolean fileContainsString(File file, String s) throws IOException {
         final byte[] content = new byte[(int) file.length()];
         final FileInputStream stream = new FileInputStream(file);
         //noinspection TryFinallyCanBeTryWithResources
diff --git a/androidx/room/integration/testapp/test/RxJava2Test.java b/androidx/room/integration/testapp/test/RxJava2Test.java
index 9878cd2..5ea9efc 100644
--- a/androidx/room/integration/testapp/test/RxJava2Test.java
+++ b/androidx/room/integration/testapp/test/RxJava2Test.java
@@ -40,6 +40,9 @@
 import java.util.Collections;
 import java.util.List;
 
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.functions.Predicate;
 import io.reactivex.observers.TestObserver;
@@ -135,6 +138,42 @@
     }
 
     @Test
+    public void maybeUsers_keepMaybeReference() throws InterruptedException {
+        User[] users = TestUtil.createUsersArray(1, 2);
+        mUserDao.insertAll(users);
+        TestObserver<User> testObserver1 = new TestObserver<>();
+        Maybe<User> maybe1 = mUserDao.maybeUserById(1);
+        Disposable disposable1 = maybe1.observeOn(mTestScheduler)
+                .subscribeWith(testObserver1);
+        drain();
+        testObserver1.assertComplete();
+        // since this is a clean db, it is ok to rely on the order for the test.
+        testObserver1.assertValue(users[0]);
+
+        TestObserver<User> testObserver2 = new TestObserver<>();
+        Maybe<User> maybe2 = mUserDao.maybeUserById(2);
+        Disposable disposable2 = maybe2.observeOn(mTestScheduler)
+                .subscribeWith(testObserver2);
+        drain();
+        testObserver2.assertComplete();
+        // since this is a clean db, it is ok to rely on the order for the test.
+        testObserver2.assertValue(users[1]);
+
+        TestObserver<User> testObserver3 = new TestObserver<>();
+
+        Disposable disposable3 = maybe1.observeOn(mTestScheduler)
+                .subscribeWith(testObserver3);
+        drain();
+        testObserver3.assertComplete();
+        // since this is a clean db, it is ok to rely on the order for the test.
+        testObserver3.assertValue(users[0]);
+
+        disposable1.dispose();
+        disposable2.dispose();
+        disposable3.dispose();
+    }
+
+    @Test
     public void singleUser_Empty() throws InterruptedException {
         TestObserver<User> testObserver = new TestObserver<>();
         Disposable disposable = mUserDao.singleUserById(3).observeOn(mTestScheduler)
@@ -186,6 +225,40 @@
     }
 
     @Test
+    public void singleUser_keepSingleReference() throws InterruptedException {
+        User[] users = TestUtil.createUsersArray(1, 2);
+        mUserDao.insertAll(users);
+        TestObserver<User> testObserver1 = new TestObserver<>();
+        Single<User> userSingle1 = mUserDao.singleUserById(1);
+        Disposable disposable1 = userSingle1.observeOn(mTestScheduler)
+                .subscribeWith(testObserver1);
+        drain();
+        testObserver1.assertComplete();
+        testObserver1.assertValue(users[0]);
+        disposable1.dispose();
+
+        // how get single for 2
+        TestObserver<User> testObserver2 = new TestObserver<>();
+        Single<User> userSingle2 = mUserDao.singleUserById(2);
+        Disposable disposable2 = userSingle2.observeOn(mTestScheduler)
+                .subscribeWith(testObserver2);
+        drain();
+        testObserver2.assertComplete();
+        testObserver2.assertValue(users[1]);
+        disposable2.dispose();
+
+        // now re-use the first single
+        TestObserver<User> testObserver3 = new TestObserver<>();
+        Disposable disposable3 = userSingle1.observeOn(mTestScheduler)
+                .subscribeWith(testObserver3);
+        drain();
+        testObserver3.assertComplete();
+        testObserver3.assertValue(users[0]);
+        disposable3.dispose();
+    }
+
+
+    @Test
     public void observeOnce() throws InterruptedException {
         User user = TestUtil.createUser(3);
         mUserDao.insert(user);
@@ -239,6 +312,33 @@
     }
 
     @Test
+    public void observeFlowable_keepReference() throws InterruptedException {
+        User[] users = TestUtil.createUsersArray(1, 2);
+        mUserDao.insertAll(users);
+        drain();
+
+        TestSubscriber<User> consumer1 = new TestSubscriber<>();
+        Flowable<User> flowable1 = mUserDao.flowableUserById(1);
+        Disposable disposable1 = flowable1.subscribeWith(consumer1);
+        drain();
+        consumer1.assertValue(users[0]);
+
+        TestSubscriber<User> consumer2 = new TestSubscriber<>();
+        Disposable disposable2 = mUserDao.flowableUserById(2).subscribeWith(consumer2);
+        drain();
+        consumer2.assertValue(users[1]);
+
+        TestSubscriber<User> consumer3 = new TestSubscriber<>();
+        Disposable disposable3 = flowable1.subscribeWith(consumer3);
+        drain();
+        consumer3.assertValue(users[0]);
+
+        disposable1.dispose();
+        disposable2.dispose();
+        disposable3.dispose();
+    }
+
+    @Test
     public void flowableCountUsers() throws InterruptedException {
         TestSubscriber<Integer> consumer = new TestSubscriber<>();
         mUserDao.flowableCountUsers()
diff --git a/androidx/slice/SliceManager.java b/androidx/slice/SliceManager.java
index f8b36e5..63c56e8 100644
--- a/androidx/slice/SliceManager.java
+++ b/androidx/slice/SliceManager.java
@@ -27,6 +27,7 @@
 import androidx.core.content.PermissionChecker;
 import androidx.core.os.BuildCompat;
 
+import java.util.Collection;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
@@ -210,6 +211,18 @@
     public abstract void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri);
 
     /**
+     * Obtains a list of slices that are descendants of the specified Uri.
+     * <p>
+     * Not all slice providers will implement this functionality, in which case,
+     * an empty collection will be returned.
+     *
+     * @param uri The uri to look for descendants under.
+     * @return All slices within the space.
+     * @see SliceProvider#onGetSliceDescendants(Uri)
+     */
+    public abstract @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri);
+
+    /**
      * Class that listens to changes in {@link Slice}s.
      */
     public interface SliceCallback {
diff --git a/androidx/slice/SliceManagerCompat.java b/androidx/slice/SliceManagerCompat.java
index 8abacd1..1badbb4 100644
--- a/androidx/slice/SliceManagerCompat.java
+++ b/androidx/slice/SliceManagerCompat.java
@@ -28,6 +28,7 @@
 import androidx.slice.compat.SliceProviderCompat;
 import androidx.slice.widget.SliceLiveData;
 
+import java.util.Collection;
 import java.util.Set;
 
 
@@ -73,4 +74,9 @@
     public Uri mapIntentToUri(@NonNull Intent intent) {
         return SliceProviderCompat.mapIntentToUri(mContext, intent);
     }
+
+    @Override
+    public Collection<Uri> getSliceDescendants(Uri uri) {
+        return SliceProviderCompat.getSliceDescendants(mContext, uri);
+    }
 }
diff --git a/androidx/slice/SliceManagerTest.java b/androidx/slice/SliceManagerTest.java
index 2c6731d..5564f72 100644
--- a/androidx/slice/SliceManagerTest.java
+++ b/androidx/slice/SliceManagerTest.java
@@ -43,6 +43,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.concurrent.Executor;
 
 @RunWith(AndroidJUnit4.class)
@@ -147,6 +149,26 @@
         verify(mSliceProvider).onMapIntentToUri(eq(intent));
     }
 
+    @Test
+    public void testGetDescendants() {
+        Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(mContext.getPackageName())
+                .build();
+        Collection<Uri> collection = Arrays.asList(
+                uri,
+                uri.buildUpon().appendPath("1").build(),
+                uri.buildUpon().appendPath("2").build()
+        );
+        when(mSliceProvider.onGetSliceDescendants(any(Uri.class)))
+                .thenReturn(collection);
+
+        Collection<Uri> allUris = mManager.getSliceDescendants(uri);
+
+        assertEquals(allUris, collection);
+        verify(mSliceProvider).onGetSliceDescendants(eq(uri));
+    }
+
     public static class TestSliceProvider extends SliceProvider {
 
         public static SliceProvider sSliceProviderReceiver;
@@ -189,5 +211,13 @@
                 sSliceProviderReceiver.onSliceUnpinned(sliceUri);
             }
         }
+
+        @Override
+        public Collection<Uri> onGetSliceDescendants(Uri uri) {
+            if (sSliceProviderReceiver != null) {
+                return sSliceProviderReceiver.onGetSliceDescendants(uri);
+            }
+            return super.onGetSliceDescendants(uri);
+        }
     }
 }
diff --git a/androidx/slice/SliceManagerWrapper.java b/androidx/slice/SliceManagerWrapper.java
index f322a19..e0c0342 100644
--- a/androidx/slice/SliceManagerWrapper.java
+++ b/androidx/slice/SliceManagerWrapper.java
@@ -30,6 +30,7 @@
 import androidx.annotation.RestrictTo;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
@@ -71,15 +72,18 @@
     @Nullable
     @Override
     public androidx.slice.Slice bindSlice(@NonNull Uri uri) {
-        return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
-                mContext.getContentResolver(), uri, mSpecs));
+        return SliceConvert.wrap(mManager.bindSlice(uri, mSpecs));
     }
 
     @Nullable
     @Override
     public androidx.slice.Slice bindSlice(@NonNull Intent intent) {
-        return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
-                mContext, intent, mSpecs));
+        return SliceConvert.wrap(mManager.bindSlice(intent, mSpecs));
+    }
+
+    @Override
+    public Collection<Uri> getSliceDescendants(Uri uri) {
+        return mManager.getSliceDescendants(uri);
     }
 
     @Nullable
diff --git a/androidx/slice/SliceMetadata.java b/androidx/slice/SliceMetadata.java
index 9a4648f..8a4ee03 100644
--- a/androidx/slice/SliceMetadata.java
+++ b/androidx/slice/SliceMetadata.java
@@ -20,6 +20,8 @@
 import static android.app.slice.Slice.HINT_HORIZONTAL;
 import static android.app.slice.Slice.HINT_PARTIAL;
 import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.SUBTYPE_MAX;
+import static android.app.slice.Slice.SUBTYPE_VALUE;
 import static android.app.slice.SliceItem.FORMAT_INT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
@@ -29,8 +31,7 @@
 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
 import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
 import static androidx.slice.core.SliceHints.HINT_TTL;
-import static androidx.slice.core.SliceHints.SUBTYPE_MAX;
-import static androidx.slice.core.SliceHints.SUBTYPE_VALUE;
+import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
 import static androidx.slice.widget.EventInfo.ROW_TYPE_PROGRESS;
 import static androidx.slice.widget.EventInfo.ROW_TYPE_SLIDER;
 
@@ -51,6 +52,8 @@
 import androidx.slice.widget.RowContent;
 import androidx.slice.widget.SliceView;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -66,6 +69,7 @@
     @IntDef({
             LOADED_NONE, LOADED_PARTIAL, LOADED_ALL
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface SliceLoadingState{}
 
     /**
@@ -196,7 +200,7 @@
      * Gets the range information associated with a progress bar or input range associated with this
      * slice, if it exists.
      *
-     * @return a pair where the first item is the current value of the range and the second item is
+     * @return a pair where the first item is the minimum value of the range and the second item is
      * the maximum value of the range.
      */
     @Nullable
@@ -206,15 +210,34 @@
             RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
             SliceItem range = rc.getRange();
             SliceItem maxItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
-            SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
-            int max = maxItem != null ? maxItem.getInt() : -1;
-            int current = currentItem != null ? currentItem.getInt() : -1;
-            return new Pair<>(current, max);
+            SliceItem minItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN);
+            int max = maxItem != null ? maxItem.getInt() : 100; // default max of range
+            int min = minItem != null ? minItem.getInt() : 0; // default min of range
+            return new Pair<>(min, max);
         }
         return null;
     }
 
     /**
+     * Gets the current value for a progress bar or input range associated with this slice, if it
+     * exists, -1 if unknown.
+     *
+     * @return the current value of a progress bar or input range associated with this slice.
+     */
+    @NonNull
+    public int getRangeValue() {
+        if (mTemplateType == ROW_TYPE_SLIDER
+                || mTemplateType == ROW_TYPE_PROGRESS) {
+            RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
+            SliceItem range = rc.getRange();
+            SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
+            return currentItem != null ? currentItem.getInt() : -1;
+        }
+        return -1;
+
+    }
+
+    /**
      * @return the list of keywords associated with the provided slice, null if no keywords were
      * specified or an empty list if the slice was specified to have no keywords.
      */
diff --git a/androidx/slice/SliceMetadataTest.java b/androidx/slice/SliceMetadataTest.java
index d378e2e..650114e 100644
--- a/androidx/slice/SliceMetadataTest.java
+++ b/androidx/slice/SliceMetadataTest.java
@@ -47,7 +47,6 @@
 import androidx.slice.builders.GridRowBuilder;
 import androidx.slice.builders.ListBuilder;
 import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
 import androidx.slice.core.SliceActionImpl;
 import androidx.slice.core.SliceHints;
 import androidx.slice.render.SliceRenderActivity;
@@ -452,15 +451,20 @@
         ListBuilder lb = new ListBuilder(mContext, uri, ListBuilder.INFINITY);
         lb.addInputRange(new ListBuilder.InputRangeBuilder(lb)
                 .setTitle("another title")
-                .setValue(5)
+                .setValue(7)
+                .setMin(5)
                 .setMax(10)
-                .setAction(pi));
+                .setInputAction(pi));
 
         Slice sliderSlice = lb.build();
         SliceMetadata sliderInfo = SliceMetadata.from(mContext, sliderSlice);
+
         Pair<Integer, Integer> values = sliderInfo.getRange();
         assertEquals(5, (int) values.first);
         assertEquals(10, (int) values.second);
+
+        int currentValue = sliderInfo.getRangeValue();
+        assertEquals(7, currentValue);
     }
 
     @Test
@@ -476,9 +480,8 @@
         Slice sliderSlice = lb.build();
         SliceMetadata progressInfo = SliceMetadata.from(mContext, sliderSlice);
         Pair<Integer, Integer> values = progressInfo.getRange();
-        assertEquals(5, (int) values.first);
+        assertEquals(0, (int) values.first);
         assertEquals(10, (int) values.second);
-
     }
 
     @Test
@@ -593,7 +596,7 @@
     public void testIsPermissionSlice() {
         Uri uri = Uri.parse("content://pkg/slice");
         Slice permissionSlice =
-                SliceProviderCompat.createPermissionSlice(mContext, uri, mContext.getPackageName());
+                SliceProvider.createPermissionSlice(mContext, uri, mContext.getPackageName());
 
         SliceMetadata metadata = SliceMetadata.from(mContext, permissionSlice);
         assertEquals(true, metadata.isPermissionSlice());
diff --git a/androidx/slice/SliceProvider.java b/androidx/slice/SliceProvider.java
index 51ffea6..7ec9232 100644
--- a/androidx/slice/SliceProvider.java
+++ b/androidx/slice/SliceProvider.java
@@ -15,22 +15,61 @@
  */
 package androidx.slice;
 
+import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.SliceProvider.SLICE_TYPE;
+
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_INTENT;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PKG;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PROVIDER_PKG;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE_DESCENDANTS;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_DESCENDANTS;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_PINNED_SPECS;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_INTENT;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_ONLY_INTENT;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_PIN;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_SLICE;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_UNPIN;
+import static androidx.slice.compat.SliceProviderCompat.addSpecs;
+import static androidx.slice.compat.SliceProviderCompat.getSpecs;
+import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ProviderInfo;
+import android.content.pm.PackageManager;
 import android.database.ContentObserver;
+import android.database.Cursor;
 import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.StrictMode;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.core.app.CoreComponentFactory;
 import androidx.core.os.BuildCompat;
-import androidx.slice.compat.ContentProviderWrapper;
-import androidx.slice.compat.SliceProviderCompat;
+import androidx.slice.compat.CompatPinnedList;
 import androidx.slice.compat.SliceProviderWrapperContainer;
+import androidx.slice.core.R;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Set;
 
 /**
@@ -72,20 +111,21 @@
  *
  * @see android.app.slice.Slice
  */
-public abstract class SliceProvider extends ContentProviderWrapper {
+public abstract class SliceProvider extends ContentProvider implements
+        CoreComponentFactory.CompatWrapped {
 
     private static Set<SliceSpec> sSpecs;
 
-    @Override
-    public void attachInfo(Context context, ProviderInfo info) {
-        ContentProvider impl;
-        if (BuildCompat.isAtLeastP()) {
-            impl = new SliceProviderWrapperContainer.SliceProviderWrapper(this);
-        } else {
-            impl = new SliceProviderCompat(this);
-        }
-        super.attachInfo(context, info, impl);
-    }
+    private static final String TAG = "SliceProvider";
+
+    private static final String DATA_PREFIX = "slice_data_";
+    private static final long SLICE_BIND_ANR = 2000;
+
+    private static final boolean DEBUG = false;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private CompatPinnedList mPinnedList;
+
+    private String mCallback;
 
     /**
      * Implement this to initialize your slice provider on startup.
@@ -104,6 +144,228 @@
      */
     public abstract boolean onCreateSliceProvider();
 
+    @Override
+    public Object getWrapper() {
+        if (BuildCompat.isAtLeastP()) {
+            return new SliceProviderWrapperContainer.SliceProviderWrapper(this);
+        }
+        return null;
+    }
+
+    @Override
+    public final boolean onCreate() {
+        mPinnedList = new CompatPinnedList(getContext(),
+                DATA_PREFIX + getClass().getName());
+        return onCreateSliceProvider();
+    }
+
+    @Override
+    public final String getType(Uri uri) {
+        if (DEBUG) Log.d(TAG, "getFormat " + uri);
+        return SLICE_TYPE;
+    }
+
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        if (method.equals(METHOD_SLICE)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            if (Binder.getCallingUid() != Process.myUid()) {
+                getContext().enforceUriPermission(uri, Binder.getCallingPid(),
+                        Binder.getCallingUid(),
+                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+                        "Slice binding requires write access to the uri");
+            }
+            Set<SliceSpec> specs = getSpecs(extras);
+
+            Slice s = handleBindSlice(uri, specs, getCallingPackage());
+            Bundle b = new Bundle();
+            b.putParcelable(EXTRA_SLICE, s.toBundle());
+            return b;
+        } else if (method.equals(METHOD_MAP_INTENT)) {
+            Intent intent = extras.getParcelable(EXTRA_INTENT);
+            Uri uri = onMapIntentToUri(intent);
+            Bundle b = new Bundle();
+            if (uri != null) {
+                Set<SliceSpec> specs = getSpecs(extras);
+                Slice s = handleBindSlice(uri, specs, getCallingPackage());
+                b.putParcelable(EXTRA_SLICE, s.toBundle());
+            } else {
+                b.putParcelable(EXTRA_SLICE, null);
+            }
+            return b;
+        } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
+            Intent intent = extras.getParcelable(EXTRA_INTENT);
+            Uri uri = onMapIntentToUri(intent);
+            Bundle b = new Bundle();
+            b.putParcelable(EXTRA_SLICE, uri);
+            return b;
+        } else if (method.equals(METHOD_PIN)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            Set<SliceSpec> specs = getSpecs(extras);
+            String pkg = extras.getString(EXTRA_PKG);
+            if (mPinnedList.addPin(uri, pkg, specs)) {
+                handleSlicePinned(uri);
+            }
+            return null;
+        } else if (method.equals(METHOD_UNPIN)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            String pkg = extras.getString(EXTRA_PKG);
+            if (mPinnedList.removePin(uri, pkg)) {
+                handleSliceUnpinned(uri);
+            }
+            return null;
+        } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            Bundle b = new Bundle();
+            addSpecs(b, mPinnedList.getSpecs(uri));
+            return b;
+        } else if (method.equals(METHOD_GET_DESCENDANTS)) {
+            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+            Bundle b = new Bundle();
+            b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
+                    new ArrayList<>(handleGetDescendants(uri)));
+            return b;
+        }
+        return super.call(method, arg, extras);
+    }
+
+    private Collection<Uri> handleGetDescendants(Uri uri) {
+        mCallback = "onGetSliceDescendants";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            return onGetSliceDescendants(uri);
+        } finally {
+            mHandler.removeCallbacks(mAnr);
+        }
+    }
+
+    private void handleSlicePinned(final Uri sliceUri) {
+        mCallback = "onSlicePinned";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            onSlicePinned(sliceUri);
+        } finally {
+            mHandler.removeCallbacks(mAnr);
+        }
+    }
+
+    private void handleSliceUnpinned(final Uri sliceUri) {
+        mCallback = "onSliceUnpinned";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            onSliceUnpinned(sliceUri);
+        } finally {
+            mHandler.removeCallbacks(mAnr);
+        }
+    }
+
+    private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
+            final String callingPkg) {
+        // This can be removed once Slice#bindSlice is removed and everyone is using
+        // SliceManager#bindSlice.
+        String pkg = callingPkg != null ? callingPkg
+                : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+        if (Binder.getCallingUid() != Process.myUid()) {
+            try {
+                getContext().enforceUriPermission(sliceUri,
+                        Binder.getCallingPid(), Binder.getCallingUid(),
+                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+                        "Slice binding requires write access to Uri");
+            } catch (SecurityException e) {
+                return createPermissionSlice(getContext(), sliceUri, pkg);
+            }
+        }
+        return onBindSliceStrict(sliceUri, specs);
+    }
+
+    /**
+     * Generate a slice that contains a permission request.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static Slice createPermissionSlice(Context context, Uri sliceUri,
+            String callingPackage) {
+        Slice.Builder parent = new Slice.Builder(sliceUri);
+
+        Slice.Builder action = new Slice.Builder(parent)
+                .addHints(HINT_TITLE, HINT_SHORTCUT)
+                .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+                        new Slice.Builder(parent).build(), null);
+
+        parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+                .addText(getPermissionString(context, callingPackage), null)
+                .addSubSlice(action.build())
+                .build());
+
+        return parent.addHints(HINT_PERMISSION_REQUEST).build();
+    }
+
+    /**
+     * Create a PendingIntent pointing at the permission dialog.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+            String callingPackage) {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(context.getPackageName(),
+                "androidx.slice.compat.SlicePermissionActivity"));
+        intent.putExtra(EXTRA_BIND_URI, sliceUri);
+        intent.putExtra(EXTRA_PKG, callingPackage);
+        intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+        // Unique pending intent.
+        intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+                .build());
+
+        return PendingIntent.getActivity(context, 0, intent, 0);
+    }
+
+    /**
+     * Get string describing permission request.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static CharSequence getPermissionString(Context context, String callingPackage) {
+        PackageManager pm = context.getPackageManager();
+        try {
+            return context.getString(R.string.abc_slices_permission_request,
+                    pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+                    context.getApplicationInfo().loadLabel(pm));
+        } catch (PackageManager.NameNotFoundException e) {
+            // This shouldn't be possible since the caller is verified.
+            throw new RuntimeException("Unknown calling app", e);
+        }
+    }
+
+    private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
+        StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+        mCallback = "onBindSlice";
+        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+        try {
+            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+                    .detectAll()
+                    .penaltyDeath()
+                    .build());
+            SliceProvider.setSpecs(specs);
+            try {
+                return onBindSlice(sliceUri);
+            } finally {
+                SliceProvider.setSpecs(null);
+                mHandler.removeCallbacks(mAnr);
+            }
+        } finally {
+            StrictMode.setThreadPolicy(oldPolicy);
+        }
+    }
+
+    private final Runnable mAnr = new Runnable() {
+        @Override
+        public void run() {
+            Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
+            Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+        }
+    };
+
     /**
      * Implemented to create a slice.
      * <p>
@@ -166,6 +428,75 @@
     }
 
     /**
+     * Obtains a list of slices that are descendants of the specified Uri.
+     * <p>
+     * Implementing this is optional for a SliceProvider, but does provide a good
+     * discovery mechanism for finding slice Uris.
+     *
+     * @param uri The uri to look for descendants under.
+     * @return All slices within the space.
+     * @see androidx.slice.SliceManager#getSliceDescendants(Uri)
+     */
+    public Collection<Uri> onGetSliceDescendants(Uri uri) {
+        return Collections.emptyList();
+    }
+
+    @Nullable
+    @Override
+    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable String selection, @Nullable String[] selectionArgs,
+            @Nullable String sortOrder) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    @RequiresApi(28)
+    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    @RequiresApi(16)
+    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable String selection, @Nullable String[] selectionArgs,
+            @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
+        return 0;
+    }
+
+    @Override
+    public final int delete(@NonNull Uri uri, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public final int update(@NonNull Uri uri, @Nullable ContentValues values,
+            @Nullable String selection, @Nullable String[] selectionArgs) {
+        return 0;
+    }
+
+    @Nullable
+    @Override
+    @RequiresApi(19)
+    public final Uri canonicalize(@NonNull Uri url) {
+        return null;
+    }
+
+    /**
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/androidx/slice/SliceTestProvider.java b/androidx/slice/SliceTestProvider.java
index d6f1628..519e5b8 100644
--- a/androidx/slice/SliceTestProvider.java
+++ b/androidx/slice/SliceTestProvider.java
@@ -33,6 +33,7 @@
 
     @Override
     public boolean onCreateSliceProvider() {
+        getContext().getPackageName();
         return true;
     }
 
diff --git a/androidx/slice/SliceUtils.java b/androidx/slice/SliceUtils.java
index 0424800..442b324 100644
--- a/androidx/slice/SliceUtils.java
+++ b/androidx/slice/SliceUtils.java
@@ -30,6 +30,8 @@
 import static androidx.slice.SliceMetadata.LOADED_PARTIAL;
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.content.Context;
 import android.net.Uri;
 import android.text.TextUtils;
@@ -43,6 +45,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.annotation.Retention;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -116,6 +119,7 @@
         public static final int MODE_CONVERT = 2;
 
         @IntDef({MODE_THROW, MODE_REMOVE, MODE_CONVERT})
+        @Retention(SOURCE)
         @interface FormatMode {
         }
 
diff --git a/androidx/slice/builders/ListBuilder.java b/androidx/slice/builders/ListBuilder.java
index c0b5bc3..1c81ebd 100644
--- a/androidx/slice/builders/ListBuilder.java
+++ b/androidx/slice/builders/ListBuilder.java
@@ -38,9 +38,10 @@
 import androidx.slice.builders.impl.TemplateBuilderImpl;
 import androidx.slice.core.SliceHints;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 
-
 /**
  * A slice can be constructed with ListBuilder.
  * <p>
@@ -124,6 +125,7 @@
     @IntDef({
             LARGE_IMAGE, SMALL_IMAGE, ICON_IMAGE, UNKNOWN_IMAGE
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface ImageMode{}
 
     /**
@@ -653,6 +655,15 @@
         }
 
         /**
+         * Set the lower limit of the range. The default is 0.
+         */
+        @NonNull
+        public InputRangeBuilder setMin(int min) {
+            mImpl.setMin(min);
+            return this;
+        }
+
+        /**
          * Set the upper limit of the range. The default is 100.
          */
         @NonNull
diff --git a/androidx/slice/builders/impl/ListBuilder.java b/androidx/slice/builders/impl/ListBuilder.java
index 084b85a..80a477e 100644
--- a/androidx/slice/builders/impl/ListBuilder.java
+++ b/androidx/slice/builders/impl/ListBuilder.java
@@ -141,6 +141,11 @@
      */
     interface RangeBuilder {
         /**
+         * Set the lower limit.
+         */
+        void setMin(int min);
+
+        /**
          * Set the upper limit.
          */
         void setMax(int max);
diff --git a/androidx/slice/builders/impl/ListBuilderV1Impl.java b/androidx/slice/builders/impl/ListBuilderV1Impl.java
index f2ff884..c771b62 100644
--- a/androidx/slice/builders/impl/ListBuilderV1Impl.java
+++ b/androidx/slice/builders/impl/ListBuilderV1Impl.java
@@ -27,6 +27,9 @@
 import static android.app.slice.Slice.HINT_TITLE;
 import static android.app.slice.Slice.SUBTYPE_COLOR;
 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
+import static android.app.slice.Slice.SUBTYPE_MAX;
+import static android.app.slice.Slice.SUBTYPE_RANGE;
+import static android.app.slice.Slice.SUBTYPE_VALUE;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
@@ -36,10 +39,8 @@
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
 import static androidx.slice.core.SliceHints.HINT_TTL;
-import static androidx.slice.core.SliceHints.SUBTYPE_MAX;
 import static androidx.slice.core.SliceHints.SUBTYPE_MILLIS;
-import static androidx.slice.core.SliceHints.SUBTYPE_RANGE;
-import static androidx.slice.core.SliceHints.SUBTYPE_VALUE;
+import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
 
 import android.app.PendingIntent;
 import android.net.Uri;
@@ -158,6 +159,7 @@
      * Builder to construct an input row.
      */
     public static class RangeBuilderImpl extends TemplateBuilderImpl implements RangeBuilder {
+        private int mMin = 0;
         private int mMax = 100;
         private int mValue = 0;
         private CharSequence mTitle;
@@ -170,6 +172,11 @@
         }
 
         @Override
+        public void setMin(int min) {
+            mMin = min;
+        }
+
+        @Override
         public void setMax(int max) {
             mMax = max;
         }
@@ -216,6 +223,7 @@
                 builder.addSubSlice(mPrimaryAction.buildSlice(sb), null /* subtype */);
             }
             builder.addHints(HINT_LIST_ITEM)
+                    .addInt(mMin, SUBTYPE_MIN)
                     .addInt(mMax, SUBTYPE_MAX)
                     .addInt(mValue, SUBTYPE_VALUE);
         }
diff --git a/androidx/slice/compat/ContentProviderWrapper.java b/androidx/slice/compat/ContentProviderWrapper.java
deleted file mode 100644
index 45c6ffc..0000000
--- a/androidx/slice/compat/ContentProviderWrapper.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.slice.compat;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.ProviderInfo;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-
-/**
- * @hide
- */
-// TODO: Remove as soon as we have better systems in place for this.
-@RestrictTo(Scope.LIBRARY)
-public class ContentProviderWrapper extends ContentProvider {
-
-    private ContentProvider mImpl;
-
-    /**
-     * Triggers an attach with the object to wrap.
-     */
-    public void attachInfo(Context context, ProviderInfo info, ContentProvider impl) {
-        mImpl = impl;
-        super.attachInfo(context, info);
-        mImpl.attachInfo(context, info);
-    }
-
-    @Override
-    public final boolean onCreate() {
-        return mImpl.onCreate();
-    }
-
-    @Nullable
-    @Override
-    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
-            @Nullable String selection, @Nullable String[] selectionArgs,
-            @Nullable String sortOrder) {
-        return mImpl.query(uri, projection, selection, selectionArgs, sortOrder);
-    }
-
-    @Nullable
-    @Override
-    @RequiresApi(28)
-    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
-            @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
-        return mImpl.query(uri, projection, queryArgs, cancellationSignal);
-    }
-
-    @Nullable
-    @Override
-    @RequiresApi(16)
-    public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
-            @Nullable String selection, @Nullable String[] selectionArgs,
-            @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
-        return mImpl.query(uri, projection, selection, selectionArgs, sortOrder,
-                cancellationSignal);
-    }
-
-    @Nullable
-    @Override
-    public final String getType(@NonNull Uri uri) {
-        return mImpl.getType(uri);
-    }
-
-    @Nullable
-    @Override
-    public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
-        return mImpl.insert(uri, values);
-    }
-
-    @Override
-    public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
-        return mImpl.bulkInsert(uri, values);
-    }
-
-    @Override
-    public final int delete(@NonNull Uri uri, @Nullable String selection,
-            @Nullable String[] selectionArgs) {
-        return mImpl.delete(uri, selection, selectionArgs);
-    }
-
-    @Override
-    public final int update(@NonNull Uri uri, @Nullable ContentValues values,
-            @Nullable String selection, @Nullable String[] selectionArgs) {
-        return mImpl.update(uri, values, selection, selectionArgs);
-    }
-
-    @Nullable
-    @Override
-    public final Bundle call(@NonNull String method, @Nullable String arg,
-            @Nullable Bundle extras) {
-        return mImpl.call(method, arg, extras);
-    }
-
-    @Nullable
-    @Override
-    @RequiresApi(19)
-    public final Uri canonicalize(@NonNull Uri url) {
-        return mImpl.canonicalize(url);
-    }
-}
diff --git a/androidx/slice/compat/SliceProviderCompat.java b/androidx/slice/compat/SliceProviderCompat.java
index 182b3c2..61bc65e 100644
--- a/androidx/slice/compat/SliceProviderCompat.java
+++ b/androidx/slice/compat/SliceProviderCompat.java
@@ -15,48 +15,33 @@
  */
 package androidx.slice.compat;
 
-import static android.app.slice.Slice.HINT_SHORTCUT;
-import static android.app.slice.Slice.HINT_TITLE;
 import static android.app.slice.SliceProvider.SLICE_TYPE;
 
-import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
-
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.ContentProvider;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.database.Cursor;
 import android.net.Uri;
-import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.Parcelable;
-import android.os.Process;
 import android.os.RemoteException;
-import android.os.StrictMode;
-import android.os.StrictMode.ThreadPolicy;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 import androidx.slice.Slice;
-import androidx.slice.SliceProvider;
 import androidx.slice.SliceSpec;
-import androidx.slice.core.R;
 import androidx.slice.core.SliceHints;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -64,9 +49,8 @@
  * @hide
  */
 @RestrictTo(Scope.LIBRARY)
-public class SliceProviderCompat extends ContentProvider {
-
-    private static final String TAG = "SliceProvider";
+public class SliceProviderCompat {
+    private static final String TAG = "SliceProviderCompat";
 
     public static final String EXTRA_BIND_URI = "slice_uri";
     public static final String METHOD_SLICE = "bind_slice";
@@ -75,6 +59,7 @@
     public static final String METHOD_UNPIN = "unpin_slice";
     public static final String METHOD_GET_PINNED_SPECS = "get_specs";
     public static final String METHOD_MAP_ONLY_INTENT = "map_only";
+    public static final String METHOD_GET_DESCENDANTS = "get_descendants";
 
     public static final String EXTRA_INTENT = "slice_intent";
     public static final String EXTRA_SLICE = "slice";
@@ -82,244 +67,7 @@
     public static final String EXTRA_SUPPORTED_SPECS_REVS = "revs";
     public static final String EXTRA_PKG = "pkg";
     public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
-    private static final String DATA_PREFIX = "slice_data_";
-
-    private static final long SLICE_BIND_ANR = 2000;
-
-    private static final boolean DEBUG = false;
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-    private SliceProvider mSliceProvider;
-    private CompatPinnedList mPinnedList;
-
-    private String mCallback;
-
-    public SliceProviderCompat(SliceProvider provider) {
-        mSliceProvider = provider;
-    }
-
-    @Override
-    public boolean onCreate() {
-        mPinnedList = new CompatPinnedList(getContext(),
-                DATA_PREFIX + mSliceProvider.getClass().getName());
-        return mSliceProvider.onCreateSliceProvider();
-    }
-
-    @Override
-    public final int update(Uri uri, ContentValues values, String selection,
-            String[] selectionArgs) {
-        if (DEBUG) Log.d(TAG, "update " + uri);
-        return 0;
-    }
-
-    @Override
-    public final int delete(Uri uri, String selection, String[] selectionArgs) {
-        if (DEBUG) Log.d(TAG, "delete " + uri);
-        return 0;
-    }
-
-    @Override
-    public final Cursor query(Uri uri, String[] projection, String selection,
-            String[] selectionArgs, String sortOrder) {
-        if (DEBUG) Log.d(TAG, "query " + uri);
-        return null;
-    }
-
-    @Override
-    public final Cursor query(Uri uri, String[] projection, String selection, String[]
-            selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
-        if (DEBUG) Log.d(TAG, "query " + uri);
-        return null;
-    }
-
-    @Override
-    public final Cursor query(Uri uri, String[] projection, Bundle queryArgs,
-            CancellationSignal cancellationSignal) {
-        if (DEBUG) Log.d(TAG, "query " + uri);
-        return null;
-    }
-
-    @Override
-    public final Uri insert(Uri uri, ContentValues values) {
-        if (DEBUG) Log.d(TAG, "insert " + uri);
-        return null;
-    }
-
-    @Override
-    public final String getType(Uri uri) {
-        if (DEBUG) Log.d(TAG, "getFormat " + uri);
-        return SLICE_TYPE;
-    }
-
-    @Override
-    public Bundle call(String method, String arg, Bundle extras) {
-        if (method.equals(METHOD_SLICE)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            if (Binder.getCallingUid() != Process.myUid()) {
-                getContext().enforceUriPermission(uri, Binder.getCallingPid(),
-                        Binder.getCallingUid(),
-                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
-                        "Slice binding requires the permission BIND_SLICE");
-            }
-            Set<SliceSpec> specs = getSpecs(extras);
-
-            Slice s = handleBindSlice(uri, specs, getCallingPackage());
-            Bundle b = new Bundle();
-            b.putParcelable(EXTRA_SLICE, s.toBundle());
-            return b;
-        } else if (method.equals(METHOD_MAP_INTENT)) {
-            Intent intent = extras.getParcelable(EXTRA_INTENT);
-            Uri uri = mSliceProvider.onMapIntentToUri(intent);
-            Bundle b = new Bundle();
-            if (uri != null) {
-                Set<SliceSpec> specs = getSpecs(extras);
-                Slice s = handleBindSlice(uri, specs, getCallingPackage());
-                b.putParcelable(EXTRA_SLICE, s.toBundle());
-            } else {
-                b.putParcelable(EXTRA_SLICE, null);
-            }
-            return b;
-        } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
-            Intent intent = extras.getParcelable(EXTRA_INTENT);
-            Uri uri = mSliceProvider.onMapIntentToUri(intent);
-            Bundle b = new Bundle();
-            b.putParcelable(EXTRA_SLICE, uri);
-            return b;
-        } else if (method.equals(METHOD_PIN)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            Set<SliceSpec> specs = getSpecs(extras);
-            String pkg = extras.getString(EXTRA_PKG);
-            if (mPinnedList.addPin(uri, pkg, specs)) {
-                handleSlicePinned(uri);
-            }
-            return null;
-        } else if (method.equals(METHOD_UNPIN)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            String pkg = extras.getString(EXTRA_PKG);
-            if (mPinnedList.removePin(uri, pkg)) {
-                handleSliceUnpinned(uri);
-            }
-            return null;
-        } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
-            Uri uri = extras.getParcelable(EXTRA_BIND_URI);
-            Bundle b = new Bundle();
-            addSpecs(b, mPinnedList.getSpecs(uri));
-            return b;
-        }
-        return super.call(method, arg, extras);
-    }
-
-    private void handleSlicePinned(final Uri sliceUri) {
-        mCallback = "onSlicePinned";
-        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
-        try {
-            mSliceProvider.onSlicePinned(sliceUri);
-        } finally {
-            mHandler.removeCallbacks(mAnr);
-        }
-    }
-
-    private void handleSliceUnpinned(final Uri sliceUri) {
-        mCallback = "onSliceUnpinned";
-        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
-        try {
-            mSliceProvider.onSliceUnpinned(sliceUri);
-        } finally {
-            mHandler.removeCallbacks(mAnr);
-        }
-    }
-
-    private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
-            final String callingPkg) {
-        // This can be removed once Slice#bindSlice is removed and everyone is using
-        // SliceManager#bindSlice.
-        String pkg = callingPkg != null ? callingPkg
-                : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
-        if (Binder.getCallingUid() != Process.myUid()) {
-            try {
-                getContext().enforceUriPermission(sliceUri,
-                        Binder.getCallingPid(), Binder.getCallingUid(),
-                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
-                        "Slice binding requires write access to Uri");
-            } catch (SecurityException e) {
-                return createPermissionSlice(getContext(), sliceUri, pkg);
-            }
-        }
-        return onBindSliceStrict(sliceUri, specs);
-    }
-
-    /**
-     * Generate a slice that contains a permission request.
-     */
-    public static Slice createPermissionSlice(Context context, Uri sliceUri,
-            String callingPackage) {
-        Slice.Builder parent = new Slice.Builder(sliceUri);
-
-        Slice.Builder action = new Slice.Builder(parent)
-                .addHints(HINT_TITLE, HINT_SHORTCUT)
-                .addAction(createPermissionIntent(context, sliceUri, callingPackage),
-                        new Slice.Builder(parent).build(), null);
-
-        parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
-                .addText(getPermissionString(context, callingPackage), null)
-                .addSubSlice(action.build())
-                .build());
-
-        return parent.addHints(HINT_PERMISSION_REQUEST).build();
-    }
-
-    /**
-     * Create a PendingIntent pointing at the permission dialog.
-     */
-    public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
-            String callingPackage) {
-        Intent intent = new Intent();
-        intent.setComponent(new ComponentName(context.getPackageName(),
-                "androidx.slice.compat.SlicePermissionActivity"));
-        intent.putExtra(EXTRA_BIND_URI, sliceUri);
-        intent.putExtra(EXTRA_PKG, callingPackage);
-        intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
-        // Unique pending intent.
-        intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
-                .build());
-
-        return PendingIntent.getActivity(context, 0, intent, 0);
-    }
-
-    /**
-     * Get string describing permission request.
-     */
-    public static CharSequence getPermissionString(Context context, String callingPackage) {
-        PackageManager pm = context.getPackageManager();
-        try {
-            return context.getString(R.string.abc_slices_permission_request,
-                    pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
-                    context.getApplicationInfo().loadLabel(pm));
-        } catch (PackageManager.NameNotFoundException e) {
-            // This shouldn't be possible since the caller is verified.
-            throw new RuntimeException("Unknown calling app", e);
-        }
-    }
-
-    private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
-        ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
-        mCallback = "onBindSlice";
-        mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
-        try {
-            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
-                    .detectAll()
-                    .penaltyDeath()
-                    .build());
-            SliceProvider.setSpecs(specs);
-            try {
-                return mSliceProvider.onBindSlice(sliceUri);
-            } finally {
-                SliceProvider.setSpecs(null);
-                mHandler.removeCallbacks(mAnr);
-            }
-        } finally {
-            StrictMode.setThreadPolicy(oldPolicy);
-        }
-    }
+    public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
 
     /**
      * Compat version of {@link Slice#bindSlice}.
@@ -357,7 +105,10 @@
         }
     }
 
-    private static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
+    /**
+     * Compat way to push specs through the call.
+     */
+    public static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
         ArrayList<String> types = new ArrayList<>();
         ArrayList<Integer> revs = new ArrayList<>();
         for (SliceSpec spec : supportedSpecs) {
@@ -368,7 +119,10 @@
         extras.putIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS, revs);
     }
 
-    private static Set<SliceSpec> getSpecs(Bundle extras) {
+    /**
+     * Compat way to push specs through the call.
+     */
+    public static Set<SliceSpec> getSpecs(Bundle extras) {
         ArraySet<SliceSpec> specs = new ArraySet<>();
         ArrayList<String> types = extras.getStringArrayList(EXTRA_SUPPORTED_SPECS);
         ArrayList<Integer> revs = extras.getIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS);
@@ -549,11 +303,22 @@
         }
     }
 
-    private final Runnable mAnr = new Runnable() {
-        @Override
-        public void run() {
-            Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
-            Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+    /**
+     * Compat version of {@link android.app.slice.SliceManager#getSliceDescendants(Uri)}
+     */
+    public static @NonNull Collection<Uri> getSliceDescendants(Context context, @NonNull Uri uri) {
+        ContentResolver resolver = context.getContentResolver();
+        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(EXTRA_BIND_URI, uri);
+            final Bundle res = provider.call(METHOD_GET_DESCENDANTS, null, extras);
+            return res.getParcelableArrayList(EXTRA_SLICE_DESCENDANTS);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to get slice descendants", e);
         }
-    };
+        return Collections.emptyList();
+    }
+
+    private SliceProviderCompat() {
+    }
 }
diff --git a/androidx/slice/compat/SliceProviderWrapperContainer.java b/androidx/slice/compat/SliceProviderWrapperContainer.java
index 4db52f0..8641530 100644
--- a/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -22,7 +22,9 @@
 import android.app.slice.Slice;
 import android.app.slice.SliceProvider;
 import android.app.slice.SliceSpec;
+import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ProviderInfo;
 import android.net.Uri;
 
 import androidx.annotation.NonNull;
@@ -30,6 +32,7 @@
 import androidx.collection.ArraySet;
 import androidx.slice.SliceConvert;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -50,8 +53,14 @@
         }
 
         @Override
+        public void attachInfo(Context context, ProviderInfo info) {
+            mSliceProvider.attachInfo(context, info);
+            super.attachInfo(context, info);
+        }
+
+        @Override
         public boolean onCreate() {
-            return mSliceProvider.onCreateSliceProvider();
+            return true;
         }
 
         @Override
@@ -74,6 +83,11 @@
             mSliceProvider.onSliceUnpinned(sliceUri);
         }
 
+        @Override
+        public Collection<Uri> onGetSliceDescendants(Uri uri) {
+            return mSliceProvider.onGetSliceDescendants(uri);
+        }
+
         /**
          * Maps intents to uris.
          */
diff --git a/androidx/slice/core/SliceHints.java b/androidx/slice/core/SliceHints.java
index e35ae35..c29ad56 100644
--- a/androidx/slice/core/SliceHints.java
+++ b/androidx/slice/core/SliceHints.java
@@ -18,34 +18,24 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import androidx.annotation.IntDef;
 import androidx.annotation.RestrictTo;
 
+import java.lang.annotation.Retention;
+
 /**
  * Temporary class to contain hint constants for slices to be used.
  * @hide
  */
 @RestrictTo(LIBRARY_GROUP)
 public class SliceHints {
-    /**
-     * Subtype to range an item representing a range.
-     */
-    public static final String SUBTYPE_RANGE = "range";
 
     /**
-     * Subtype indicating that this content is the maximum value for a range.
+     * Subtype indicating that this content is the minimum value for a range.
      */
-    public static final String SUBTYPE_MAX = "max";
-
-    /**
-     * Subtype indicating that this content is the current value for a range.
-     */
-    public static final String SUBTYPE_VALUE = "value";
-
-    /**
-     * Key to retrieve an extra added to an intent when the value of an input range has changed.
-     */
-    public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
+    public static final String SUBTYPE_MIN = "min";
 
     /**
      * The meta-data key that allows an activity to easily be linked directly to a slice.
@@ -83,6 +73,7 @@
     @IntDef({
             LARGE_IMAGE, SMALL_IMAGE, ICON_IMAGE, UNKNOWN_IMAGE
     })
+    @Retention(SOURCE)
     public @interface ImageMode{}
 
     /**
diff --git a/androidx/slice/render/SliceCreator.java b/androidx/slice/render/SliceCreator.java
index e0bccfd..2312b36 100644
--- a/androidx/slice/render/SliceCreator.java
+++ b/androidx/slice/render/SliceCreator.java
@@ -36,11 +36,11 @@
 
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
 import androidx.slice.builders.GridRowBuilder;
 import androidx.slice.builders.ListBuilder;
 import androidx.slice.builders.MessagingSliceBuilder;
 import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
 import androidx.slice.view.test.R;
 
 import java.util.Arrays;
@@ -126,7 +126,7 @@
     private Slice createWeather(Uri sliceUri) {
         SliceAction primaryAction = new SliceAction(getBroadcastIntent(ACTION_TOAST,
                 "open weather app"),
-                IconCompat.createWithResource(getContext(), R.drawable.weather_1),
+                IconCompat.createWithResource(getContext(), R.drawable.weather_1), SMALL_IMAGE,
                 "Weather is happening!");
         ListBuilder b = new ListBuilder(getContext(), sliceUri, -TimeUnit.HOURS.toMillis(8));
         GridRowBuilder gb = new GridRowBuilder(b);
@@ -486,9 +486,10 @@
                         .setTitle("Star rating")
                         .setSubtitle("Pick a rating from 0 to 5")
                         .setThumb(icon)
+                        .setMin(5)
                         .setInputAction(getBroadcastIntent(ACTION_TOAST, "range changed"))
-                        .setMax(5)
-                        .setValue(3)
+                        .setMax(10)
+                        .setValue(7)
                         .setPrimaryAction(primaryAction)
                         .setContentDescription("Slider for star ratings"))
                 .build();
@@ -512,7 +513,7 @@
     }
 
     private Slice createPermissionSlice(Uri uri) {
-        return SliceProviderCompat.createPermissionSlice(getContext(), uri,
+        return SliceProvider.createPermissionSlice(getContext(), uri,
                 getContext().getPackageName());
     }
 
diff --git a/androidx/slice/widget/EventInfo.java b/androidx/slice/widget/EventInfo.java
index 377fbd6..9b3af24 100644
--- a/androidx/slice/widget/EventInfo.java
+++ b/androidx/slice/widget/EventInfo.java
@@ -19,6 +19,9 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.RestrictTo;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Represents information associated with a logged event on {@link SliceView}.
  */
@@ -37,6 +40,7 @@
             ROW_TYPE_SLIDER,
             ROW_TYPE_PROGRESS,
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface SliceRowType {}
 
     /**
@@ -76,6 +80,7 @@
             ACTION_TYPE_TOGGLE, ACTION_TYPE_BUTTON, ACTION_TYPE_SLIDER, ACTION_TYPE_CONTENT,
             ACTION_TYPE_SEE_MORE
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface SliceActionType{}
 
     /**
@@ -109,6 +114,7 @@
     @IntDef({
             POSITION_START, POSITION_END, POSITION_CELL
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface SliceButtonPosition{}
 
     /**
diff --git a/androidx/slice/widget/GridRowView.java b/androidx/slice/widget/GridRowView.java
index 2335aaa..4b8078c 100644
--- a/androidx/slice/widget/GridRowView.java
+++ b/androidx/slice/widget/GridRowView.java
@@ -17,7 +17,6 @@
 package androidx.slice.widget;
 
 import static android.app.slice.Slice.HINT_LARGE;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
 import static android.app.slice.Slice.HINT_NO_TINT;
 import static android.app.slice.Slice.HINT_TITLE;
 import static android.app.slice.SliceItem.FORMAT_ACTION;
@@ -133,16 +132,9 @@
         }
     }
 
-    /**
-     * This is called when GridView is presented in small format.
-     */
     @Override
     public void setSlice(Slice slice) {
-        resetView();
-        mRowIndex = 0;
-        SliceItem item = SliceQuery.find(slice, FORMAT_SLICE, HINT_LIST_ITEM, null);
-        mGridContent = new GridContent(getContext(), item);
-        populateViews(mGridContent);
+        // Nothing to do
     }
 
     /**
@@ -164,7 +156,7 @@
                     EventInfo.ROW_TYPE_GRID, mRowIndex);
             Pair<SliceItem, EventInfo> tagItem = new Pair<>(gc.getContentIntent(), info);
             mViewContainer.setTag(tagItem);
-            makeClickable(mViewContainer);
+            makeClickable(mViewContainer, true);
         }
         CharSequence contentDescr = gc.getContentDescription();
         if (contentDescr != null) {
@@ -222,7 +214,7 @@
         info.setPosition(EventInfo.POSITION_CELL, index, total);
         Pair<SliceItem, EventInfo> tagItem = new Pair<>(seeMoreItem, info);
         seeMoreView.setTag(tagItem);
-        makeClickable(seeMoreView);
+        makeClickable(seeMoreView, true);
     }
 
     /**
@@ -301,7 +293,7 @@
                 info.setPosition(EventInfo.POSITION_CELL, index, total);
                 Pair<SliceItem, EventInfo> tagItem = new Pair<>(contentIntentItem, info);
                 cellContainer.setTag(tagItem);
-                makeClickable(cellContainer);
+                makeClickable(cellContainer, true);
             }
         }
     }
@@ -347,10 +339,12 @@
         return addedView != null;
     }
 
-    private void makeClickable(View layout) {
-        layout.setOnClickListener(this);
-        layout.setBackground(SliceViewUtil.getDrawable(getContext(),
-                android.R.attr.selectableItemBackground));
+    private void makeClickable(View layout, boolean isClickable) {
+        layout.setOnClickListener(isClickable ? this : null);
+        layout.setBackground(isClickable
+                ? SliceViewUtil.getDrawable(getContext(), android.R.attr.selectableItemBackground)
+                : null);
+        layout.setClickable(isClickable);
     }
 
     @Override
@@ -365,7 +359,7 @@
                     mObserver.onSliceAction(info, actionItem);
                 }
             } catch (PendingIntent.CanceledException e) {
-                Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+                Log.e(TAG, "PendingIntent for slice cannot be sent", e);
             }
         }
     }
@@ -373,5 +367,6 @@
     @Override
     public void resetView() {
         mViewContainer.removeAllViews();
+        makeClickable(mViewContainer, false);
     }
 }
diff --git a/androidx/slice/widget/LargeSliceAdapter.java b/androidx/slice/widget/LargeSliceAdapter.java
index 19f232f..e09405c 100644
--- a/androidx/slice/widget/LargeSliceAdapter.java
+++ b/androidx/slice/widget/LargeSliceAdapter.java
@@ -19,7 +19,6 @@
 import static android.app.slice.Slice.HINT_HORIZONTAL;
 import static android.app.slice.Slice.SUBTYPE_MESSAGE;
 import static android.app.slice.Slice.SUBTYPE_SOURCE;
-import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
 import static android.app.slice.SliceItem.FORMAT_TEXT;
 
@@ -29,6 +28,7 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
@@ -64,15 +64,30 @@
     private SliceView.OnSliceActionListener mSliceObserver;
     private int mColor;
     private AttributeSet mAttrs;
+    private int mDefStyleAttr;
+    private int mDefStyleRes;
     private List<SliceItem> mSliceActions;
     private boolean mShowLastUpdated;
     private long mLastUpdated;
+    private SliceView mParent;
+    private LargeTemplateView mTemplateView;
 
     public LargeSliceAdapter(Context context) {
         mContext = context;
         setHasStableIds(true);
     }
 
+    /**
+     * Sets the SliceView parent and the template parent.
+     */
+    public void setParents(SliceView parent, LargeTemplateView templateView) {
+        mParent = parent;
+        mTemplateView = templateView;
+    }
+
+    /**
+     * Sets the observer to pass down to child views.
+     */
     public void setSliceObserver(SliceView.OnSliceActionListener observer) {
         mSliceObserver = observer;
     }
@@ -105,8 +120,10 @@
     /**
      * Sets the attribute set to use for views in the list.
      */
-    public void setStyle(AttributeSet attrs) {
+    public void setStyle(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         mAttrs = attrs;
+        mDefStyleAttr = defStyleAttr;
+        mDefStyleRes = defStyleRes;
         notifyDataSetChanged();
     }
 
@@ -151,17 +168,7 @@
     @Override
     public void onBindViewHolder(SliceViewHolder holder, int position) {
         SliceWrapper slice = mSlices.get(position);
-        if (holder.mSliceView != null) {
-            final boolean isHeader = position == HEADER_INDEX;
-            holder.mSliceView.setTint(mColor);
-            holder.mSliceView.setStyle(mAttrs);
-            holder.mSliceView.setSliceItem(slice.mItem, isHeader, position, mSliceObserver);
-            if (isHeader && holder.mSliceView instanceof RowView) {
-                holder.mSliceView.setSliceActions(mSliceActions);
-                holder.mSliceView.setLastUpdated(mLastUpdated);
-                holder.mSliceView.setShowLastUpdated(mShowLastUpdated);
-            }
-        }
+        holder.bind(slice.mItem, position);
     }
 
     private void notifyHeaderChanged() {
@@ -184,7 +191,8 @@
                         null);
                 break;
         }
-        ((SliceChildView) v).setMode(MODE_LARGE);
+        int mode = mParent != null ? mParent.getMode() : MODE_LARGE;
+        ((SliceChildView) v).setMode(mode);
         return v;
     }
 
@@ -221,12 +229,53 @@
     /**
      * A {@link RecyclerView.ViewHolder} for presenting slices in {@link LargeSliceAdapter}.
      */
-    public static class SliceViewHolder extends RecyclerView.ViewHolder {
-        public final SliceChildView mSliceView;
+    public class SliceViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener,
+            View.OnClickListener {
+        public final SliceChildView mSliceChildView;
 
         public SliceViewHolder(View itemView) {
             super(itemView);
-            mSliceView = itemView instanceof SliceChildView ? (SliceChildView) itemView : null;
+            mSliceChildView = itemView instanceof SliceChildView ? (SliceChildView) itemView : null;
+        }
+
+        void bind(SliceItem item, int position) {
+            if (mSliceChildView == null || item == null) {
+                return;
+            }
+            // Click listener used to pipe click events to parent
+            mSliceChildView.setOnClickListener(this);
+            // Touch listener used to pipe events to touch feedback drawable
+            mSliceChildView.setOnTouchListener(this);
+
+            final boolean isHeader = position == HEADER_INDEX;
+            mSliceChildView.setTint(mColor);
+            mSliceChildView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
+            mSliceChildView.setSliceItem(item, isHeader, position, mSliceObserver);
+            if (isHeader && mSliceChildView instanceof RowView) {
+                mSliceChildView.setSliceActions(mSliceActions);
+                mSliceChildView.setLastUpdated(mLastUpdated);
+                mSliceChildView.setShowLastUpdated(mShowLastUpdated);
+            }
+            int[] info = new int[2];
+            info[0] = ListContent.getRowType(mContext, item, isHeader, mSliceActions);
+            info[1] = position;
+            mSliceChildView.setTag(info);
+        }
+
+        @Override
+        public void onClick(View v) {
+            if (mParent != null) {
+                mParent.setClickInfo((int[]) v.getTag());
+                mParent.performClick();
+            }
+        }
+
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            if (mTemplateView != null) {
+                mTemplateView.onForegroundActivated(event);
+            }
+            return false;
         }
     }
 
@@ -253,12 +302,8 @@
             while (items.hasNext()) {
                 SliceItem i = items.next();
                 builder.append(i.getFormat());
-                //i.removeHint(Slice.HINT_SELECTED);
                 builder.append(i.getHints());
                 switch (i.getFormat()) {
-                    case FORMAT_IMAGE:
-                        builder.append(i.getIcon());
-                        break;
                     case FORMAT_TEXT:
                         builder.append(i.getText());
                         break;
diff --git a/androidx/slice/widget/LargeTemplateView.java b/androidx/slice/widget/LargeTemplateView.java
index 36409e8..6632097 100644
--- a/androidx/slice/widget/LargeTemplateView.java
+++ b/androidx/slice/widget/LargeTemplateView.java
@@ -16,8 +16,14 @@
 
 package androidx.slice.widget;
 
+import static android.app.slice.Slice.HINT_HORIZONTAL;
+
 import android.content.Context;
+import android.os.Build;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
 
 import androidx.annotation.RestrictTo;
 import androidx.recyclerview.widget.LinearLayoutManager;
@@ -26,6 +32,7 @@
 import androidx.slice.SliceItem;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -34,6 +41,8 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 public class LargeTemplateView extends SliceChildView {
 
+    private SliceView mParent;
+    private final View mForeground;
     private final LargeSliceAdapter mAdapter;
     private final RecyclerView mRecyclerView;
     private Slice mSlice;
@@ -41,6 +50,7 @@
     private ListContent mListContent;
     private List<SliceItem> mDisplayedItems = new ArrayList<>();
     private int mDisplayedItemsHeight = 0;
+    private int[] mLoc = new int[2];
 
     public LargeTemplateView(Context context) {
         super(context);
@@ -49,6 +59,23 @@
         mAdapter = new LargeSliceAdapter(context);
         mRecyclerView.setAdapter(mAdapter);
         addView(mRecyclerView);
+
+        mForeground = new View(getContext());
+        mForeground.setBackground(SliceViewUtil.getDrawable(getContext(),
+                android.R.attr.selectableItemBackground));
+        addView(mForeground);
+
+        FrameLayout.LayoutParams lp = (LayoutParams) mForeground.getLayoutParams();
+        lp.width = LayoutParams.MATCH_PARENT;
+        lp.height = LayoutParams.MATCH_PARENT;
+        mForeground.setLayoutParams(lp);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mParent = (SliceView) getParent();
+        mAdapter.setParents(mParent, this);
     }
 
     @Override
@@ -61,20 +88,62 @@
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
 
+    /**
+     * Called when the foreground view handling touch feedback should be activated.
+     * @param event the event to handle.
+     */
+    public void onForegroundActivated(MotionEvent event) {
+        if (mParent != null && !mParent.isSliceViewClickable()) {
+            // Only show highlight if clickable
+            mForeground.setPressed(false);
+            return;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            mForeground.getLocationOnScreen(mLoc);
+            final int x = (int) (event.getRawX() - mLoc[0]);
+            final int y = (int) (event.getRawY() - mLoc[1]);
+            mForeground.getBackground().setHotspot(x, y);
+        }
+        int action = event.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            mForeground.setPressed(true);
+        } else if (action == MotionEvent.ACTION_CANCEL
+                || action == MotionEvent.ACTION_UP
+                || action == MotionEvent.ACTION_MOVE) {
+            mForeground.setPressed(false);
+        }
+    }
+
+    @Override
+    public void setMode(int newMode) {
+        super.setMode(newMode);
+        updateDisplayedItems(getMeasuredHeight());
+    }
+
     @Override
     public int getActualHeight() {
         return mDisplayedItemsHeight;
     }
 
     @Override
-    public void setTint(int tint) {
-        super.setTint(tint);
-        populate();
+    public int getSmallHeight() {
+        if (mListContent == null || mListContent.getHeaderItem() == null) {
+            return 0;
+        }
+        SliceItem headerItem = mListContent.getHeaderItem();
+        if (headerItem.hasHint(HINT_HORIZONTAL)) {
+            GridContent gc = new GridContent(getContext(), headerItem);
+            return gc.getSmallHeight();
+        } else {
+            RowContent rc = new RowContent(getContext(), headerItem, mListContent.hasHeader());
+            return rc.getSmallHeight();
+        }
     }
 
     @Override
-    public @SliceView.SliceMode int getMode() {
-        return SliceView.MODE_LARGE;
+    public void setTint(int tint) {
+        super.setTint(tint);
+        populate();
     }
 
     @Override
@@ -97,9 +166,9 @@
     }
 
     @Override
-    public void setStyle(AttributeSet attrs) {
-        super.setStyle(attrs);
-        mAdapter.setStyle(attrs);
+    public void setStyle(AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+        super.setStyle(attrs, defStyleAttrs, defStyleRes);
+        mAdapter.setStyle(attrs, defStyleAttrs, defStyleRes);
     }
 
     @Override
@@ -148,7 +217,12 @@
             mDisplayedItems = mListContent.getRowItems();
         }
         mDisplayedItemsHeight = ListContent.getListHeight(getContext(), mDisplayedItems);
-        mAdapter.setSliceItems(mDisplayedItems, mTintColor);
+        if (getMode() == SliceView.MODE_LARGE) {
+            mAdapter.setSliceItems(mDisplayedItems, mTintColor);
+        } else if (getMode() == SliceView.MODE_SMALL) {
+            mAdapter.setSliceItems(
+                    Collections.singletonList(mDisplayedItems.get(0)), mTintColor);
+        }
     }
 
     @Override
diff --git a/androidx/slice/widget/ListContent.java b/androidx/slice/widget/ListContent.java
index 3d92ce8..1cbf357 100644
--- a/androidx/slice/widget/ListContent.java
+++ b/androidx/slice/widget/ListContent.java
@@ -102,12 +102,23 @@
     }
 
     /**
+     * Expects the provided list of items to be filtered (i.e. only things that can be turned into
+     * GridContent or RowContent) and in order (i.e. first item could be a header).
+     *
      * @return the total height of all the rows contained in the provided list.
      */
     public static int getListHeight(Context context, List<SliceItem> listItems) {
+        if (listItems == null) {
+            return 0;
+        }
         int height = 0;
+        boolean hasRealHeader = false;
+        if (!listItems.isEmpty()) {
+            SliceItem maybeHeader = listItems.get(0);
+            hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
+        }
         for (int i = 0; i < listItems.size(); i++) {
-            height += getHeight(context, listItems.get(i), i == 0 /* isHeader */);
+            height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */);
         }
         return height;
     }
@@ -192,6 +203,7 @@
         return mSeeMoreItem;
     }
 
+    @NonNull
     public ArrayList<SliceItem> getRowItems() {
         return mRowItems;
     }
@@ -207,11 +219,25 @@
      * @return the type of template that the header represents.
      */
     public int getHeaderTemplateType() {
-        if (mHeaderItem != null) {
-            if (mHeaderItem.hasHint(HINT_HORIZONTAL)) {
+        return getRowType(mContext, mHeaderItem, true, mSliceActions);
+    }
+
+    /**
+     * The type of template that the provided row item represents.
+     *
+     * @param context context used for this slice.
+     * @param rowItem the row item to determine the template type of.
+     * @param isHeader whether this row item is used as a header.
+     * @param actions the actions associated with this slice, only matter if this row is the header.
+     * @return the type of template the provided row item represents.
+     */
+    public static int getRowType(Context context, SliceItem rowItem, boolean isHeader,
+                                 List<SliceItem> actions) {
+        if (rowItem != null) {
+            if (rowItem.hasHint(HINT_HORIZONTAL)) {
                 return EventInfo.ROW_TYPE_GRID;
             } else {
-                RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
+                RowContent rc = new RowContent(context, rowItem, isHeader);
                 SliceItem actionItem = rc.getPrimaryAction();
                 SliceAction primaryAction = null;
                 if (actionItem != null) {
@@ -223,9 +249,9 @@
                             : EventInfo.ROW_TYPE_PROGRESS;
                 } else if (primaryAction != null && primaryAction.isToggle()) {
                     return EventInfo.ROW_TYPE_TOGGLE;
-                } else if (mSliceActions != null) {
-                    for (int i = 0; i < mSliceActions.size(); i++) {
-                        if (new SliceActionImpl(mSliceActions.get(i)).isToggle()) {
+                } else if (isHeader && actions != null) {
+                    for (int i = 0; i < actions.size(); i++) {
+                        if (new SliceActionImpl(actions.get(i)).isToggle()) {
                             return EventInfo.ROW_TYPE_TOGGLE;
                         }
                     }
@@ -284,9 +310,12 @@
         return null;
     }
 
-    private static boolean isValidHeader(SliceItem sliceItem) {
+    /**
+     * @return whether the provided slice item is a valid header.
+     */
+    public static boolean isValidHeader(SliceItem sliceItem) {
         if (FORMAT_SLICE.equals(sliceItem.getFormat()) && !sliceItem.hasAnyHints(HINT_LIST_ITEM,
-                HINT_ACTIONS, HINT_KEYWORDS)) {
+                HINT_ACTIONS, HINT_KEYWORDS, HINT_SEE_MORE)) {
              // Minimum valid header is a slice with text
             SliceItem item = SliceQuery.find(sliceItem, FORMAT_TEXT, (String) null, null);
             return item != null;
diff --git a/androidx/slice/widget/RowContent.java b/androidx/slice/widget/RowContent.java
index 728715e..bc3826f 100644
--- a/androidx/slice/widget/RowContent.java
+++ b/androidx/slice/widget/RowContent.java
@@ -23,6 +23,7 @@
 import static android.app.slice.Slice.HINT_SUMMARY;
 import static android.app.slice.Slice.HINT_TITLE;
 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
+import static android.app.slice.Slice.SUBTYPE_RANGE;
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
@@ -34,7 +35,6 @@
 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
 import static androidx.slice.core.SliceHints.HINT_TTL;
-import static androidx.slice.core.SliceHints.SUBTYPE_RANGE;
 
 import android.content.Context;
 import android.text.TextUtils;
@@ -75,14 +75,14 @@
     private int mLineCount = 0;
     private int mMaxHeight;
     private int mMinHeight;
-    private int mMaxRangeHeight;
+    private int mRangeHeight;
 
     public RowContent(Context context, SliceItem rowSlice, boolean isHeader) {
         populate(rowSlice, isHeader);
         mMaxHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_max_height);
         mMinHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_min_height);
-        mMaxRangeHeight = context.getResources().getDimensionPixelSize(
-                R.dimen.abc_slice_row_range_max_height);
+        mRangeHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.abc_slice_row_range_height);
     }
 
     /**
@@ -278,8 +278,8 @@
      * @return the height to display a row at when it is used as a small template.
      */
     public int getSmallHeight() {
-        return (getRange() != null && mLineCount > 1)
-                ? mMaxRangeHeight
+        return getRange() != null
+                ? getActualHeight()
                 : mMaxHeight;
     }
 
@@ -290,10 +290,15 @@
         if (!isValid()) {
             return 0;
         }
-        if (getRange() != null && mLineCount > 1) {
-            return mMaxRangeHeight;
+        int rowHeight = (getLineCount() > 1 || mIsHeader) ? mMaxHeight : mMinHeight;
+        if (getRange() != null) {
+            if (getLineCount() > 0) {
+                rowHeight += mRangeHeight;
+            } else {
+                rowHeight = mIsHeader ? mMaxHeight : mRangeHeight;
+            }
         }
-        return (getLineCount() > 1 || mIsHeader) ? mMaxHeight : mMinHeight;
+        return rowHeight;
     }
 
     private static boolean hasText(SliceItem textSlice) {
@@ -319,6 +324,7 @@
                 || mTitleItem != null
                 || mSubtitleItem != null
                 || mEndItems.size() > 0
+                || mRange != null
                 || isDefaultSeeMore();
     }
 
diff --git a/androidx/slice/widget/RowView.java b/androidx/slice/widget/RowView.java
index dcbf354..a7101fe 100644
--- a/androidx/slice/widget/RowView.java
+++ b/androidx/slice/widget/RowView.java
@@ -16,22 +16,26 @@
 
 package androidx.slice.widget;
 
-import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
+import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
 import static android.app.slice.Slice.HINT_NO_TINT;
 import static android.app.slice.Slice.HINT_PARTIAL;
 import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.SUBTYPE_MAX;
 import static android.app.slice.Slice.SUBTYPE_TOGGLE;
+import static android.app.slice.Slice.SUBTYPE_VALUE;
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
 import static android.app.slice.SliceItem.FORMAT_SLICE;
 import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
 
-import static androidx.slice.core.SliceHints.EXTRA_RANGE_VALUE;
 import static androidx.slice.core.SliceHints.ICON_IMAGE;
 import static androidx.slice.core.SliceHints.SMALL_IMAGE;
-import static androidx.slice.core.SliceHints.SUBTYPE_MAX;
-import static androidx.slice.core.SliceHints.SUBTYPE_VALUE;
+import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
+import static androidx.slice.widget.EventInfo.ACTION_TYPE_BUTTON;
+import static androidx.slice.widget.EventInfo.ACTION_TYPE_TOGGLE;
+import static androidx.slice.widget.EventInfo.ROW_TYPE_LIST;
+import static androidx.slice.widget.EventInfo.ROW_TYPE_TOGGLE;
 import static androidx.slice.widget.SliceView.MODE_SMALL;
 
 import android.app.PendingIntent.CanceledException;
@@ -48,14 +52,11 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
-import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.SeekBar;
-import android.widget.Switch;
 import android.widget.TextView;
-import android.widget.ToggleButton;
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.RestrictTo;
@@ -84,16 +85,16 @@
     // The number of items that fit on the right hand side of a small slice
     private static final int MAX_END_ITEMS = 3;
 
+    private LinearLayout mRootView;
     private LinearLayout mStartContainer;
     private LinearLayout mContent;
     private TextView mPrimaryText;
     private TextView mSecondaryText;
     private TextView mLastUpdatedText;
     private View mDivider;
-    private ArrayList<CompoundButton> mToggles = new ArrayList<>();
+    private ArrayList<SliceActionView> mToggles = new ArrayList<>();
     private LinearLayout mEndContainer;
-    private SeekBar mSeekBar;
-    private ProgressBar mProgressBar;
+    private ProgressBar mRangeBar;
     private View mSeeMoreView;
 
     private int mRowIndex;
@@ -104,15 +105,16 @@
 
     private int mImageSize;
     private int mIconSize;
-    private int mPadding;
+    private int mRangeHeight;
 
     public RowView(Context context) {
         super(context);
         mIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_icon_size);
         mImageSize = getContext().getResources().getDimensionPixelSize(
                 R.dimen.abc_slice_small_image_size);
-        mPadding = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_padding);
-        inflate(context, R.layout.abc_slice_small_template, this);
+        mRootView = (LinearLayout) LayoutInflater.from(context).inflate(
+                R.layout.abc_slice_small_template, this, false);
+        addView(mRootView);
 
         mStartContainer = (LinearLayout) findViewById(R.id.icon_frame);
         mContent = (LinearLayout) findViewById(android.R.id.content);
@@ -121,8 +123,9 @@
         mLastUpdatedText = (TextView) findViewById(R.id.last_updated);
         mDivider = findViewById(R.id.divider);
         mEndContainer = (LinearLayout) findViewById(android.R.id.widget_frame);
-        mSeekBar = (SeekBar) findViewById(R.id.seek_bar);
-        mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
+
+        mRangeHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.abc_slice_row_range_height);
     }
 
 
@@ -136,6 +139,16 @@
     public int getActualHeight() {
         return mRowContent != null && mRowContent.isValid() ? mRowContent.getActualHeight() : 0;
     }
+    /**
+     * @return height row content (i.e. title, subtitle) without the height of the range element.
+     */
+    private int getRowContentHeight() {
+        int rowHeight = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight();
+        if (mRangeBar != null) {
+            rowHeight -= mRangeHeight;
+        }
+        return rowHeight;
+    }
 
     @Override
     public void setTint(@ColorInt int tintColor) {
@@ -164,9 +177,37 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int height = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight();
-        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int totalHeight = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight();
+        int rowHeight = getRowContentHeight();
+        if (rowHeight != 0) {
+            // Might be gone if we have range / progress but nothing else
+            mRootView.setVisibility(View.VISIBLE);
+            heightMeasureSpec = MeasureSpec.makeMeasureSpec(rowHeight, MeasureSpec.EXACTLY);
+            measureChild(mRootView, widthMeasureSpec, heightMeasureSpec);
+        } else {
+            mRootView.setVisibility(View.GONE);
+        }
+        if (mRangeBar != null) {
+            int rangeMeasureSpec = MeasureSpec.makeMeasureSpec(mRangeHeight, MeasureSpec.EXACTLY);
+            measureChild(mRangeBar, widthMeasureSpec, rangeMeasureSpec);
+        }
+
+        int totalHeightSpec = MeasureSpec.makeMeasureSpec(totalHeight, MeasureSpec.EXACTLY);
+        super.onMeasure(widthMeasureSpec, totalHeightSpec);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        mRootView.layout(0, 0, mRootView.getMeasuredWidth(), getRowContentHeight());
+        if (mRangeBar != null) {
+            mRangeBar.layout(0, getRowContentHeight(), mRangeBar.getMeasuredWidth(),
+                    getRowContentHeight() + mRangeHeight);
+        }
+    }
+
+    @Override
+    public void setSlice(Slice slice) {
+        // Nothing to do
     }
 
     /**
@@ -177,25 +218,12 @@
             SliceView.OnSliceActionListener observer) {
         setSliceActionListener(observer);
         mRowIndex = index;
-        mIsHeader = isHeader;
+        mIsHeader = ListContent.isValidHeader(slice);
         mHeaderActions = null;
         mRowContent = new RowContent(getContext(), slice, mIsHeader);
         populateViews();
     }
 
-    /**
-     * This is called when RowView is being used as a small template.
-     */
-    @Override
-    public void setSlice(Slice slice) {
-        mRowIndex = 0;
-        mIsHeader = true;
-        mHeaderActions = null;
-        ListContent lc = new ListContent(getContext(), slice);
-        mRowContent = new RowContent(getContext(), lc.getHeaderItem(), true /* isHeader */);
-        populateViews();
-    }
-
     private void populateViews() {
         resetView();
         if (mRowContent.isDefaultSeeMore()) {
@@ -209,11 +237,7 @@
         boolean showStart = false;
         final SliceItem startItem = mRowContent.getStartItem();
         if (startItem != null) {
-            final EventInfo info = new EventInfo(getMode(),
-                    EventInfo.ACTION_TYPE_BUTTON,
-                    EventInfo.ROW_TYPE_LIST, mRowIndex);
-            info.setPosition(EventInfo.POSITION_START, 0, 1);
-            showStart = addItem(startItem, mTintColor, true /* isStart */, 0 /* padding */, info);
+            showStart = addItem(startItem, mTintColor, true /* isStart */);
         }
         mStartContainer.setVisibility(showStart ? View.VISIBLE : View.GONE);
 
@@ -237,8 +261,8 @@
             mRowAction = new SliceActionImpl(primaryAction);
             if (mRowAction.isToggle()) {
                 // If primary action is a toggle, add it and we're done
-                addToggle(mRowAction, mTintColor, mEndContainer);
-                setViewClickable(this, true);
+                addAction(mRowAction, mTintColor, mEndContainer, false /* isStart */);
+                setViewClickable(mRootView, true);
                 return;
             }
         }
@@ -246,7 +270,7 @@
         final SliceItem range = mRowContent.getRange();
         if (range != null) {
             if (mRowAction != null) {
-                setViewClickable(mContent, true);
+                setViewClickable(mRootView, true);
             }
             addRange(range);
             return;
@@ -260,7 +284,7 @@
         }
         boolean hasRowAction = mRowAction != null;
         if (endItems.isEmpty()) {
-            if (hasRowAction) setViewClickable(this, true);
+            if (hasRowAction) setViewClickable(mRootView, true);
             return;
         }
 
@@ -271,12 +295,7 @@
         for (int i = 0; i < endItems.size(); i++) {
             final SliceItem endItem = endItems.get(i);
             if (itemCount < MAX_END_ITEMS) {
-                final EventInfo info = new EventInfo(getMode(),
-                        EventInfo.ACTION_TYPE_BUTTON,
-                        EventInfo.ROW_TYPE_LIST, mRowIndex);
-                info.setPosition(EventInfo.POSITION_END, i,
-                        Math.min(endItems.size(), MAX_END_ITEMS));
-                if (addItem(endItem, mTintColor, false /* isStart */, mPadding, info)) {
+                if (addItem(endItem, mTintColor, false /* isStart */)) {
                     if (FORMAT_ACTION.equals(endItem.getFormat())) {
                         hasEndItemAction = true;
                     }
@@ -296,17 +315,15 @@
             if (itemCount > 0 && hasEndItemAction) {
                 setViewClickable(mContent, true);
             } else {
-                setViewClickable(this, true);
+                setViewClickable(mRootView, true);
             }
-        } else {
+        } else if (mRowContent.endItemsContainAction() && itemCount == 1) {
             // If the only end item is an action, make the whole row clickable.
-            if (mRowContent.endItemsContainAction() && itemCount == 1) {
-                SliceItem unwrappedActionItem = endItems.get(0).getSlice().getItems().get(0);
-                if (!SUBTYPE_TOGGLE.equals(unwrappedActionItem.getSubType())) {
-                    mRowAction = new SliceActionImpl(endItems.get(0));
-                }
-                setViewClickable(this, true);
+            SliceItem unwrappedActionItem = endItems.get(0).getSlice().getItems().get(0);
+            if (!SUBTYPE_TOGGLE.equals(unwrappedActionItem.getSubType())) {
+                mRowAction = new SliceActionImpl(endItems.get(0));
             }
+            setViewClickable(mRootView, true);
         }
     }
 
@@ -343,34 +360,47 @@
 
     private void addRange(final SliceItem range) {
         final boolean isSeekBar = FORMAT_ACTION.equals(range.getFormat());
-        final ProgressBar progressBar = isSeekBar ? mSeekBar : mProgressBar;
-        SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
-        if (max != null) {
-            progressBar.setMax(max.getInt());
-        }
-        SliceItem progress = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
-        if (progress != null) {
-            progressBar.setProgress(progress.getInt());
-        }
-        progressBar.setVisibility(View.VISIBLE);
+        final ProgressBar progressBar = isSeekBar
+                ? new SeekBar(getContext())
+                : new ProgressBar(getContext(), null, android.R.attr.progressBarStyleHorizontal);
         if (mTintColor != -1) {
             Drawable drawable = DrawableCompat.wrap(progressBar.getProgressDrawable());
             DrawableCompat.setTint(drawable, mTintColor);
-            mProgressBar.setProgressDrawable(drawable);
+            progressBar.setProgressDrawable(drawable);
         }
+        // TODO: Need to handle custom accessibility for min
+        SliceItem min = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN);
+        int minValue = 0;
+        if (min != null) {
+            minValue = min.getInt();
+        }
+        SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
+        if (max != null) {
+            progressBar.setMax(max.getInt() - minValue);
+        }
+        SliceItem progress = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
+        if (progress != null) {
+            progressBar.setProgress(progress.getInt() - minValue);
+        }
+        progressBar.setVisibility(View.VISIBLE);
+        addView(progressBar);
+        mRangeBar = progressBar;
         if (isSeekBar) {
             SliceItem thumb = SliceQuery.find(range, FORMAT_IMAGE);
+            SeekBar seekBar = (SeekBar) mRangeBar;
             if (thumb != null) {
-                mSeekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
+                seekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
             }
             if (mTintColor != -1) {
-                Drawable drawable = DrawableCompat.wrap(mSeekBar.getThumb());
+                Drawable drawable = DrawableCompat.wrap(seekBar.getThumb());
                 DrawableCompat.setTint(drawable, mTintColor);
-                mSeekBar.setThumb(drawable);
+                seekBar.setThumb(drawable);
             }
-            mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            final int finalMinValue = minValue;
+            seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                 @Override
                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                    progress += finalMinValue;
                     try {
                         // TODO: sending this PendingIntent should be rate limited.
                         range.fireAction(getContext(),
@@ -388,85 +418,45 @@
     }
 
     /**
-     * Add a toggle view to container.
+     * Add an action view to the container.
      */
-    private void addToggle(final SliceActionImpl actionContent, int color, ViewGroup container) {
-        // Check if this is a custom toggle
-        final CompoundButton toggle;
-        if (actionContent.isToggle() && !actionContent.isDefaultToggle()) {
-            IconCompat checkedIcon = actionContent.getIcon();
-            if (color != -1) {
-                // TODO - Should custom toggle buttons be tinted? What if the app wants diff
-                // colors per state?
-                checkedIcon.setTint(color);
-            }
-            toggle = new ToggleButton(getContext());
-            ((ToggleButton) toggle).setTextOff("");
-            ((ToggleButton) toggle).setTextOn("");
-            toggle.setBackground(checkedIcon.loadDrawable(getContext()));
-            container.addView(toggle);
-            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) toggle.getLayoutParams();
-            lp.width = mIconSize;
-            lp.height = mIconSize;
-        } else {
-            toggle = new Switch(getContext());
-            container.addView(toggle);
+    private void addAction(final SliceActionImpl actionContent, int color, ViewGroup container,
+                           boolean isStart) {
+        SliceActionView sav = new SliceActionView(getContext());
+        container.addView(sav);
+
+        boolean isToggle = actionContent.isToggle();
+        int actionType = isToggle ? ACTION_TYPE_TOGGLE : ACTION_TYPE_BUTTON;
+        int rowType = isToggle ? ROW_TYPE_TOGGLE : ROW_TYPE_LIST;
+        EventInfo info = new EventInfo(getMode(), actionType, rowType, mRowIndex);
+        if (isStart) {
+            info.setPosition(EventInfo.POSITION_START, 0, 1);
         }
-        CharSequence contentDesc = actionContent.getContentDescription();
-        if (contentDesc != null) {
-            toggle.setContentDescription(contentDesc);
+        sav.setAction(actionContent, info, mObserver, color);
+
+        if (isToggle) {
+            mToggles.add(sav);
         }
-        toggle.setChecked(actionContent.isChecked());
-        toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            @Override
-            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-                try {
-                    Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
-                    actionContent.getActionItem().fireAction(getContext(), i);
-                    if (mObserver != null) {
-                        final EventInfo info = new EventInfo(getMode(),
-                                EventInfo.ACTION_TYPE_TOGGLE,
-                                EventInfo.ROW_TYPE_TOGGLE, mRowIndex);
-                        info.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
-                        mObserver.onSliceAction(info, actionContent.getSliceItem());
-                    }
-                } catch (CanceledException e) {
-                    toggle.setSelected(!isChecked);
-                }
-            }
-        });
-        mToggles.add(toggle);
     }
 
     /**
      * Adds simple items to a container. Simple items include actions with icons, images, or
      * timestamps.
      */
-    private boolean addItem(SliceItem sliceItem, int color, boolean isStart, int padding,
-            final EventInfo info) {
+    private boolean addItem(SliceItem sliceItem, int color, boolean isStart) {
         IconCompat icon = null;
         int imageMode = 0;
         SliceItem timeStamp = null;
-        SliceActionImpl actionContent = null;
         ViewGroup container = isStart ? mStartContainer : mEndContainer;
         if (FORMAT_SLICE.equals(sliceItem.getFormat())) {
-            // It's an action.... let's make it so
             if (sliceItem.hasHint(HINT_SHORTCUT)) {
-                actionContent = new SliceActionImpl(sliceItem);
+                addAction(new SliceActionImpl(sliceItem), color, container, isStart);
+                return true;
             } else {
                 sliceItem = sliceItem.getSlice().getItems().get(0);
             }
         }
-        if (actionContent != null) {
-            if (actionContent.isToggle()) {
-                addToggle(actionContent, color, container);
-                return true;
-            }
-            icon = actionContent.getIcon();
-            if (icon != null) {
-                imageMode = actionContent.getImageMode();
-            }
-        }
+
         if (FORMAT_IMAGE.equals(sliceItem.getFormat())) {
             icon = sliceItem.getIcon();
             imageMode = sliceItem.hasHint(HINT_NO_TINT) ? SMALL_IMAGE : ICON_IMAGE;
@@ -475,23 +465,19 @@
         }
         View addedView = null;
         if (icon != null) {
+            boolean isIcon = imageMode == ICON_IMAGE;
             ImageView iv = new ImageView(getContext());
             iv.setImageDrawable(icon.loadDrawable(getContext()));
-            int size = mImageSize;
-            if (imageMode == ICON_IMAGE) {
-                if (color != -1) {
-                    iv.setColorFilter(color);
-                }
-                size = mIconSize;
-            }
-            if (actionContent != null && actionContent.getContentDescription() != null) {
-                iv.setContentDescription(actionContent.getContentDescription());
+            if (isIcon && color != -1) {
+                iv.setColorFilter(color);
             }
             container.addView(iv);
             LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams();
-            lp.width = size;
-            lp.height = size;
-            lp.setMarginStart(padding);
+            lp.width = mImageSize;
+            lp.height = mImageSize;
+            iv.setLayoutParams(lp);
+            int p = isIcon ? mIconSize / 2 : 0;
+            iv.setPadding(p, p, p, p);
             addedView = iv;
         } else if (timeStamp != null) {
             TextView tv = new TextView(getContext());
@@ -501,24 +487,6 @@
             container.addView(tv);
             addedView = tv;
         }
-        if (actionContent != null && addedView != null) {
-            final SliceActionImpl finalAction = actionContent;
-            addedView.setOnClickListener(new View.OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    try {
-                        finalAction.getActionItem().fireAction(null, null);
-                        if (mObserver != null) {
-                            mObserver.onSliceAction(info, finalAction.getSliceItem());
-                        }
-                    } catch (CanceledException e) {
-                        e.printStackTrace();
-                    }
-                }
-            });
-            addedView.setBackground(SliceViewUtil.getDrawable(getContext(),
-                    android.R.attr.selectableItemBackground));
-        }
         return addedView != null;
     }
 
@@ -536,7 +504,7 @@
                     }
                     mRowContent.getSlice().fireAction(null, null);
                 } catch (CanceledException e) {
-                    Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+                    Log.e(TAG, "PendingIntent for slice cannot be sent", e);
                 }
             }
         });
@@ -544,7 +512,7 @@
             b.setTextColor(mTintColor);
         }
         mSeeMoreView = b;
-        addView(mSeeMoreView);
+        mRootView.addView(mSeeMoreView);
     }
 
     @Override
@@ -559,7 +527,7 @@
                     mObserver.onSliceAction(info, mRowAction.getSliceItem());
                 }
             } catch (CanceledException e) {
-                Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+                Log.e(TAG, "PendingIntent for slice cannot be sent", e);
             }
         } else if (mToggles.size() == 1) {
             // If there is only one toggle and no row action, just toggle it.
@@ -569,14 +537,16 @@
 
     private void setViewClickable(View layout, boolean isClickable) {
         layout.setOnClickListener(isClickable ? this : null);
-        layout.setBackground(isClickable ? SliceViewUtil.getDrawable(getContext(),
-                android.R.attr.selectableItemBackground) : null);
+        layout.setBackground(isClickable
+                ? SliceViewUtil.getDrawable(getContext(), android.R.attr.selectableItemBackground)
+                : null);
         layout.setClickable(isClickable);
     }
 
     @Override
     public void resetView() {
-        setViewClickable(this, false);
+        mRootView.setVisibility(View.VISIBLE);
+        setViewClickable(mRootView, false);
         setViewClickable(mContent, false);
         mStartContainer.removeAllViews();
         mEndContainer.removeAllViews();
@@ -585,10 +555,11 @@
         mToggles.clear();
         mRowAction = null;
         mDivider.setVisibility(View.GONE);
-        mSeekBar.setVisibility(View.GONE);
-        mProgressBar.setVisibility(View.GONE);
+        if (mRangeBar != null) {
+            removeView(mRangeBar);
+        }
         if (mSeeMoreView != null) {
-            removeView(mSeeMoreView);
+            mRootView.removeView(mSeeMoreView);
         }
     }
 }
diff --git a/androidx/slice/widget/ShortcutView.java b/androidx/slice/widget/ShortcutView.java
index d200512..37ef17b 100644
--- a/androidx/slice/widget/ShortcutView.java
+++ b/androidx/slice/widget/ShortcutView.java
@@ -39,6 +39,7 @@
 import android.graphics.drawable.ShapeDrawable;
 import android.graphics.drawable.shapes.OvalShape;
 import android.net.Uri;
+import android.util.Log;
 import android.widget.ImageView;
 
 import androidx.annotation.RestrictTo;
@@ -131,7 +132,7 @@
                     mObserver.onSliceAction(ei, interactedItem);
                 }
             } catch (CanceledException e) {
-                e.printStackTrace();
+                Log.e(TAG, "PendingIntent for slice cannot be sent", e);
             }
         }
         return true;
diff --git a/androidx/slice/widget/SliceActionView.java b/androidx/slice/widget/SliceActionView.java
new file mode 100644
index 0000000..ffe4587
--- /dev/null
+++ b/androidx/slice/widget/SliceActionView.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2018 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 androidx.slice.widget;
+
+import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.slice.core.SliceHints.ICON_IMAGE;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.CompoundButton;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.Switch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.slice.core.SliceActionImpl;
+import androidx.slice.view.R;
+
+/**
+ * Supports displaying {@link androidx.slice.core.SliceActionImpl}s.
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class SliceActionView extends FrameLayout implements View.OnClickListener,
+        CompoundButton.OnCheckedChangeListener {
+    private static final String TAG = "SliceActionView";
+
+    private static final int[] STATE_CHECKED = {
+            android.R.attr.state_checked
+    };
+
+    private SliceActionImpl mSliceAction;
+    private EventInfo mEventInfo;
+    private SliceView.OnSliceActionListener mObserver;
+
+    private View mActionView;
+
+    private int mIconSize;
+    private int mImageSize;
+
+    public SliceActionView(Context context) {
+        super(context);
+        Resources res = getContext().getResources();
+        mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size);
+        mImageSize = res.getDimensionPixelSize(R.dimen.abc_slice_small_image_size);
+    }
+
+    /**
+     * Populates the view with the provided action.
+     */
+    public void setAction(@NonNull SliceActionImpl action, EventInfo info,
+                          SliceView.OnSliceActionListener listener, int color) {
+        mSliceAction = action;
+        mEventInfo = info;
+        mObserver = listener;
+        mActionView = null;
+
+        if (action.isDefaultToggle()) {
+            Switch switchView = new Switch(getContext());
+            addView(switchView);
+            switchView.setChecked(action.isChecked());
+            switchView.setOnCheckedChangeListener(this);
+            switchView.setMinimumHeight(mImageSize);
+            switchView.setMinimumWidth(mImageSize);
+            if (color != -1) {
+                // TODO - find nice way to tint toggles
+            }
+            mActionView = switchView;
+
+        } else if (action.getIcon() != null) {
+            if (action.isToggle()) {
+                ImageToggle imageToggle = new ImageToggle(getContext());
+                imageToggle.setChecked(action.isChecked());
+                mActionView = imageToggle;
+            } else {
+                mActionView = new ImageView(getContext());
+            }
+            addView(mActionView);
+
+            Drawable d = mSliceAction.getIcon().loadDrawable(getContext());
+            ((ImageView) mActionView).setImageDrawable(d);
+            if (color != -1 && mSliceAction.getImageMode() == ICON_IMAGE) {
+                // TODO - Consider allowing option for untinted custom toggles
+                DrawableCompat.setTint(d, color);
+            }
+            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mActionView.getLayoutParams();
+            lp.width = mImageSize;
+            lp.height = mImageSize;
+            mActionView.setLayoutParams(lp);
+            int p = action.getImageMode() == ICON_IMAGE ? mIconSize / 2 : 0;
+            mActionView.setPadding(p, p, p, p);
+            mActionView.setBackground(SliceViewUtil.getDrawable(getContext(),
+                    android.R.attr.selectableItemBackground));
+            mActionView.setOnClickListener(this);
+        }
+
+        if (mActionView != null) {
+            CharSequence contentDescription = action.getContentDescription() != null
+                    ? action.getContentDescription()
+                    : action.getTitle();
+            mActionView.setContentDescription(contentDescription);
+        }
+    }
+
+    /**
+     * Toggles this action if it is toggleable.
+     */
+    public void toggle() {
+        if (mActionView != null && mSliceAction != null && mSliceAction.isToggle()) {
+            ((Checkable) mActionView).toggle();
+        }
+    }
+
+    /**
+     * @return the action represented in this view.
+     */
+    @Nullable
+    public SliceActionImpl getAction() {
+        return mSliceAction;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mSliceAction == null || mActionView == null) {
+            return;
+        }
+        sendAction();
+    }
+
+    @Override
+    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+        if (mSliceAction == null || mActionView == null) {
+            return;
+        }
+        sendAction();
+    }
+
+    private void sendAction() {
+        // TODO - Show loading indicator here?
+        try {
+            PendingIntent pi = mSliceAction.getAction();
+            if (mSliceAction.isToggle()) {
+                // Update the intent extra state
+                boolean isChecked = ((Checkable) mActionView).isChecked();
+                Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
+                mSliceAction.getActionItem().fireAction(getContext(), i);
+
+                // Update event info state
+                if (mEventInfo != null) {
+                    mEventInfo.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
+                }
+            } else {
+                mSliceAction.getActionItem().fireAction(null, null); 
+            }
+            if (mObserver != null && mEventInfo != null) {
+                mObserver.onSliceAction(mEventInfo, mSliceAction.getSliceItem());
+            }
+        } catch (PendingIntent.CanceledException e) {
+            if (mActionView instanceof Checkable) {
+                mActionView.setSelected(!((Checkable) mActionView).isChecked());
+            }
+            Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+        }
+    }
+
+    /**
+     * Simple class allowing a toggleable image button.
+     */
+    private static class ImageToggle extends ImageView implements Checkable, View.OnClickListener {
+        private boolean mIsChecked;
+        private View.OnClickListener mListener;
+
+        ImageToggle(Context context) {
+            super(context);
+            super.setOnClickListener(this);
+        }
+
+        @Override
+        public void onClick(View v) {
+            toggle();
+        }
+
+        @Override
+        public void toggle() {
+            setChecked(!isChecked());
+        }
+
+        @Override
+        public void setChecked(boolean checked) {
+            if (mIsChecked != checked) {
+                mIsChecked = checked;
+                refreshDrawableState();
+                if (mListener != null) {
+                    mListener.onClick(this);
+                }
+            }
+        }
+
+        @Override
+        public void setOnClickListener(View.OnClickListener listener) {
+            mListener = listener;
+        }
+
+        @Override
+        public boolean isChecked() {
+            return mIsChecked;
+        }
+
+        @Override
+        public int[] onCreateDrawableState(int extraSpace) {
+            final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+            if (mIsChecked) {
+                mergeDrawableStates(drawableState, STATE_CHECKED);
+            }
+            return drawableState;
+        }
+    }
+}
diff --git a/androidx/slice/widget/SliceChildView.java b/androidx/slice/widget/SliceChildView.java
index 2e52246..2400e73 100644
--- a/androidx/slice/widget/SliceChildView.java
+++ b/androidx/slice/widget/SliceChildView.java
@@ -138,9 +138,9 @@
     /**
      * Populates style information for this view.
      */
-    public void setStyle(AttributeSet attrs) {
+    public void setStyle(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
-                R.attr.sliceViewStyle, R.style.Widget_SliceView);
+                defStyleAttr, defStyleRes);
         try {
             int themeColor = a.getColor(R.styleable.SliceView_tintColor, -1);
             mTintColor = themeColor != -1 ? themeColor : mTintColor;
diff --git a/androidx/slice/widget/SliceView.java b/androidx/slice/widget/SliceView.java
index bf1218b..855bded 100644
--- a/androidx/slice/widget/SliceView.java
+++ b/androidx/slice/widget/SliceView.java
@@ -16,10 +16,10 @@
 
 package androidx.slice.widget;
 
-import static android.app.slice.Slice.HINT_HORIZONTAL;
 import static android.app.slice.Slice.SUBTYPE_COLOR;
 import static android.app.slice.SliceItem.FORMAT_INT;
 
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.ColorDrawable;
@@ -41,10 +41,13 @@
 import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.SliceMetadata;
+import androidx.slice.core.SliceActionImpl;
 import androidx.slice.core.SliceHints;
 import androidx.slice.core.SliceQuery;
 import androidx.slice.view.R;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 
 /**
@@ -78,7 +81,7 @@
  * </pre>
  * @see SliceLiveData
  */
-public class SliceView extends ViewGroup implements Observer<Slice> {
+public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener {
 
     private static final String TAG = "SliceView";
 
@@ -103,6 +106,7 @@
     @IntDef({
             MODE_SMALL, MODE_LARGE, MODE_SHORTCUT
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface SliceMode {}
 
     /**
@@ -122,6 +126,7 @@
 
     private int mMode = MODE_LARGE;
     private Slice mCurrentSlice;
+    private ListContent mListContent;
     private SliceChildView mCurrentView;
     private List<SliceItem> mActions;
     private ActionRow mActionRow;
@@ -136,16 +141,20 @@
     private int mActionRowHeight;
 
     private AttributeSet mAttrs;
+    private int mDefStyleAttr;
+    private int mDefStyleRes;
     private int mThemeTintColor = -1;
 
     private OnSliceActionListener mSliceObserver;
     private int mTouchSlopSquared;
     private View.OnLongClickListener mLongClickListener;
+    private View.OnClickListener mOnClickListener;
     private int mDownX;
     private int mDownY;
     private boolean mPressing;
     private boolean mInLongpress;
     private Handler mHandler;
+    int[] mClickInfo;
 
     public SliceView(Context context) {
         this(context, null);
@@ -168,20 +177,16 @@
 
     private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         mAttrs = attrs;
+        mDefStyleAttr = defStyleAttr;
+        mDefStyleRes = defStyleRes;
         TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
                 defStyleAttr, defStyleRes);
+
         try {
             mThemeTintColor = a.getColor(R.styleable.SliceView_tintColor, -1);
         } finally {
             a.recycle();
         }
-        // TODO: action row background should support light / dark / maybe presenter customization
-        mActionRow = new ActionRow(getContext(), true);
-        mActionRow.setBackground(new ColorDrawable(0xffeeeeee));
-        mCurrentView = new LargeTemplateView(getContext());
-        mCurrentView.setMode(getMode());
-        addView(mCurrentView.getView(), getChildLp(mCurrentView.getView()));
-        addView(mActionRow, getChildLp(mActionRow));
         mShortcutSize = getContext().getResources()
                 .getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
         mMinLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height);
@@ -189,13 +194,68 @@
         mActionRowHeight = getResources().getDimensionPixelSize(
                 R.dimen.abc_slice_action_row_height);
 
+        mCurrentView = new LargeTemplateView(getContext());
+        mCurrentView.setMode(getMode());
+        addView(mCurrentView, getChildLp(mCurrentView));
+
+        // TODO: action row background should support light / dark / maybe presenter customization
+        mActionRow = new ActionRow(getContext(), true);
+        mActionRow.setBackground(new ColorDrawable(0xffeeeeee));
+        addView(mActionRow, getChildLp(mActionRow));
+
         final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
         mTouchSlopSquared = slop * slop;
         mHandler = new Handler();
+
+        super.setOnClickListener(this);
+    }
+
+    /**
+     * Indicates whether this view reacts to click events or not.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public boolean isSliceViewClickable() {
+        return mOnClickListener != null
+                || (mListContent != null && mListContent.getPrimaryAction() != null);
+    }
+
+    /**
+     * Sets the event info for logging a click.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public void setClickInfo(int[] info) {
+        mClickInfo = info;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mListContent != null && mListContent.getPrimaryAction() != null) {
+            try {
+                SliceActionImpl sa = new SliceActionImpl(mListContent.getPrimaryAction());
+                sa.getAction().send();
+                if (mSliceObserver != null && mClickInfo != null && mClickInfo.length > 1) {
+                    EventInfo eventInfo = new EventInfo(getMode(),
+                            EventInfo.ACTION_TYPE_CONTENT, mClickInfo[0], mClickInfo[1]);
+                    mSliceObserver.onSliceAction(eventInfo, mListContent.getPrimaryAction());
+                }
+            } catch (PendingIntent.CanceledException e) {
+                Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+            }
+        } else if (mOnClickListener != null) {
+            mOnClickListener.onClick(this);
+        }
+    }
+
+    @Override
+    public void setOnClickListener(View.OnClickListener listener) {
+        mOnClickListener = listener;
     }
 
     @Override
     public void setOnLongClickListener(View.OnLongClickListener listener) {
+        super.setOnLongClickListener(listener);
         mLongClickListener = listener;
     }
 
@@ -353,7 +413,18 @@
     }
 
     /**
+     * @return the slice being used to populate this view.
+     */
+    @Nullable
+    public Slice getSlice() {
+        return mCurrentSlice;
+    }
+
+    /**
      * Returns the slice actions presented in this view.
+     * <p>
+     * Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions
+     * set on the view have been adjusted using {@link #setSliceActions(List)}.
      */
     @Nullable
     public List<SliceItem> getSliceActions() {
@@ -466,53 +537,37 @@
         return mShowActions;
     }
 
-    private SliceChildView createView(int mode, boolean isGrid) {
-        switch (mode) {
-            case MODE_SHORTCUT:
-                return new ShortcutView(getContext());
-            case MODE_SMALL:
-                return isGrid ? new GridRowView(getContext()) : new RowView(getContext());
-        }
-        return new LargeTemplateView(getContext());
-    }
-
     private void reinflate() {
         if (mCurrentSlice == null) {
             mCurrentView.resetView();
             return;
         }
-        ListContent lc = new ListContent(getContext(), mCurrentSlice);
-        if (!lc.isValid()) {
+        mListContent = new ListContent(getContext(), mCurrentSlice);
+        if (!mListContent.isValid()) {
             mCurrentView.resetView();
-            mCurrentView.setVisibility(View.GONE);
             return;
         }
+
         // TODO: Smarter mapping here from one state to the next.
         int mode = getMode();
-        boolean reuseView = mode == mCurrentView.getMode();
-        SliceItem header = lc.getHeaderItem();
-        boolean isSmallGrid = header != null && SliceQuery.hasHints(header, HINT_HORIZONTAL);
-        if (reuseView && mode == MODE_SMALL) {
-            reuseView = (mCurrentView instanceof GridRowView) == isSmallGrid;
-        }
-        if (!reuseView) {
+        boolean isCurrentViewShortcut = mCurrentView instanceof ShortcutView;
+        if (mode == MODE_SHORTCUT && !isCurrentViewShortcut) {
             removeAllViews();
-            mCurrentView = createView(mode, isSmallGrid);
-            if (mSliceObserver != null) {
-                mCurrentView.setSliceActionListener(mSliceObserver);
-            }
-            addView(mCurrentView.getView(), getChildLp(mCurrentView.getView()));
-            addView(mActionRow, getChildLp(mActionRow));
-            mCurrentView.setMode(mode);
+            mCurrentView = new ShortcutView(getContext());
+            addView(mCurrentView, getChildLp(mCurrentView));
+        } else if (mode != MODE_SHORTCUT && isCurrentViewShortcut) {
+            removeAllViews();
+            mCurrentView = new LargeTemplateView(getContext());
+            addView(mCurrentView, getChildLp(mCurrentView));
         }
-        // Scrolling
-        if (mode == MODE_LARGE && (mCurrentView instanceof LargeTemplateView)) {
+        mCurrentView.setMode(mode);
+
+        mCurrentView.setSliceActionListener(mSliceObserver);
+        if (mCurrentView instanceof LargeTemplateView) {
             ((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
         }
-        // Styles
-        mCurrentView.setStyle(mAttrs);
+        mCurrentView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
         mCurrentView.setTint(getTintColor());
-        mCurrentView.setVisibility(lc.isValid() ? View.VISIBLE : View.GONE);
 
         // Check if the slice content is expired and show when it was last updated
         SliceMetadata sliceMetadata = SliceMetadata.from(getContext(), mCurrentSlice);
diff --git a/androidx/webkit/WebViewClientCompat.java b/androidx/webkit/WebViewClientCompat.java
new file mode 100644
index 0000000..8c690f9
--- /dev/null
+++ b/androidx/webkit/WebViewClientCompat.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 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 androidx.webkit;
+
+import android.os.Build;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import org.chromium.support_lib_boundary.WebViewClientBoundaryInterface;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationHandler;
+
+/**
+ * Compatibility version of {@link android.webkit.WebViewClient}.
+ */
+// Note: some methods are marked as RequiresApi 21, because only an up-to-date WebView APK would
+// ever invoke these methods (and WebView can only be updated on Lollipop and above). The app can
+// still construct a WebViewClientCompat on a pre-Lollipop devices, and explicitly invoke these
+// methods, so each of these methods must also handle this case.
+public class WebViewClientCompat extends WebViewClient implements WebViewClientBoundaryInterface {
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @IntDef(value = {
+            WebViewClient.SAFE_BROWSING_THREAT_UNKNOWN,
+            WebViewClient.SAFE_BROWSING_THREAT_MALWARE,
+            WebViewClient.SAFE_BROWSING_THREAT_PHISHING,
+            WebViewClient.SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SafeBrowsingThreat {}
+
+    @Override
+    public void onPageCommitVisible(@NonNull WebView view, @NonNull String url) {
+    }
+
+    /**
+     * Invoked by chromium for the {@code onReceivedError} event. Applications are not meant to
+     * override this, and should instead override the non-final {@code onReceivedError} method.
+     * TODO(ntfschr): link to that method once it's implemented.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public final void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+            /* WebResourceError */ @NonNull InvocationHandler error) {
+        // TODO(ntfschr): implement this (b/73151460).
+    }
+
+    @Override
+    public void onReceivedHttpError(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @NonNull WebResourceResponse errorResponse) {
+    }
+
+    /**
+     * Invoked by chromium for the {@code onSafeBrowsingHit} event. Applications are not meant to
+     * override this, and should instead override the non-final {@code onSafeBrowsingHit} method.
+     * TODO(ntfschr): link to that method once it's implemented.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public final void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+            @SafeBrowsingThreat int threatType,
+            /* SafeBrowsingResponse */ @NonNull InvocationHandler callback) {
+        // TODO(ntfschr): implement this (b/73151460).
+    }
+
+    // Default behavior in WebViewClient is to invoke the other (deprecated)
+    // shouldOverrideUrlLoading method.
+    @Override
+    @SuppressWarnings("deprecation")
+    @RequiresApi(21)
+    public boolean shouldOverrideUrlLoading(@NonNull WebView view,
+            @NonNull WebResourceRequest request) {
+        if (Build.VERSION.SDK_INT < 21) return false;
+        return shouldOverrideUrlLoading(view, request.getUrl().toString());
+    }
+}
diff --git a/androidx/webkit/WebViewCompat.java b/androidx/webkit/WebViewCompat.java
index bdd53c7..88de335 100644
--- a/androidx/webkit/WebViewCompat.java
+++ b/androidx/webkit/WebViewCompat.java
@@ -318,7 +318,7 @@
     @SuppressWarnings("NewApi")
     private static void checkThread(WebView webview) {
         if (BuildCompat.isAtLeastP()) {
-            if (webview.getLooper() != Looper.myLooper()) {
+            if (webview.getWebViewLooper() != Looper.myLooper()) {
                 throw new RuntimeException("A WebView method was called on thread '"
                         + Thread.currentThread().getName() + "'. "
                         + "All WebView methods must be called on the same thread. "
diff --git a/androidx/widget/BaseLayout.java b/androidx/widget/BaseLayout.java
new file mode 100644
index 0000000..127006e
--- /dev/null
+++ b/androidx/widget/BaseLayout.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroup.MarginLayoutParams;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.StyleRes;
+
+import java.util.ArrayList;
+
+class BaseLayout extends ViewGroup {
+    private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+    BaseLayout(@NonNull Context context) {
+        super(context);
+    }
+
+    BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @RequiresApi(21)
+    BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean checkLayoutParams(LayoutParams p) {
+        return p instanceof MarginLayoutParams;
+    }
+
+    @Override
+    public LayoutParams generateDefaultLayoutParams() {
+        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new MarginLayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(LayoutParams lp) {
+        if (lp instanceof MarginLayoutParams) {
+            return lp;
+        }
+        return new MarginLayoutParams(lp);
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int count = getChildCount();
+
+        final boolean measureMatchParentChildren =
+                View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.EXACTLY
+                        || View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.EXACTLY;
+        mMatchParentChildren.clear();
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+        int childState = 0;
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE) {
+                measureChildWithMargins(
+                        child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+                maxWidth = Math.max(maxWidth,
+                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+                maxHeight = Math.max(maxHeight,
+                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+                childState = childState | child.getMeasuredState();
+                if (measureMatchParentChildren) {
+                    if (lp.width == LayoutParams.MATCH_PARENT
+                            || lp.height == LayoutParams.MATCH_PARENT) {
+                        mMatchParentChildren.add(child);
+                    }
+                }
+            }
+        }
+
+        // Account for padding too
+        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
+        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
+
+        // Check against our minimum height and width
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        if (Build.VERSION.SDK_INT >= 23) {
+            // Check against our foreground's minimum height and width
+            final Drawable drawable = getForeground();
+            if (drawable != null) {
+                maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+                maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+            }
+        }
+
+        setMeasuredDimension(
+                resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+                resolveSizeAndState(maxHeight, heightMeasureSpec,
+                        childState << View.MEASURED_HEIGHT_STATE_SHIFT));
+
+        count = mMatchParentChildren.size();
+        if (count > 1) {
+            for (int i = 0; i < count; i++) {
+                final View child = mMatchParentChildren.get(i);
+                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+                final int childWidthMeasureSpec;
+                if (lp.width == LayoutParams.MATCH_PARENT) {
+                    final int width = Math.max(0, getMeasuredWidth()
+                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
+                            - lp.leftMargin - lp.rightMargin);
+                    childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+                            width, View.MeasureSpec.EXACTLY);
+                } else {
+                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+                            getPaddingLeftWithForeground() + getPaddingRightWithForeground()
+                                    + lp.leftMargin + lp.rightMargin, lp.width);
+                }
+
+                final int childHeightMeasureSpec;
+                if (lp.height == LayoutParams.MATCH_PARENT) {
+                    final int height = Math.max(0, getMeasuredHeight()
+                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
+                            - lp.topMargin - lp.bottomMargin);
+                    childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+                            height, View.MeasureSpec.EXACTLY);
+                } else {
+                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                            getPaddingTopWithForeground() + getPaddingBottomWithForeground()
+                                    + lp.topMargin + lp.bottomMargin, lp.height);
+                }
+
+                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int count = getChildCount();
+
+        final int parentLeft = getPaddingLeftWithForeground();
+        final int parentRight = right - left - getPaddingRightWithForeground();
+
+        final int parentTop = getPaddingTopWithForeground();
+        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE) {
+                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+                final int width = child.getMeasuredWidth();
+                final int height = child.getMeasuredHeight();
+
+                int childLeft;
+                int childTop;
+
+                childLeft = parentLeft + (parentRight - parentLeft - width) / 2
+                        + lp.leftMargin - lp.rightMargin;
+
+                childTop = parentTop + (parentBottom - parentTop - height) / 2
+                        + lp.topMargin - lp.bottomMargin;
+
+                child.layout(childLeft, childTop, childLeft + width, childTop + height);
+            }
+        }
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    private int getPaddingLeftWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(getPaddingLeft(), 0) :
+                getPaddingLeft() + 0;
+    }
+
+    private int getPaddingRightWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(getPaddingRight(), 0) :
+                getPaddingRight() + 0;
+    }
+
+    private int getPaddingTopWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(getPaddingTop(), 0) :
+                getPaddingTop() + 0;
+    }
+
+    private int getPaddingBottomWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(getPaddingBottom(), 0) :
+                getPaddingBottom() + 0;
+    }
+
+    // A stub method for View's isForegroundInsidePadding() which is hidden.
+    // Always returns true for now, since the default value is true.
+    // See View's isForegroundInsidePadding method.
+    private boolean isForegroundInsidePadding() {
+        return true;
+    }
+}
diff --git a/androidx/widget/MediaControlView2.java b/androidx/widget/MediaControlView2.java
new file mode 100644
index 0000000..51d427f
--- /dev/null
+++ b/androidx/widget/MediaControlView2.java
@@ -0,0 +1,1901 @@
+/*
+ * Copyright (C) 2017 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 androidx.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.R;
+import androidx.media.SessionToken2;
+// import androidx.mediarouter.app.MediaRouteButton;
+// import androidx.mediarouter.media.MediaRouter;
+// import androidx.mediarouter.media.MediaRouteSelector;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * @hide
+ * A View that contains the controls for MediaPlayer2.
+ * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
+ * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
+ *
+ * <p>
+ * <em> MediaControlView2 can be initialized in two different ways: </em>
+ * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
+ * adds it to the view.
+ * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
+ *
+ * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController,
+ * which is necessary to communicate with MediaSession2. In the second option, however, the
+ * developer needs to manually retrieve a MediaController instance and set it to MediaControlView2
+ * by calling setController(MediaController controller).
+ *
+ * <p>
+ * There is no separate method that handles the show/hide behavior for MediaControlView2. Instead,
+ * one can directly change the visibility of this view by calling View.setVisibility(int). The
+ * values supported are View.VISIBLE and View.GONE.
+ * In addition, the following customization is supported:
+ * Set focus to the play/pause button by calling requestPlayButtonFocus().
+ *
+ * <p>
+ * It is also possible to add custom buttons with custom icons and actions inside MediaControlView2.
+ * Those buttons will be shown when the overflow button is clicked.
+ * See VideoView2#setCustomActions for more details on how to add.
+ */
+@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
+@RestrictTo(LIBRARY_GROUP)
+public class MediaControlView2 extends BaseLayout {
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({
+            BUTTON_PLAY_PAUSE,
+            BUTTON_FFWD,
+            BUTTON_REW,
+            BUTTON_NEXT,
+            BUTTON_PREV,
+            BUTTON_SUBTITLE,
+            BUTTON_FULL_SCREEN,
+            BUTTON_OVERFLOW,
+            BUTTON_MUTE,
+            BUTTON_ASPECT_RATIO,
+            BUTTON_SETTINGS
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Button {}
+
+    /**
+     * MediaControlView2 button value for playing and pausing media.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_PLAY_PAUSE = 1;
+    /**
+     * MediaControlView2 button value for jumping 30 seconds forward.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_FFWD = 2;
+    /**
+     * MediaControlView2 button value for jumping 10 seconds backward.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_REW = 3;
+    /**
+     * MediaControlView2 button value for jumping to next media.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_NEXT = 4;
+    /**
+     * MediaControlView2 button value for jumping to previous media.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_PREV = 5;
+    /**
+     * MediaControlView2 button value for showing/hiding subtitle track.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_SUBTITLE = 6;
+    /**
+     * MediaControlView2 button value for toggling full screen.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_FULL_SCREEN = 7;
+    /**
+     * MediaControlView2 button value for showing/hiding overflow buttons.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_OVERFLOW = 8;
+    /**
+     * MediaControlView2 button value for muting audio.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_MUTE = 9;
+    /**
+     * MediaControlView2 button value for adjusting aspect ratio of view.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_ASPECT_RATIO = 10;
+    /**
+     * MediaControlView2 button value for showing/hiding settings page.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int BUTTON_SETTINGS = 11;
+
+    private static final String TAG = "MediaControlView2";
+
+    static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
+
+    // TODO: Make these constants public api to support custom video view.
+    // TODO: Combine these constants into one regarding TrackInfo.
+    static final String KEY_VIDEO_TRACK_COUNT = "VideoTrackCount";
+    static final String KEY_AUDIO_TRACK_COUNT = "AudioTrackCount";
+    static final String KEY_SUBTITLE_TRACK_COUNT = "SubtitleTrackCount";
+    static final String KEY_PLAYBACK_SPEED = "PlaybackSpeed";
+    static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
+    static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
+    static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";
+
+    // TODO: Remove this once integrating with MediaSession2 & MediaMetadata2
+    static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
+    static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";
+
+    // String for sending command to show subtitle to MediaSession.
+    static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
+    // String for sending command to hide subtitle to MediaSession.
+    static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
+    // TODO: remove once the implementation is revised
+    public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
+    // String for sending command to select audio track to MediaSession.
+    static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";
+    // String for sending command to set playback speed to MediaSession.
+    static final String COMMAND_SET_PLAYBACK_SPEED = "SetPlaybackSpeed";
+    // String for sending command to mute audio to MediaSession.
+    static final String COMMAND_MUTE = "Mute";
+    // String for sending command to unmute audio to MediaSession.
+    static final String COMMAND_UNMUTE = "Unmute";
+
+    private static final int SETTINGS_MODE_AUDIO_TRACK = 0;
+    private static final int SETTINGS_MODE_PLAYBACK_SPEED = 1;
+    private static final int SETTINGS_MODE_HELP = 2;
+    private static final int SETTINGS_MODE_SUBTITLE_TRACK = 3;
+    private static final int SETTINGS_MODE_VIDEO_QUALITY = 4;
+    private static final int SETTINGS_MODE_MAIN = 5;
+    private static final int PLAYBACK_SPEED_1x_INDEX = 3;
+
+    private static final int MEDIA_TYPE_DEFAULT = 0;
+    private static final int MEDIA_TYPE_MUSIC = 1;
+    private static final int MEDIA_TYPE_ADVERTISEMENT = 2;
+
+    private static final int SIZE_TYPE_EMBEDDED = 0;
+    private static final int SIZE_TYPE_FULL = 1;
+    // TODO: add support for Minimal size type.
+    private static final int SIZE_TYPE_MINIMAL = 2;
+
+    private static final int MAX_PROGRESS = 1000;
+    private static final int DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000;
+    private static final int REWIND_TIME_MS = 10000;
+    private static final int FORWARD_TIME_MS = 30000;
+    private static final int AD_SKIP_WAIT_TIME_MS = 5000;
+    private static final int RESOURCE_NON_EXISTENT = -1;
+    private static final String RESOURCE_EMPTY = "";
+
+    private Resources mResources;
+    private MediaControllerCompat mController;
+    private MediaControllerCompat.TransportControls mControls;
+    private PlaybackStateCompat mPlaybackState;
+    private MediaMetadataCompat mMetadata;
+    private int mDuration;
+    private int mPrevState;
+    private int mPrevWidth;
+    private int mPrevHeight;
+    private int mOriginalLeftBarWidth;
+    private int mVideoTrackCount;
+    private int mAudioTrackCount;
+    private int mSubtitleTrackCount;
+    private int mSettingsMode;
+    private int mSelectedSubtitleTrackIndex;
+    private int mSelectedAudioTrackIndex;
+    private int mSelectedVideoQualityIndex;
+    private int mSelectedSpeedIndex;
+    private int mEmbeddedSettingsItemWidth;
+    private int mFullSettingsItemWidth;
+    private int mEmbeddedSettingsItemHeight;
+    private int mFullSettingsItemHeight;
+    private int mSettingsWindowMargin;
+    private int mMediaType;
+    private int mSizeType;
+    private int mOrientation;
+    private long mPlaybackActions;
+    private boolean mDragging;
+    private boolean mIsFullScreen;
+    private boolean mOverflowExpanded;
+    private boolean mIsStopped;
+    private boolean mSubtitleIsEnabled;
+    private boolean mSeekAvailable;
+    private boolean mIsAdvertisement;
+    private boolean mIsMute;
+    private boolean mNeedUXUpdate;
+
+    // Relating to Title Bar View
+    private ViewGroup mRoot;
+    private View mTitleBar;
+    private TextView mTitleView;
+    private View mAdExternalLink;
+    private ImageButton mBackButton;
+    // TODO (b/77158231) revive
+    // private MediaRouteButton mRouteButton;
+    // private MediaRouteSelector mRouteSelector;
+
+    // Relating to Center View
+    private ViewGroup mCenterView;
+    private View mTransportControls;
+    private ImageButton mPlayPauseButton;
+    private ImageButton mFfwdButton;
+    private ImageButton mRewButton;
+    private ImageButton mNextButton;
+    private ImageButton mPrevButton;
+
+    // Relating to Minimal Extra View
+    private LinearLayout mMinimalExtraView;
+
+    // Relating to Progress Bar View
+    private ProgressBar mProgress;
+    private View mProgressBuffer;
+
+    // Relating to Bottom Bar View
+    private ViewGroup mBottomBar;
+
+    // Relating to Bottom Bar Left View
+    private ViewGroup mBottomBarLeftView;
+    private ViewGroup mTimeView;
+    private TextView mEndTime;
+    private TextView mCurrentTime;
+    private TextView mAdSkipView;
+    private StringBuilder mFormatBuilder;
+    private Formatter mFormatter;
+
+    // Relating to Bottom Bar Right View
+    private ViewGroup mBottomBarRightView;
+    private ViewGroup mBasicControls;
+    private ViewGroup mExtraControls;
+    private ViewGroup mCustomButtons;
+    private ImageButton mSubtitleButton;
+    private ImageButton mFullScreenButton;
+    private ImageButton mOverflowButtonRight;
+    private ImageButton mOverflowButtonLeft;
+    private ImageButton mMuteButton;
+    private ImageButton mVideoQualityButton;
+    private ImageButton mSettingsButton;
+    private TextView mAdRemainingView;
+
+    // Relating to Settings List View
+    private ListView mSettingsListView;
+    private PopupWindow mSettingsWindow;
+    private SettingsAdapter mSettingsAdapter;
+    private SubSettingsAdapter mSubSettingsAdapter;
+    private List<String> mSettingsMainTextsList;
+    private List<String> mSettingsSubTextsList;
+    private List<Integer> mSettingsIconIdsList;
+    private List<String> mSubtitleDescriptionsList;
+    private List<String> mAudioTrackList;
+    private List<String> mVideoQualityList;
+    private List<String> mPlaybackSpeedTextList;
+    private List<Float> mPlaybackSpeedList;
+
+    public MediaControlView2(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+//        super((instance, superProvider, privateProvider) ->
+//                ApiLoader.getProvider().createMediaControlView2(
+//                        (MediaControlView2) instance, superProvider, privateProvider,
+//                        attrs, defStyleAttr, defStyleRes),
+//                context, attrs, defStyleAttr, defStyleRes);
+//        mProvider.initialize(attrs, defStyleAttr, defStyleRes);
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mResources = getContext().getResources();
+        // Inflate MediaControlView2 from XML
+        mRoot = makeControllerView();
+        addView(mRoot);
+    }
+
+    /**
+     * Sets MediaSession2 token to control corresponding MediaSession2.
+     */
+    public void setMediaSessionToken(SessionToken2 token) {
+        //mProvider.setMediaSessionToken_impl(token);
+    }
+
+    /**
+     * Registers a callback to be invoked when the fullscreen mode should be changed.
+     * @param l The callback that will be run
+     */
+    public void setOnFullScreenListener(OnFullScreenListener l) {
+        //mProvider.setOnFullScreenListener_impl(l);
+    }
+
+    /**
+     * @hide TODO: remove once the implementation is revised
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setController(MediaControllerCompat controller) {
+        mController = controller;
+        if (controller != null) {
+            mControls = controller.getTransportControls();
+            // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
+            // be called before the callback is called
+            mPlaybackState = mController.getPlaybackState();
+            mMetadata = mController.getMetadata();
+            updateDuration();
+            updateTitle();
+
+            mController.registerCallback(new MediaControllerCallback());
+        }
+    }
+
+    /**
+     * Changes the visibility state of an individual button. Default value is View.Visible.
+     *
+     * @param button the {@code Button} assigned to individual buttons
+     * <ul>
+     * <li>{@link #BUTTON_PLAY_PAUSE}
+     * <li>{@link #BUTTON_FFWD}
+     * <li>{@link #BUTTON_REW}
+     * <li>{@link #BUTTON_NEXT}
+     * <li>{@link #BUTTON_PREV}
+     * <li>{@link #BUTTON_SUBTITLE}
+     * <li>{@link #BUTTON_FULL_SCREEN}
+     * <li>{@link #BUTTON_MUTE}
+     * <li>{@link #BUTTON_OVERFLOW}
+     * <li>{@link #BUTTON_ASPECT_RATIO}
+     * <li>{@link #BUTTON_SETTINGS}
+     * </ul>
+     * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setButtonVisibility(@Button int button, /*@Visibility*/ int visibility) {
+        // TODO: add member variables for Fast-Forward/Prvious/Rewind buttons to save visibility in
+        // order to prevent being overriden inside updateLayout().
+        switch (button) {
+            case MediaControlView2.BUTTON_PLAY_PAUSE:
+                if (mPlayPauseButton != null && canPause()) {
+                    mPlayPauseButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_FFWD:
+                if (mFfwdButton != null && canSeekForward()) {
+                    mFfwdButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_REW:
+                if (mRewButton != null && canSeekBackward()) {
+                    mRewButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_NEXT:
+                if (mNextButton != null) {
+                    mNextButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_PREV:
+                if (mPrevButton != null) {
+                    mPrevButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_SUBTITLE:
+                if (mSubtitleButton != null && mSubtitleTrackCount > 0) {
+                    mSubtitleButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_FULL_SCREEN:
+                if (mFullScreenButton != null) {
+                    mFullScreenButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_OVERFLOW:
+                if (mOverflowButtonRight != null) {
+                    mOverflowButtonRight.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_MUTE:
+                if (mMuteButton != null) {
+                    mMuteButton.setVisibility(visibility);
+                }
+                break;
+            case MediaControlView2.BUTTON_SETTINGS:
+                if (mSettingsButton != null) {
+                    mSettingsButton.setVisibility(visibility);
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     *  Requests focus for the play/pause button.
+     */
+    public void requestPlayButtonFocus() {
+        if (mPlayPauseButton != null) {
+            mPlayPauseButton.requestFocus();
+        }
+    }
+
+    /**
+     * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
+     * Application should handle the fullscreen mode accordingly.
+     */
+    public interface OnFullScreenListener {
+        /**
+         * Called to indicate a fullscreen mode change.
+         */
+        void onFullScreen(View view, boolean fullScreen);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return MediaControlView2.class.getName();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    // TODO: Should this function be removed?
+    @Override
+    public boolean onTrackballEvent(MotionEvent ev) {
+        return false;
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        // Update layout when this view's width changes in order to avoid any UI overlap between
+        // transport controls.
+        if (mPrevWidth != getMeasuredWidth()
+                || mPrevHeight != getMeasuredHeight() || mNeedUXUpdate) {
+            // Dismiss SettingsWindow if it is showing.
+            mSettingsWindow.dismiss();
+
+            // These views may not have been initialized yet.
+            if (mTransportControls.getWidth() == 0 || mTimeView.getWidth() == 0) {
+                return;
+            }
+
+            int currWidth = getMeasuredWidth();
+            int currHeight = getMeasuredHeight();
+            WindowManager manager = (WindowManager) getContext().getApplicationContext()
+                    .getSystemService(Context.WINDOW_SERVICE);
+            Point screenSize = new Point();
+            manager.getDefaultDisplay().getSize(screenSize);
+            int screenWidth = screenSize.x;
+            int screenHeight = screenSize.y;
+            int fullIconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_full_icon_size);
+            int embeddedIconSize = mResources.getDimensionPixelSize(
+                    R.dimen.mcv2_embedded_icon_size);
+            int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
+
+            // TODO: add support for Advertisement Mode.
+            if (mMediaType == MEDIA_TYPE_DEFAULT) {
+                // Max number of icons inside BottomBarRightView for Music mode is 4.
+                int maxIconCount = 4;
+                updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
+                        currHeight, screenWidth, screenHeight);
+
+            } else if (mMediaType == MEDIA_TYPE_MUSIC) {
+                if (mNeedUXUpdate) {
+                    // One-time operation for Music media type
+                    mBasicControls.removeView(mMuteButton);
+                    mExtraControls.addView(mMuteButton, 0);
+                    mVideoQualityButton.setVisibility(View.GONE);
+                    if (mFfwdButton != null) {
+                        mFfwdButton.setVisibility(View.GONE);
+                    }
+                    if (mRewButton != null) {
+                        mRewButton.setVisibility(View.GONE);
+                    }
+                }
+                mNeedUXUpdate = false;
+
+                // Max number of icons inside BottomBarRightView for Music mode is 3.
+                int maxIconCount = 3;
+                updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
+                        currHeight, screenWidth, screenHeight);
+            }
+            mPrevWidth = currWidth;
+            mPrevHeight = currHeight;
+        }
+        // TODO: move this to a different location.
+        // Update title bar parameters in order to avoid overlap between title view and the right
+        // side of the title bar.
+        updateTitleBarLayout();
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+
+        // TODO: Merge the below code with disableUnsupportedButtons().
+        if (mPlayPauseButton != null) {
+            mPlayPauseButton.setEnabled(enabled);
+        }
+        if (mFfwdButton != null) {
+            mFfwdButton.setEnabled(enabled);
+        }
+        if (mRewButton != null) {
+            mRewButton.setEnabled(enabled);
+        }
+        if (mNextButton != null) {
+            mNextButton.setEnabled(enabled);
+        }
+        if (mPrevButton != null) {
+            mPrevButton.setEnabled(enabled);
+        }
+        if (mProgress != null) {
+            mProgress.setEnabled(enabled);
+        }
+        disableUnsupportedButtons();
+    }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        super.onVisibilityAggregated(isVisible);
+
+        if (isVisible) {
+            disableUnsupportedButtons();
+            removeCallbacks(mUpdateProgress);
+            post(mUpdateProgress);
+        } else {
+            removeCallbacks(mUpdateProgress);
+        }
+    }
+
+    // TODO (b/77158231) revive once androidx.mediarouter.* packagaes are available.
+    /*
+    void setRouteSelector(MediaRouteSelector selector) {
+        mRouteSelector = selector;
+        if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
+            mRouteButton.setRouteSelector(selector, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+            mRouteButton.setVisibility(View.VISIBLE);
+        } else {
+            mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
+            mRouteButton.setVisibility(View.GONE);
+        }
+    }
+    */
+
+    ///////////////////////////////////////////////////
+    // Protected or private methods
+    ///////////////////////////////////////////////////
+
+    private boolean isPlaying() {
+        if (mPlaybackState != null) {
+            return mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING;
+        }
+        return false;
+    }
+
+    private int getCurrentPosition() {
+        mPlaybackState = mController.getPlaybackState();
+        if (mPlaybackState != null) {
+            return (int) mPlaybackState.getPosition();
+        }
+        return 0;
+    }
+
+    private int getBufferPercentage() {
+        if (mDuration == 0) {
+            return 0;
+        }
+        mPlaybackState = mController.getPlaybackState();
+        if (mPlaybackState != null) {
+            long bufferedPos = mPlaybackState.getBufferedPosition();
+            return (bufferedPos == -1) ? -1 : (int) (bufferedPos * 100 / mDuration);
+        }
+        return 0;
+    }
+
+    private boolean canPause() {
+        if (mPlaybackState != null) {
+            return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_PAUSE) != 0;
+        }
+        return true;
+    }
+
+    private boolean canSeekBackward() {
+        if (mPlaybackState != null) {
+            return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_REWIND) != 0;
+        }
+        return true;
+    }
+
+    private boolean canSeekForward() {
+        if (mPlaybackState != null) {
+            return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0;
+        }
+        return true;
+    }
+
+    /**
+     * Create the view that holds the widgets that control playback.
+     * Derived classes can override this to create their own.
+     *
+     * @return The controller view.
+     */
+    // TODO: This was "protected". Determine if it should be protected in MCV2.
+    private ViewGroup makeControllerView() {
+        ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
+        initControllerView(root);
+        return root;
+    }
+
+    // TODO(b/76444971) make sure this is compatible with ApiHelper's one in updatable.
+    private View inflateLayout(Context context, int resId) {
+        LayoutInflater inflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        return inflater.inflate(resId, null);
+    }
+
+    private void initControllerView(ViewGroup v) {
+        // Relating to Title Bar View
+        mTitleBar = v.findViewById(R.id.title_bar);
+        mTitleView = v.findViewById(R.id.title_text);
+        mAdExternalLink = v.findViewById(R.id.ad_external_link);
+        mBackButton = v.findViewById(R.id.back);
+        if (mBackButton != null) {
+            mBackButton.setOnClickListener(mBackListener);
+            mBackButton.setVisibility(View.GONE);
+        }
+        // TODO (b/77158231) revive
+        // mRouteButton = v.findViewById(R.id.cast);
+
+        // Relating to Center View
+        mCenterView = v.findViewById(R.id.center_view);
+        mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
+        mCenterView.addView(mTransportControls);
+
+        // Relating to Minimal Extra View
+        mMinimalExtraView = (LinearLayout) v.findViewById(R.id.minimal_extra_view);
+        LinearLayout.LayoutParams params =
+                (LinearLayout.LayoutParams) mMinimalExtraView.getLayoutParams();
+        int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_size);
+        int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
+        params.setMargins(0, (iconSize + marginSize * 2) * (-1), 0, 0);
+        mMinimalExtraView.setLayoutParams(params);
+        mMinimalExtraView.setVisibility(View.GONE);
+
+        // Relating to Progress Bar View
+        mProgress = v.findViewById(R.id.progress);
+        if (mProgress != null) {
+            if (mProgress instanceof SeekBar) {
+                SeekBar seeker = (SeekBar) mProgress;
+                seeker.setOnSeekBarChangeListener(mSeekListener);
+                seeker.setProgressDrawable(mResources.getDrawable(R.drawable.custom_progress));
+                seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
+            }
+            mProgress.setMax(MAX_PROGRESS);
+        }
+        mProgressBuffer = v.findViewById(R.id.progress_buffer);
+
+        // Relating to Bottom Bar View
+        mBottomBar = v.findViewById(R.id.bottom_bar);
+
+        // Relating to Bottom Bar Left View
+        mBottomBarLeftView = v.findViewById(R.id.bottom_bar_left);
+        mTimeView = v.findViewById(R.id.time);
+        mEndTime = v.findViewById(R.id.time_end);
+        mCurrentTime = v.findViewById(R.id.time_current);
+        mAdSkipView = v.findViewById(R.id.ad_skip_time);
+        mFormatBuilder = new StringBuilder();
+        mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+        // Relating to Bottom Bar Right View
+        mBottomBarRightView = v.findViewById(R.id.bottom_bar_right);
+        mBasicControls = v.findViewById(R.id.basic_controls);
+        mExtraControls = v.findViewById(R.id.extra_controls);
+        mCustomButtons = v.findViewById(R.id.custom_buttons);
+        mSubtitleButton = v.findViewById(R.id.subtitle);
+        if (mSubtitleButton != null) {
+            mSubtitleButton.setOnClickListener(mSubtitleListener);
+        }
+        mFullScreenButton = v.findViewById(R.id.fullscreen);
+        if (mFullScreenButton != null) {
+            mFullScreenButton.setOnClickListener(mFullScreenListener);
+            // TODO: Show Fullscreen button when only it is possible.
+        }
+        mOverflowButtonRight = v.findViewById(R.id.overflow_right);
+        if (mOverflowButtonRight != null) {
+            mOverflowButtonRight.setOnClickListener(mOverflowRightListener);
+        }
+        mOverflowButtonLeft = v.findViewById(R.id.overflow_left);
+        if (mOverflowButtonLeft != null) {
+            mOverflowButtonLeft.setOnClickListener(mOverflowLeftListener);
+        }
+        mMuteButton = v.findViewById(R.id.mute);
+        if (mMuteButton != null) {
+            mMuteButton.setOnClickListener(mMuteButtonListener);
+        }
+        mSettingsButton = v.findViewById(R.id.settings);
+        if (mSettingsButton != null) {
+            mSettingsButton.setOnClickListener(mSettingsButtonListener);
+        }
+        mVideoQualityButton = v.findViewById(R.id.video_quality);
+        if (mVideoQualityButton != null) {
+            mVideoQualityButton.setOnClickListener(mVideoQualityListener);
+        }
+        mAdRemainingView = v.findViewById(R.id.ad_remaining);
+
+        // Relating to Settings List View
+        initializeSettingsLists();
+        mSettingsListView = (ListView) inflateLayout(getContext(), R.layout.settings_list);
+        mSettingsAdapter = new SettingsAdapter(mSettingsMainTextsList, mSettingsSubTextsList,
+                mSettingsIconIdsList);
+        mSubSettingsAdapter = new SubSettingsAdapter(null, 0);
+        mSettingsListView.setAdapter(mSettingsAdapter);
+        mSettingsListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mSettingsListView.setOnItemClickListener(mSettingsItemClickListener);
+
+        mEmbeddedSettingsItemWidth = mResources.getDimensionPixelSize(
+                R.dimen.mcv2_embedded_settings_width);
+        mFullSettingsItemWidth = mResources.getDimensionPixelSize(R.dimen.mcv2_full_settings_width);
+        mEmbeddedSettingsItemHeight = mResources.getDimensionPixelSize(
+                R.dimen.mcv2_embedded_settings_height);
+        mFullSettingsItemHeight = mResources.getDimensionPixelSize(
+                R.dimen.mcv2_full_settings_height);
+        mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(
+                R.dimen.mcv2_settings_offset);
+        mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth,
+                ViewGroup.LayoutParams.WRAP_CONTENT, true);
+    }
+
+    /**
+     * Disable pause or seek buttons if the stream cannot be paused or seeked.
+     * This requires the control interface to be a MediaPlayerControlExt
+     */
+    private void disableUnsupportedButtons() {
+        try {
+            if (mPlayPauseButton != null && !canPause()) {
+                mPlayPauseButton.setEnabled(false);
+            }
+            if (mRewButton != null && !canSeekBackward()) {
+                mRewButton.setEnabled(false);
+            }
+            if (mFfwdButton != null && !canSeekForward()) {
+                mFfwdButton.setEnabled(false);
+            }
+            // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
+            // this scheme can break the case when applications want to allow seek through the
+            // progress bar but disable forward/backward buttons.
+            //
+            // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
+            // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
+            // shouldn't arise in existing applications.
+            if (mProgress != null && !canSeekBackward() && !canSeekForward()) {
+                mProgress.setEnabled(false);
+            }
+        } catch (IncompatibleClassChangeError ex) {
+            // We were given an old version of the interface, that doesn't have
+            // the canPause/canSeekXYZ methods. This is OK, it just means we
+            // assume the media can be paused and seeked, and so we don't disable
+            // the buttons.
+        }
+    }
+
+    private final Runnable mUpdateProgress = new Runnable() {
+        @Override
+        public void run() {
+            int pos = setProgress();
+            boolean isShowing = getVisibility() == View.VISIBLE;
+            if (!mDragging && isShowing && isPlaying()) {
+                postDelayed(mUpdateProgress,
+                        DEFAULT_PROGRESS_UPDATE_TIME_MS - (pos % DEFAULT_PROGRESS_UPDATE_TIME_MS));
+            }
+        }
+    };
+
+    private String stringForTime(int timeMs) {
+        int totalSeconds = timeMs / 1000;
+
+        int seconds = totalSeconds % 60;
+        int minutes = (totalSeconds / 60) % 60;
+        int hours = totalSeconds / 3600;
+
+        mFormatBuilder.setLength(0);
+        if (hours > 0) {
+            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+        } else {
+            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+        }
+    }
+
+    private int setProgress() {
+        if (mController == null || mDragging) {
+            return 0;
+        }
+        int positionOnProgressBar = 0;
+        int currentPosition = getCurrentPosition();
+        if (mDuration > 0) {
+            positionOnProgressBar = (int) (MAX_PROGRESS * (long) currentPosition / mDuration);
+        }
+        if (mProgress != null && currentPosition != mDuration) {
+            mProgress.setProgress(positionOnProgressBar);
+            // If the media is a local file, there is no need to set a buffer, so set secondary
+            // progress to maximum.
+            if (getBufferPercentage() < 0) {
+                mProgress.setSecondaryProgress(MAX_PROGRESS);
+            } else {
+                mProgress.setSecondaryProgress(getBufferPercentage() * 10);
+            }
+        }
+
+        if (mEndTime != null) {
+            mEndTime.setText(stringForTime(mDuration));
+
+        }
+        if (mCurrentTime != null) {
+            mCurrentTime.setText(stringForTime(currentPosition));
+        }
+
+        if (mIsAdvertisement) {
+            // Update the remaining number of seconds until the first 5 seconds of the
+            // advertisement.
+            if (mAdSkipView != null) {
+                if (currentPosition <= AD_SKIP_WAIT_TIME_MS) {
+                    if (mAdSkipView.getVisibility() == View.GONE) {
+                        mAdSkipView.setVisibility(View.VISIBLE);
+                    }
+                    String skipTimeText = mResources.getString(
+                            R.string.MediaControlView2_ad_skip_wait_time,
+                            ((AD_SKIP_WAIT_TIME_MS - currentPosition) / 1000 + 1));
+                    mAdSkipView.setText(skipTimeText);
+                } else {
+                    if (mAdSkipView.getVisibility() == View.VISIBLE) {
+                        mAdSkipView.setVisibility(View.GONE);
+                        mNextButton.setEnabled(true);
+                        mNextButton.clearColorFilter();
+                    }
+                }
+            }
+            // Update the remaining number of seconds of the advertisement.
+            if (mAdRemainingView != null) {
+                int remainingTime =
+                        (mDuration - currentPosition < 0) ? 0 : (mDuration - currentPosition);
+                String remainingTimeText = mResources.getString(
+                        R.string.MediaControlView2_ad_remaining_time,
+                        stringForTime(remainingTime));
+                mAdRemainingView.setText(remainingTimeText);
+            }
+        }
+        return currentPosition;
+    }
+
+    private void togglePausePlayState() {
+        if (isPlaying()) {
+            mControls.pause();
+            mPlayPauseButton.setImageDrawable(
+                    mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+            mPlayPauseButton.setContentDescription(
+                    mResources.getString(R.string.mcv2_play_button_desc));
+        } else {
+            mControls.play();
+            mPlayPauseButton.setImageDrawable(
+                    mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
+            mPlayPauseButton.setContentDescription(
+                    mResources.getString(R.string.mcv2_pause_button_desc));
+        }
+    }
+
+    // There are two scenarios that can trigger the seekbar listener to trigger:
+    //
+    // The first is the user using the touchpad to adjust the posititon of the
+    // seekbar's thumb. In this case onStartTrackingTouch is called followed by
+    // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
+    // We're setting the field "mDragging" to true for the duration of the dragging
+    // session to avoid jumps in the position in case of ongoing playback.
+    //
+    // The second scenario involves the user operating the scroll ball, in this
+    // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
+    // we will simply apply the updated position without suspending regular updates.
+    private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+        @Override
+        public void onStartTrackingTouch(SeekBar bar) {
+            if (!mSeekAvailable) {
+                return;
+            }
+
+            mDragging = true;
+
+            // By removing these pending progress messages we make sure
+            // that a) we won't update the progress while the user adjusts
+            // the seekbar and b) once the user is done dragging the thumb
+            // we will post one of these messages to the queue again and
+            // this ensures that there will be exactly one message queued up.
+            removeCallbacks(mUpdateProgress);
+
+            // Check if playback is currently stopped. In this case, update the pause button to
+            // show the play image instead of the replay image.
+            if (mIsStopped) {
+                mPlayPauseButton.setImageDrawable(
+                        mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+                mPlayPauseButton.setContentDescription(
+                        mResources.getString(R.string.mcv2_play_button_desc));
+                mIsStopped = false;
+            }
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+            if (!mSeekAvailable) {
+                return;
+            }
+            if (!fromUser) {
+                // We're not interested in programmatically generated changes to
+                // the progress bar's position.
+                return;
+            }
+            if (mDuration > 0) {
+                int position = (int) (((long) mDuration * progress) / MAX_PROGRESS);
+                mControls.seekTo(position);
+
+                if (mCurrentTime != null) {
+                    mCurrentTime.setText(stringForTime(position));
+                }
+            }
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar bar) {
+            if (!mSeekAvailable) {
+                return;
+            }
+            mDragging = false;
+
+            setProgress();
+
+            // Ensure that progress is properly updated in the future,
+            // the call to show() does not guarantee this because it is a
+            // no-op if we are already showing.
+            post(mUpdateProgress);
+        }
+    };
+
+    private final View.OnClickListener mPlayPauseListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            togglePausePlayState();
+        }
+    };
+
+    private final View.OnClickListener mRewListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            int pos = getCurrentPosition() - REWIND_TIME_MS;
+            mControls.seekTo(pos);
+            setProgress();
+        }
+    };
+
+    private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            int pos = getCurrentPosition() + FORWARD_TIME_MS;
+            mControls.seekTo(pos);
+            setProgress();
+        }
+    };
+
+    private final View.OnClickListener mNextListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mControls.skipToNext();
+        }
+    };
+
+    private final View.OnClickListener mPrevListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mControls.skipToPrevious();
+        }
+    };
+
+    private final View.OnClickListener mBackListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            // TODO: implement
+        }
+    };
+
+    private final View.OnClickListener mSubtitleListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mSettingsMode = SETTINGS_MODE_SUBTITLE_TRACK;
+            mSubSettingsAdapter.setTexts(mSubtitleDescriptionsList);
+            mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
+            displaySettingsWindow(mSubSettingsAdapter);
+        }
+    };
+
+    private final View.OnClickListener mVideoQualityListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mSettingsMode = SETTINGS_MODE_VIDEO_QUALITY;
+            mSubSettingsAdapter.setTexts(mVideoQualityList);
+            mSubSettingsAdapter.setCheckPosition(mSelectedVideoQualityIndex);
+            displaySettingsWindow(mSubSettingsAdapter);
+        }
+    };
+
+    private final View.OnClickListener mFullScreenListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final boolean isEnteringFullScreen = !mIsFullScreen;
+            // TODO: Re-arrange the button layouts according to the UX.
+            if (isEnteringFullScreen) {
+                mFullScreenButton.setImageDrawable(
+                        mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
+            } else {
+                mFullScreenButton.setImageDrawable(
+                        mResources.getDrawable(R.drawable.ic_fullscreen, null));
+            }
+            Bundle args = new Bundle();
+            args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
+            mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);
+
+            mIsFullScreen = isEnteringFullScreen;
+        }
+    };
+
+    private final View.OnClickListener mOverflowRightListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mBasicControls.setVisibility(View.GONE);
+            mExtraControls.setVisibility(View.VISIBLE);
+        }
+    };
+
+    private final View.OnClickListener mOverflowLeftListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mBasicControls.setVisibility(View.VISIBLE);
+            mExtraControls.setVisibility(View.GONE);
+        }
+    };
+
+    private final View.OnClickListener mMuteButtonListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (!mIsMute) {
+                mMuteButton.setImageDrawable(
+                        mResources.getDrawable(R.drawable.ic_mute, null));
+                mMuteButton.setContentDescription(
+                        mResources.getString(R.string.mcv2_muted_button_desc));
+                mIsMute = true;
+                mController.sendCommand(COMMAND_MUTE, null, null);
+            } else {
+                mMuteButton.setImageDrawable(
+                        mResources.getDrawable(R.drawable.ic_unmute, null));
+                mMuteButton.setContentDescription(
+                        mResources.getString(R.string.mcv2_unmuted_button_desc));
+                mIsMute = false;
+                mController.sendCommand(COMMAND_UNMUTE, null, null);
+            }
+        }
+    };
+
+    private final View.OnClickListener mSettingsButtonListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mSettingsMode = SETTINGS_MODE_MAIN;
+            mSettingsAdapter.setSubTexts(mSettingsSubTextsList);
+            displaySettingsWindow(mSettingsAdapter);
+        }
+    };
+
+    private final AdapterView.OnItemClickListener mSettingsItemClickListener =
+            new AdapterView.OnItemClickListener() {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            switch (mSettingsMode) {
+                case SETTINGS_MODE_MAIN:
+                    if (position == SETTINGS_MODE_AUDIO_TRACK) {
+                        mSubSettingsAdapter.setTexts(mAudioTrackList);
+                        mSubSettingsAdapter.setCheckPosition(mSelectedAudioTrackIndex);
+                        mSettingsMode = SETTINGS_MODE_AUDIO_TRACK;
+                    } else if (position == SETTINGS_MODE_PLAYBACK_SPEED) {
+                        mSubSettingsAdapter.setTexts(mPlaybackSpeedTextList);
+                        mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
+                        mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED;
+                    } else if (position == SETTINGS_MODE_HELP) {
+                        // TODO: implement this.
+                        mSettingsWindow.dismiss();
+                        return;
+                    }
+                    displaySettingsWindow(mSubSettingsAdapter);
+                    break;
+                case SETTINGS_MODE_AUDIO_TRACK:
+                    if (position != mSelectedAudioTrackIndex) {
+                        mSelectedAudioTrackIndex = position;
+                        if (mAudioTrackCount > 0) {
+                            Bundle extra = new Bundle();
+                            extra.putInt(KEY_SELECTED_AUDIO_INDEX, position);
+                            mController.sendCommand(COMMAND_SELECT_AUDIO_TRACK, extra, null);
+                        }
+                        mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK,
+                                mSubSettingsAdapter.getMainText(position));
+                    }
+                    mSettingsWindow.dismiss();
+                    break;
+                case SETTINGS_MODE_PLAYBACK_SPEED:
+                    if (position != mSelectedSpeedIndex) {
+                        mSelectedSpeedIndex = position;
+                        Bundle extra = new Bundle();
+                        extra.putFloat(KEY_PLAYBACK_SPEED, mPlaybackSpeedList.get(position));
+                        mController.sendCommand(COMMAND_SET_PLAYBACK_SPEED, extra, null);
+                        mSettingsSubTextsList.set(SETTINGS_MODE_PLAYBACK_SPEED,
+                                mSubSettingsAdapter.getMainText(position));
+                    }
+                    mSettingsWindow.dismiss();
+                    break;
+                case SETTINGS_MODE_HELP:
+                    // TODO: implement this.
+                    break;
+                case SETTINGS_MODE_SUBTITLE_TRACK:
+                    if (position != mSelectedSubtitleTrackIndex) {
+                        mSelectedSubtitleTrackIndex = position;
+                        if (position > 0) {
+                            Bundle extra = new Bundle();
+                            extra.putInt(KEY_SELECTED_SUBTITLE_INDEX, position - 1);
+                            mController.sendCommand(COMMAND_SHOW_SUBTITLE, extra, null);
+                            mSubtitleButton.setImageDrawable(
+                                    mResources.getDrawable(R.drawable.ic_subtitle_on, null));
+                            mSubtitleButton.setContentDescription(
+                                    mResources.getString(R.string.mcv2_cc_is_on));
+                            mSubtitleIsEnabled = true;
+                        } else {
+                            mController.sendCommand(COMMAND_HIDE_SUBTITLE, null, null);
+                            mSubtitleButton.setImageDrawable(
+                                    mResources.getDrawable(R.drawable.ic_subtitle_off, null));
+                            mSubtitleButton.setContentDescription(
+                                    mResources.getString(R.string.mcv2_cc_is_off));
+                            mSubtitleIsEnabled = false;
+                        }
+                    }
+                    mSettingsWindow.dismiss();
+                    break;
+                case SETTINGS_MODE_VIDEO_QUALITY:
+                    // TODO: add support for video quality
+                    mSelectedVideoQualityIndex = position;
+                    mSettingsWindow.dismiss();
+                    break;
+            }
+        }
+    };
+
+    private void updateDuration() {
+        if (mMetadata != null) {
+            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) {
+                mDuration = (int) mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
+                // update progress bar
+                setProgress();
+            }
+        }
+    }
+
+    private void updateTitle() {
+        if (mMetadata != null) {
+            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) {
+                mTitleView.setText(mMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
+            }
+        }
+    }
+
+    // The title bar is made up of two separate LinearLayouts. If the sum of the two bars are
+    // greater than the length of the title bar, reduce the size of the left bar (which makes the
+    // TextView that contains the title of the media file shrink).
+    private void updateTitleBarLayout() {
+        if (mTitleBar != null) {
+            int titleBarWidth = mTitleBar.getWidth();
+
+            View leftBar = mTitleBar.findViewById(R.id.title_bar_left);
+            View rightBar = mTitleBar.findViewById(R.id.title_bar_right);
+            int leftBarWidth = leftBar.getWidth();
+            int rightBarWidth = rightBar.getWidth();
+
+            RelativeLayout.LayoutParams params =
+                    (RelativeLayout.LayoutParams) leftBar.getLayoutParams();
+            if (leftBarWidth + rightBarWidth > titleBarWidth) {
+                params.width = titleBarWidth - rightBarWidth;
+                mOriginalLeftBarWidth = leftBarWidth;
+            } else if (leftBarWidth + rightBarWidth < titleBarWidth && mOriginalLeftBarWidth != 0) {
+                params.width = mOriginalLeftBarWidth;
+                mOriginalLeftBarWidth = 0;
+            }
+            leftBar.setLayoutParams(params);
+        }
+    }
+
+    private void updateAudioMetadata() {
+        if (mMediaType != MEDIA_TYPE_MUSIC) {
+            return;
+        }
+
+        if (mMetadata != null) {
+            String titleText = "";
+            String artistText = "";
+            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) {
+                titleText = mMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
+            } else {
+                titleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+            }
+
+            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_ARTIST)) {
+                artistText = mMetadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
+            } else {
+                artistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+            }
+
+            // Update title for Embedded size type
+            mTitleView.setText(titleText + " - " + artistText);
+
+            // Set to true to update layout inside onMeasure()
+            mNeedUXUpdate = true;
+        }
+    }
+
+    private void updateLayout() {
+        if (mIsAdvertisement) {
+            mRewButton.setVisibility(View.GONE);
+            mFfwdButton.setVisibility(View.GONE);
+            mPrevButton.setVisibility(View.GONE);
+            mTimeView.setVisibility(View.GONE);
+
+            mAdSkipView.setVisibility(View.VISIBLE);
+            mAdRemainingView.setVisibility(View.VISIBLE);
+            mAdExternalLink.setVisibility(View.VISIBLE);
+
+            mProgress.setEnabled(false);
+            mNextButton.setEnabled(false);
+            mNextButton.setColorFilter(R.color.gray);
+        } else {
+            mRewButton.setVisibility(View.VISIBLE);
+            mFfwdButton.setVisibility(View.VISIBLE);
+            mPrevButton.setVisibility(View.VISIBLE);
+            mTimeView.setVisibility(View.VISIBLE);
+
+            mAdSkipView.setVisibility(View.GONE);
+            mAdRemainingView.setVisibility(View.GONE);
+            mAdExternalLink.setVisibility(View.GONE);
+
+            mProgress.setEnabled(true);
+            mNextButton.setEnabled(true);
+            mNextButton.clearColorFilter();
+            disableUnsupportedButtons();
+        }
+    }
+
+    private void updateLayout(int maxIconCount, int fullIconSize, int embeddedIconSize,
+             int marginSize, int currWidth, int currHeight, int screenWidth, int screenHeight) {
+        int fullBottomBarRightWidthMax = fullIconSize * maxIconCount
+                + marginSize * (maxIconCount * 2);
+        int embeddedBottomBarRightWidthMax = embeddedIconSize * maxIconCount
+                + marginSize * (maxIconCount * 2);
+        int fullWidth = mTransportControls.getWidth() + mTimeView.getWidth()
+                + fullBottomBarRightWidthMax;
+        int embeddedWidth = mTimeView.getWidth() + embeddedBottomBarRightWidthMax;
+        int screenMaxLength = Math.max(screenWidth, screenHeight);
+
+        if (fullWidth > screenMaxLength) {
+            // TODO: screen may be smaller than the length needed for Full size.
+        }
+
+        boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? (currWidth == screenMaxLength) :
+                (currWidth == screenWidth && currHeight == screenHeight);
+
+        if (isFullSize) {
+            if (mSizeType != SIZE_TYPE_FULL) {
+                updateLayoutForSizeChange(SIZE_TYPE_FULL);
+                if (mMediaType == MEDIA_TYPE_MUSIC) {
+                    mTitleView.setVisibility(View.GONE);
+                }
+            }
+        } else if (embeddedWidth <= currWidth) {
+            if (mSizeType != SIZE_TYPE_EMBEDDED) {
+                updateLayoutForSizeChange(SIZE_TYPE_EMBEDDED);
+                if (mMediaType == MEDIA_TYPE_MUSIC) {
+                    mTitleView.setVisibility(View.VISIBLE);
+                }
+            }
+        } else {
+            if (mSizeType != SIZE_TYPE_MINIMAL) {
+                updateLayoutForSizeChange(SIZE_TYPE_MINIMAL);
+                if (mMediaType == MEDIA_TYPE_MUSIC) {
+                    mTitleView.setVisibility(View.GONE);
+                }
+            }
+        }
+    }
+
+    private void updateLayoutForSizeChange(int sizeType) {
+        mSizeType = sizeType;
+        RelativeLayout.LayoutParams timeViewParams =
+                (RelativeLayout.LayoutParams) mTimeView.getLayoutParams();
+        SeekBar seeker = (SeekBar) mProgress;
+        switch (mSizeType) {
+            case SIZE_TYPE_EMBEDDED:
+                // Relating to Title Bar
+                mTitleBar.setVisibility(View.VISIBLE);
+                mBackButton.setVisibility(View.GONE);
+
+                // Relating to Full Screen Button
+                mMinimalExtraView.setVisibility(View.GONE);
+                mFullScreenButton = mBottomBarRightView.findViewById(R.id.fullscreen);
+                mFullScreenButton.setOnClickListener(mFullScreenListener);
+
+                // Relating to Center View
+                mCenterView.removeAllViews();
+                mBottomBarLeftView.removeView(mTransportControls);
+                mBottomBarLeftView.setVisibility(View.GONE);
+                mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
+                mCenterView.addView(mTransportControls);
+
+                // Relating to Progress Bar
+                seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
+                mProgressBuffer.setVisibility(View.VISIBLE);
+
+                // Relating to Bottom Bar
+                mBottomBar.setVisibility(View.VISIBLE);
+                if (timeViewParams.getRules()[RelativeLayout.LEFT_OF] != 0) {
+                    timeViewParams.removeRule(RelativeLayout.LEFT_OF);
+                    timeViewParams.addRule(RelativeLayout.RIGHT_OF, R.id.bottom_bar_left);
+                }
+                break;
+            case SIZE_TYPE_FULL:
+                // Relating to Title Bar
+                mTitleBar.setVisibility(View.VISIBLE);
+                mBackButton.setVisibility(View.VISIBLE);
+
+                // Relating to Full Screen Button
+                mMinimalExtraView.setVisibility(View.GONE);
+                mFullScreenButton = mBottomBarRightView.findViewById(R.id.fullscreen);
+                mFullScreenButton.setOnClickListener(mFullScreenListener);
+
+                // Relating to Center View
+                mCenterView.removeAllViews();
+                mBottomBarLeftView.removeView(mTransportControls);
+                mTransportControls = inflateTransportControls(R.layout.full_transport_controls);
+                mBottomBarLeftView.addView(mTransportControls, 0);
+                mBottomBarLeftView.setVisibility(View.VISIBLE);
+
+                // Relating to Progress Bar
+                seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
+                mProgressBuffer.setVisibility(View.VISIBLE);
+
+                // Relating to Bottom Bar
+                mBottomBar.setVisibility(View.VISIBLE);
+                if (timeViewParams.getRules()[RelativeLayout.RIGHT_OF] != 0) {
+                    timeViewParams.removeRule(RelativeLayout.RIGHT_OF);
+                    timeViewParams.addRule(RelativeLayout.LEFT_OF, R.id.bottom_bar_right);
+                }
+                break;
+            case SIZE_TYPE_MINIMAL:
+                // Relating to Title Bar
+                mTitleBar.setVisibility(View.GONE);
+                mBackButton.setVisibility(View.GONE);
+
+                // Relating to Full Screen Button
+                mMinimalExtraView.setVisibility(View.VISIBLE);
+                mFullScreenButton = mMinimalExtraView.findViewById(R.id.minimal_fullscreen);
+                mFullScreenButton.setOnClickListener(mFullScreenListener);
+
+                // Relating to Center View
+                mCenterView.removeAllViews();
+                mBottomBarLeftView.removeView(mTransportControls);
+                mTransportControls = inflateTransportControls(R.layout.minimal_transport_controls);
+                mCenterView.addView(mTransportControls);
+
+                // Relating to Progress Bar
+                seeker.setThumb(null);
+                mProgressBuffer.setVisibility(View.GONE);
+
+                // Relating to Bottom Bar
+                mBottomBar.setVisibility(View.GONE);
+                break;
+        }
+        mTimeView.setLayoutParams(timeViewParams);
+
+        if (isPlaying()) {
+            mPlayPauseButton.setImageDrawable(
+                    mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
+            mPlayPauseButton.setContentDescription(
+                    mResources.getString(R.string.mcv2_pause_button_desc));
+        } else {
+            mPlayPauseButton.setImageDrawable(
+                    mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+            mPlayPauseButton.setContentDescription(
+                    mResources.getString(R.string.mcv2_play_button_desc));
+        }
+
+        if (mIsFullScreen) {
+            mFullScreenButton.setImageDrawable(
+                    mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
+        } else {
+            mFullScreenButton.setImageDrawable(
+                    mResources.getDrawable(R.drawable.ic_fullscreen, null));
+        }
+    }
+
+    private View inflateTransportControls(int layoutId) {
+        View v = inflateLayout(getContext(), layoutId);
+        mPlayPauseButton = v.findViewById(R.id.pause);
+        if (mPlayPauseButton != null) {
+            mPlayPauseButton.requestFocus();
+            mPlayPauseButton.setOnClickListener(mPlayPauseListener);
+        }
+        mFfwdButton = v.findViewById(R.id.ffwd);
+        if (mFfwdButton != null) {
+            mFfwdButton.setOnClickListener(mFfwdListener);
+            if (mMediaType == MEDIA_TYPE_MUSIC) {
+                mFfwdButton.setVisibility(View.GONE);
+            }
+        }
+        mRewButton = v.findViewById(R.id.rew);
+        if (mRewButton != null) {
+            mRewButton.setOnClickListener(mRewListener);
+            if (mMediaType == MEDIA_TYPE_MUSIC) {
+                mRewButton.setVisibility(View.GONE);
+            }
+        }
+        // TODO: Add support for Next and Previous buttons
+        mNextButton = v.findViewById(R.id.next);
+        if (mNextButton != null) {
+            mNextButton.setOnClickListener(mNextListener);
+            mNextButton.setVisibility(View.GONE);
+        }
+        mPrevButton = v.findViewById(R.id.prev);
+        if (mPrevButton != null) {
+            mPrevButton.setOnClickListener(mPrevListener);
+            mPrevButton.setVisibility(View.GONE);
+        }
+        return v;
+    }
+
+    private void initializeSettingsLists() {
+        mSettingsMainTextsList = new ArrayList<String>();
+        mSettingsMainTextsList.add(
+                mResources.getString(R.string.MediaControlView2_audio_track_text));
+        mSettingsMainTextsList.add(
+                mResources.getString(R.string.MediaControlView2_playback_speed_text));
+        mSettingsMainTextsList.add(
+                mResources.getString(R.string.MediaControlView2_help_text));
+
+        mSettingsSubTextsList = new ArrayList<String>();
+        mSettingsSubTextsList.add(
+                mResources.getString(R.string.MediaControlView2_audio_track_none_text));
+        mSettingsSubTextsList.add(
+                mResources.getStringArray(
+                        R.array.MediaControlView2_playback_speeds)[PLAYBACK_SPEED_1x_INDEX]);
+        mSettingsSubTextsList.add(RESOURCE_EMPTY);
+
+        mSettingsIconIdsList = new ArrayList<Integer>();
+        mSettingsIconIdsList.add(R.drawable.ic_audiotrack);
+        mSettingsIconIdsList.add(R.drawable.ic_play_circle_filled);
+        mSettingsIconIdsList.add(R.drawable.ic_help);
+
+        mAudioTrackList = new ArrayList<String>();
+        mAudioTrackList.add(
+                mResources.getString(R.string.MediaControlView2_audio_track_none_text));
+
+        mVideoQualityList = new ArrayList<String>();
+        mVideoQualityList.add(
+                mResources.getString(R.string.MediaControlView2_video_quality_auto_text));
+
+        mPlaybackSpeedTextList = new ArrayList<String>(Arrays.asList(
+                mResources.getStringArray(R.array.MediaControlView2_playback_speeds)));
+        // Select the "1x" speed as the default value.
+        mSelectedSpeedIndex = PLAYBACK_SPEED_1x_INDEX;
+
+        mPlaybackSpeedList = new ArrayList<Float>();
+        int[] speeds = mResources.getIntArray(R.array.speed_multiplied_by_100);
+        for (int i = 0; i < speeds.length; i++) {
+            float speed = (float) speeds[i] / 100.0f;
+            mPlaybackSpeedList.add(speed);
+        }
+    }
+
+    private void displaySettingsWindow(BaseAdapter adapter) {
+        // Set Adapter
+        mSettingsListView.setAdapter(adapter);
+
+        // Set width of window
+        int itemWidth = (mSizeType == SIZE_TYPE_EMBEDDED)
+                ? mEmbeddedSettingsItemWidth : mFullSettingsItemWidth;
+        mSettingsWindow.setWidth(itemWidth);
+
+        // Calculate height of window and show
+        int itemHeight = (mSizeType == SIZE_TYPE_EMBEDDED)
+                ? mEmbeddedSettingsItemHeight : mFullSettingsItemHeight;
+        int totalHeight = adapter.getCount() * itemHeight;
+        mSettingsWindow.dismiss();
+        mSettingsWindow.showAsDropDown(this, mSettingsWindowMargin,
+                mSettingsWindowMargin - totalHeight, Gravity.BOTTOM | Gravity.RIGHT);
+    }
+
+    @RequiresApi(26) // TODO correct minSdk API use incompatibilities and remove before release.
+    private class MediaControllerCallback extends MediaControllerCompat.Callback {
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            mPlaybackState = state;
+
+            // Update pause button depending on playback state for the following two reasons:
+            //   1) Need to handle case where app customizes playback state behavior when app
+            //      activity is resumed.
+            //   2) Need to handle case where the media file reaches end of duration.
+            if (mPlaybackState.getState() != mPrevState) {
+                switch (mPlaybackState.getState()) {
+                    case PlaybackStateCompat.STATE_PLAYING:
+                        mPlayPauseButton.setImageDrawable(
+                                mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
+                        mPlayPauseButton.setContentDescription(
+                                mResources.getString(R.string.mcv2_pause_button_desc));
+                        removeCallbacks(mUpdateProgress);
+                        post(mUpdateProgress);
+                        break;
+                    case PlaybackStateCompat.STATE_PAUSED:
+                        mPlayPauseButton.setImageDrawable(
+                                mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+                        mPlayPauseButton.setContentDescription(
+                                mResources.getString(R.string.mcv2_play_button_desc));
+                        break;
+                    case PlaybackStateCompat.STATE_STOPPED:
+                        mPlayPauseButton.setImageDrawable(
+                                mResources.getDrawable(R.drawable.ic_replay_circle_filled, null));
+                        mPlayPauseButton.setContentDescription(
+                                mResources.getString(R.string.mcv2_replay_button_desc));
+                        mIsStopped = true;
+                        break;
+                    default:
+                        break;
+                }
+                mPrevState = mPlaybackState.getState();
+            }
+
+            if (mPlaybackActions != mPlaybackState.getActions()) {
+                long newActions = mPlaybackState.getActions();
+                if ((newActions & PlaybackStateCompat.ACTION_PAUSE) != 0) {
+                    mPlayPauseButton.setVisibility(View.VISIBLE);
+                }
+                if ((newActions & PlaybackStateCompat.ACTION_REWIND) != 0
+                        && mMediaType != MEDIA_TYPE_MUSIC) {
+                    if (mRewButton != null) {
+                        mRewButton.setVisibility(View.VISIBLE);
+                    }
+                }
+                if ((newActions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0
+                        && mMediaType != MEDIA_TYPE_MUSIC) {
+                    if (mFfwdButton != null) {
+                        mFfwdButton.setVisibility(View.VISIBLE);
+                    }
+                }
+                if ((newActions & PlaybackStateCompat.ACTION_SEEK_TO) != 0) {
+                    mSeekAvailable = true;
+                } else {
+                    mSeekAvailable = false;
+                }
+                mPlaybackActions = newActions;
+            }
+
+            // Add buttons if custom actions are present.
+            List<PlaybackStateCompat.CustomAction> customActions =
+                    mPlaybackState.getCustomActions();
+            mCustomButtons.removeAllViews();
+            if (customActions.size() > 0) {
+                for (final PlaybackStateCompat.CustomAction action : customActions) {
+                    ImageButton button = new ImageButton(getContext(),
+                            null /* AttributeSet */, 0 /* Style */);
+                    // TODO: Apply R.style.BottomBarButton to this button using library context.
+                    // Refer Constructor with argument (int defStyleRes) of View.java
+                    button.setImageResource(action.getIcon());
+                    button.setTooltipText(action.getName());
+                    final String actionString = action.getAction().toString();
+                    button.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            // TODO: Currently, we are just sending extras that came from session.
+                            // Is it the right behavior?
+                            mControls.sendCustomAction(actionString, action.getExtras());
+                            setVisibility(View.VISIBLE);
+                        }
+                    });
+                    mCustomButtons.addView(button);
+                }
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadataCompat metadata) {
+            mMetadata = metadata;
+            updateDuration();
+            updateTitle();
+            updateAudioMetadata();
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            switch (event) {
+                case EVENT_UPDATE_TRACK_STATUS:
+                    mVideoTrackCount = extras.getInt(KEY_VIDEO_TRACK_COUNT);
+                    // If there is one or more audio tracks, and this information has not been
+                    // reflected into the Settings window yet, automatically check the first track.
+                    // Otherwise, the Audio Track selection will be defaulted to "None".
+                    mAudioTrackCount = extras.getInt(KEY_AUDIO_TRACK_COUNT);
+                    mAudioTrackList = new ArrayList<String>();
+                    if (mAudioTrackCount > 0) {
+                        // TODO: add more text about track info.
+                        for (int i = 0; i < mAudioTrackCount; i++) {
+                            String track = mResources.getString(
+                                    R.string.MediaControlView2_audio_track_number_text, i + 1);
+                            mAudioTrackList.add(track);
+                        }
+                        // Change sub text inside the Settings window.
+                        mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK,
+                                mAudioTrackList.get(0));
+                    } else {
+                        mAudioTrackList.add(mResources.getString(
+                                R.string.MediaControlView2_audio_track_none_text));
+                    }
+                    if (mVideoTrackCount == 0 && mAudioTrackCount > 0) {
+                        mMediaType = MEDIA_TYPE_MUSIC;
+                    }
+
+                    mSubtitleTrackCount = extras.getInt(KEY_SUBTITLE_TRACK_COUNT);
+                    mSubtitleDescriptionsList = new ArrayList<String>();
+                    if (mSubtitleTrackCount > 0) {
+                        mSubtitleButton.setVisibility(View.VISIBLE);
+                        mSubtitleButton.setEnabled(true);
+                        mSubtitleDescriptionsList.add(mResources.getString(
+                                R.string.MediaControlView2_subtitle_off_text));
+                        for (int i = 0; i < mSubtitleTrackCount; i++) {
+                            String track = mResources.getString(
+                                    R.string.MediaControlView2_subtitle_track_number_text, i + 1);
+                            mSubtitleDescriptionsList.add(track);
+                        }
+                    } else {
+                        mSubtitleButton.setVisibility(View.GONE);
+                        mSubtitleButton.setEnabled(false);
+                    }
+                    break;
+                case EVENT_UPDATE_MEDIA_TYPE_STATUS:
+                    boolean newStatus = extras.getBoolean(KEY_STATE_IS_ADVERTISEMENT);
+                    if (newStatus != mIsAdvertisement) {
+                        mIsAdvertisement = newStatus;
+                        updateLayout();
+                    }
+                    break;
+            }
+        }
+    }
+
+    private class SettingsAdapter extends BaseAdapter {
+        private List<Integer> mIconIds;
+        private List<String> mMainTexts;
+        private List<String> mSubTexts;
+
+        SettingsAdapter(List<String> mainTexts, @Nullable List<String> subTexts,
+                @Nullable List<Integer> iconIds) {
+            mMainTexts = mainTexts;
+            mSubTexts = subTexts;
+            mIconIds = iconIds;
+        }
+
+        public void updateSubTexts(List<String> subTexts) {
+            mSubTexts = subTexts;
+            notifyDataSetChanged();
+        }
+
+        public String getMainText(int position) {
+            if (mMainTexts != null) {
+                if (position < mMainTexts.size()) {
+                    return mMainTexts.get(position);
+                }
+            }
+            return RESOURCE_EMPTY;
+        }
+
+        @Override
+        public int getCount() {
+            return (mMainTexts == null) ? 0 : mMainTexts.size();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            // Auto-generated method stub--does not have any purpose here
+            // TODO: implement this.
+            return 0;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            // Auto-generated method stub--does not have any purpose here
+            // TODO: implement this.
+            return null;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup container) {
+            View row;
+            if (mSizeType == SIZE_TYPE_FULL) {
+                row = inflateLayout(getContext(), R.layout.full_settings_list_item);
+            } else {
+                row = inflateLayout(getContext(), R.layout.embedded_settings_list_item);
+            }
+            TextView mainTextView = (TextView) row.findViewById(R.id.main_text);
+            TextView subTextView = (TextView) row.findViewById(R.id.sub_text);
+            ImageView iconView = (ImageView) row.findViewById(R.id.icon);
+
+            // Set main text
+            mainTextView.setText(mMainTexts.get(position));
+
+            // Remove sub text and center the main text if sub texts do not exist at all or the sub
+            // text at this particular position is empty.
+            if (mSubTexts == null || RESOURCE_EMPTY.equals(mSubTexts.get(position))) {
+                subTextView.setVisibility(View.GONE);
+            } else {
+                // Otherwise, set sub text.
+                subTextView.setText(mSubTexts.get(position));
+            }
+
+            // Remove main icon and set visibility to gone if icons are set to null or the icon at
+            // this particular position is set to RESOURCE_NON_EXISTENT.
+            if (mIconIds == null || mIconIds.get(position) == RESOURCE_NON_EXISTENT) {
+                iconView.setVisibility(View.GONE);
+            } else {
+                // Otherwise, set main icon.
+                iconView.setImageDrawable(mResources.getDrawable(mIconIds.get(position), null));
+            }
+            return row;
+        }
+
+        public void setSubTexts(List<String> subTexts) {
+            mSubTexts = subTexts;
+        }
+    }
+
+    // TODO: extend this class from SettingsAdapter
+    private class SubSettingsAdapter extends BaseAdapter {
+        private List<String> mTexts;
+        private int mCheckPosition;
+
+        SubSettingsAdapter(List<String> texts, int checkPosition) {
+            mTexts = texts;
+            mCheckPosition = checkPosition;
+        }
+
+        public String getMainText(int position) {
+            if (mTexts != null) {
+                if (position < mTexts.size()) {
+                    return mTexts.get(position);
+                }
+            }
+            return RESOURCE_EMPTY;
+        }
+
+        @Override
+        public int getCount() {
+            return (mTexts == null) ? 0 : mTexts.size();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            // Auto-generated method stub--does not have any purpose here
+            // TODO: implement this.
+            return 0;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            // Auto-generated method stub--does not have any purpose here
+            // TODO: implement this.
+            return null;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup container) {
+            View row;
+            if (mSizeType == SIZE_TYPE_FULL) {
+                row = inflateLayout(getContext(), R.layout.full_sub_settings_list_item);
+            } else {
+                row = inflateLayout(getContext(), R.layout.embedded_sub_settings_list_item);
+            }
+            TextView textView = (TextView) row.findViewById(R.id.text);
+            ImageView checkView = (ImageView) row.findViewById(R.id.check);
+
+            textView.setText(mTexts.get(position));
+            if (position != mCheckPosition) {
+                checkView.setVisibility(View.INVISIBLE);
+            }
+            return row;
+        }
+
+        public void setTexts(List<String> texts) {
+            mTexts = texts;
+        }
+
+        public void setCheckPosition(int checkPosition) {
+            mCheckPosition = checkPosition;
+        }
+    }
+}
diff --git a/androidx/widget/MediaUtils2.java b/androidx/widget/MediaUtils2.java
new file mode 100644
index 0000000..29eb9f6
--- /dev/null
+++ b/androidx/widget/MediaUtils2.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.util.Log;
+
+import java.io.IOException;
+
+// Borrowed from com.android.compatibility.common.util.MediaUtils
+class MediaUtils2 {
+    private static final String TAG = "MediaUtils2";
+    private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+
+    /**
+     * Returns true iff all audio and video tracks are supported
+     */
+    static boolean hasCodecsForResource(Context context, int resourceId) {
+        try {
+            AssetFileDescriptor afd = null;
+            MediaExtractor ex = null;
+            try {
+                afd = context.getResources().openRawResourceFd(resourceId);
+                ex = new MediaExtractor();
+                ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
+                return hasCodecsForMedia(ex);
+            } finally {
+                if (ex != null) {
+                    ex.release();
+                }
+                if (afd != null) {
+                    afd.close();
+                }
+            }
+        } catch (IOException e) {
+            Log.i(TAG, "could not open resource");
+        }
+        return false;
+    }
+
+    /**
+     * Returns true iff all audio and video tracks are supported
+     */
+    static boolean hasCodecsForMedia(MediaExtractor ex) {
+        for (int i = 0; i < ex.getTrackCount(); ++i) {
+            MediaFormat format = ex.getTrackFormat(i);
+            // only check for audio and video codecs
+            String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase();
+            if (!mime.startsWith("audio/") && !mime.startsWith("video/")) {
+                continue;
+            }
+            if (!canDecode(format)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static boolean canDecode(MediaFormat format) {
+        if (sMCL.findDecoderForFormat(format) == null) {
+            Log.i(TAG, "no decoder for " + format);
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/androidx/widget/VideoSurfaceView.java b/androidx/widget/VideoSurfaceView.java
new file mode 100644
index 0000000..eafa6f3
--- /dev/null
+++ b/androidx/widget/VideoSurfaceView.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import static androidx.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.media.MediaPlayer;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
+    private static final String TAG = "VideoSurfaceView";
+    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+    private SurfaceHolder mSurfaceHolder = null;
+    private SurfaceListener mSurfaceListener = null;
+    private MediaPlayer mMediaPlayer;
+    // A flag to indicate taking over other view should be proceed.
+    private boolean mIsTakingOverOldView;
+    private VideoViewInterface mOldView;
+
+
+    VideoSurfaceView(Context context) {
+        this(context, null);
+    }
+
+    VideoSurfaceView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        getHolder().addCallback(this);
+    }
+
+    @RequiresApi(21)
+    VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
+                            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        getHolder().addCallback(this);
+    }
+
+    ////////////////////////////////////////////////////
+    // implements VideoViewInterface
+    ////////////////////////////////////////////////////
+
+    @Override
+    public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+        Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceHolder: " + mSurfaceHolder);
+        if (mp == null || !hasAvailableSurface()) {
+            return false;
+        }
+        mp.setDisplay(mSurfaceHolder);
+        return true;
+    }
+
+    @Override
+    public void setSurfaceListener(SurfaceListener l) {
+        mSurfaceListener = l;
+    }
+
+    @Override
+    public int getViewType() {
+        return VIEW_TYPE_SURFACEVIEW;
+    }
+
+    @Override
+    public void setMediaPlayer(MediaPlayer mp) {
+        mMediaPlayer = mp;
+        if (mIsTakingOverOldView) {
+            takeOver(mOldView);
+        }
+    }
+
+    @Override
+    public void takeOver(@NonNull VideoViewInterface oldView) {
+        if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+            ((View) oldView).setVisibility(GONE);
+            mIsTakingOverOldView = false;
+            mOldView = null;
+            if (mSurfaceListener != null) {
+                mSurfaceListener.onSurfaceTakeOverDone(this);
+            }
+        } else {
+            mIsTakingOverOldView = true;
+            mOldView = oldView;
+        }
+    }
+
+    @Override
+    public boolean hasAvailableSurface() {
+        return (mSurfaceHolder != null && mSurfaceHolder.getSurface() != null);
+    }
+
+    ////////////////////////////////////////////////////
+    // implements SurfaceHolder.Callback
+    ////////////////////////////////////////////////////
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        Log.d(TAG, "surfaceCreated: mSurfaceHolder: " + mSurfaceHolder + ", new holder: " + holder);
+        mSurfaceHolder = holder;
+        if (mIsTakingOverOldView) {
+            takeOver(mOldView);
+        } else {
+            assignSurfaceToMediaPlayer(mMediaPlayer);
+        }
+
+        if (mSurfaceListener != null) {
+            Rect rect = mSurfaceHolder.getSurfaceFrame();
+            mSurfaceListener.onSurfaceCreated(this, rect.width(), rect.height());
+        }
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        if (mSurfaceListener != null) {
+            mSurfaceListener.onSurfaceChanged(this, width, height);
+        }
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        // After we return from this we can't use the surface any more
+        mSurfaceHolder = null;
+        if (mSurfaceListener != null) {
+            mSurfaceListener.onSurfaceDestroyed(this);
+        }
+    }
+
+    // TODO: Investigate the way to move onMeasure() code into FrameLayout.
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+        int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+        if (DEBUG) {
+            Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+                    + MeasureSpec.toString(heightMeasureSpec) + ")");
+            Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+            Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+            Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+        }
+
+        int width = getDefaultSize(videoWidth, widthMeasureSpec);
+        int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+        if (videoWidth > 0 && videoHeight > 0) {
+            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+            width = widthSpecSize;
+            height = heightSpecSize;
+
+            // for compatibility, we adjust size based on aspect ratio
+            if (videoWidth * height < width * videoHeight) {
+                width = height * videoWidth / videoHeight;
+                if (DEBUG) {
+                    Log.d(TAG, "image too wide, correcting. width: " + width);
+                }
+            } else if (videoWidth * height > width * videoHeight) {
+                height = width * videoHeight / videoWidth;
+                if (DEBUG) {
+                    Log.d(TAG, "image too tall, correcting. height: " + height);
+                }
+            }
+        } else {
+            // no size yet, just adopt the given spec sizes
+        }
+        setMeasuredDimension(width, height);
+        if (DEBUG) {
+            Log.i(TAG, "end of onMeasure()");
+            Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "ViewType: SurfaceView / Visibility: " + getVisibility()
+                + " / surfaceHolder: " + mSurfaceHolder;
+    }
+}
diff --git a/androidx/widget/VideoTextureView.java b/androidx/widget/VideoTextureView.java
new file mode 100644
index 0000000..836fdc3
--- /dev/null
+++ b/androidx/widget/VideoTextureView.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import static androidx.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.media.MediaPlayer;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class VideoTextureView extends TextureView
+        implements VideoViewInterface, TextureView.SurfaceTextureListener {
+    private static final String TAG = "VideoTextureView";
+    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+
+    private Surface mSurface;
+    private SurfaceListener mSurfaceListener;
+    private MediaPlayer mMediaPlayer;
+    // A flag to indicate taking over other view should be proceed.
+    private boolean mIsTakingOverOldView;
+    private VideoViewInterface mOldView;
+
+    VideoTextureView(Context context) {
+        this(context, null);
+    }
+
+    VideoTextureView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    VideoTextureView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        setSurfaceTextureListener(this);
+    }
+
+    ////////////////////////////////////////////////////
+    // implements VideoViewInterface
+    ////////////////////////////////////////////////////
+
+    @Override
+    public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+        if (mp == null || !hasAvailableSurface()) {
+            // Surface is not ready.
+            return false;
+        }
+        mp.setSurface(mSurface);
+        return true;
+    }
+
+    @Override
+    public void setSurfaceListener(SurfaceListener l) {
+        mSurfaceListener = l;
+    }
+
+    @Override
+    public int getViewType() {
+        return VIEW_TYPE_TEXTUREVIEW;
+    }
+
+    @Override
+    public void setMediaPlayer(MediaPlayer mp) {
+        mMediaPlayer = mp;
+        if (mIsTakingOverOldView) {
+            takeOver(mOldView);
+        }
+    }
+
+    @Override
+    public void takeOver(@NonNull VideoViewInterface oldView) {
+        if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+            ((View) oldView).setVisibility(GONE);
+            mIsTakingOverOldView = false;
+            mOldView = null;
+            if (mSurfaceListener != null) {
+                mSurfaceListener.onSurfaceTakeOverDone(this);
+            }
+        } else {
+            mIsTakingOverOldView = true;
+            mOldView = oldView;
+        }
+    }
+
+    @Override
+    public boolean hasAvailableSurface() {
+        return mSurface != null && mSurface.isValid();
+    }
+
+    ////////////////////////////////////////////////////
+    // implements TextureView.SurfaceTextureListener
+    ////////////////////////////////////////////////////
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+        mSurface = new Surface(surfaceTexture);
+        if (mIsTakingOverOldView) {
+            takeOver(mOldView);
+        } else {
+            assignSurfaceToMediaPlayer(mMediaPlayer);
+        }
+        if (mSurfaceListener != null) {
+            mSurfaceListener.onSurfaceCreated(this, width, height);
+        }
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+        if (mSurfaceListener != null) {
+            mSurfaceListener.onSurfaceChanged(this, width, height);
+        }
+        // requestLayout();  // TODO: figure out if it should be called here?
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+        // no-op
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+        if (mSurfaceListener != null) {
+            mSurfaceListener.onSurfaceDestroyed(this);
+        }
+        mSurface = null;
+        return true;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+        int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+        if (DEBUG) {
+            Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+                    + MeasureSpec.toString(heightMeasureSpec) + ")");
+            Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+            Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+            Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+        }
+
+        int width = getDefaultSize(videoWidth, widthMeasureSpec);
+        int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+        if (videoWidth > 0 && videoHeight > 0) {
+            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+            width = widthSpecSize;
+            height = heightSpecSize;
+
+            // for compatibility, we adjust size based on aspect ratio
+            if (videoWidth * height < width * videoHeight) {
+                width = height * videoWidth / videoHeight;
+                if (DEBUG) {
+                    Log.d(TAG, "image too wide, correcting. width: " + width);
+                }
+            } else if (videoWidth * height > width * videoHeight) {
+                height = width * videoHeight / videoWidth;
+                if (DEBUG) {
+                    Log.d(TAG, "image too tall, correcting. height: " + height);
+                }
+            }
+        } else {
+            // no size yet, just adopt the given spec sizes
+        }
+        setMeasuredDimension(width, height);
+        if (DEBUG) {
+            Log.i(TAG, "end of onMeasure()");
+            Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+        }
+    }
+}
diff --git a/androidx/widget/VideoView2.java b/androidx/widget/VideoView2.java
new file mode 100644
index 0000000..3cb1717
--- /dev/null
+++ b/androidx/widget/VideoView2.java
@@ -0,0 +1,1785 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.PlaybackParams;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.VideoView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.R;
+import androidx.media.SessionToken2;
+import androidx.palette.graphics.Palette;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+// TODO: Replace MediaSession wtih MediaSession2 once MediaSession2 is submitted.
+/**
+ * @hide
+ * Displays a video file.  VideoView2 class is a View class which is wrapping {@link MediaPlayer}
+ * so that developers can easily implement a video rendering application.
+ *
+ * <p>
+ * <em> Data sources that VideoView2 supports : </em>
+ * VideoView2 can play video files and audio-only files as
+ * well. It can load from various sources such as resources or content providers. The supported
+ * media file formats are the same as {@link MediaPlayer}.
+ *
+ * <p>
+ * <em> View type can be selected : </em>
+ * VideoView2 can render videos on top of TextureView as well as
+ * SurfaceView selectively. The default is SurfaceView and it can be changed using
+ * {@link #setViewType(int)} method. Using SurfaceView is recommended in most cases for saving
+ * battery. TextureView might be preferred for supporting various UIs such as animation and
+ * translucency.
+ *
+ * <p>
+ * <em> Differences between {@link VideoView} class : </em>
+ * VideoView2 covers and inherits the most of
+ * VideoView's functionalities. The main differences are
+ * <ul>
+ * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * selectively while VideoView inherits SurfaceView class.
+ * <li> VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is
+ * attached to VideoView2 by default. If a developer does not want to use the default
+ * MediaControlView2, needs to set enableControlView attribute to false. For instance,
+ * <pre>
+ * &lt;VideoView2
+ *     android:id="@+id/video_view"
+ *     xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
+ *     widget:enableControlView="false" /&gt;
+ * </pre>
+ * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute
+ * to false and assign the customed media control widget using {@link #setMediaControlView2}.
+ * <li> VideoView2 is integrated with MediaPlayer while VideoView is integrated with MediaPlayer.
+ * <li> VideoView2 is integrated with MediaSession and so it responses with media key events.
+ * A VideoView2 keeps a MediaSession instance internally and connects it to a corresponding
+ * MediaControlView2 instance.
+ * </p>
+ * </ul>
+ *
+ * <p>
+ * <em> Audio focus and audio attributes : </em>
+ * By default, VideoView2 requests audio focus with
+ * {@link AudioManager#AUDIOFOCUS_GAIN}. Use {@link #setAudioFocusRequest(int)} to change this
+ * behavior. The default {@link AudioAttributes} used during playback have a usage of
+ * {@link AudioAttributes#USAGE_MEDIA} and a content type of
+ * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to
+ * modify them.
+ *
+ * <p>
+ * Note: VideoView2 does not retain its full state when going into the background. In particular, it
+ * does not restore the current play state, play position, selected tracks. Applications should save
+ * and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and
+ * {@link android.app.Activity#onRestoreInstanceState}.
+ */
+@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
+@RestrictTo(LIBRARY_GROUP)
+public class VideoView2 extends BaseLayout implements VideoViewInterface.SurfaceListener {
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({
+            VIEW_TYPE_TEXTUREVIEW,
+            VIEW_TYPE_SURFACEVIEW
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ViewType {}
+
+    /**
+     * Indicates video is rendering on SurfaceView.
+     *
+     * @see #setViewType
+     */
+    public static final int VIEW_TYPE_SURFACEVIEW = 0;
+
+    /**
+     * Indicates video is rendering on TextureView.
+     *
+     * @see #setViewType
+     */
+    public static final int VIEW_TYPE_TEXTUREVIEW = 1;
+
+    private static final String TAG = "VideoView2";
+    private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+    private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+
+    private static final int STATE_ERROR = -1;
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PREPARING = 1;
+    private static final int STATE_PREPARED = 2;
+    private static final int STATE_PLAYING = 3;
+    private static final int STATE_PAUSED = 4;
+    private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+    private static final int INVALID_TRACK_INDEX = -1;
+    private static final float INVALID_SPEED = 0f;
+
+    private static final int SIZE_TYPE_EMBEDDED = 0;
+    private static final int SIZE_TYPE_FULL = 1;
+    // TODO: add support for Minimal size type.
+    private static final int SIZE_TYPE_MINIMAL = 2;
+
+    private AccessibilityManager mAccessibilityManager;
+    private AudioManager mAudioManager;
+    private AudioAttributes mAudioAttributes;
+    private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+    private boolean mAudioFocused = false;
+
+    private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
+    private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
+    private VideoView2.OnFullScreenRequestListener mFullScreenRequestListener;
+
+    private VideoViewInterface mCurrentView;
+    private VideoTextureView mTextureView;
+    private VideoSurfaceView mSurfaceView;
+
+    private MediaPlayer mMediaPlayer;
+    private DataSourceDesc mDsd;
+    private MediaControlView2 mMediaControlView;
+    private MediaSessionCompat mMediaSession;
+    private MediaControllerCompat mMediaController;
+    private MediaMetadata2 mMediaMetadata;
+    private MediaMetadataRetriever mRetriever;
+    private boolean mNeedUpdateMediaType;
+    private Bundle mMediaTypeData;
+    private String mTitle;
+
+    // TODO: move music view inside SurfaceView/TextureView or implement VideoViewInterface.
+    private WindowManager mManager;
+    private Resources mResources;
+    private View mMusicView;
+    private Drawable mMusicAlbumDrawable;
+    private String mMusicTitleText;
+    private String mMusicArtistText;
+    private boolean mIsMusicMediaType;
+    private int mPrevWidth;
+    private int mPrevHeight;
+    private int mDominantColor;
+    private int mSizeType;
+
+    private PlaybackStateCompat.Builder mStateBuilder;
+    private List<PlaybackStateCompat.CustomAction> mCustomActionList;
+
+    private int mTargetState = STATE_IDLE;
+    private int mCurrentState = STATE_IDLE;
+    private int mCurrentBufferPercentage;
+    private long mSeekWhenPrepared;  // recording the seek position while preparing
+
+    private int mVideoWidth;
+    private int mVideoHeight;
+
+    private ArrayList<Integer> mVideoTrackIndices;
+    private ArrayList<Integer> mAudioTrackIndices;
+    // private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+    // private SubtitleController mSubtitleController;
+
+    // selected video/audio/subtitle track index as MediaPlayer returns
+    private int mSelectedVideoTrackIndex;
+    private int mSelectedAudioTrackIndex;
+    private int mSelectedSubtitleTrackIndex;
+
+    // private SubtitleView mSubtitleView;
+    private boolean mSubtitleEnabled;
+
+    private float mSpeed;
+    // TODO: Remove mFallbackSpeed when integration with MediaPlayer's new setPlaybackParams().
+    // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
+    private float mFallbackSpeed;  // keep the original speed before 'pause' is called.
+    private float mVolumeLevelFloat;
+    private int mVolumeLevel;
+
+    private long mShowControllerIntervalMs;
+
+    // private MediaRouter mMediaRouter;
+    // private MediaRouteSelector mRouteSelector;
+    // private MediaRouter.RouteInfo mRoute;
+    // private RoutePlayer mRoutePlayer;
+
+    // TODO (b/77158231)
+    /*
+    private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
+        @Override
+        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+                // Stop local playback (if necessary)
+                resetPlayer();
+                mRoute = route;
+                mRoutePlayer = new RoutePlayer(getContext(), route);
+                mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
+                    @Override
+                    public void onPlayerStateChanged(MediaItemStatus itemStatus) {
+                        PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
+                        psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
+                        long position = itemStatus.getContentPosition();
+                        switch (itemStatus.getPlaybackState()) {
+                            case MediaItemStatus.PLAYBACK_STATE_PENDING:
+                                psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
+                                mCurrentState = STATE_IDLE;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+                                psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
+                                mCurrentState = STATE_PLAYING;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+                                psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
+                                mCurrentState = STATE_PAUSED;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+                                psBuilder.setState(
+                                        PlaybackStateCompat.STATE_BUFFERING, position, 0);
+                                mCurrentState = STATE_PAUSED;
+                                break;
+                            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+                                psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
+                                mCurrentState = STATE_PLAYBACK_COMPLETED;
+                                break;
+                        }
+
+                        PlaybackStateCompat pbState = psBuilder.build();
+                        mMediaSession.setPlaybackState(pbState);
+
+                        MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
+                        mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+                                itemStatus.getContentDuration());
+                        mMediaSession.setMetadata(mmBuilder.build());
+                    }
+                });
+                // Start remote playback (if necessary)
+                mRoutePlayer.openVideo(mDsd);
+            }
+        }
+
+        @Override
+        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
+            if (mRoute != null && mRoutePlayer != null) {
+                mRoutePlayer.release();
+                mRoutePlayer = null;
+            }
+            if (mRoute == route) {
+                mRoute = null;
+            }
+            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+                // TODO: Resume local playback  (if necessary)
+                openVideo(mDsd);
+            }
+        }
+    };
+    */
+
+    public VideoView2(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mVideoWidth = 0;
+        mVideoHeight = 0;
+        mSpeed = 1.0f;
+        mFallbackSpeed = mSpeed;
+        mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+        // TODO: add attributes to get this value.
+        mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
+
+        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
+                .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+        setFocusable(true);
+        setFocusableInTouchMode(true);
+        requestFocus();
+
+        // TODO: try to keep a single child at a time rather than always having both.
+        mTextureView = new VideoTextureView(getContext());
+        mSurfaceView = new VideoSurfaceView(getContext());
+        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT);
+        mTextureView.setLayoutParams(params);
+        mSurfaceView.setLayoutParams(params);
+        mTextureView.setSurfaceListener(this);
+        mSurfaceView.setSurfaceListener(this);
+
+        addView(mTextureView);
+        addView(mSurfaceView);
+
+        // mSubtitleView = new SubtitleView(getContext());
+        // mSubtitleView.setLayoutParams(params);
+        // mSubtitleView.setBackgroundColor(0);
+        // addView(mSubtitleView);
+
+        boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+                "http://schemas.android.com/apk/res/android",
+                "enableControlView", true);
+        if (enableControlView) {
+            mMediaControlView = new MediaControlView2(getContext());
+        }
+
+        mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
+                "http://schemas.android.com/apk/res/android",
+                "enableSubtitle", false);
+
+        // TODO: Choose TextureView when SurfaceView cannot be created.
+        // Choose surface view by default
+        int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
+                : attrs.getAttributeIntValue(
+                "http://schemas.android.com/apk/res/android",
+                "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
+        if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            Log.d(TAG, "viewType attribute is surfaceView.");
+            mTextureView.setVisibility(View.GONE);
+            mSurfaceView.setVisibility(View.VISIBLE);
+            mCurrentView = mSurfaceView;
+        } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            Log.d(TAG, "viewType attribute is textureView.");
+            mTextureView.setVisibility(View.VISIBLE);
+            mSurfaceView.setVisibility(View.GONE);
+            mCurrentView = mTextureView;
+        }
+
+        // TODO (b/77158231)
+        /*
+        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+        builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+        mRouteSelector = builder.build();
+        */
+    }
+
+    /**
+     * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+     * instance if any.
+     *
+     * @param mediaControlView a media control view2 instance.
+     * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+     */
+    public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+        mMediaControlView = mediaControlView;
+        mShowControllerIntervalMs = intervalMs;
+        // TODO: Call MediaControlView2.setRouteSelector only when cast availalbe.
+        // TODO (b/77158231)
+        // mMediaControlView.setRouteSelector(mRouteSelector);
+
+        if (isAttachedToWindow()) {
+            attachMediaControlView();
+        }
+    }
+
+    /**
+     * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+     * {@link #setMediaControlView2} method.
+     */
+    public MediaControlView2 getMediaControlView2() {
+        return mMediaControlView;
+    }
+
+    /**
+     * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+     * if any.
+     *
+     * @param metadata a MediaMetadata2 instance.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setMediaMetadata(MediaMetadata2 metadata) {
+      //mProvider.setMediaMetadata_impl(metadata);
+    }
+
+    /**
+     * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+     * default or by {@link #setMediaMetadata} method.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public MediaMetadata2 getMediaMetadata() {
+        return mMediaMetadata;
+    }
+
+    /**
+     * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+     * using. This method should be called when VideoView2 is attached to window, or it throws
+     * IllegalStateException, since internal MediaSession instance is not available until
+     * this view is attached to window. Please check {@link android.view.View#isAttachedToWindow}
+     * before calling this method.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     * @hide  TODO: remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public MediaControllerCompat getMediaController() {
+        if (mMediaSession == null) {
+            throw new IllegalStateException("MediaSession instance is not available.");
+        }
+        return mMediaController;
+    }
+
+    /**
+     * Returns {@link androidx.media.SessionToken2} so that developers create their own
+     * {@link androidx.media.MediaController2} instance. This method should be called when
+     * VideoView2 is attached to window, or it throws IllegalStateException.
+     *
+     * @throws IllegalStateException if interal MediaSession is not created yet.
+     * @hide
+     */
+    public SessionToken2 getMediaSessionToken() {
+        //return mProvider.getMediaSessionToken_impl();
+        return null;
+    }
+
+    /**
+     * Shows or hides closed caption or subtitles if there is any.
+     * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+     * Default behavior of VideoView2 is not showing subtitle.
+     * @param enable shows closed caption or subtitles if this value is true, or hides.
+     */
+    public void setSubtitleEnabled(boolean enable) {
+        if (enable != mSubtitleEnabled) {
+            selectOrDeselectSubtitle(enable);
+        }
+        mSubtitleEnabled = enable;
+    }
+
+    /**
+     * Returns true if showing subtitle feature is enabled or returns false.
+     * Although there is no subtitle track or closed caption, it can return true, if the feature
+     * has been enabled by {@link #setSubtitleEnabled}.
+     */
+    public boolean isSubtitleEnabled() {
+        return mSubtitleEnabled;
+    }
+
+    /**
+     * Sets playback speed.
+     *
+     * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+     * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+     * maximum speed that internal engine supports, system will determine best handling or it will
+     * be reset to the normal speed 1.0f.
+     * @param speed the playback speed. It should be positive.
+     */
+    // TODO: Support this via MediaController2.
+    public void setSpeed(float speed) {
+        if (speed <= 0.0f) {
+            Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+            return;
+        }
+        mSpeed = speed;
+        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+            applySpeed();
+        }
+        updatePlaybackState();
+    }
+
+    /**
+     * Sets which type of audio focus will be requested during the playback, or configures playback
+     * to not request audio focus. Valid values for focus requests are
+     * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+     * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+     * requested when playback starts. You can for instance use this when playing a silent animation
+     * through this class, and you don't want to affect other audio applications playing in the
+     * background.
+     *
+     * @param focusGain the type of audio focus gain that will be requested, or
+     *                  {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+     *                  playback.
+     */
+    public void setAudioFocusRequest(int focusGain) {
+        if (focusGain != AudioManager.AUDIOFOCUS_NONE
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+            throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+        }
+        mAudioFocusType = focusGain;
+    }
+
+    /**
+     * Sets the {@link AudioAttributes} to be used during the playback of the video.
+     *
+     * @param attributes non-null <code>AudioAttributes</code>.
+     */
+    public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+        if (attributes == null) {
+            throw new IllegalArgumentException("Illegal null AudioAttributes");
+        }
+        mAudioAttributes = attributes;
+    }
+
+    /**
+     * Sets video path.
+     *
+     * @param path the path of the video.
+     *
+     * @hide TODO remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setVideoPath(String path) {
+        setVideoUri(Uri.parse(path));
+    }
+
+    /**
+     * Sets video URI.
+     *
+     * @param uri the URI of the video.
+     *
+     * @hide TODO remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setVideoUri(Uri uri) {
+        setVideoUri(uri, null);
+    }
+
+    /**
+     * Sets video URI using specific headers.
+     *
+     * @param uri     the URI of the video.
+     * @param headers the headers for the URI request.
+     *                Note that the cross domain redirection is allowed by default, but that can be
+     *                changed with key/value pairs through the headers parameter with
+     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+     *                to disallow or allow cross domain redirection.
+     *
+     * @hide TODO remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setVideoUri(Uri uri, Map<String, String> headers) {
+        mSeekWhenPrepared = 0;
+        openVideo(uri, headers);
+    }
+
+    /**
+     * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+     * object to VideoView2 is {@link #setDataSource}.
+     * @param mediaItem the MediaItem2 to play
+     * @see #setDataSource
+     */
+    public void setMediaItem(@NonNull MediaItem2 mediaItem) {
+        //mProvider.setMediaItem_impl(mediaItem);
+    }
+
+    /**
+     * Sets {@link DataSourceDesc} object to render using VideoView2.
+     * @param dataSource the {@link DataSourceDesc} object to play.
+     * @see #setMediaItem
+     * @hide
+     */
+    public void setDataSource(@NonNull DataSourceDesc dataSource) {
+        //mProvider.setDataSource_impl(dataSource);
+    }
+
+    /**
+     * Selects which view will be used to render video between SurfacView and TextureView.
+     *
+     * @param viewType the view type to render video
+     * <ul>
+     * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+     * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+     * </ul>
+     */
+    public void setViewType(@ViewType int viewType) {
+        if (viewType == mCurrentView.getViewType()) {
+            return;
+        }
+        VideoViewInterface targetView;
+        if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+            Log.d(TAG, "switching to TextureView");
+            targetView = mTextureView;
+        } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+            Log.d(TAG, "switching to SurfaceView");
+            targetView = mSurfaceView;
+        } else {
+            throw new IllegalArgumentException("Unknown view type: " + viewType);
+        }
+        ((View) targetView).setVisibility(View.VISIBLE);
+        targetView.takeOver(mCurrentView);
+        requestLayout();
+    }
+
+    /**
+     * Returns view type.
+     *
+     * @return view type. See {@see setViewType}.
+     */
+    @ViewType
+    public int getViewType() {
+        return mCurrentView.getViewType();
+    }
+
+    /**
+     * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+     *
+     * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+     *                   {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+     *                   buttons in {@link MediaControlView2}.
+     * @param executor executor to run callbacks on.
+     * @param listener A listener to be called when a custom button is clicked.
+     * @hide  TODO remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+            Executor executor, OnCustomActionListener listener) {
+        mCustomActionList = actionList;
+        mCustomActionListenerRecord = new Pair<>(executor, listener);
+
+        // Create a new playback builder in order to clear existing the custom actions.
+        mStateBuilder = null;
+        updatePlaybackState();
+    }
+
+    /**
+     * Registers a callback to be invoked when a view type change is done.
+     * {@see #setViewType(int)}
+     * @param l The callback that will be run
+     * @hide
+     */
+    @VisibleForTesting
+    @RestrictTo(LIBRARY_GROUP)
+    public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) {
+        mViewTypeChangedListener = l;
+    }
+
+    /**
+     * Registers a callback to be invoked when the fullscreen mode should be changed.
+     * @param l The callback that will be run
+     * @hide  TODO remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void setFullScreenRequestListener(OnFullScreenRequestListener l) {
+        mFullScreenRequestListener = l;
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Create MediaSession
+        mMediaSession = new MediaSessionCompat(getContext(), "VideoView2MediaSession");
+        mMediaSession.setCallback(new MediaSessionCallback());
+        mMediaSession.setActive(true);
+        mMediaController = mMediaSession.getController();
+        // TODO (b/77158231)
+        // mMediaRouter = MediaRouter.getInstance(getContext());
+        // mMediaRouter.setMediaSession(mMediaSession);
+        // mMediaRouter.addCallback(mRouteSelector, mRouterCallback);
+        attachMediaControlView();
+        // TODO: remove this after moving MediaSession creating code inside initializing VideoView2
+        if (mCurrentState == STATE_PREPARED) {
+            extractTracks();
+            extractMetadata();
+            extractAudioMetadata();
+            if (mNeedUpdateMediaType) {
+                mMediaSession.sendSessionEvent(
+                        MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+                        mMediaTypeData);
+                mNeedUpdateMediaType = false;
+            }
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mMediaSession.release();
+        mMediaSession = null;
+        mMediaController = null;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return VideoView2.class.getName();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (DEBUG) {
+            Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState);
+        }
+        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+            if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+                toggleMediaControlViewVisibility();
+            }
+        }
+
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+            if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+                toggleMediaControlViewVisibility();
+            }
+        }
+
+        return super.onTrackballEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        // TODO: Test touch event handling logic thoroughly and simplify the logic.
+        return super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (mIsMusicMediaType) {
+            if (mPrevWidth != getMeasuredWidth()
+                    || mPrevHeight != getMeasuredHeight()) {
+                int currWidth = getMeasuredWidth();
+                int currHeight = getMeasuredHeight();
+                Point screenSize = new Point();
+                mManager.getDefaultDisplay().getSize(screenSize);
+                int screenWidth = screenSize.x;
+                int screenHeight = screenSize.y;
+
+                if (currWidth == screenWidth && currHeight == screenHeight) {
+                    int orientation = retrieveOrientation();
+                    if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+                        inflateMusicView(R.layout.full_landscape_music);
+                    } else {
+                        inflateMusicView(R.layout.full_portrait_music);
+                    }
+
+                    if (mSizeType != SIZE_TYPE_FULL) {
+                        mSizeType = SIZE_TYPE_FULL;
+                        // Remove existing mFadeOut callback
+                        mMediaControlView.removeCallbacks(mFadeOut);
+                        mMediaControlView.setVisibility(View.VISIBLE);
+                    }
+                } else {
+                    if (mSizeType != SIZE_TYPE_EMBEDDED) {
+                        mSizeType = SIZE_TYPE_EMBEDDED;
+                        inflateMusicView(R.layout.embedded_music);
+                        // Add new mFadeOut callback
+                        mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+                    }
+                }
+                mPrevWidth = currWidth;
+                mPrevHeight = currHeight;
+            }
+        }
+    }
+
+    /**
+     * Interface definition of a callback to be invoked when the view type has been changed.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    @RestrictTo(LIBRARY_GROUP)
+    public interface OnViewTypeChangedListener {
+        /**
+         * Called when the view type has been changed.
+         * @see #setViewType(int)
+         * @param view the View whose view type is changed
+         * @param viewType
+         * <ul>
+         * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+         * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+         * </ul>
+         */
+        void onViewTypeChanged(View view, @ViewType int viewType);
+    }
+
+    /**
+     * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
+     * Application should handle the fullscreen mode accordingly.
+     * @hide  TODO remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public interface OnFullScreenRequestListener {
+        /**
+         * Called to indicate a fullscreen mode change.
+         */
+        void onFullScreenRequest(View view, boolean fullScreen);
+    }
+
+    /**
+     * Interface definition of a callback to be invoked to inform that a custom action is performed.
+     * @hide  TODO remove
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public interface OnCustomActionListener {
+        /**
+         * Called to indicate that a custom action is performed.
+         *
+         * @param action The action that was originally sent in the
+         *               {@link PlaybackStateCompat.CustomAction}.
+         * @param extras Optional extras.
+         */
+        void onCustomAction(String action, Bundle extras);
+    }
+
+    ///////////////////////////////////////////////////
+    // Implements VideoViewInterface.SurfaceListener
+    ///////////////////////////////////////////////////
+
+    @Override
+    public void onSurfaceCreated(View view, int width, int height) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+                    + ", " + view.toString());
+        }
+        if (needToStart()) {
+            mMediaController.getTransportControls().play();
+        }
+    }
+
+    @Override
+    public void onSurfaceDestroyed(View view) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+                    + ", mTargetState=" + mTargetState + ", " + view.toString());
+        }
+    }
+
+    @Override
+    public void onSurfaceChanged(View view, int width, int height) {
+        // TODO: Do we need to call requestLayout here?
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+                    + ", " + view.toString());
+        }
+    }
+
+    @Override
+    public void onSurfaceTakeOverDone(VideoViewInterface view) {
+        if (DEBUG) {
+            Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+        }
+        mCurrentView = view;
+        if (mViewTypeChangedListener != null) {
+            mViewTypeChangedListener.onViewTypeChanged(this, view.getViewType());
+        }
+        if (needToStart()) {
+            mMediaController.getTransportControls().play();
+        }
+    }
+
+    ///////////////////////////////////////////////////
+    // Protected or private methods
+    ///////////////////////////////////////////////////
+
+    private void attachMediaControlView() {
+        // Get MediaController from MediaSession and set it inside MediaControlView
+        mMediaControlView.setController(mMediaSession.getController());
+
+        LayoutParams params =
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        addView(mMediaControlView, params);
+    }
+
+    private boolean isInPlaybackState() {
+        // TODO (b/77158231)
+        // return (mMediaPlayer != null || mRoutePlayer != null)
+        return (mMediaPlayer != null)
+                && mCurrentState != STATE_ERROR
+                && mCurrentState != STATE_IDLE
+                && mCurrentState != STATE_PREPARING;
+    }
+
+    private boolean needToStart() {
+        // TODO (b/77158231)
+        // return (mMediaPlayer != null || mRoutePlayer != null)
+        return (mMediaPlayer != null)
+                && isAudioGranted()
+                && isWaitingPlayback();
+    }
+
+    private boolean isWaitingPlayback() {
+        return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
+    }
+
+    private boolean isAudioGranted() {
+        return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
+    }
+
+    AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
+            new AudioManager.OnAudioFocusChangeListener() {
+        @Override
+        public void onAudioFocusChange(int focusChange) {
+            switch (focusChange) {
+                case AudioManager.AUDIOFOCUS_GAIN:
+                    mAudioFocused = true;
+                    if (needToStart()) {
+                        mMediaController.getTransportControls().play();
+                    }
+                    break;
+                case AudioManager.AUDIOFOCUS_LOSS:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    // There is no way to distinguish pause() by transient
+                    // audio focus loss and by other explicit actions.
+                    // TODO: If we can distinguish those cases, change the code to resume when it
+                    // gains audio focus again for AUDIOFOCUS_LOSS_TRANSIENT and
+                    // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
+                    mAudioFocused = false;
+                    if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
+                        mMediaController.getTransportControls().pause();
+                    } else {
+                        mTargetState = STATE_PAUSED;
+                    }
+            }
+        }
+    };
+
+    private void requestAudioFocus(int focusType) {
+        int result;
+        if (android.os.Build.VERSION.SDK_INT >= 26) {
+            AudioFocusRequest focusRequest;
+            focusRequest = new AudioFocusRequest.Builder(focusType)
+                    .setAudioAttributes(mAudioAttributes)
+                    .setOnAudioFocusChangeListener(mAudioFocusListener)
+                    .build();
+            result = mAudioManager.requestAudioFocus(focusRequest);
+        } else {
+            result = mAudioManager.requestAudioFocus(mAudioFocusListener,
+                    AudioManager.STREAM_MUSIC,
+                    focusType);
+        }
+        if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+            mAudioFocused = false;
+        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            mAudioFocused = true;
+        } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+            mAudioFocused = false;
+        }
+    }
+
+    // Creates a MediaPlayer instance and prepare playback.
+    private void openVideo(Uri uri, Map<String, String> headers) {
+        resetPlayer();
+        if (isRemotePlayback()) {
+            // TODO (b/77158231)
+            // mRoutePlayer.openVideo(dsd);
+            return;
+        }
+
+        try {
+            Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+            mMediaPlayer = new MediaPlayer();
+            mSurfaceView.setMediaPlayer(mMediaPlayer);
+            mTextureView.setMediaPlayer(mMediaPlayer);
+            mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+            final Context context = getContext();
+            // TODO: Add timely firing logic for more accurate sync between CC and video frame
+            // mSubtitleController = new SubtitleController(context);
+            // mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+            // mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+
+            mMediaPlayer.setOnPreparedListener(mPreparedListener);
+            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+            mMediaPlayer.setOnCompletionListener(mCompletionListener);
+            mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
+            mMediaPlayer.setOnErrorListener(mErrorListener);
+            mMediaPlayer.setOnInfoListener(mInfoListener);
+            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+
+            mCurrentBufferPercentage = -1;
+            mMediaPlayer.setDataSource(getContext(), uri, headers);
+            mMediaPlayer.setAudioAttributes(mAudioAttributes);
+            // mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
+            // we don't set the target state here either, but preserve the
+            // target state that was there before.
+            mCurrentState = STATE_PREPARING;
+            mMediaPlayer.prepareAsync();
+
+            // Save file name as title since the file may not have a title Metadata.
+            mTitle = uri.getPath();
+            String scheme = uri.getScheme();
+            if (scheme != null && scheme.equals("file")) {
+                mTitle = uri.getLastPathSegment();
+            }
+            mRetriever = new MediaMetadataRetriever();
+            mRetriever.setDataSource(getContext(), uri);
+
+            if (DEBUG) {
+                Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+        } catch (IOException | IllegalArgumentException ex) {
+            Log.w(TAG, "Unable to open content: " + uri, ex);
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            mErrorListener.onError(mMediaPlayer,
+                    MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
+        }
+    }
+
+    /*
+     * Reset the media player in any state
+     */
+    private void resetPlayer() {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.reset();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+            mTextureView.setMediaPlayer(null);
+            mSurfaceView.setMediaPlayer(null);
+            mCurrentState = STATE_IDLE;
+            mTargetState = STATE_IDLE;
+            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                mAudioManager.abandonAudioFocus(null);
+            }
+        }
+        mVideoWidth = 0;
+        mVideoHeight = 0;
+    }
+
+    private void updatePlaybackState() {
+        if (mStateBuilder == null) {
+            /*
+            // Get the capabilities of the player for this stream
+            mMetadata = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
+                    MediaPlayer.BYPASS_METADATA_FILTER);
+
+            // Add Play action as default
+            long playbackActions = PlaybackStateCompat.ACTION_PLAY;
+            if (mMetadata != null) {
+                if (!mMetadata.has(Metadata.PAUSE_AVAILABLE)
+                        || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) {
+                    playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
+                }
+                if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE)
+                        || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
+                    playbackActions |= PlaybackStateCompat.ACTION_REWIND;
+                }
+                if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE)
+                        || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
+                    playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
+                }
+                if (!mMetadata.has(Metadata.SEEK_AVAILABLE)
+                        || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) {
+                    playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
+                }
+            } else {
+                playbackActions |= (PlaybackStateCompat.ACTION_PAUSE
+                        | PlaybackStateCompat.ACTION_REWIND
+                        | PlaybackStateCompat.ACTION_FAST_FORWARD
+                        | PlaybackStateCompat.ACTION_SEEK_TO);
+            }
+            */
+            // TODO determine the actionable list based the metadata info.
+            long playbackActions = PlaybackStateCompat.ACTION_PLAY
+                    | PlaybackStateCompat.ACTION_PAUSE
+                    | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
+                    | PlaybackStateCompat.ACTION_SEEK_TO;
+            mStateBuilder = new PlaybackStateCompat.Builder();
+            mStateBuilder.setActions(playbackActions);
+
+            if (mCustomActionList != null) {
+                for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
+                    mStateBuilder.addCustomAction(action);
+                }
+            }
+        }
+        mStateBuilder.setState(getCorrespondingPlaybackState(),
+                mMediaPlayer.getCurrentPosition(), mSpeed);
+        if (mCurrentState != STATE_ERROR
+                && mCurrentState != STATE_IDLE
+                && mCurrentState != STATE_PREPARING) {
+            // TODO: this should be replaced with MediaPlayer2.getBufferedPosition() once it is
+            // implemented.
+            if (mCurrentBufferPercentage == -1) {
+                mStateBuilder.setBufferedPosition(-1);
+            } else {
+                mStateBuilder.setBufferedPosition(
+                        (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
+            }
+        }
+
+        // Set PlaybackState for MediaSession
+        if (mMediaSession != null) {
+            PlaybackStateCompat state = mStateBuilder.build();
+            mMediaSession.setPlaybackState(state);
+        }
+    }
+
+    private int getCorrespondingPlaybackState() {
+        switch (mCurrentState) {
+            case STATE_ERROR:
+                return PlaybackStateCompat.STATE_ERROR;
+            case STATE_IDLE:
+                return PlaybackStateCompat.STATE_NONE;
+            case STATE_PREPARING:
+                return PlaybackStateCompat.STATE_CONNECTING;
+            case STATE_PREPARED:
+                return PlaybackStateCompat.STATE_PAUSED;
+            case STATE_PLAYING:
+                return PlaybackStateCompat.STATE_PLAYING;
+            case STATE_PAUSED:
+                return PlaybackStateCompat.STATE_PAUSED;
+            case STATE_PLAYBACK_COMPLETED:
+                return PlaybackStateCompat.STATE_STOPPED;
+            default:
+                return -1;
+        }
+    }
+
+    private final Runnable mFadeOut = new Runnable() {
+        @Override
+        public void run() {
+            if (mCurrentState == STATE_PLAYING) {
+                mMediaControlView.setVisibility(View.GONE);
+            }
+        }
+    };
+
+    private void showController() {
+        // TODO: Decide what to show when the state is not in playback state
+        if (mMediaControlView == null || !isInPlaybackState()
+                || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
+            return;
+        }
+        mMediaControlView.removeCallbacks(mFadeOut);
+        mMediaControlView.setVisibility(View.VISIBLE);
+        if (mShowControllerIntervalMs != 0
+                && !mAccessibilityManager.isTouchExplorationEnabled()) {
+            mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+        }
+    }
+
+    private void toggleMediaControlViewVisibility() {
+        if (mMediaControlView.getVisibility() == View.VISIBLE) {
+            mMediaControlView.removeCallbacks(mFadeOut);
+            mMediaControlView.setVisibility(View.GONE);
+        } else {
+            showController();
+        }
+    }
+
+    private void applySpeed() {
+        if (android.os.Build.VERSION.SDK_INT < 23) {
+            // TODO: MediaPlayer2 will cover this, or implement with SoundPool.
+            return;
+        }
+        PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+        if (mSpeed != params.getSpeed()) {
+            try {
+                params.setSpeed(mSpeed);
+                mMediaPlayer.setPlaybackParams(params);
+                mFallbackSpeed = mSpeed;
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+                // TODO: should revise this part after integrating with MP2.
+                // If mSpeed had an illegal value for speed rate, system will determine best
+                // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
+                // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
+                // use mFallbackSpeed instead.
+                float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+                if (fallbackSpeed > 0.0f) {
+                    mFallbackSpeed = fallbackSpeed;
+                }
+                mSpeed = mFallbackSpeed;
+            }
+        }
+    }
+
+    private boolean isRemotePlayback() {
+        if (mMediaController == null) {
+            return false;
+        }
+        PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+        return playbackInfo != null
+                && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
+    }
+
+    private void selectOrDeselectSubtitle(boolean select) {
+        if (!isInPlaybackState()) {
+            return;
+        }
+    /*
+        if (select) {
+            if (mSubtitleTrackIndices.size() > 0) {
+                // TODO: make this selection dynamic
+                mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
+                mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
+                mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
+                mSubtitleView.setVisibility(View.VISIBLE);
+            }
+        } else {
+            if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+                mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
+                mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+                mSubtitleView.setVisibility(View.GONE);
+            }
+        }
+    */
+    }
+
+    private void extractTracks() {
+        MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+        mVideoTrackIndices = new ArrayList<>();
+        mAudioTrackIndices = new ArrayList<>();
+        /*
+        mSubtitleTrackIndices = new ArrayList<>();
+        mSubtitleController.reset();
+        */
+        for (int i = 0; i < trackInfos.length; ++i) {
+            int trackType = trackInfos[i].getTrackType();
+            if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+                mVideoTrackIndices.add(i);
+            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+                mAudioTrackIndices.add(i);
+                /*
+            } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
+                    || trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
+                SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
+                if (track != null) {
+                    mSubtitleTrackIndices.add(new Pair<>(i, track));
+                }
+                */
+            }
+        }
+        // Select first tracks as default
+        if (mVideoTrackIndices.size() > 0) {
+            mSelectedVideoTrackIndex = 0;
+        }
+        if (mAudioTrackIndices.size() > 0) {
+            mSelectedAudioTrackIndex = 0;
+        }
+        if (mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0) {
+            mIsMusicMediaType = true;
+        }
+
+        Bundle data = new Bundle();
+        data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+        data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+        /*
+        data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
+        if (mSubtitleTrackIndices.size() > 0) {
+            selectOrDeselectSubtitle(mSubtitleEnabled);
+        }
+        */
+        mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+    }
+
+    private void extractMetadata() {
+        // Get and set duration and title values as MediaMetadata for MediaControlView2
+        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+        if (title != null) {
+            mTitle = title;
+        }
+        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+        builder.putLong(
+                MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+        if (mMediaSession != null) {
+            mMediaSession.setMetadata(builder.build());
+        }
+    }
+
+    private void extractAudioMetadata() {
+        if (!mIsMusicMediaType) {
+            return;
+        }
+
+        mResources = getResources();
+        mManager = (WindowManager) getContext().getApplicationContext()
+                .getSystemService(Context.WINDOW_SERVICE);
+
+        byte[] album = mRetriever.getEmbeddedPicture();
+        if (album != null) {
+            Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
+            mMusicAlbumDrawable = new BitmapDrawable(bitmap);
+
+            // TODO: replace with visualizer
+            Palette.Builder builder = Palette.from(bitmap);
+            builder.generate(new Palette.PaletteAsyncListener() {
+                @Override
+                public void onGenerated(Palette palette) {
+                    // TODO: add dominant color for default album image.
+                    mDominantColor = palette.getDominantColor(0);
+                    if (mMusicView != null) {
+                        mMusicView.setBackgroundColor(mDominantColor);
+                    }
+                }
+            });
+        } else {
+            mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
+        }
+
+        String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+        if (title != null) {
+            mMusicTitleText = title;
+        } else {
+            mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+        }
+
+        String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+        if (artist != null) {
+            mMusicArtistText = artist;
+        } else {
+            mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+        }
+
+        // Send title and artist string to MediaControlView2
+        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+        builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
+        builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
+        mMediaSession.setMetadata(builder.build());
+
+        // Display Embedded mode as default
+        removeView(mSurfaceView);
+        removeView(mTextureView);
+        inflateMusicView(R.layout.embedded_music);
+    }
+
+    private int retrieveOrientation() {
+        DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
+        int width = dm.widthPixels;
+        int height = dm.heightPixels;
+
+        return (height > width)
+                ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+                : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+    }
+
+    private void inflateMusicView(int layoutId) {
+        removeView(mMusicView);
+
+        LayoutInflater inflater = (LayoutInflater) getContext()
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View v = inflater.inflate(layoutId, null);
+        v.setBackgroundColor(mDominantColor);
+
+        ImageView albumView = v.findViewById(R.id.album);
+        if (albumView != null) {
+            albumView.setImageDrawable(mMusicAlbumDrawable);
+        }
+
+        TextView titleView = v.findViewById(R.id.title);
+        if (titleView != null) {
+            titleView.setText(mMusicTitleText);
+        }
+
+        TextView artistView = v.findViewById(R.id.artist);
+        if (artistView != null) {
+            artistView.setText(mMusicArtistText);
+        }
+
+        mMusicView = v;
+        addView(mMusicView, 0);
+    }
+
+    /*
+    OnSubtitleDataListener mSubtitleListener =
+            new OnSubtitleDataListener() {
+                @Override
+                public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+                                + ", getCurrentPosition: " + mp.getCurrentPosition()
+                                + ", getStartTimeUs(): " + data.getStartTimeUs()
+                                + ", diff: "
+                                + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
+                                + "ms, getDurationUs(): " + data.getDurationUs());
+
+                    }
+                    final int index = data.getTrackIndex();
+                    if (index != mSelectedSubtitleTrackIndex) {
+                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+                                + ", selected track index: " + mSelectedSubtitleTrackIndex);
+                        return;
+                    }
+                    for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
+                        if (p.first == index) {
+                            SubtitleTrack track = p.second;
+                            track.onData(data);
+                        }
+                    }
+                }
+            };
+            */
+
+    MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+            new MediaPlayer.OnVideoSizeChangedListener() {
+                @Override
+                public void onVideoSizeChanged(
+                        MediaPlayer mp, int width, int height) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
+                    }
+                    mVideoWidth = mp.getVideoWidth();
+                    mVideoHeight = mp.getVideoHeight();
+                    if (DEBUG) {
+                        Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+                                + mVideoHeight);
+                    }
+                    if (mVideoWidth != 0 && mVideoHeight != 0) {
+                        requestLayout();
+                    }
+                }
+            };
+    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
+        @Override
+        public void onPrepared(MediaPlayer mp) {
+            if (DEBUG) {
+                Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            mCurrentState = STATE_PREPARED;
+            // Create and set playback state for MediaControlView2
+            updatePlaybackState();
+
+            // TODO: change this to send TrackInfos to MediaControlView2
+            // TODO: create MediaSession when initializing VideoView2
+            if (mMediaSession != null) {
+                extractTracks();
+            }
+
+            if (mMediaControlView != null) {
+                mMediaControlView.setEnabled(true);
+            }
+            int videoWidth = mp.getVideoWidth();
+            int videoHeight = mp.getVideoHeight();
+
+            // mSeekWhenPrepared may be changed after seekTo() call
+            long seekToPosition = mSeekWhenPrepared;
+            if (seekToPosition != 0) {
+                mMediaController.getTransportControls().seekTo(seekToPosition);
+            }
+
+            if (videoWidth != 0 && videoHeight != 0) {
+                if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
+                    if (DEBUG) {
+                        Log.i(TAG, "OnPreparedListener() : ");
+                        Log.i(TAG, " video size: " + videoWidth + "/" + videoHeight);
+                        Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/"
+                                + getMeasuredHeight());
+                        Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+                    }
+                    mVideoWidth = videoWidth;
+                    mVideoHeight = videoHeight;
+                    requestLayout();
+                }
+
+                if (needToStart()) {
+                    mMediaController.getTransportControls().play();
+                }
+            } else {
+                // We don't know the video size yet, but should start anyway.
+                // The video size might be reported to us later.
+                if (needToStart()) {
+                    mMediaController.getTransportControls().play();
+                }
+            }
+            // Get and set duration and title values as MediaMetadata for MediaControlView2
+            MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+            // TODO: Get title via other public APIs.
+            /*
+            if (mMetadata != null && mMetadata.has(Metadata.TITLE)) {
+                mTitle = mMetadata.getString(Metadata.TITLE);
+            }
+            */
+            builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+            builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+            if (mMediaSession != null) {
+                mMediaSession.setMetadata(builder.build());
+
+                // TODO: merge this code with the above code when integrating with
+                // MediaSession2.
+                if (mNeedUpdateMediaType) {
+                    mMediaSession.sendSessionEvent(
+                            MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
+                    mNeedUpdateMediaType = false;
+                }
+            }
+        }
+    };
+
+    MediaPlayer.OnSeekCompleteListener mSeekCompleteListener =
+            new MediaPlayer.OnSeekCompleteListener() {
+                @Override
+                public void onSeekComplete(MediaPlayer mp) {
+                    updatePlaybackState();
+                }
+    };
+
+    MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
+        @Override
+        public void onCompletion(MediaPlayer mp) {
+            mCurrentState = STATE_PLAYBACK_COMPLETED;
+            mTargetState = STATE_PLAYBACK_COMPLETED;
+            updatePlaybackState();
+            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                mAudioManager.abandonAudioFocus(null);
+            }
+        }
+    };
+
+    MediaPlayer.OnInfoListener mInfoListener = new MediaPlayer.OnInfoListener() {
+        @Override
+        public boolean onInfo(MediaPlayer mp, int what, int extra) {
+            if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE) {
+                extractTracks();
+            }
+            return true;
+        }
+    };
+
+    MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() {
+        @Override
+        public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
+            if (DEBUG) {
+                Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+            }
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            updatePlaybackState();
+
+            if (mMediaControlView != null) {
+                mMediaControlView.setVisibility(View.GONE);
+            }
+            return true;
+        }
+    };
+
+    MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+            new MediaPlayer.OnBufferingUpdateListener() {
+        @Override
+        public void onBufferingUpdate(MediaPlayer mp, int percent) {
+            mCurrentBufferPercentage = percent;
+            updatePlaybackState();
+        }
+    };
+
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        @Override
+        public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+            if (isRemotePlayback()) {
+                // TODO (b/77158231)
+                // mRoutePlayer.onCommand(command, args, receiver);
+            } else {
+                switch (command) {
+                    case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+                        /*
+                        int subtitleIndex = args.getInt(
+                                MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
+                                INVALID_TRACK_INDEX);
+                        if (subtitleIndex != INVALID_TRACK_INDEX) {
+                            int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
+                            if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
+                                mSelectedSubtitleTrackIndex = subtitleTrackIndex;
+                                setSubtitleEnabled(true);
+                            }
+                        }
+                        */
+                        break;
+                    case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+                        setSubtitleEnabled(false);
+                        break;
+                    case MediaControlView2.COMMAND_SET_FULLSCREEN:
+                        if (mFullScreenRequestListener != null) {
+                            mFullScreenRequestListener.onFullScreenRequest(
+                                    VideoView2.this,
+                                    args.getBoolean(MediaControlView2.ARGUMENT_KEY_FULLSCREEN));
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
+                        int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
+                                INVALID_TRACK_INDEX);
+                        if (audioIndex != INVALID_TRACK_INDEX) {
+                            int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
+                            if (audioTrackIndex != mSelectedAudioTrackIndex) {
+                                mSelectedAudioTrackIndex = audioTrackIndex;
+                                mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
+                            }
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
+                        float speed = args.getFloat(
+                                MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
+                        if (speed != INVALID_SPEED && speed != mSpeed) {
+                            setSpeed(speed);
+                            mSpeed = speed;
+                        }
+                        break;
+                    case MediaControlView2.COMMAND_MUTE:
+                        mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
+                        break;
+                    case MediaControlView2.COMMAND_UNMUTE:
+                        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
+                        break;
+                }
+            }
+            showController();
+        }
+
+        @Override
+        public void onCustomAction(final String action, final Bundle extras) {
+            mCustomActionListenerRecord.first.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCustomActionListenerRecord.second.onCustomAction(action, extras);
+                }
+            });
+            showController();
+        }
+
+        @Override
+        public void onPlay() {
+            if (!isAudioGranted()) {
+                requestAudioFocus(mAudioFocusType);
+            }
+
+            if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
+                if (isRemotePlayback()) {
+                    // TODO (b/77158231)
+                    // mRoutePlayer.onPlay();
+                } else {
+                    applySpeed();
+                    mMediaPlayer.start();
+                    mCurrentState = STATE_PLAYING;
+                    updatePlaybackState();
+                }
+                mCurrentState = STATE_PLAYING;
+            }
+            mTargetState = STATE_PLAYING;
+            if (DEBUG) {
+                Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            showController();
+        }
+
+        @Override
+        public void onPause() {
+            if (isInPlaybackState()) {
+                if (isRemotePlayback()) {
+                    // TODO (b/77158231)
+                    // mRoutePlayer.onPause();
+                    mCurrentState = STATE_PAUSED;
+                } else if (mMediaPlayer.isPlaying()) {
+                    mMediaPlayer.pause();
+                    mCurrentState = STATE_PAUSED;
+                    updatePlaybackState();
+                }
+            }
+            mTargetState = STATE_PAUSED;
+            if (DEBUG) {
+                Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+                        + ", mTargetState=" + mTargetState);
+            }
+            showController();
+        }
+
+        @Override
+        public void onSeekTo(long pos) {
+            if (isInPlaybackState()) {
+                if (isRemotePlayback()) {
+                    // TODO (b/77158231)
+                    // mRoutePlayer.onSeekTo(pos);
+                } else {
+                    // TODO Refactor VideoView2 with FooImplBase and FooImplApiXX.
+                    if (android.os.Build.VERSION.SDK_INT < 26) {
+                        mMediaPlayer.seekTo((int) pos);
+                    } else {
+                        mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
+                    }
+                    mSeekWhenPrepared = 0;
+                }
+            } else {
+                mSeekWhenPrepared = pos;
+            }
+            showController();
+        }
+
+        @Override
+        public void onStop() {
+            if (isRemotePlayback()) {
+                // TODO (b/77158231)
+                // mRoutePlayer.onStop();
+            } else {
+                resetPlayer();
+            }
+            showController();
+        }
+    }
+}
diff --git a/androidx/widget/VideoView2Test.java b/androidx/widget/VideoView2Test.java
new file mode 100644
index 0000000..7f3dcfc
--- /dev/null
+++ b/androidx/widget/VideoView2Test.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import static android.content.Context.KEYGUARD_SERVICE;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.media.AudioAttributesCompat;
+import androidx.media.test.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * Test {@link VideoView2}.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // TODO: KITKAT
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class VideoView2Test {
+    /** Debug TAG. **/
+    private static final String TAG = "VideoView2Test";
+    /** The maximum time to wait for an operation. */
+    private static final long   TIME_OUT = 15000L;
+    /** The interval time to wait for completing an operation. */
+    private static final long   OPERATION_INTERVAL  = 1500L;
+    /** The duration of R.raw.testvideo. */
+    private static final int    TEST_VIDEO_DURATION = 11047;
+    /** The full name of R.raw.testvideo. */
+    private static final String VIDEO_NAME   = "testvideo.3gp";
+    /** delta for duration in case user uses different decoders on different
+        hardware that report a duration that's different by a few milliseconds */
+    private static final int DURATION_DELTA = 100;
+    /** AudioAttributes to be used by this player */
+    private static final AudioAttributesCompat AUDIO_ATTR = new AudioAttributesCompat.Builder()
+            .setUsage(AudioAttributes.USAGE_GAME)
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .build();
+    private Instrumentation mInstrumentation;
+    private Activity mActivity;
+    private KeyguardManager mKeyguardManager;
+    private VideoView2 mVideoView;
+    private MediaControllerCompat mController;
+    private String mVideoPath;
+
+    @Rule
+    public ActivityTestRule<VideoView2TestActivity> mActivityRule =
+            new ActivityTestRule<>(VideoView2TestActivity.class);
+
+    @Before
+    public void setup() throws Throwable {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mKeyguardManager = (KeyguardManager)
+                mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
+        mActivity = mActivityRule.getActivity();
+        mVideoView = (VideoView2) mActivity.findViewById(R.id.videoview);
+        mVideoPath = prepareSampleVideo();
+
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // Keep screen on while testing.
+                mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                mActivity.setTurnScreenOn(true);
+                mActivity.setShowWhenLocked(true);
+                mKeyguardManager.requestDismissKeyguard(mActivity, null);
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+
+        final View.OnAttachStateChangeListener mockAttachListener =
+                mock(View.OnAttachStateChangeListener.class);
+        if (!mVideoView.isAttachedToWindow()) {
+            mVideoView.addOnAttachStateChangeListener(mockAttachListener);
+            verify(mockAttachListener, timeout(TIME_OUT)).onViewAttachedToWindow(same(mVideoView));
+        }
+        mController = mVideoView.getMediaController();
+    }
+
+    @After
+    public void tearDown() throws Throwable {
+        /** call media controller's stop */
+    }
+
+    private boolean hasCodec() {
+        return MediaUtils2.hasCodecsForResource(mActivity, R.raw.testvideo);
+    }
+
+    private String prepareSampleVideo() throws IOException {
+        try (InputStream source = mActivity.getResources().openRawResource(R.raw.testvideo);
+             OutputStream target = mActivity.openFileOutput(VIDEO_NAME, Context.MODE_PRIVATE)) {
+            final byte[] buffer = new byte[1024];
+            for (int len = source.read(buffer); len > 0; len = source.read(buffer)) {
+                target.write(buffer, 0, len);
+            }
+        }
+
+        return mActivity.getFileStreamPath(VIDEO_NAME).getAbsolutePath();
+    }
+
+    @UiThreadTest
+    @Test
+    public void testConstructor() {
+        new VideoView2(mActivity);
+        new VideoView2(mActivity, null);
+        new VideoView2(mActivity, null, 0);
+    }
+
+    @Test
+    public void testPlayVideo() throws Throwable {
+        // Don't run the test if the codec isn't supported.
+        if (!hasCodec()) {
+            Log.i(TAG, "SKIPPING testPlayVideo(): codec is not supported");
+            return;
+        }
+
+        final MediaControllerCompat.Callback mockControllerCallback =
+                mock(MediaControllerCompat.Callback.class);
+        final MediaControllerCompat.Callback callbackHelper = new MediaControllerCompat.Callback() {
+            @Override
+            public void onPlaybackStateChanged(PlaybackStateCompat state) {
+                mockControllerCallback.onPlaybackStateChanged(state);
+            }
+        };
+
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mController.registerCallback(callbackHelper);
+                mVideoView.setVideoPath(mVideoPath);
+                mController.getTransportControls().play();
+            }
+        });
+        ArgumentCaptor<PlaybackStateCompat> someState =
+                ArgumentCaptor.forClass(PlaybackStateCompat.class);
+        verify(mockControllerCallback, timeout(TIME_OUT).atLeast(3)).onPlaybackStateChanged(
+                someState.capture());
+        List<PlaybackStateCompat> states = someState.getAllValues();
+        assertEquals(PlaybackStateCompat.STATE_PAUSED, states.get(0).getState());
+        assertEquals(PlaybackStateCompat.STATE_PLAYING, states.get(1).getState());
+        assertEquals(PlaybackStateCompat.STATE_STOPPED, states.get(2).getState());
+    }
+
+    @Test
+    public void testPlayVideoOnTextureView() throws Throwable {
+        // Don't run the test if the codec isn't supported.
+        if (!hasCodec()) {
+            Log.i(TAG, "SKIPPING testPlayVideoOnTextureView(): codec is not supported");
+            return;
+        }
+        final VideoView2.OnViewTypeChangedListener mockViewTypeListener =
+                mock(VideoView2.OnViewTypeChangedListener.class);
+        final MediaControllerCompat.Callback mockControllerCallback =
+                mock(MediaControllerCompat.Callback.class);
+        final MediaControllerCompat.Callback callbackHelper = new MediaControllerCompat.Callback() {
+            @Override
+            public void onPlaybackStateChanged(PlaybackStateCompat state) {
+                mockControllerCallback.onPlaybackStateChanged(state);
+            }
+        };
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVideoView.setOnViewTypeChangedListener(mockViewTypeListener);
+                mVideoView.setViewType(mVideoView.VIEW_TYPE_TEXTUREVIEW);
+                mController.registerCallback(callbackHelper);
+                mVideoView.setVideoPath(mVideoPath);
+            }
+        });
+        verify(mockViewTypeListener, timeout(TIME_OUT))
+                .onViewTypeChanged(mVideoView, VideoView2.VIEW_TYPE_TEXTUREVIEW);
+
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mController.getTransportControls().play();
+            }
+        });
+        ArgumentCaptor<PlaybackStateCompat> someState =
+                ArgumentCaptor.forClass(PlaybackStateCompat.class);
+        verify(mockControllerCallback, timeout(TIME_OUT).atLeast(3)).onPlaybackStateChanged(
+                someState.capture());
+        List<PlaybackStateCompat> states = someState.getAllValues();
+        assertEquals(PlaybackStateCompat.STATE_PAUSED, states.get(0).getState());
+        assertEquals(PlaybackStateCompat.STATE_PLAYING, states.get(1).getState());
+        assertEquals(PlaybackStateCompat.STATE_STOPPED, states.get(2).getState());
+    }
+}
diff --git a/androidx/widget/VideoView2TestActivity.java b/androidx/widget/VideoView2TestActivity.java
new file mode 100644
index 0000000..26a9c92
--- /dev/null
+++ b/androidx/widget/VideoView2TestActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.media.test.R;
+
+/**
+ * A minimal application for {@link VideoView2} test.
+ */
+public class VideoView2TestActivity extends Activity {
+    /**
+     * Called with the activity is first created.
+     */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.videoview2_layout);
+    }
+}
diff --git a/androidx/widget/VideoViewInterface.java b/androidx/widget/VideoViewInterface.java
new file mode 100644
index 0000000..eca8c82
--- /dev/null
+++ b/androidx/widget/VideoViewInterface.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 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 androidx.widget;
+
+import android.media.MediaPlayer;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+interface VideoViewInterface {
+    /**
+     * Assigns the view's surface to the given MediaPlayer instance.
+     *
+     * @param mp MediaPlayer
+     * @return true if the surface is successfully assigned, false if not. It will fail to assign
+     *         if any of MediaPlayer or surface is unavailable.
+     */
+    boolean assignSurfaceToMediaPlayer(MediaPlayer mp);
+    void setSurfaceListener(SurfaceListener l);
+    int getViewType();
+    void setMediaPlayer(MediaPlayer mp);
+
+    /**
+     * Takes over oldView. It means that the MediaPlayer will start rendering on this view.
+     * The visibility of oldView will be set as {@link View.GONE}. If the view doesn't have a
+     * MediaPlayer instance or its surface is not available, the actual execution is deferred until
+     * a MediaPlayer instance is set by {@link #setMediaPlayer} or its surface becomes available.
+     * {@link SurfaceListener.onSurfaceTakeOverDone} will be called when the actual execution is
+     * done.
+     *
+     * @param oldView The view that MediaPlayer is currently rendering on.
+     */
+    void takeOver(@NonNull VideoViewInterface oldView);
+
+    /**
+     * Indicates if the view's surface is available.
+     *
+     * @return true if the surface is available.
+     */
+    boolean hasAvailableSurface();
+
+    /**
+     * An instance of VideoViewInterface calls these surface notification methods accordingly if
+     * a listener has been registered via {@link #setSurfaceListener(SurfaceListener)}.
+     */
+    interface SurfaceListener {
+        void onSurfaceCreated(View view, int width, int height);
+        void onSurfaceDestroyed(View view);
+        void onSurfaceChanged(View view, int width, int height);
+        void onSurfaceTakeOverDone(VideoViewInterface view);
+    }
+}
diff --git a/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index dbdf5e1..cdc3867 100644
--- a/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -30,6 +30,7 @@
 import android.net.NetworkRequest;
 import android.net.Proxy;
 import android.net.Uri;
+import android.net.dns.ResolvUtil;
 import android.net.http.SslError;
 import android.os.Build;
 import android.os.Bundle;
@@ -119,6 +120,8 @@
 
         // Also initializes proxy system properties.
         mCm.bindProcessToNetwork(mNetwork);
+        mCm.setProcessDefaultNetworkForHostResolution(
+                ResolvUtil.getNetworkWithUseLocalNameserversFlag(mNetwork));
 
         // Proxy system properties must be initialized before setContentView is called because
         // setContentView initializes the WebView logic which in turn reads the system properties.
diff --git a/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java b/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java
index 95ec83d..7479d9a 100644
--- a/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java
+++ b/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java
@@ -32,6 +32,7 @@
 import android.net.Proxy;
 import android.net.TrafficStats;
 import android.net.Uri;
+import android.net.dns.ResolvUtil;
 import android.net.http.SslError;
 import android.os.Bundle;
 import android.telephony.CarrierConfigManager;
@@ -115,6 +116,8 @@
             requestNetworkForCaptivePortal();
         } else {
             mCm.bindProcessToNetwork(mNetwork);
+            mCm.setProcessDefaultNetworkForHostResolution(
+                    ResolvUtil.getNetworkWithUseLocalNameserversFlag(mNetwork));
             // Start initial page load so WebView finishes loading proxy settings.
             // Actual load of mUrl is initiated by MyWebViewClient.
             mWebView.loadData("", "text/html", null);
diff --git a/com/android/clockwork/bluetooth/BluetoothShardRunner.java b/com/android/clockwork/bluetooth/BluetoothShardRunner.java
index 4f0fecd..43db87b 100644
--- a/com/android/clockwork/bluetooth/BluetoothShardRunner.java
+++ b/com/android/clockwork/bluetooth/BluetoothShardRunner.java
@@ -69,12 +69,12 @@
 
     @MainThread
     void stopProxyShard() {
-        mCompanionShardStops += 1;
         if (mProxyShard != null) {
              if (Log.isLoggable(TAG, Log.DEBUG)) {
                  Log.d(TAG, "BluetoothShardRunner Stopping CompanionProxyShard.");
              }
             Util.close(mProxyShard);
+            mCompanionShardStops += 1;
         }
         mProxyShard = null;
     }
diff --git a/com/android/clockwork/bluetooth/CompanionProxyShard.java b/com/android/clockwork/bluetooth/CompanionProxyShard.java
index 3f04725..05b0bf8 100644
--- a/com/android/clockwork/bluetooth/CompanionProxyShard.java
+++ b/com/android/clockwork/bluetooth/CompanionProxyShard.java
@@ -246,11 +246,6 @@
                         Log.d(TAG, "CompanionProxyShard [ " + mInstance + " ] Request to start"
                                 + " companion sysproxy network");
                     }
-                    if (companionIsNotAvailable()) {
-                        Log.e(TAG, "CompanionProxyShard [ " + mInstance + " ] Unable to start"
-                                + " sysproxy bluetooth off or companion unpaired");
-                        return;
-                    }
                     if (mState.checkState(State.SYSPROXY_CONNECTED)) {
                         if (Log.isLoggable(TAG, Log.DEBUG)) {
                             Log.d(TAG, "CompanionProxyShard [ " + mInstance + " ] companion proxy"
@@ -302,6 +297,7 @@
                     }
                     if (!mIsClosed && mState.current() != State.SYSPROXY_DISCONNECTED) {
                         disconnectAndNotify(Reason.SYSPROXY_DISCONNECTED);
+                        setUpRetry();
                     }
                     mState.advanceState(mInstance, State.SYSPROXY_DISCONNECTED);
                     break;
@@ -324,18 +320,22 @@
                     mHandler.removeMessages(WHAT_START_SYSPROXY);
                     mHandler.removeMessages(WHAT_RESET_CONNECTION);
                     disconnectNativeInBackground();
-                    // Setup a reconnect sequence if shard has not been closed.
-                    if (!mIsClosed) {
-                        final int nextRetry = mReconnectBackoff.getNextBackoff();
-                        mHandler.sendEmptyMessageDelayed(WHAT_START_SYSPROXY, nextRetry * 1000);
-                        Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Proxy reset"
-                                + " Attempting reconnect in " + nextRetry + " seconds");
-                    }
+                    setUpRetry();
                     break;
             }
         }
     };
 
+    private void setUpRetry() {
+        // Setup a reconnect sequence if shard has not been closed.
+        if (!mIsClosed) {
+            final int nextRetry = mReconnectBackoff.getNextBackoff();
+            mHandler.sendEmptyMessageDelayed(WHAT_START_SYSPROXY, nextRetry * 1000);
+            Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Proxy reset"
+                    + " Attempting reconnect in " + nextRetry + " seconds");
+        }
+    }
+
     /** Use binder API to directly request rfcomm socket from bluetooth module */
     @MainThread
     private void getBluetoothSocket() {
@@ -571,29 +571,6 @@
         }
     }
 
-    /** Check if bluetooth is on and companion paired before connecting to sysproxy */
-    private boolean companionIsNotAvailable() {
-        return !isBluetoothOn() || companionHasBecomeUnpaired();
-    }
-
-    private boolean companionHasBecomeUnpaired() {
-        final boolean unpaired = mCompanionDevice.getBondState() == BluetoothDevice.BOND_NONE;
-        if (unpaired) {
-            Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Companion has become unpaired");
-        }
-        return unpaired;
-    }
-
-    private boolean isBluetoothOn() {
-        final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        if (adapter != null && adapter.isEnabled()) {
-            return true;
-        }
-        Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Bluetooth adapter is off or in"
-                + " unknown state");
-        return false;
-    }
-
     private abstract static class DefaultPriorityAsyncTask<Params, Progress, Result>
             extends AsyncTask<Params, Progress, Result> {
 
diff --git a/com/android/clockwork/bluetooth/CompanionProxyShardTest.java b/com/android/clockwork/bluetooth/CompanionProxyShardTest.java
index 386e00b..7a23747 100644
--- a/com/android/clockwork/bluetooth/CompanionProxyShardTest.java
+++ b/com/android/clockwork/bluetooth/CompanionProxyShardTest.java
@@ -224,12 +224,14 @@
                 CompanionProxyShard.State.SYSPROXY_DISCONNECTED);
         when(mockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
 
+        ShadowLooper.pauseMainLooper();
         mCompanionProxyShard.onStartNetwork();
 
+        ShadowLooper.runMainLooperOneTask();
         assertEquals(0, mCompanionProxyShard.connectNativeCount);
         verify(mockParcelFileDescriptor, never()).detachFd();
 
-        assertEquals(CompanionProxyShard.State.SYSPROXY_DISCONNECTED,
+        assertEquals(CompanionProxyShard.State.BLUETOOTH_SOCKET_REQUESTING,
                 mCompanionProxyShard.mState.current());
 
         ensureMessageQueueEmpty();
@@ -247,7 +249,7 @@
 
         ShadowLooper.runMainLooperOneTask();
 
-        assertEquals(CompanionProxyShard.State.SYSPROXY_DISCONNECTED,
+        assertEquals(CompanionProxyShard.State.BLUETOOTH_SOCKET_REQUESTING,
                 mCompanionProxyShard.mState.current());
         ensureMessageQueueEmpty();
         // Restore bluetooth adapter to return a valid instance
@@ -261,11 +263,12 @@
 
         when(mockBluetoothAdapter.isEnabled()).thenReturn(false);
 
+        ShadowLooper.pauseMainLooper();
         mCompanionProxyShard.onStartNetwork();
 
         ShadowLooper.runMainLooperOneTask();
 
-        assertEquals(CompanionProxyShard.State.SYSPROXY_DISCONNECTED,
+        assertEquals(CompanionProxyShard.State.BLUETOOTH_SOCKET_REQUESTING,
                 mCompanionProxyShard.mState.current());
         ensureMessageQueueEmpty();
      }
diff --git a/com/android/clockwork/bluetooth/WearBluetoothMediator.java b/com/android/clockwork/bluetooth/WearBluetoothMediator.java
index 1ed769c..14034bf 100644
--- a/com/android/clockwork/bluetooth/WearBluetoothMediator.java
+++ b/com/android/clockwork/bluetooth/WearBluetoothMediator.java
@@ -509,7 +509,6 @@
         @WorkerThread
         @Override
         public void handleMessage(Message msg) {
-            Log.e(TAG, "CMM handleMessage" + msg);
             if (Log.isLoggable(TAG, Log.DEBUG)) {
                 Log.d(TAG, "handleMessage: " + msg);
             }
diff --git a/com/android/ex/photo/ActionBarWrapper.java b/com/android/ex/photo/ActionBarWrapper.java
index ae62197..6d4d4d2 100644
--- a/com/android/ex/photo/ActionBarWrapper.java
+++ b/com/android/ex/photo/ActionBarWrapper.java
@@ -1,8 +1,7 @@
 package com.android.ex.photo;
 
-
+import android.app.ActionBar;
 import android.graphics.drawable.Drawable;
-import android.support.v7.app.ActionBar;
 
 /**
  * Wrapper around {@link ActionBar}.
diff --git a/com/android/ex/photo/PhotoViewActivity.java b/com/android/ex/photo/PhotoViewActivity.java
index a5c4a43..7b53918 100644
--- a/com/android/ex/photo/PhotoViewActivity.java
+++ b/com/android/ex/photo/PhotoViewActivity.java
@@ -21,14 +21,14 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
+import android.support.v4.app.FragmentActivity;
 import android.view.Menu;
 import android.view.MenuItem;
 
 /**
  * Activity to view the contents of an album.
  */
-public class PhotoViewActivity extends AppCompatActivity
+public class PhotoViewActivity extends FragmentActivity
         implements PhotoViewController.ActivityInterface {
 
     private PhotoViewController mController;
@@ -41,7 +41,7 @@
         mController.onCreate(savedInstanceState);
     }
 
-    protected PhotoViewController createController() {
+    public PhotoViewController createController() {
         return new PhotoViewController(this);
     }
 
@@ -122,7 +122,7 @@
     @Override
     public ActionBarInterface getActionBarInterface() {
         if (mActionBar == null) {
-            mActionBar = new ActionBarWrapper(getSupportActionBar());
+            mActionBar = new ActionBarWrapper(getActionBar());
         }
         return mActionBar;
     }
diff --git a/com/android/ims/MmTelFeatureConnection.java b/com/android/ims/MmTelFeatureConnection.java
index de8f928..b2472c8 100644
--- a/com/android/ims/MmTelFeatureConnection.java
+++ b/com/android/ims/MmTelFeatureConnection.java
@@ -451,7 +451,11 @@
         mRegistrationCallbackManager.close();
         mCapabilityCallbackManager.close();
         try {
-            getServiceInterface(mBinder).setListener(null);
+            synchronized (mLock) {
+                if (isBinderAlive()) {
+                    getServiceInterface(mBinder).setListener(null);
+                }
+            }
         } catch (RemoteException e) {
             Log.w(TAG, "closeConnection: couldn't remove listener!");
         }
diff --git a/com/android/internal/app/ColorDisplayController.java b/com/android/internal/app/ColorDisplayController.java
index 278d31a..f1539ee 100644
--- a/com/android/internal/app/ColorDisplayController.java
+++ b/com/android/internal/app/ColorDisplayController.java
@@ -365,6 +365,10 @@
      * Get the current color mode.
      */
     public int getColorMode() {
+        if (getAccessibilityTransformActivated()) {
+            return COLOR_MODE_SATURATED;
+        }
+
         final int colorMode = System.getIntForUser(mContext.getContentResolver(),
             System.DISPLAY_COLOR_MODE, -1, mUserId);
         if (colorMode < COLOR_MODE_NATURAL || colorMode > COLOR_MODE_SATURATED) {
@@ -416,6 +420,18 @@
                 R.integer.config_nightDisplayColorTemperatureDefault);
     }
 
+    /**
+     * Returns true if any Accessibility color transforms are enabled.
+     */
+    public boolean getAccessibilityTransformActivated() {
+        final ContentResolver cr = mContext.getContentResolver();
+        return
+            Secure.getIntForUser(cr, Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
+                    0, mUserId) == 1
+            || Secure.getIntForUser(cr, Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
+                    0, mUserId) == 1;
+    }
+
     private void onSettingChanged(@NonNull String setting) {
         if (DEBUG) {
             Slog.d(TAG, "onSettingChanged: " + setting);
@@ -441,6 +457,10 @@
                 case System.DISPLAY_COLOR_MODE:
                     mCallback.onDisplayColorModeChanged(getColorMode());
                     break;
+                case Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED:
+                case Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED:
+                    mCallback.onAccessibilityTransformChanged(getAccessibilityTransformActivated());
+                    break;
             }
         }
     }
@@ -471,6 +491,12 @@
                         false /* notifyForDescendants */, mContentObserver, mUserId);
                 cr.registerContentObserver(System.getUriFor(System.DISPLAY_COLOR_MODE),
                         false /* notifyForDecendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(
+                        Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED),
+                        false /* notifyForDecendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(
+                        Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED),
+                        false /* notifyForDecendants */, mContentObserver, mUserId);
             }
         }
     }
@@ -531,5 +557,12 @@
          * @param displayColorMode the color mode
          */
         default void onDisplayColorModeChanged(int displayColorMode) {}
+
+        /**
+         * Callback invoked when Accessibility color transforms change.
+         *
+         * @param state the state Accessibility color transforms (true of active)
+         */
+        default void onAccessibilityTransformChanged(boolean state) {}
     }
 }
diff --git a/com/android/internal/app/SuspendedAppActivity.java b/com/android/internal/app/SuspendedAppActivity.java
new file mode 100644
index 0000000..25af355
--- /dev/null
+++ b/com/android/internal/app/SuspendedAppActivity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 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.internal.app;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Slog;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+
+public class SuspendedAppActivity extends AlertActivity
+        implements DialogInterface.OnClickListener {
+    private static final String TAG = "SuspendedAppActivity";
+
+    public static final String EXTRA_SUSPENDED_PACKAGE =
+            "SuspendedAppActivity.extra.SUSPENDED_PACKAGE";
+    public static final String EXTRA_SUSPENDING_PACKAGE =
+            "SuspendedAppActivity.extra.SUSPENDING_PACKAGE";
+    public static final String EXTRA_DIALOG_MESSAGE = "SuspendedAppActivity.extra.DIALOG_MESSAGE";
+    public static final String EXTRA_MORE_DETAILS_INTENT =
+            "SuspendedAppActivity.extra.MORE_DETAILS_INTENT";
+
+    private Intent mMoreDetailsIntent;
+    private int mUserId;
+
+    private CharSequence getAppLabel(String packageName) {
+        final PackageManager pm = getPackageManager();
+        try {
+            return pm.getApplicationInfoAsUser(packageName, 0, mUserId).loadLabel(pm);
+        } catch (PackageManager.NameNotFoundException ne) {
+            Slog.e(TAG, "Package " + packageName + " not found", ne);
+        }
+        return packageName;
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+        super.onCreate(icicle);
+
+        final Intent intent = getIntent();
+        mMoreDetailsIntent = intent.getParcelableExtra(EXTRA_MORE_DETAILS_INTENT);
+        mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, -1);
+        if (mUserId < 0) {
+            Slog.wtf(TAG, "Invalid user: " + mUserId);
+            finish();
+            return;
+        }
+        final String suppliedMessage = intent.getStringExtra(EXTRA_DIALOG_MESSAGE);
+        final CharSequence suspendedAppLabel = getAppLabel(
+                intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE));
+        final CharSequence dialogMessage;
+        if (suppliedMessage == null) {
+            dialogMessage = getString(R.string.app_suspended_default_message,
+                    suspendedAppLabel,
+                    getAppLabel(intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE)));
+        } else {
+            dialogMessage = String.format(getResources().getConfiguration().getLocales().get(0),
+                    suppliedMessage, suspendedAppLabel);
+        }
+
+        final AlertController.AlertParams ap = mAlertParams;
+        ap.mTitle = getString(R.string.app_suspended_title);
+        ap.mMessage = dialogMessage;
+        ap.mPositiveButtonText = getString(android.R.string.ok);
+        if (mMoreDetailsIntent != null) {
+            ap.mNeutralButtonText = getString(R.string.app_suspended_more_details);
+        }
+        ap.mPositiveButtonListener = ap.mNeutralButtonListener = this;
+        setupAlert();
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        switch (which) {
+            case AlertDialog.BUTTON_NEUTRAL:
+                startActivityAsUser(mMoreDetailsIntent, UserHandle.of(mUserId));
+                Slog.i(TAG, "Started more details activity");
+                break;
+        }
+        finish();
+    }
+}
diff --git a/com/android/internal/os/BatteryStatsHelper.java b/com/android/internal/os/BatteryStatsHelper.java
index 1e5bd18..b49aace 100644
--- a/com/android/internal/os/BatteryStatsHelper.java
+++ b/com/android/internal/os/BatteryStatsHelper.java
@@ -657,7 +657,7 @@
      * {@link #removeHiddenBatterySippers(List)}.
      */
     private void addAmbientDisplayUsage() {
-        long ambientDisplayMs = mStats.getScreenDozeTime(mRawRealtimeUs, mStatsType);
+        long ambientDisplayMs = mStats.getScreenDozeTime(mRawRealtimeUs, mStatsType) / 1000;
         double power = mPowerProfile.getAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY)
                 * ambientDisplayMs / (60 * 60 * 1000);
         if (power > 0) {
diff --git a/com/android/internal/os/BatteryStatsImpl.java b/com/android/internal/os/BatteryStatsImpl.java
index 89f6156..5da3874 100644
--- a/com/android/internal/os/BatteryStatsImpl.java
+++ b/com/android/internal/os/BatteryStatsImpl.java
@@ -21,10 +21,13 @@
 import android.app.ActivityManager;
 import android.bluetooth.BluetoothActivityEnergyInfo;
 import android.bluetooth.UidTraffic;
+import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.database.ContentObserver;
+import android.hardware.usb.UsbManager;
 import android.net.ConnectivityManager;
 import android.net.NetworkStats;
 import android.net.Uri;
@@ -766,7 +769,10 @@
     int mCameraOnNesting;
     StopwatchTimer mCameraOnTimer;
 
-    int mUsbDataState; // 0: unknown, 1: disconnected, 2: connected
+    private static final int USB_DATA_UNKNOWN = 0;
+    private static final int USB_DATA_DISCONNECTED = 1;
+    private static final int USB_DATA_CONNECTED = 2;
+    int mUsbDataState = USB_DATA_UNKNOWN;
 
     int mGpsSignalQualityBin = -1;
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -5241,8 +5247,30 @@
         }
     }
 
-    public void noteUsbConnectionStateLocked(boolean connected) {
-        int newState = connected ? 2 : 1;
+    private void registerUsbStateReceiver(Context context) {
+        final IntentFilter usbStateFilter = new IntentFilter();
+        usbStateFilter.addAction(UsbManager.ACTION_USB_STATE);
+        context.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final boolean state = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false);
+                synchronized (BatteryStatsImpl.this) {
+                    noteUsbConnectionStateLocked(state);
+                }
+            }
+        }, usbStateFilter);
+        synchronized (this) {
+            if (mUsbDataState == USB_DATA_UNKNOWN) {
+                final Intent usbState = context.registerReceiver(null, usbStateFilter);
+                final boolean initState = usbState != null && usbState.getBooleanExtra(
+                        UsbManager.USB_CONNECTED, false);
+                noteUsbConnectionStateLocked(initState);
+            }
+        }
+    }
+
+    private void noteUsbConnectionStateLocked(boolean connected) {
+        int newState = connected ? USB_DATA_CONNECTED : USB_DATA_DISCONNECTED;
         if (mUsbDataState != newState) {
             mUsbDataState = newState;
             if (connected) {
@@ -13218,6 +13246,7 @@
 
     public void systemServicesReady(Context context) {
         mConstants.startObserving(context.getContentResolver());
+        registerUsbStateReceiver(context);
     }
 
     @VisibleForTesting
diff --git a/com/android/internal/os/RuntimeInit.java b/com/android/internal/os/RuntimeInit.java
index bb5a0ad..a9cd5c8 100644
--- a/com/android/internal/os/RuntimeInit.java
+++ b/com/android/internal/os/RuntimeInit.java
@@ -34,6 +34,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.Objects;
 import java.util.TimeZone;
 import java.util.logging.LogManager;
 import org.apache.harmony.luni.internal.util.TimezoneGetter;
@@ -67,8 +68,12 @@
      * but apps can override that behavior.
      */
     private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
+        public volatile boolean mTriggered = false;
+
         @Override
         public void uncaughtException(Thread t, Throwable e) {
+            mTriggered = true;
+
             // Don't re-enter if KillApplicationHandler has already run
             if (mCrashing) return;
 
@@ -96,12 +101,33 @@
     /**
      * Handle application death from an uncaught exception.  The framework
      * catches these for the main threads, so this should only matter for
-     * threads created by applications.  Before this method runs,
-     * {@link LoggingHandler} will already have logged details.
+     * threads created by applications. Before this method runs, the given
+     * instance of {@link LoggingHandler} should already have logged details
+     * (and if not it is run first).
      */
     private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
+        private final LoggingHandler mLoggingHandler;
+
+        /**
+         * Create a new KillApplicationHandler that follows the given LoggingHandler.
+         * If {@link #uncaughtException(Thread, Throwable) uncaughtException} is called
+         * on the created instance without {@code loggingHandler} having been triggered,
+         * {@link LoggingHandler#uncaughtException(Thread, Throwable)
+         * loggingHandler.uncaughtException} will be called first.
+         *
+         * @param loggingHandler the {@link LoggingHandler} expected to have run before
+         *     this instance's {@link #uncaughtException(Thread, Throwable) uncaughtException}
+         *     is being called.
+         */
+        public KillApplicationHandler(LoggingHandler loggingHandler) {
+            this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
+        }
+
+        @Override
         public void uncaughtException(Thread t, Throwable e) {
             try {
+                ensureLogging(t, e);
+
                 // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
                 if (mCrashing) return;
                 mCrashing = true;
@@ -132,6 +158,33 @@
                 System.exit(10);
             }
         }
+
+        /**
+         * Ensures that the logging handler has been triggered.
+         *
+         * See b/73380984. This reinstates the pre-O behavior of
+         *
+         *   {@code thread.getUncaughtExceptionHandler().uncaughtException(thread, e);}
+         *
+         * logging the exception (in addition to killing the app). This behavior
+         * was never documented / guaranteed but helps in diagnostics of apps
+         * using the pattern.
+         *
+         * If this KillApplicationHandler is invoked the "regular" way (by
+         * {@link Thread#dispatchUncaughtException(Throwable)
+         * Thread.dispatchUncaughtException} in case of an uncaught exception)
+         * then the pre-handler (expected to be {@link #mLoggingHandler}) will already
+         * have run. Otherwise, we manually invoke it here.
+         */
+        private void ensureLogging(Thread t, Throwable e) {
+            if (!mLoggingHandler.mTriggered) {
+                try {
+                    mLoggingHandler.uncaughtException(t, e);
+                } catch (Throwable loggingThrowable) {
+                    // Ignored.
+                }
+            }
+        }
     }
 
     protected static final void commonInit() {
@@ -141,8 +194,9 @@
          * set handlers; these apply to all threads in the VM. Apps can replace
          * the default handler, but not the pre handler.
          */
-        Thread.setUncaughtExceptionPreHandler(new LoggingHandler());
-        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler());
+        LoggingHandler loggingHandler = new LoggingHandler();
+        Thread.setUncaughtExceptionPreHandler(loggingHandler);
+        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
 
         /*
          * Install a TimezoneGetter subclass for ZoneInfo.db
diff --git a/com/android/internal/os/ZygoteConnection.java b/com/android/internal/os/ZygoteConnection.java
index 5d40a73..f537e3e 100644
--- a/com/android/internal/os/ZygoteConnection.java
+++ b/com/android/internal/os/ZygoteConnection.java
@@ -166,6 +166,11 @@
             return null;
         }
 
+        if (parsedArgs.hiddenApiAccessLogSampleRate != -1) {
+            handleHiddenApiAccessLogSampleRate(parsedArgs.hiddenApiAccessLogSampleRate);
+            return null;
+        }
+
         if (parsedArgs.permittedCapabilities != 0 || parsedArgs.effectiveCapabilities != 0) {
             throw new ZygoteSecurityException("Client may not specify capabilities: " +
                     "permitted=0x" + Long.toHexString(parsedArgs.permittedCapabilities) +
@@ -294,6 +299,15 @@
         }
     }
 
+    private void handleHiddenApiAccessLogSampleRate(int percent) {
+        try {
+            ZygoteInit.setHiddenApiAccessLogSampleRate(percent);
+            mSocketOutStream.writeInt(0);
+        } catch (IOException ioe) {
+            throw new IllegalStateException("Error writing to command socket", ioe);
+        }
+    }
+
     protected void preload() {
         ZygoteInit.lazyPreload();
     }
@@ -461,6 +475,12 @@
         String[] apiBlacklistExemptions;
 
         /**
+         * Sampling rate for logging hidden API accesses to the event log. This is sent to the
+         * pre-forked zygote at boot time, or when it changes, via --hidden-api-log-sampling-rate.
+         */
+        int hiddenApiAccessLogSampleRate = -1;
+
+        /**
          * Constructs instance and parses args
          * @param args zygote command-line args
          * @throws IllegalArgumentException
@@ -483,6 +503,7 @@
 
             boolean seenRuntimeArgs = false;
 
+            boolean expectRuntimeArgs = true;
             for ( /* curArg */ ; curArg < args.length; curArg++) {
                 String arg = args[curArg];
 
@@ -612,6 +633,7 @@
                     preloadPackageCacheKey = args[++curArg];
                 } else if (arg.equals("--preload-default")) {
                     preloadDefault = true;
+                    expectRuntimeArgs = false;
                 } else if (arg.equals("--start-child-zygote")) {
                     startChildZygote = true;
                 } else if (arg.equals("--set-api-blacklist-exemptions")) {
@@ -619,6 +641,16 @@
                     // with the regular fork command.
                     apiBlacklistExemptions = Arrays.copyOfRange(args, curArg + 1, args.length);
                     curArg = args.length;
+                    expectRuntimeArgs = false;
+                } else if (arg.startsWith("--hidden-api-log-sampling-rate=")) {
+                    String rateStr = arg.substring(arg.indexOf('=') + 1);
+                    try {
+                        hiddenApiAccessLogSampleRate = Integer.parseInt(rateStr);
+                    } catch (NumberFormatException nfe) {
+                        throw new IllegalArgumentException(
+                                "Invalid log sampling rate: " + rateStr, nfe);
+                    }
+                    expectRuntimeArgs = false;
                 } else {
                     break;
                 }
@@ -633,7 +665,7 @@
                     throw new IllegalArgumentException(
                             "Unexpected arguments after --preload-package.");
                 }
-            } else if (!preloadDefault && apiBlacklistExemptions == null) {
+            } else if (expectRuntimeArgs) {
                 if (!seenRuntimeArgs) {
                     throw new IllegalArgumentException("Unexpected argument : " + args[curArg]);
                 }
diff --git a/com/android/internal/os/ZygoteInit.java b/com/android/internal/os/ZygoteInit.java
index c5d41db..6f58365 100644
--- a/com/android/internal/os/ZygoteInit.java
+++ b/com/android/internal/os/ZygoteInit.java
@@ -26,8 +26,8 @@
 import android.icu.util.ULocale;
 import android.opengl.EGL14;
 import android.os.Build;
-import android.os.IInstalld;
 import android.os.Environment;
+import android.os.IInstalld;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -44,16 +44,16 @@
 import android.system.StructCapUserData;
 import android.system.StructCapUserHeader;
 import android.text.Hyphenator;
-import android.util.TimingsTraceLog;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
+import android.util.TimingsTraceLog;
 import android.webkit.WebViewFactory;
 import android.widget.TextView;
 
 import com.android.internal.logging.MetricsLogger;
-
 import com.android.internal.util.Preconditions;
+
 import dalvik.system.DexFile;
 import dalvik.system.VMRuntime;
 import dalvik.system.ZygoteHooks;
@@ -67,8 +67,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.security.Security;
 import java.security.Provider;
+import java.security.Security;
 
 /**
  * Startup class for the zygote process.
@@ -518,6 +518,10 @@
         VMRuntime.getRuntime().setHiddenApiExemptions(exemptions);
     }
 
+    public static void setHiddenApiAccessLogSampleRate(int percent) {
+        VMRuntime.getRuntime().setHiddenApiAccessLogSamplingRate(percent);
+    }
+
     /**
      * Creates a PathClassLoader for the given class path that is associated with a shared
      * namespace, i.e., this classloader can access platform-private native libraries. The
diff --git a/com/android/internal/telephony/CarrierIdentifier.java b/com/android/internal/telephony/CarrierIdentifier.java
index 56110a2..233adf1 100644
--- a/com/android/internal/telephony/CarrierIdentifier.java
+++ b/com/android/internal/telephony/CarrierIdentifier.java
@@ -568,7 +568,7 @@
 
         /*
          * Write Carrier Identification Matching event, logging with the
-         * carrierId, gid1 and carrier list version to differentiate below cases of metrics:
+         * carrierId, mccmnc, gid1 and carrier list version to differentiate below cases of metrics:
          * 1) unknown mccmnc - the Carrier Id provider contains no rule that matches the
          * read mccmnc.
          * 2) the Carrier Id provider contains some rule(s) that match the read mccmnc,
@@ -578,7 +578,8 @@
          */
         String unknownGid1ToLog = ((maxScore & CarrierMatchingRule.SCORE_GID1) == 0
                 && !TextUtils.isEmpty(subscriptionRule.mGid1)) ? subscriptionRule.mGid1 : null;
-        String unknownMccmncToLog = (maxScore == CarrierMatchingRule.SCORE_INVALID
+        String unknownMccmncToLog = ((maxScore == CarrierMatchingRule.SCORE_INVALID
+                || (maxScore & CarrierMatchingRule.SCORE_GID1) == 0)
                 && !TextUtils.isEmpty(subscriptionRule.mMccMnc)) ? subscriptionRule.mMccMnc : null;
         TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent(
                 mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId,
diff --git a/com/android/internal/telephony/InboundSmsTracker.java b/com/android/internal/telephony/InboundSmsTracker.java
index c63ccc8..36c6996 100644
--- a/com/android/internal/telephony/InboundSmsTracker.java
+++ b/com/android/internal/telephony/InboundSmsTracker.java
@@ -195,7 +195,7 @@
         mAddress = cursor.getString(InboundSmsHandler.ADDRESS_COLUMN);
         mDisplayAddress = cursor.getString(InboundSmsHandler.DISPLAY_ADDRESS_COLUMN);
 
-        if (cursor.isNull(InboundSmsHandler.COUNT_COLUMN)) {
+        if (cursor.getInt(InboundSmsHandler.COUNT_COLUMN) == 1) {
             // single-part message
             long rowId = cursor.getLong(InboundSmsHandler.ID_COLUMN);
             mReferenceNumber = -1;
@@ -250,8 +250,8 @@
             values.put("display_originating_addr", mDisplayAddress);
             values.put("reference_number", mReferenceNumber);
             values.put("sequence", mSequenceNumber);
-            values.put("count", mMessageCount);
         }
+        values.put("count", mMessageCount);
         values.put("message_body", mMessageBody);
         return values;
     }
diff --git a/com/android/internal/telephony/PhoneSubInfoController.java b/com/android/internal/telephony/PhoneSubInfoController.java
index 4cace9d..a371191 100644
--- a/com/android/internal/telephony/PhoneSubInfoController.java
+++ b/com/android/internal/telephony/PhoneSubInfoController.java
@@ -491,10 +491,6 @@
         return uiccApp.getIccRecords().getIccSimChallengeResponse(authType, data);
     }
 
-    public String getGroupIdLevel1(String callingPackage) {
-        return getGroupIdLevel1ForSubscriber(getDefaultSubscription(), callingPackage);
-    }
-
     public String getGroupIdLevel1ForSubscriber(int subId, String callingPackage) {
         Phone phone = getPhone(subId);
         if (phone != null) {
diff --git a/com/android/internal/telephony/ServiceStateTracker.java b/com/android/internal/telephony/ServiceStateTracker.java
index 6c94cb2..bdf8bb5 100644
--- a/com/android/internal/telephony/ServiceStateTracker.java
+++ b/com/android/internal/telephony/ServiceStateTracker.java
@@ -279,6 +279,7 @@
             if (DBG) log("SubscriptionListener.onSubscriptionInfoChanged");
             // Set the network type, in case the radio does not restore it.
             int subId = mPhone.getSubId();
+            ServiceStateTracker.this.mPrevSubId = mPreviousSubId.get();
             if (mPreviousSubId.getAndSet(subId) != subId) {
                 if (SubscriptionManager.isValidSubscriptionId(subId)) {
                     Context context = mPhone.getContext();
@@ -1064,7 +1065,9 @@
             case EVENT_SIM_READY:
                 // Reset the mPreviousSubId so we treat a SIM power bounce
                 // as a first boot.  See b/19194287
-                mOnSubscriptionsChangedListener.mPreviousSubId.set(-1);
+                mOnSubscriptionsChangedListener.mPreviousSubId.set(
+                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+                mPrevSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
                 mIsSimReady = true;
                 pollState();
                 // Signal strength polling stops when radio is off
@@ -3471,17 +3474,19 @@
     }
 
     /**
-     * Cancels all notifications posted to NotificationManager. These notifications for restricted
-     * state and rejection cause for cs registration are no longer valid after the SIM has been
-     * removed.
+     * Cancels all notifications posted to NotificationManager for this subId. These notifications
+     * for restricted state and rejection cause for cs registration are no longer valid after the
+     * SIM has been removed.
      */
     private void cancelAllNotifications() {
-        if (DBG) log("setNotification: cancelAllNotifications");
+        if (DBG) log("cancelAllNotifications: mPrevSubId=" + mPrevSubId);
         NotificationManager notificationManager = (NotificationManager)
                 mPhone.getContext().getSystemService(Context.NOTIFICATION_SERVICE);
-        notificationManager.cancel(PS_NOTIFICATION);
-        notificationManager.cancel(CS_NOTIFICATION);
-        notificationManager.cancel(CS_REJECT_CAUSE_NOTIFICATION);
+        if (SubscriptionManager.isValidSubscriptionId(mPrevSubId)) {
+            notificationManager.cancel(Integer.toString(mPrevSubId), PS_NOTIFICATION);
+            notificationManager.cancel(Integer.toString(mPrevSubId), CS_NOTIFICATION);
+            notificationManager.cancel(Integer.toString(mPrevSubId), CS_REJECT_CAUSE_NOTIFICATION);
+        }
     }
 
     /**
@@ -3596,7 +3601,7 @@
 
         if (DBG) {
             log("setNotification, create notification, notifyType: " + notifyType
-                    + ", title: " + title + ", details: " + details);
+                    + ", title: " + title + ", details: " + details + ", subId: " + mSubId);
         }
 
         mNotification = new Notification.Builder(context)
@@ -3617,24 +3622,24 @@
 
         if (notifyType == PS_DISABLED || notifyType == CS_DISABLED) {
             // cancel previous post notification
-            notificationManager.cancel(notificationId);
+            notificationManager.cancel(Integer.toString(mSubId), notificationId);
         } else {
             boolean show = false;
-            if (mNewSS.isEmergencyOnly() && notifyType == CS_EMERGENCY_ENABLED) {
+            if (mSS.isEmergencyOnly() && notifyType == CS_EMERGENCY_ENABLED) {
                 // if reg state is emergency only, always show restricted emergency notification.
                 show = true;
             } else if (notifyType == CS_REJECT_CAUSE_ENABLED) {
                 // always show notification due to CS reject irrespective of service state.
                 show = true;
-            } else if (mNewSS.getState() == ServiceState.STATE_IN_SERVICE) {
+            } else if (mSS.getState() == ServiceState.STATE_IN_SERVICE) {
                 // for non in service states, we have system UI and signal bar to indicate limited
                 // service. No need to show notification again. This also helps to mitigate the
                 // issue if phone go to OOS and camp to other networks and received restricted ind.
                 show = true;
             }
-            // update restricted state notification
+            // update restricted state notification for this subId
             if (show) {
-                notificationManager.notify(notificationId, mNotification);
+                notificationManager.notify(Integer.toString(mSubId), notificationId, mNotification);
             }
         }
     }
diff --git a/com/android/internal/telephony/SubscriptionController.java b/com/android/internal/telephony/SubscriptionController.java
index daf6436..500094b 100644
--- a/com/android/internal/telephony/SubscriptionController.java
+++ b/com/android/internal/telephony/SubscriptionController.java
@@ -64,7 +64,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
 
 /**
@@ -94,7 +93,7 @@
     private ScLocalLog mLocalLog = new ScLocalLog(MAX_LOCAL_LOG_LINES);
 
     /* The Cache of Active SubInfoRecord(s) list of currently in use SubInfoRecord(s) */
-    private AtomicReference<List<SubscriptionInfo>> mCacheActiveSubInfoList = new AtomicReference();
+    private final List<SubscriptionInfo> mCacheActiveSubInfoList = new ArrayList<>();
 
     /**
      * Copied from android.util.LocalLog with flush() adding flush and line number
@@ -368,7 +367,7 @@
                             subList = new ArrayList<SubscriptionInfo>();
                         }
                         subList.add(subInfo);
-                }
+                    }
                 }
             } else {
                 if (DBG) logd("Query fail");
@@ -598,6 +597,11 @@
      */
     @Override
     public List<SubscriptionInfo> getActiveSubscriptionInfoList(String callingPackage) {
+        if (!isSubInfoReady()) {
+            if (DBG) logdl("[getActiveSubInfoList] Sub Controller not ready");
+            return null;
+        }
+
         boolean canReadAllPhoneState;
         try {
             canReadAllPhoneState = TelephonyPermissions.checkReadPhoneState(mContext,
@@ -607,96 +611,57 @@
             canReadAllPhoneState = false;
         }
 
-        // Perform the operation as ourselves. If the caller cannot read phone state, they may still
-        // have carrier privileges for that subscription, so we always need to make the query and
-        // then filter the results.
-        List<SubscriptionInfo> subList;
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            if (!isSubInfoReady()) {
-                if (DBG) logdl("[getActiveSubInfoList] Sub Controller not ready");
-                return null;
+        synchronized (mCacheActiveSubInfoList) {
+            // If the caller can read all phone state, just return the full list.
+            if (canReadAllPhoneState) {
+                return new ArrayList<>(mCacheActiveSubInfoList);
             }
 
-            // Get the active subscription info list from the cache if the cache is not null
-            List<SubscriptionInfo> tmpCachedSubList = mCacheActiveSubInfoList.get();
-            if (tmpCachedSubList != null) {
-                if (DBG_CACHE) {
-                    for (SubscriptionInfo si : tmpCachedSubList) {
-                        logd("[getActiveSubscriptionInfoList] Getting Cached subInfo=" + si);
-                    }
-                }
-                subList = tmpCachedSubList;
-            } else {
-                if (DBG_CACHE) {
-                    logd("[getActiveSubscriptionInfoList] Cached subInfo is null");
-                }
-                return null;
-            }
-        } finally {
-            Binder.restoreCallingIdentity(identity);
+            // Filter the list to only include subscriptions which the caller can manage.
+            return mCacheActiveSubInfoList.stream()
+                    .filter(subscriptionInfo -> {
+                        try {
+                            return TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext,
+                                    subscriptionInfo.getSubscriptionId(), callingPackage,
+                                    "getActiveSubscriptionInfoList");
+                        } catch (SecurityException e) {
+                            return false;
+                        }
+                    })
+                    .collect(Collectors.toList());
         }
-
-        // If the caller can read all phone state, just return the full list.
-        if (canReadAllPhoneState) {
-            return new ArrayList<>(subList);
-        }
-
-        // Filter the list to only include subscriptions which the (restored) caller can manage.
-        return subList.stream()
-                .filter(subscriptionInfo -> {
-                    try {
-                        return TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext,
-                                subscriptionInfo.getSubscriptionId(), callingPackage,
-                                "getActiveSubscriptionInfoList");
-                    } catch (SecurityException e) {
-                        return false;
-                    }
-                })
-                .collect(Collectors.toList());
     }
 
     /**
      * Refresh the cache of SubInfoRecord(s) of the currently inserted SIM(s)
      */
-    @VisibleForTesting
-    protected void refreshCachedActiveSubscriptionInfoList() {
-
-        // Now that all security checks passes, perform the operation as ourselves.
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            if (!isSubInfoReady()) {
-                if (DBG_CACHE) {
-                    logdl("[refreshCachedActiveSubscriptionInfoList] "
-                            + "Sub Controller not ready ");
-                }
-                return;
-            }
-
-            List<SubscriptionInfo> subList = getSubInfo(
-                    SubscriptionManager.SIM_SLOT_INDEX + ">=0", null);
-
-            if (subList != null) {
-                // FIXME: Unnecessary when an insertion sort is used!
-                subList.sort(SUBSCRIPTION_INFO_COMPARATOR);
-
-                if (DBG_CACHE) {
-                    logdl("[refreshCachedActiveSubscriptionInfoList]- " + subList.size()
-                            + " infos return");
-                }
-            } else {
-                if (DBG_CACHE) logdl("[refreshCachedActiveSubscriptionInfoList]- no info return");
-            }
-
+    @VisibleForTesting  // For mockito to mock this method
+    public void refreshCachedActiveSubscriptionInfoList() {
+        if (!isSubInfoReady()) {
             if (DBG_CACHE) {
-                for (SubscriptionInfo si : subList) {
-                    logd("[refreshCachedActiveSubscriptionInfoList] Setting Cached subInfo=" + si);
+                logdl("[refreshCachedActiveSubscriptionInfoList] "
+                        + "Sub Controller not ready ");
+            }
+            return;
+        }
+
+        synchronized (mCacheActiveSubInfoList) {
+            mCacheActiveSubInfoList.clear();
+            List<SubscriptionInfo> activeSubscriptionInfoList = getSubInfo(
+                    SubscriptionManager.SIM_SLOT_INDEX + ">=0", null);
+            if (activeSubscriptionInfoList != null) {
+                mCacheActiveSubInfoList.addAll(activeSubscriptionInfoList);
+            }
+            if (DBG_CACHE) {
+                if (!mCacheActiveSubInfoList.isEmpty()) {
+                    for (SubscriptionInfo si : mCacheActiveSubInfoList) {
+                        logd("[refreshCachedActiveSubscriptionInfoList] Setting Cached info="
+                                + si);
+                    }
+                } else {
+                    logdl("[refreshCachedActiveSubscriptionInfoList]- no info return");
                 }
             }
-            mCacheActiveSubInfoList.set(subList);
-
-        } finally {
-            Binder.restoreCallingIdentity(identity);
         }
     }
 
diff --git a/com/android/internal/telephony/dataconnection/DataServiceManager.java b/com/android/internal/telephony/dataconnection/DataServiceManager.java
index 8c3f751..5e01686 100644
--- a/com/android/internal/telephony/dataconnection/DataServiceManager.java
+++ b/com/android/internal/telephony/dataconnection/DataServiceManager.java
@@ -16,10 +16,18 @@
 
 package com.android.internal.telephony.dataconnection;
 
+import static android.telephony.AccessNetworkConstants.TransportType.WLAN;
+import static android.telephony.AccessNetworkConstants.TransportType.WWAN;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.net.LinkProperties;
 import android.os.AsyncResult;
 import android.os.Handler;
@@ -28,7 +36,7 @@
 import android.os.PersistableBundle;
 import android.os.RegistrantList;
 import android.os.RemoteException;
-import android.telephony.AccessNetworkConstants;
+import android.os.ServiceManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.Rlog;
 import android.telephony.data.DataCallResponse;
@@ -41,8 +49,10 @@
 
 import com.android.internal.telephony.Phone;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -58,6 +68,8 @@
     private final Phone mPhone;
 
     private final CarrierConfigManager mCarrierConfigManager;
+    private final AppOpsManager mAppOps;
+    private final IPackageManager mPackageManager;
 
     private final int mTransportType;
 
@@ -73,14 +85,10 @@
 
     private final RegistrantList mDataCallListChangedRegistrants = new RegistrantList();
 
+    // not final because it is set by the onServiceConnected method
+    private ComponentName mComponentName;
+
     private class DataServiceManagerDeathRecipient implements IBinder.DeathRecipient {
-
-        private final ComponentName mComponentName;
-
-        DataServiceManagerDeathRecipient(ComponentName name) {
-            mComponentName = name;
-        }
-
         @Override
         public void binderDied() {
             // TODO: try to rebind the service.
@@ -89,12 +97,53 @@
         }
     }
 
+    private void grantPermissionsToService(String packageName) {
+        final String[] pkgToGrant = {packageName};
+        try {
+            mPackageManager.grantDefaultPermissionsToEnabledTelephonyDataServices(
+                    pkgToGrant, mPhone.getContext().getUserId());
+            mAppOps.setMode(AppOpsManager.OP_MANAGE_IPSEC_TUNNELS, mPhone.getContext().getUserId(),
+                    pkgToGrant[0], AppOpsManager.MODE_ALLOWED);
+        } catch (RemoteException e) {
+            loge("Binder to package manager died, permission grant for DataService failed.");
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Loop through all DataServices installed on the system and revoke permissions from any that
+     * are not currently the WWAN or WLAN data service.
+     */
+    private void revokePermissionsFromUnusedDataServices() {
+        // Except the current data services from having their permissions removed.
+        Set<String> dataServices = getAllDataServicePackageNames();
+        for (int transportType : new int[] {WWAN, WLAN}) {
+            dataServices.remove(getDataServicePackageName(transportType));
+        }
+
+        try {
+            String[] dataServicesArray = new String[dataServices.size()];
+            dataServices.toArray(dataServicesArray);
+            mPackageManager.revokeDefaultPermissionsFromDisabledTelephonyDataServices(
+                    dataServicesArray, mPhone.getContext().getUserId());
+            for (String pkg : dataServices) {
+                mAppOps.setMode(AppOpsManager.OP_MANAGE_IPSEC_TUNNELS,
+                        mPhone.getContext().getUserId(),
+                        pkg, AppOpsManager.MODE_ERRORED);
+            }
+        } catch (RemoteException e) {
+            loge("Binder to package manager died; failed to revoke DataService permissions.");
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
     private final class CellularDataServiceConnection implements ServiceConnection {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             if (DBG) log("onServiceConnected");
+            mComponentName = name;
             mIDataService = IDataService.Stub.asInterface(service);
-            mDeathRecipient = new DataServiceManagerDeathRecipient(name);
+            mDeathRecipient = new DataServiceManagerDeathRecipient();
             mBound = true;
 
             try {
@@ -186,16 +235,22 @@
         mBound = false;
         mCarrierConfigManager = (CarrierConfigManager) phone.getContext().getSystemService(
                 Context.CARRIER_CONFIG_SERVICE);
-
+        mPackageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+        mAppOps = (AppOpsManager) phone.getContext().getSystemService(Context.APP_OPS_SERVICE);
         bindDataService();
     }
 
     private void bindDataService() {
+        // Start by cleaning up all packages that *shouldn't* have permissions.
+        revokePermissionsFromUnusedDataServices();
+
         String packageName = getDataServicePackageName();
         if (TextUtils.isEmpty(packageName)) {
             loge("Can't find the binding package");
             return;
         }
+        // Then pre-emptively grant the permissions to the package we will bind.
+        grantPermissionsToService(packageName);
 
         try {
             if (!mPhone.getContext().bindService(
@@ -209,18 +264,55 @@
         }
     }
 
+    @NonNull
+    private Set<String> getAllDataServicePackageNames() {
+        // Cowardly using the public PackageManager interface here.
+        // Note: This matches only packages that were installed on the system image. If we ever
+        // expand the permissions model to allow CarrierPrivileged packages, then this will need
+        // to be updated.
+        List<ResolveInfo> dataPackages =
+                mPhone.getContext().getPackageManager().queryIntentServices(
+                        new Intent(DataService.DATA_SERVICE_INTERFACE),
+                                PackageManager.MATCH_SYSTEM_ONLY);
+        HashSet<String> packageNames = new HashSet<>();
+        for (ResolveInfo info : dataPackages) {
+            if (info.serviceInfo == null) continue;
+            packageNames.add(info.serviceInfo.packageName);
+        }
+        return packageNames;
+    }
+
+    /**
+     * Get the data service package name for our current transport type.
+     *
+     * @return package name of the data service package for the the current transportType.
+     */
     private String getDataServicePackageName() {
+        return getDataServicePackageName(mTransportType);
+    }
+
+    /**
+     * Get the data service package by transport type.
+     *
+     * When we bind to a DataService package, we need to revoke permissions from stale
+     * packages; we need to exclude data packages for all transport types, so we need to
+     * to be able to query by transport type.
+     *
+     * @param transportType either WWAN or WLAN
+     * @return package name of the data service package for the specified transportType.
+     */
+    private String getDataServicePackageName(int transportType) {
         String packageName;
         int resourceId;
         String carrierConfig;
 
-        switch (mTransportType) {
-            case AccessNetworkConstants.TransportType.WWAN:
+        switch (transportType) {
+            case WWAN:
                 resourceId = com.android.internal.R.string.config_wwan_data_service_package;
                 carrierConfig = CarrierConfigManager
                         .KEY_CARRIER_DATA_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING;
                 break;
-            case AccessNetworkConstants.TransportType.WLAN:
+            case WLAN:
                 resourceId = com.android.internal.R.string.config_wlan_data_service_package;
                 carrierConfig = CarrierConfigManager
                         .KEY_CARRIER_DATA_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING;
diff --git a/com/android/internal/telephony/dataconnection/DcTracker.java b/com/android/internal/telephony/dataconnection/DcTracker.java
index 6c38967..bdd334f 100644
--- a/com/android/internal/telephony/dataconnection/DcTracker.java
+++ b/com/android/internal/telephony/dataconnection/DcTracker.java
@@ -1976,8 +1976,8 @@
         // a dun-profiled connection so we can't share an existing one
         // On GSM/LTE we can share existing apn connections provided they support
         // this type.
-        if (apnContext.getApnType() != PhoneConstants.APN_TYPE_DUN ||
-                teardownForDun() == false) {
+        if (!apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DUN)
+                || ServiceState.isGsm(mPhone.getServiceState().getRilDataRadioTechnology())) {
             dcac = checkForCompatibleConnectedApnContext(apnContext);
             if (dcac != null) {
                 // Get the dcacApnSetting for the connection we want to share.
diff --git a/com/android/internal/telephony/ims/ImsResolver.java b/com/android/internal/telephony/ims/ImsResolver.java
index c6a7efa..8059ec0 100644
--- a/com/android/internal/telephony/ims/ImsResolver.java
+++ b/com/android/internal/telephony/ims/ImsResolver.java
@@ -29,6 +29,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.telephony.CarrierConfigManager;
@@ -88,8 +89,10 @@
     private static final int HANDLER_CONFIG_CHANGED = 2;
     // A query has been started for an ImsService to relay the features they support.
     private static final int HANDLER_START_DYNAMIC_FEATURE_QUERY = 3;
-    // A query to request ImsService features has completed.
-    private static final int HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE = 4;
+    // A query to request ImsService features has completed or the ImsService has updated features.
+    private static final int HANDLER_DYNAMIC_FEATURE_CHANGE = 4;
+    // Testing: Overrides the current configuration for ImsService binding
+    private static final int HANDLER_OVERRIDE_IMS_SERVICE_CONFIG = 5;
 
     // Delay between dynamic ImsService queries.
     private static final int DELAY_DYNAMIC_QUERY_MS = 5000;
@@ -328,6 +331,8 @@
     private final Object mBoundServicesLock = new Object();
     private final int mNumSlots;
     private final boolean mIsDynamicBinding;
+    // Package name of the default device service.
+    private String mDeviceService;
 
     // Synchronize all messages on a handler to ensure that the cache includes the most recent
     // version of the installed ImsServices.
@@ -345,7 +350,7 @@
             }
             case HANDLER_CONFIG_CHANGED: {
                 int slotId = (Integer) msg.obj;
-                maybeRebindService(slotId);
+                carrierConfigChanged(slotId);
                 break;
             }
             case HANDLER_START_DYNAMIC_FEATURE_QUERY: {
@@ -353,7 +358,7 @@
                 startDynamicQuery(info);
                 break;
             }
-            case HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE: {
+            case HANDLER_DYNAMIC_FEATURE_CHANGE: {
                 SomeArgs args = (SomeArgs) msg.obj;
                 ComponentName name = (ComponentName) args.arg1;
                 Set<ImsFeatureConfiguration.FeatureSlotPair> features =
@@ -362,6 +367,25 @@
                 dynamicQueryComplete(name, features);
                 break;
             }
+            case HANDLER_OVERRIDE_IMS_SERVICE_CONFIG: {
+                int slotId = msg.arg1;
+                // arg2 will be equal to 1 if it is a carrier service.
+                boolean isCarrierImsService = (msg.arg2 == 1);
+                String packageName = (String) msg.obj;
+                if (isCarrierImsService) {
+                    Log.i(TAG, "overriding carrier ImsService - slot=" + slotId + " packageName="
+                            + packageName);
+                    maybeRebindService(slotId, packageName);
+                } else {
+                    Log.i(TAG, "overriding device ImsService -  packageName=" + packageName);
+                    if (packageName == null || packageName.isEmpty()) {
+                        unbindImsService(getImsServiceInfoFromCache(mDeviceService));
+                    }
+                    mDeviceService = packageName;
+                    bindImsService(getImsServiceInfoFromCache(packageName));
+                }
+                break;
+            }
             default:
                 return false;
         }
@@ -377,7 +401,7 @@
                         Set<ImsFeatureConfiguration.FeatureSlotPair> features) {
                     Log.d(TAG, "onComplete called for name: " + name + "features:"
                             + printFeatures(features));
-                    handleFeatureQueryComplete(name, features);
+                    handleFeaturesChanged(name, features);
                 }
 
                 @Override
@@ -387,8 +411,6 @@
                 }
             };
 
-    // Package name of the default device service.
-    private String mDeviceService;
     // Array index corresponds to slot Id associated with the service package name.
     private String[] mCarrierServices;
     // List index corresponds to Slot Id, Maps ImsFeature.FEATURE->bound ImsServiceController
@@ -594,6 +616,36 @@
         return null;
     }
 
+    // Used for testing only.
+    public boolean overrideImsServiceConfiguration(int slotId, boolean isCarrierService,
+            String packageName) {
+        if (slotId < 0 || slotId >= mNumSlots) {
+            Log.w(TAG, "overrideImsServiceConfiguration: invalid slotId!");
+            return false;
+        }
+
+        if (packageName == null) {
+            Log.w(TAG, "overrideImsServiceConfiguration: null packageName!");
+            return false;
+        }
+
+        // encode boolean to int for Message.
+        int carrierService = isCarrierService ? 1 : 0;
+        Message.obtain(mHandler, HANDLER_OVERRIDE_IMS_SERVICE_CONFIG, slotId, carrierService,
+                packageName).sendToTarget();
+        return true;
+    }
+
+    // used for testing only.
+    public String getImsServiceConfiguration(int slotId, boolean isCarrierService) {
+        if (slotId < 0 || slotId >= mNumSlots) {
+            Log.w(TAG, "getImsServiceConfiguration: invalid slotId!");
+            return "";
+        }
+
+        return isCarrierService ? mCarrierServices[slotId] : mDeviceService;
+    }
+
     private void putImsController(int slotId, int feature, ImsServiceController controller) {
         if (slotId < 0 || slotId >= mNumSlots || feature <= ImsFeature.FEATURE_INVALID
                 || feature >= ImsFeature.FEATURE_MAX) {
@@ -901,6 +953,21 @@
     }
 
     /**
+     * Implementation of
+     * {@link ImsServiceController.ImsServiceControllerCallbacks#imsServiceFeaturesChanged, which
+     * notify the ImsResolver of a change to the supported ImsFeatures of a connected ImsService.
+     */
+    public void imsServiceFeaturesChanged(ImsFeatureConfiguration config,
+            ImsServiceController controller) {
+        if (controller == null || config == null) {
+            return;
+        }
+        Log.i(TAG, "imsServiceFeaturesChanged: config=" + config.getServiceFeatures()
+                + ", ComponentName=" + controller.getComponentName());
+        handleFeaturesChanged(controller.getComponentName(), config.getServiceFeatures());
+    }
+
+    /**
      * Determines if the features specified should cause a bind or keep a binding active to an
      * ImsService.
      * @return true if MMTEL or RCS features are present, false if they are not or only
@@ -917,22 +984,31 @@
     // Possibly rebind to another ImsService if currently installed ImsServices were changed or if
     // the SIM card has changed.
     // Called from the handler ONLY
-    private void maybeRebindService(int slotId) {
+    private void maybeRebindService(int slotId, String newPackageName) {
         if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
             // not specified, replace package on all slots.
             for (int i = 0; i < mNumSlots; i++) {
-                updateBoundCarrierServices(i);
+                updateBoundCarrierServices(i, newPackageName);
             }
         } else {
-            updateBoundCarrierServices(slotId);
+            updateBoundCarrierServices(slotId, newPackageName);
         }
 
     }
 
-    private void updateBoundCarrierServices(int slotId) {
+    private void carrierConfigChanged(int slotId) {
         int subId = mSubscriptionManagerProxy.getSubId(slotId);
-        String newPackageName = mCarrierConfigManager.getConfigForSubId(subId).getString(
-                CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null);
+        PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId);
+        if (config != null) {
+            String newPackageName = config.getString(
+                    CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null);
+            maybeRebindService(slotId, newPackageName);
+        } else {
+            Log.w(TAG, "carrierConfigChanged: CarrierConfig is null!");
+        }
+    }
+
+    private void updateBoundCarrierServices(int slotId, String newPackageName) {
         if (slotId > SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mNumSlots) {
             String oldPackageName = mCarrierServices[slotId];
             mCarrierServices[slotId] = newPackageName;
@@ -997,12 +1073,12 @@
     /**
      * Schedules the processing of a completed query.
      */
-    private void handleFeatureQueryComplete(ComponentName name,
+    private void handleFeaturesChanged(ComponentName name,
             Set<ImsFeatureConfiguration.FeatureSlotPair> features) {
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = name;
         args.arg2 = features;
-        mHandler.obtainMessage(HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE, args).sendToTarget();
+        mHandler.obtainMessage(HANDLER_DYNAMIC_FEATURE_CHANGE, args).sendToTarget();
     }
 
     // Starts a dynamic query. Called from handler ONLY.
@@ -1022,7 +1098,7 @@
             Set<ImsFeatureConfiguration.FeatureSlotPair> features) {
         ImsServiceInfo service = getImsServiceInfoFromCache(name.getPackageName());
         if (service == null) {
-            Log.w(TAG, "handleFeatureQueryComplete: Couldn't find cached info for name: "
+            Log.w(TAG, "handleFeaturesChanged: Couldn't find cached info for name: "
                     + name);
             return;
         }
@@ -1047,7 +1123,7 @@
     public boolean isResolvingBinding() {
         return mHandler.hasMessages(HANDLER_START_DYNAMIC_FEATURE_QUERY)
                 // We haven't processed this message yet, so it is still resolving.
-                || mHandler.hasMessages(HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE)
+                || mHandler.hasMessages(HANDLER_DYNAMIC_FEATURE_CHANGE)
                 || mFeatureQueryManager.isQueryInProgress();
     }
 
diff --git a/com/android/internal/telephony/ims/ImsServiceController.java b/com/android/internal/telephony/ims/ImsServiceController.java
index e13a8ef..e2a0f43 100644
--- a/com/android/internal/telephony/ims/ImsServiceController.java
+++ b/com/android/internal/telephony/ims/ImsServiceController.java
@@ -149,6 +149,16 @@
         }
     }
 
+    private ImsService.Listener mFeatureChangedListener = new ImsService.Listener() {
+        @Override
+        public void onUpdateSupportedImsFeatures(ImsFeatureConfiguration c) {
+            if (mCallbacks == null) {
+                return;
+            }
+            mCallbacks.imsServiceFeaturesChanged(c, ImsServiceController.this);
+        }
+    };
+
     /**
      * Defines callbacks that are used by the ImsServiceController to notify when an ImsService
      * has created or removed a new feature as well as the associated ImsServiceController.
@@ -162,6 +172,13 @@
          * Called by ImsServiceController when a new MMTEL or RCS feature has been removed.
          */
         void imsServiceFeatureRemoved(int slotId, int feature, ImsServiceController controller);
+
+        /**
+         * Called by the ImsServiceController when the ImsService has notified the framework that
+         * its features have changed.
+         */
+        void imsServiceFeaturesChanged(ImsFeatureConfiguration config,
+                ImsServiceController controller);
     }
 
     /**
@@ -552,6 +569,7 @@
         synchronized (mLock) {
             if (isServiceControllerAvailable()) {
                 Log.d(LOG_TAG, "notifyImsServiceReady");
+                mIImsServiceController.setListener(mFeatureChangedListener);
                 mIImsServiceController.notifyImsServiceReadyForFeatureCreation();
             }
         }
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
index ec5a6a0..87e51ec 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
@@ -1277,10 +1277,18 @@
                 sb.append(mContext.getText(
                         com.android.internal.R.string.serviceEnabled));
             }
+            // Record CLIR setting
+            if (mSc.equals(SC_CLIR)) {
+                mPhone.saveClirSetting(CommandsInterface.CLIR_INVOCATION);
+            }
         } else if (isDeactivate()) {
             mState = State.COMPLETE;
             sb.append(mContext.getText(
                     com.android.internal.R.string.serviceDisabled));
+            // Record CLIR setting
+            if (mSc.equals(SC_CLIR)) {
+                mPhone.saveClirSetting(CommandsInterface.CLIR_SUPPRESSION);
+            }
         } else if (isRegister()) {
             mState = State.COMPLETE;
             sb.append(mContext.getText(
diff --git a/com/android/internal/telephony/metrics/TelephonyMetrics.java b/com/android/internal/telephony/metrics/TelephonyMetrics.java
index 116fdae..a436d25 100644
--- a/com/android/internal/telephony/metrics/TelephonyMetrics.java
+++ b/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -1835,12 +1835,18 @@
         final CarrierIdMatchingResult carrierIdMatchingResult = new CarrierIdMatchingResult();
 
         if (cid != TelephonyManager.UNKNOWN_CARRIER_ID) {
+            // Successful matching event if result only has carrierId
             carrierIdMatchingResult.carrierId = cid;
+            // Unknown Gid1 event if result only has carrierId, gid1 and mccmnc
             if (gid1 != null) {
+                carrierIdMatchingResult.mccmnc = mccmnc;
                 carrierIdMatchingResult.gid1 = gid1;
             }
         } else {
-            carrierIdMatchingResult.mccmnc = mccmnc;
+            // Unknown mccmnc event if result only has mccmnc
+            if (mccmnc != null) {
+                carrierIdMatchingResult.mccmnc = mccmnc;
+            }
         }
 
         carrierIdMatching.cidTableVersion = version;
diff --git a/com/android/internal/telephony/uicc/IccIoResult.java b/com/android/internal/telephony/uicc/IccIoResult.java
index 4a35e14..b1b6e75 100644
--- a/com/android/internal/telephony/uicc/IccIoResult.java
+++ b/com/android/internal/telephony/uicc/IccIoResult.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.telephony.uicc;
 
+import android.os.Build;
 
 /**
  * {@hide}
@@ -154,6 +155,12 @@
                             + "CHV blocked"
                             + "UNBLOCK CHV blocked";
                     case 0x50: return "increase cannot be performed, Max value reached";
+                    // The definition for these status codes can be found in TS 31.102 7.3.1
+                    case 0x62: return "authentication error, application specific";
+                    case 0x64: return "authentication error, security context not supported";
+                    case 0x65: return "key freshness failure";
+                    case 0x66: return "authentication error, no memory space available";
+                    case 0x67: return "authentication error, no memory space available in EF_MUK";
                 }
                 break;
             case 0x9E: return null; // success
@@ -181,7 +188,9 @@
     @Override
     public String toString() {
         return "IccIoResult sw1:0x" + Integer.toHexString(sw1) + " sw2:0x"
-                + Integer.toHexString(sw2) + ((!success()) ? " Error: " + getErrorString() : "");
+                + Integer.toHexString(sw2) + " Payload: "
+                + ((Build.IS_DEBUGGABLE && Build.IS_ENG) ? payload : "*******")
+                + ((!success()) ? " Error: " + getErrorString() : "");
     }
 
     /**
diff --git a/com/android/internal/telephony/uicc/UiccProfile.java b/com/android/internal/telephony/uicc/UiccProfile.java
index 536cc38..d14e58c 100644
--- a/com/android/internal/telephony/uicc/UiccProfile.java
+++ b/com/android/internal/telephony/uicc/UiccProfile.java
@@ -1464,21 +1464,7 @@
             return null;
         }
         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
-        String brandName = sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
-        if (brandName == null) {
-            // Check if  CarrierConfig sets carrier name
-            CarrierConfigManager manager = (CarrierConfigManager)
-                    mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
-            int subId = SubscriptionController.getInstance().getSubIdUsingPhoneId(mPhoneId);
-            if (manager != null) {
-                PersistableBundle bundle = manager.getConfigForSubId(subId);
-                if (bundle != null && bundle.getBoolean(
-                        CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL)) {
-                    brandName = bundle.getString(CarrierConfigManager.KEY_CARRIER_NAME_STRING);
-                }
-            }
-        }
-        return brandName;
+        return sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
     }
 
     /**
diff --git a/com/android/internal/telephony/uicc/UiccSlot.java b/com/android/internal/telephony/uicc/UiccSlot.java
index 004d3b1..a0fefe3 100644
--- a/com/android/internal/telephony/uicc/UiccSlot.java
+++ b/com/android/internal/telephony/uicc/UiccSlot.java
@@ -76,9 +76,11 @@
      * Update slot. The main trigger for this is a change in the ICC Card status.
      */
     public void update(CommandsInterface ci, IccCardStatus ics, int phoneId) {
+        if (DBG) log("cardStatus update: " + ics.toString());
         synchronized (mLock) {
             CardState oldState = mCardState;
             mCardState = ics.mCardState;
+            mIccId = ics.iccid;
             mPhoneId = phoneId;
             parseAtr(ics.atr);
             mCi = ci;
@@ -104,8 +106,12 @@
                     mUiccCard.dispose();
                     mUiccCard = null;
                 }
-            } else if ((oldState == null || oldState == CardState.CARDSTATE_ABSENT)
-                    && mCardState != CardState.CARDSTATE_ABSENT) {
+            // Because mUiccCard may be updated in both IccCardStatus and IccSlotStatus, we need to
+            // create a new UiccCard instance in two scenarios:
+            //   1. mCardState is changing from ABSENT to non ABSENT.
+            //   2. The latest mCardState is not ABSENT, but there is no UiccCard instance.
+            } else if ((oldState == null || oldState == CardState.CARDSTATE_ABSENT
+                    || mUiccCard == null) && mCardState != CardState.CARDSTATE_ABSENT) {
                 // No notifications while radio is off or we just powering up
                 if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
                     if (DBG) log("update: notify card added");
@@ -136,7 +142,7 @@
      * Update slot based on IccSlotStatus.
      */
     public void update(CommandsInterface ci, IccSlotStatus iss) {
-        log("slotStatus update");
+        if (DBG) log("slotStatus update: " + iss.toString());
         synchronized (mLock) {
             mCi = ci;
             if (iss.slotState == IccSlotStatus.SlotState.SLOTSTATE_INACTIVE) {
diff --git a/com/android/internal/util/StatLogger.java b/com/android/internal/util/StatLogger.java
new file mode 100644
index 0000000..1dac136
--- /dev/null
+++ b/com/android/internal/util/StatLogger.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2018 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.internal.util;
+
+import android.os.SystemClock;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.StatLoggerProto;
+import com.android.server.StatLoggerProto.Event;
+
+import java.io.PrintWriter;
+
+/**
+ * Simple class to keep track of the number of times certain events happened and their durations for
+ * benchmarking.
+ *
+ * @hide
+ */
+public class StatLogger {
+    private static final String TAG = "StatLogger";
+
+    private final Object mLock = new Object();
+
+    private final int SIZE;
+
+    @GuardedBy("mLock")
+    private final int[] mCountStats;
+
+    @GuardedBy("mLock")
+    private final long[] mDurationStats;
+
+    @GuardedBy("mLock")
+    private final int[] mCallsPerSecond;
+
+    @GuardedBy("mLock")
+    private final long[] mDurationPerSecond;
+
+    @GuardedBy("mLock")
+    private final int[] mMaxCallsPerSecond;
+
+    @GuardedBy("mLock")
+    private final long[] mMaxDurationPerSecond;
+
+    @GuardedBy("mLock")
+    private final long[] mMaxDurationStats;
+
+    @GuardedBy("mLock")
+    private long mNextTickTime = SystemClock.elapsedRealtime() + 1000;
+
+    private final String[] mLabels;
+
+    public StatLogger(String[] eventLabels) {
+        SIZE = eventLabels.length;
+        mCountStats = new int[SIZE];
+        mDurationStats = new long[SIZE];
+        mCallsPerSecond = new int[SIZE];
+        mMaxCallsPerSecond = new int[SIZE];
+        mDurationPerSecond = new long[SIZE];
+        mMaxDurationPerSecond = new long[SIZE];
+        mMaxDurationStats = new long[SIZE];
+        mLabels = eventLabels;
+    }
+
+    /**
+     * Return the current time in the internal time unit.
+     * Call it before an event happens, and
+     * give it back to the {@link #logDurationStat(int, long)}} after the event.
+     */
+    public long getTime() {
+        return SystemClock.elapsedRealtimeNanos() / 1000;
+    }
+
+    /**
+     * @see {@link #getTime()}
+     *
+     * @return the duration in microseconds.
+     */
+    public long logDurationStat(int eventId, long start) {
+        synchronized (mLock) {
+            final long duration = getTime() - start;
+            if (eventId >= 0 && eventId < SIZE) {
+                mCountStats[eventId]++;
+                mDurationStats[eventId] += duration;
+            } else {
+                Slog.wtf(TAG, "Invalid event ID: " + eventId);
+                return duration;
+            }
+            if (mMaxDurationStats[eventId] < duration) {
+                mMaxDurationStats[eventId] = duration;
+            }
+
+            // Keep track of the per-second max.
+            final long nowRealtime = SystemClock.elapsedRealtime();
+            if (nowRealtime > mNextTickTime) {
+                if (mMaxCallsPerSecond[eventId] < mCallsPerSecond[eventId]) {
+                    mMaxCallsPerSecond[eventId] = mCallsPerSecond[eventId];
+                }
+                if (mMaxDurationPerSecond[eventId] < mDurationPerSecond[eventId]) {
+                    mMaxDurationPerSecond[eventId] = mDurationPerSecond[eventId];
+                }
+
+                mCallsPerSecond[eventId] = 0;
+                mDurationPerSecond[eventId] = 0;
+
+                mNextTickTime = nowRealtime + 1000;
+            }
+
+            mCallsPerSecond[eventId]++;
+            mDurationPerSecond[eventId] += duration;
+
+            return duration;
+        }
+    }
+
+    public void dump(PrintWriter pw, String prefix) {
+        dump(new IndentingPrintWriter(pw, "  ").setIndent(prefix));
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        synchronized (mLock) {
+            pw.println("Stats:");
+            pw.increaseIndent();
+            for (int i = 0; i < SIZE; i++) {
+                final int count = mCountStats[i];
+                final double durationMs = mDurationStats[i] / 1000.0;
+
+                pw.println(String.format(
+                        "%s: count=%d, total=%.1fms, avg=%.3fms, max calls/s=%d max dur/s=%.1fms"
+                        + " max time=%.1fms",
+                        mLabels[i], count, durationMs,
+                        (count == 0 ? 0 : durationMs / count),
+                        mMaxCallsPerSecond[i], mMaxDurationPerSecond[i] / 1000.0,
+                        mMaxDurationStats[i] / 1000.0));
+            }
+            pw.decreaseIndent();
+        }
+    }
+
+    public void dumpProto(ProtoOutputStream proto, long fieldId) {
+        synchronized (mLock) {
+            final long outer = proto.start(fieldId);
+
+            for (int i = 0; i < mLabels.length; i++) {
+                final long inner = proto.start(StatLoggerProto.EVENTS);
+
+                proto.write(Event.EVENT_ID, i);
+                proto.write(Event.LABEL, mLabels[i]);
+                proto.write(Event.COUNT, mCountStats[i]);
+                proto.write(Event.TOTAL_DURATION_MICROS, mDurationStats[i]);
+
+                proto.end(inner);
+            }
+
+            proto.end(outer);
+        }
+    }
+}
diff --git a/com/android/internal/widget/FloatingToolbar.java b/com/android/internal/widget/FloatingToolbar.java
index 35aae15..2ce5a0b 100644
--- a/com/android/internal/widget/FloatingToolbar.java
+++ b/com/android/internal/widget/FloatingToolbar.java
@@ -1706,6 +1706,7 @@
         contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
         contentContainer.setTag(FLOATING_TOOLBAR_TAG);
+        contentContainer.setClipToOutline(true);
         return contentContainer;
     }
 
diff --git a/com/android/internal/widget/MessagingGroup.java b/com/android/internal/widget/MessagingGroup.java
index 239beaa..07d78fe 100644
--- a/com/android/internal/widget/MessagingGroup.java
+++ b/com/android/internal/widget/MessagingGroup.java
@@ -20,7 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StyleRes;
-import android.app.Notification;
+import android.app.Person;
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -63,8 +63,8 @@
     private boolean mFirstLayout;
     private boolean mIsHidingAnimated;
     private boolean mNeedsGeneratedAvatar;
-    private Notification.Person mSender;
-    private boolean mAvatarsAtEnd;
+    private Person mSender;
+    private boolean mImagesAtEnd;
     private ViewGroup mImageContainer;
     private MessagingImageMessage mIsolatedMessage;
     private boolean mTransformingImages;
@@ -126,7 +126,7 @@
         return position;
     }
 
-    public void setSender(Notification.Person sender, CharSequence nameOverride) {
+    public void setSender(Person sender, CharSequence nameOverride) {
         mSender = sender;
         if (nameOverride == null) {
             nameOverride = sender.getName();
@@ -342,7 +342,7 @@
                 mAddedMessages.add(message);
             }
             boolean isImage = message instanceof MessagingImageMessage;
-            if (mAvatarsAtEnd && isImage) {
+            if (mImagesAtEnd && isImage) {
                 isolatedMessage = (MessagingImageMessage) message;
             } else {
                 if (removeFromParentIfDifferent(message, mMessageContainer)) {
@@ -466,7 +466,7 @@
         return mNeedsGeneratedAvatar;
     }
 
-    public Notification.Person getSender() {
+    public Person getSender() {
         return mSender;
     }
 
@@ -474,9 +474,9 @@
         mTransformingImages = transformingImages;
     }
 
-    public void setDisplayAvatarsAtEnd(boolean atEnd) {
-        if (mAvatarsAtEnd != atEnd) {
-            mAvatarsAtEnd = atEnd;
+    public void setDisplayImagesAtEnd(boolean atEnd) {
+        if (mImagesAtEnd != atEnd) {
+            mImagesAtEnd = atEnd;
             mImageContainer.setVisibility(atEnd ? View.VISIBLE : View.GONE);
         }
     }
diff --git a/com/android/internal/widget/MessagingLayout.java b/com/android/internal/widget/MessagingLayout.java
index 5279636..f8236c7 100644
--- a/com/android/internal/widget/MessagingLayout.java
+++ b/com/android/internal/widget/MessagingLayout.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.annotation.StyleRes;
 import android.app.Notification;
+import android.app.Person;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -79,9 +80,9 @@
     private Icon mLargeIcon;
     private boolean mIsOneToOne;
     private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
-    private Notification.Person mUser;
+    private Person mUser;
     private CharSequence mNameReplacement;
-    private boolean mIsCollapsed;
+    private boolean mDisplayImagesAtEnd;
 
     public MessagingLayout(@NonNull Context context) {
         super(context);
@@ -128,8 +129,8 @@
     }
 
     @RemotableViewMethod
-    public void setIsCollapsed(boolean isCollapsed) {
-        mIsCollapsed = isCollapsed;
+    public void setDisplayImagesAtEnd(boolean atEnd) {
+        mDisplayImagesAtEnd = atEnd;
     }
 
     @RemotableViewMethod
@@ -160,7 +161,7 @@
         for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
             CharSequence message = remoteInputHistory[i];
             newMessages.add(new Notification.MessagingStyle.Message(
-                    message, 0, (Notification.Person) null));
+                    message, 0, (Person) null));
         }
     }
 
@@ -296,13 +297,13 @@
         mIsOneToOne = oneToOne;
     }
 
-    public void setUser(Notification.Person user) {
+    public void setUser(Person user) {
         mUser = user;
         if (mUser.getIcon() == null) {
             Icon userIcon = Icon.createWithResource(getContext(),
                     com.android.internal.R.drawable.messaging_user);
             userIcon.setTint(mLayoutColor);
-            mUser.setIcon(userIcon);
+            mUser = mUser.toBuilder().setIcon(userIcon).build();
         }
     }
 
@@ -310,7 +311,7 @@
             List<MessagingMessage> messages) {
         // Let's first find our groups!
         List<List<MessagingMessage>> groups = new ArrayList<>();
-        List<Notification.Person> senders = new ArrayList<>();
+        List<Person> senders = new ArrayList<>();
 
         // Lets first find the groups
         findGroups(historicMessages, messages, groups, senders);
@@ -320,7 +321,7 @@
     }
 
     private void createGroupViews(List<List<MessagingMessage>> groups,
-            List<Notification.Person> senders) {
+            List<Person> senders) {
         mGroups.clear();
         for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
             List<MessagingMessage> group = groups.get(groupIndex);
@@ -337,9 +338,9 @@
                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
                 mAddedGroups.add(newGroup);
             }
-            newGroup.setDisplayAvatarsAtEnd(mIsCollapsed);
+            newGroup.setDisplayImagesAtEnd(mDisplayImagesAtEnd);
             newGroup.setLayoutColor(mLayoutColor);
-            Notification.Person sender = senders.get(groupIndex);
+            Person sender = senders.get(groupIndex);
             CharSequence nameOverride = null;
             if (sender != mUser && mNameReplacement != null) {
                 nameOverride = mNameReplacement;
@@ -357,7 +358,7 @@
 
     private void findGroups(List<MessagingMessage> historicMessages,
             List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
-            List<Notification.Person> senders) {
+            List<Person> senders) {
         CharSequence currentSenderKey = null;
         List<MessagingMessage> currentGroup = null;
         int histSize = historicMessages.size();
@@ -369,7 +370,7 @@
                 message = messages.get(i - histSize);
             }
             boolean isNewGroup = currentGroup == null;
-            Notification.Person sender = message.getMessage().getSenderPerson();
+            Person sender = message.getMessage().getSenderPerson();
             CharSequence key = sender == null ? null
                     : sender.getKey() == null ? sender.getName() : sender.getKey();
             isNewGroup |= !TextUtils.equals(key, currentSenderKey);
diff --git a/com/android/internal/widget/MessagingMessage.java b/com/android/internal/widget/MessagingMessage.java
index bf1c5ca..a2cc7cf 100644
--- a/com/android/internal/widget/MessagingMessage.java
+++ b/com/android/internal/widget/MessagingMessage.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.widget;
 
+import android.app.ActivityManager;
 import android.app.Notification;
 import android.view.View;
 
@@ -33,7 +34,7 @@
 
     static MessagingMessage createMessage(MessagingLayout layout,
             Notification.MessagingStyle.Message m) {
-        if (hasImage(m)) {
+        if (hasImage(m) && !ActivityManager.isLowRamDeviceStatic()) {
             return MessagingImageMessage.createMessage(layout, m);
         } else {
             return MessagingTextMessage.createMessage(layout, m);
diff --git a/com/android/keyguard/KeyguardAbsKeyInputView.java b/com/android/keyguard/KeyguardAbsKeyInputView.java
index d63ad08..00cd5a7 100644
--- a/com/android/keyguard/KeyguardAbsKeyInputView.java
+++ b/com/android/keyguard/KeyguardAbsKeyInputView.java
@@ -265,11 +265,11 @@
             mPendingLockCheck.cancel(false);
             mPendingLockCheck = null;
         }
+        reset();
     }
 
     @Override
     public void onResume(int reason) {
-        reset();
     }
 
     @Override
diff --git a/com/android/keyguard/KeyguardPINView.java b/com/android/keyguard/KeyguardPINView.java
index c1cff9e..adb2460 100644
--- a/com/android/keyguard/KeyguardPINView.java
+++ b/com/android/keyguard/KeyguardPINView.java
@@ -107,6 +107,13 @@
                 new View[]{
                         null, mEcaView, null
                 }};
+
+        View cancelBtn = findViewById(R.id.cancel_button);
+        if (cancelBtn != null) {
+            cancelBtn.setOnClickListener(view -> {
+                mCallback.reset();
+            });
+        }
     }
 
     @Override
diff --git a/com/android/keyguard/KeyguardPasswordView.java b/com/android/keyguard/KeyguardPasswordView.java
index 75c52d8..7cc37c4 100644
--- a/com/android/keyguard/KeyguardPasswordView.java
+++ b/com/android/keyguard/KeyguardPasswordView.java
@@ -205,6 +205,13 @@
             }
         });
 
+        View cancelBtn = findViewById(R.id.cancel_button);
+        if (cancelBtn != null) {
+            cancelBtn.setOnClickListener(view -> {
+                mCallback.reset();
+            });
+        }
+
         // If there's more than one IME, enable the IME switcher button
         updateSwitchImeButton();
 
diff --git a/com/android/keyguard/KeyguardPatternView.java b/com/android/keyguard/KeyguardPatternView.java
index 651831e..174dcab 100644
--- a/com/android/keyguard/KeyguardPatternView.java
+++ b/com/android/keyguard/KeyguardPatternView.java
@@ -157,6 +157,13 @@
         if (button != null) {
             button.setCallback(this);
         }
+
+        View cancelBtn = findViewById(R.id.cancel_button);
+        if (cancelBtn != null) {
+            cancelBtn.setOnClickListener(view -> {
+                mCallback.reset();
+            });
+        }
     }
 
     @Override
diff --git a/com/android/keyguard/KeyguardSimPinView.java b/com/android/keyguard/KeyguardSimPinView.java
index c71c433..42c7a56 100644
--- a/com/android/keyguard/KeyguardSimPinView.java
+++ b/com/android/keyguard/KeyguardSimPinView.java
@@ -78,6 +78,11 @@
                     }
                     break;
                 }
+                case READY: {
+                    mRemainingAttempts = -1;
+                    resetState();
+                    break;
+                }
                 default:
                     resetState();
             }
diff --git a/com/android/keyguard/KeyguardStatusView.java b/com/android/keyguard/KeyguardStatusView.java
index ce16efb..ff3af17 100644
--- a/com/android/keyguard/KeyguardStatusView.java
+++ b/com/android/keyguard/KeyguardStatusView.java
@@ -216,8 +216,7 @@
     }
 
     public void refreshTime() {
-        mClockView.setFormat12Hour(Patterns.clockView12);
-        mClockView.setFormat24Hour(Patterns.clockView24);
+        mClockView.refresh();
     }
 
     private void refresh() {
diff --git a/com/android/providers/settings/SettingsBackupAgent.java b/com/android/providers/settings/SettingsBackupAgent.java
index 313f73f..f728684 100644
--- a/com/android/providers/settings/SettingsBackupAgent.java
+++ b/com/android/providers/settings/SettingsBackupAgent.java
@@ -253,6 +253,7 @@
                     && !RESTORE_FROM_HIGHER_SDK_INT_SUPPORTED_KEYS.contains(key)) {
                 Log.w(TAG, "Not restoring unrecognized key '"
                         + key + "' from future version " + appVersionCode);
+                data.skipEntityData();
                 continue;
             }
 
diff --git a/com/android/providers/settings/SettingsHelper.java b/com/android/providers/settings/SettingsHelper.java
index ad422d8..4c98bb8 100644
--- a/com/android/providers/settings/SettingsHelper.java
+++ b/com/android/providers/settings/SettingsHelper.java
@@ -144,10 +144,7 @@
         }
 
         try {
-            if (Settings.System.SCREEN_BRIGHTNESS.equals(name)) {
-                setBrightness(Integer.parseInt(value));
-                // fall through to the ordinary write to settings
-            } else if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
+            if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
                 setSoundEffects(Integer.parseInt(value) == 1);
                 // fall through to the ordinary write to settings
             } else if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
@@ -305,10 +302,6 @@
         }
     }
 
-    private void setBrightness(int brightness) {
-        mContext.getSystemService(DisplayManager.class).setTemporaryBrightness(brightness);
-    }
-
     /* package */ byte[] getLocaleData() {
         Configuration conf = mContext.getResources().getConfiguration();
         return conf.getLocales().toLanguageTags().getBytes();
diff --git a/com/android/providers/settings/SettingsProtoDumpUtil.java b/com/android/providers/settings/SettingsProtoDumpUtil.java
index f43e719..a2263b4 100644
--- a/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -742,6 +742,9 @@
         dumpSetting(s, p,
                 Settings.Global.LOCATION_GLOBAL_KILL_SWITCH,
                 GlobalSettingsProto.Location.GLOBAL_KILL_SWITCH);
+        dumpSetting(s, p,
+                Settings.Global.GNSS_SATELLITE_BLACKLIST,
+                GlobalSettingsProto.Location.GNSS_SATELLITE_BLACKLIST);
         p.end(locationToken);
 
         final long lpmToken = p.start(GlobalSettingsProto.LOW_POWER_MODE);
@@ -2019,6 +2022,10 @@
                 SecureSettingsProto.Rotation.NUM_ROTATION_SUGGESTIONS_ACCEPTED);
         p.end(rotationToken);
 
+        dumpSetting(s, p,
+                Settings.Secure.RTT_CALLING_MODE,
+                SecureSettingsProto.RTT_CALLING_MODE);
+
         final long screensaverToken = p.start(SecureSettingsProto.SCREENSAVER);
         dumpSetting(s, p,
                 Settings.Secure.SCREENSAVER_ENABLED,
@@ -2224,6 +2231,12 @@
                 Settings.Secure.WAKE_GESTURE_ENABLED,
                 SecureSettingsProto.WAKE_GESTURE_ENABLED);
 
+        final long launcherToken = p.start(SecureSettingsProto.LAUNCHER);
+        dumpSetting(s, p,
+                Settings.Secure.SWIPE_UP_TO_SWITCH_APPS_ENABLED,
+                SecureSettingsProto.Launcher.SWIPE_UP_TO_SWITCH_APPS_ENABLED);
+        p.end(launcherToken);
+
         // Please insert new settings using the same order as in SecureSettingsProto.
         p.end(token);
 
@@ -2402,10 +2415,6 @@
                 SystemSettingsProto.Rotation.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY);
         p.end(rotationToken);
 
-        dumpSetting(s, p,
-                Settings.System.RTT_CALLING_MODE,
-                SystemSettingsProto.RTT_CALLING_MODE);
-
         final long screenToken = p.start(SystemSettingsProto.SCREEN);
         dumpSetting(s, p,
                 Settings.System.SCREEN_OFF_TIMEOUT,
diff --git a/com/android/providers/settings/SettingsProvider.java b/com/android/providers/settings/SettingsProvider.java
index a6d6250..022e306 100644
--- a/com/android/providers/settings/SettingsProvider.java
+++ b/com/android/providers/settings/SettingsProvider.java
@@ -2935,7 +2935,7 @@
         }
 
         private final class UpgradeController {
-            private static final int SETTINGS_VERSION = 162;
+            private static final int SETTINGS_VERSION = 163;
 
             private final int mUserId;
 
@@ -3709,6 +3709,21 @@
                     currentVersion = 162;
                 }
 
+                if (currentVersion == 162) {
+                    // Version 162: Add a gesture for silencing phones
+                    final SettingsState settings = getGlobalSettingsLocked();
+                    final Setting currentSetting = settings.getSettingLocked(
+                            Global.SHOW_ZEN_UPGRADE_NOTIFICATION);
+                    if (!currentSetting.isNull()
+                            && TextUtils.equals("0", currentSetting.getValue())) {
+                        settings.insertSettingLocked(
+                                Global.SHOW_ZEN_UPGRADE_NOTIFICATION, "1",
+                                null, true, SettingsState.SYSTEM_PACKAGE_NAME);
+                    }
+
+                    currentVersion = 163;
+                }
+
                 // vXXX: Add new settings above this point.
 
                 if (currentVersion != newVersion) {
diff --git a/com/android/server/AlarmManagerService.java b/com/android/server/AlarmManagerService.java
index 8ce4e64..6f4ae15 100644
--- a/com/android/server/AlarmManagerService.java
+++ b/com/android/server/AlarmManagerService.java
@@ -88,6 +88,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.LocalLog;
+import com.android.internal.util.StatLogger;
 import com.android.server.AppStateTracker.Listener;
 
 import java.io.ByteArrayOutputStream;
diff --git a/com/android/server/AppStateTracker.java b/com/android/server/AppStateTracker.java
index cec4f1a..23c5779 100644
--- a/com/android/server/AppStateTracker.java
+++ b/com/android/server/AppStateTracker.java
@@ -55,6 +55,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.internal.util.StatLogger;
 import com.android.server.ForceAppStandbyTrackerProto.ExemptedPackage;
 import com.android.server.ForceAppStandbyTrackerProto.RunAnyInBackgroundRestrictedPackages;
 
diff --git a/com/android/server/ConnectivityService.java b/com/android/server/ConnectivityService.java
index a1ef1ed..6463bed 100644
--- a/com/android/server/ConnectivityService.java
+++ b/com/android/server/ConnectivityService.java
@@ -52,6 +52,8 @@
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.PacketKeepalive;
 import android.net.IConnectivityManager;
+import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
 import android.net.INetworkManagementEventObserver;
 import android.net.INetworkPolicyListener;
 import android.net.INetworkPolicyManager;
@@ -140,6 +142,7 @@
 import com.android.server.connectivity.DataConnectionStats;
 import com.android.server.connectivity.DnsManager;
 import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
+import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
 import com.android.server.connectivity.IpConnectivityMetrics;
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.LingerMonitor;
@@ -155,6 +158,7 @@
 import com.android.server.connectivity.Tethering;
 import com.android.server.connectivity.Vpn;
 import com.android.server.connectivity.tethering.TetheringDependencies;
+import com.android.server.net.BaseNetdEventCallback;
 import com.android.server.net.BaseNetworkObserver;
 import com.android.server.net.LockdownVpnTracker;
 import com.android.server.net.NetworkPolicyManagerInternal;
@@ -256,6 +260,7 @@
     private INetworkStatsService mStatsService;
     private INetworkPolicyManager mPolicyManager;
     private NetworkPolicyManagerInternal mPolicyManagerInternal;
+    private IIpConnectivityMetrics mIpConnectivityMetrics;
 
     private String mCurrentTcpBufferSizes;
 
@@ -414,6 +419,9 @@
     // Handle changes in Private DNS settings.
     private static final int EVENT_PRIVATE_DNS_SETTINGS_CHANGED = 37;
 
+    // Handle private DNS validation status updates.
+    private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
+
     private static String eventName(int what) {
         return sMagicDecoderRing.get(what, Integer.toString(what));
     }
@@ -934,7 +942,7 @@
 
     // Used only for testing.
     // TODO: Delete this and either:
-    // 1. Give Fake SettingsProvider the ability to send settings change notifications (requires
+    // 1. Give FakeSettingsProvider the ability to send settings change notifications (requires
     //    changing ContentResolver to make registerContentObserver non-final).
     // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it
     //    by subclassing SettingsObserver.
@@ -943,6 +951,12 @@
         mHandler.sendEmptyMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON);
     }
 
+    // See FakeSettingsProvider comment above.
+    @VisibleForTesting
+    void updatePrivateDnsSettings() {
+        mHandler.sendEmptyMessage(EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+    }
+
     private void handleMobileDataAlwaysOn() {
         final boolean enable = toBool(Settings.Global.getInt(
                 mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 1));
@@ -972,8 +986,8 @@
     }
 
     private void registerPrivateDnsSettingsCallbacks() {
-        for (Uri u : DnsManager.getPrivateDnsSettingsUris()) {
-            mSettingsObserver.observe(u, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+        for (Uri uri : DnsManager.getPrivateDnsSettingsUris()) {
+            mSettingsObserver.observe(uri, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
         }
     }
 
@@ -1026,8 +1040,12 @@
         if (network == null) {
             return null;
         }
+        return getNetworkAgentInfoForNetId(network.netId);
+    }
+
+    private NetworkAgentInfo getNetworkAgentInfoForNetId(int netId) {
         synchronized (mNetworkForNetId) {
-            return mNetworkForNetId.get(network.netId);
+            return mNetworkForNetId.get(netId);
         }
     }
 
@@ -1167,9 +1185,7 @@
         }
         NetworkAgentInfo nai;
         if (vpnNetId != NETID_UNSET) {
-            synchronized (mNetworkForNetId) {
-                nai = mNetworkForNetId.get(vpnNetId);
-            }
+            nai = getNetworkAgentInfoForNetId(vpnNetId);
             if (nai != null) return nai.network;
         }
         nai = getDefaultNetwork();
@@ -1545,6 +1561,41 @@
         return true;
     }
 
+    @VisibleForTesting
+    protected final INetdEventCallback mNetdEventCallback = new BaseNetdEventCallback() {
+        @Override
+        public void onPrivateDnsValidationEvent(int netId, String ipAddress,
+                String hostname, boolean validated) {
+            try {
+                mHandler.sendMessage(mHandler.obtainMessage(
+                        EVENT_PRIVATE_DNS_VALIDATION_UPDATE,
+                        new PrivateDnsValidationUpdate(netId,
+                                InetAddress.parseNumericAddress(ipAddress),
+                                hostname, validated)));
+            } catch (IllegalArgumentException e) {
+                loge("Error parsing ip address in validation event");
+            }
+        }
+    };
+
+    @VisibleForTesting
+    protected void registerNetdEventCallback() {
+        mIpConnectivityMetrics =
+                (IIpConnectivityMetrics) IIpConnectivityMetrics.Stub.asInterface(
+                ServiceManager.getService(IpConnectivityLog.SERVICE_NAME));
+        if (mIpConnectivityMetrics == null) {
+            Slog.wtf(TAG, "Missing IIpConnectivityMetrics");
+        }
+
+        try {
+            mIpConnectivityMetrics.addNetdEventCallback(
+                    INetdEventCallback.CALLBACK_CALLER_CONNECTIVITY_SERVICE,
+                    mNetdEventCallback);
+        } catch (Exception e) {
+            loge("Error registering netd callback: " + e);
+        }
+    }
+
     private final INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() {
         @Override
         public void onUidRulesChanged(int uid, int uidRules) {
@@ -1730,6 +1781,7 @@
 
     void systemReady() {
         loadGlobalProxy();
+        registerNetdEventCallback();
 
         synchronized (this) {
             mSystemReady = true;
@@ -2155,41 +2207,21 @@
                 default:
                     return false;
                 case NetworkMonitor.EVENT_NETWORK_TESTED: {
-                    final NetworkAgentInfo nai;
-                    synchronized (mNetworkForNetId) {
-                        nai = mNetworkForNetId.get(msg.arg2);
-                    }
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                     if (nai == null) break;
 
                     final boolean valid = (msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
                     final boolean wasValidated = nai.lastValidated;
                     final boolean wasDefault = isDefaultNetwork(nai);
 
-                    final PrivateDnsConfig privateDnsCfg = (msg.obj instanceof PrivateDnsConfig)
-                            ? (PrivateDnsConfig) msg.obj : null;
                     final String redirectUrl = (msg.obj instanceof String) ? (String) msg.obj : "";
 
-                    final boolean reevaluationRequired;
-                    final String logMsg;
-                    if (valid) {
-                        reevaluationRequired = updatePrivateDns(nai, privateDnsCfg);
-                        logMsg = (DBG && (privateDnsCfg != null))
-                                 ? " with " + privateDnsCfg.toString() : "";
-                    } else {
-                        reevaluationRequired = false;
-                        logMsg = (DBG && !TextUtils.isEmpty(redirectUrl))
-                                 ? " with redirect to " + redirectUrl : "";
-                    }
                     if (DBG) {
+                        final String logMsg = !TextUtils.isEmpty(redirectUrl)
+                                 ? " with redirect to " + redirectUrl
+                                 : "";
                         log(nai.name() + " validation " + (valid ? "passed" : "failed") + logMsg);
                     }
-                    // If there is a change in Private DNS configuration,
-                    // trigger reevaluation of the network to test it.
-                    if (reevaluationRequired) {
-                        nai.networkMonitor.sendMessage(
-                                NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
-                        break;
-                    }
                     if (valid != nai.lastValidated) {
                         if (wasDefault) {
                             metricsLogger().defaultNetworkMetrics().logDefaultNetworkValidity(
@@ -2218,10 +2250,7 @@
                 case NetworkMonitor.EVENT_PROVISIONING_NOTIFICATION: {
                     final int netId = msg.arg2;
                     final boolean visible = toBool(msg.arg1);
-                    final NetworkAgentInfo nai;
-                    synchronized (mNetworkForNetId) {
-                        nai = mNetworkForNetId.get(netId);
-                    }
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
                     // If captive portal status has changed, update capabilities or disconnect.
                     if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
                         final int oldScore = nai.getCurrentScore();
@@ -2252,18 +2281,10 @@
                     break;
                 }
                 case NetworkMonitor.EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
-                    final NetworkAgentInfo nai;
-                    synchronized (mNetworkForNetId) {
-                        nai = mNetworkForNetId.get(msg.arg2);
-                    }
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                     if (nai == null) break;
 
-                    final PrivateDnsConfig cfg = (PrivateDnsConfig) msg.obj;
-                    final boolean reevaluationRequired = updatePrivateDns(nai, cfg);
-                    if (nai.lastValidated && reevaluationRequired) {
-                        nai.networkMonitor.sendMessage(
-                                NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
-                    }
+                    updatePrivateDns(nai, (PrivateDnsConfig) msg.obj);
                     break;
                 }
             }
@@ -2301,61 +2322,50 @@
         }
     }
 
+    private boolean networkRequiresValidation(NetworkAgentInfo nai) {
+        return NetworkMonitor.isValidationRequired(
+                mDefaultRequest.networkCapabilities, nai.networkCapabilities);
+    }
+
     private void handlePrivateDnsSettingsChanged() {
         final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
 
         for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
-            // Private DNS only ever applies to networks that might provide
-            // Internet access and therefore also require validation.
-            if (!NetworkMonitor.isValidationRequired(
-                    mDefaultRequest.networkCapabilities, nai.networkCapabilities)) {
-                continue;
-            }
-
-            // Notify the NetworkMonitor thread in case it needs to cancel or
-            // schedule DNS resolutions. If a DNS resolution is required the
-            // result will be sent back to us.
-            nai.networkMonitor.notifyPrivateDnsSettingsChanged(cfg);
-
-            if (!cfg.inStrictMode()) {
-                // No strict mode hostname DNS resolution needed, so just update
-                // DNS settings directly. In opportunistic and "off" modes this
-                // just reprograms netd with the network-supplied DNS servers
-                // (and of course the boolean of whether or not to attempt TLS).
-                //
-                // TODO: Consider code flow parity with strict mode, i.e. having
-                // NetworkMonitor relay the PrivateDnsConfig back to us and then
-                // performing this call at that time.
-                updatePrivateDns(nai, cfg);
+            handlePerNetworkPrivateDnsConfig(nai, cfg);
+            if (networkRequiresValidation(nai)) {
+                handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
             }
         }
     }
 
-    private boolean updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
-        final boolean reevaluationRequired = true;
-        final boolean dontReevaluate = false;
+    private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) {
+        // Private DNS only ever applies to networks that might provide
+        // Internet access and therefore also require validation.
+        if (!networkRequiresValidation(nai)) return;
 
-        final PrivateDnsConfig oldCfg = mDnsManager.updatePrivateDns(nai.network, newCfg);
+        // Notify the NetworkMonitor thread in case it needs to cancel or
+        // schedule DNS resolutions. If a DNS resolution is required the
+        // result will be sent back to us.
+        nai.networkMonitor.notifyPrivateDnsSettingsChanged(cfg);
+
+        // With Private DNS bypass support, we can proceed to update the
+        // Private DNS config immediately, even if we're in strict mode
+        // and have not yet resolved the provider name into a set of IPs.
+        updatePrivateDns(nai, cfg);
+    }
+
+    private void updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
+        mDnsManager.updatePrivateDns(nai.network, newCfg);
         updateDnses(nai.linkProperties, null, nai.network.netId);
+    }
 
-        if (newCfg == null) {
-            if (oldCfg == null) return dontReevaluate;
-            return oldCfg.useTls ? reevaluationRequired : dontReevaluate;
+    private void handlePrivateDnsValidationUpdate(PrivateDnsValidationUpdate update) {
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetId(update.netId);
+        if (nai == null) {
+            return;
         }
-
-        if (oldCfg == null) {
-            return newCfg.useTls ? reevaluationRequired : dontReevaluate;
-        }
-
-        if (oldCfg.useTls != newCfg.useTls) {
-            return reevaluationRequired;
-        }
-
-        if (newCfg.inStrictMode() && !Objects.equals(oldCfg.hostname, newCfg.hostname)) {
-            return reevaluationRequired;
-        }
-
-        return dontReevaluate;
+        mDnsManager.updatePrivateDnsValidation(update);
+        handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
     }
 
     private void updateLingerState(NetworkAgentInfo nai, long now) {
@@ -3048,6 +3058,10 @@
                 case EVENT_PRIVATE_DNS_SETTINGS_CHANGED:
                     handlePrivateDnsSettingsChanged();
                     break;
+                case EVENT_PRIVATE_DNS_VALIDATION_UPDATE:
+                    handlePrivateDnsValidationUpdate(
+                            (PrivateDnsValidationUpdate) msg.obj);
+                    break;
             }
         }
     }
@@ -3300,7 +3314,7 @@
         if (isNetworkWithLinkPropertiesBlocked(lp, uid, false)) {
             return;
         }
-        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_FORCE_REEVALUATION, uid);
+        nai.networkMonitor.forceReevaluation(uid);
     }
 
     private ProxyInfo getDefaultProxy() {
@@ -4621,6 +4635,11 @@
 
         updateRoutes(newLp, oldLp, netId);
         updateDnses(newLp, oldLp, netId);
+        // Make sure LinkProperties represents the latest private DNS status.
+        // This does not need to be done before updateDnses because the
+        // LinkProperties are not the source of the private DNS configuration.
+        // updateDnses will fetch the private DNS configuration from DnsManager.
+        mDnsManager.updatePrivateDnsStatus(netId, newLp);
 
         // Start or stop clat accordingly to network state.
         networkAgent.updateClat(mNetd);
@@ -4919,7 +4938,7 @@
     }
 
     public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) {
-        if (mNetworkForNetId.get(nai.network.netId) != nai) {
+        if (getNetworkAgentInfoForNetId(nai.network.netId) != nai) {
             // Ignore updates for disconnected networks
             return;
         }
@@ -5495,6 +5514,7 @@
         if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
             networkAgent.everConnected = true;
 
+            handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
             updateLinkProperties(networkAgent, null);
             notifyIfacesChangedForNetworkStats();
 
@@ -5911,4 +5931,4 @@
             pw.println("    Get airplane mode.");
         }
     }
-}
\ No newline at end of file
+}
diff --git a/com/android/server/InputMethodManagerService.java b/com/android/server/InputMethodManagerService.java
index 40f9476..f678eed 100644
--- a/com/android/server/InputMethodManagerService.java
+++ b/com/android/server/InputMethodManagerService.java
@@ -263,17 +263,19 @@
     private static final class DebugFlag {
         private static final Object LOCK = new Object();
         private final String mKey;
+        private final boolean mDefaultValue;
         @GuardedBy("LOCK")
         private boolean mValue;
 
         public DebugFlag(String key, boolean defaultValue) {
             mKey = key;
+            mDefaultValue = defaultValue;
             mValue = SystemProperties.getBoolean(key, defaultValue);
         }
 
         void refresh() {
             synchronized (LOCK) {
-                mValue = SystemProperties.getBoolean(mKey, true);
+                mValue = SystemProperties.getBoolean(mKey, mDefaultValue);
             }
         }
 
diff --git a/com/android/server/IpSecService.java b/com/android/server/IpSecService.java
index bde6bd8..cd90e3f 100644
--- a/com/android/server/IpSecService.java
+++ b/com/android/server/IpSecService.java
@@ -24,6 +24,8 @@
 import static android.system.OsConstants.SOCK_DGRAM;
 import static com.android.internal.util.Preconditions.checkNotNull;
 
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.IIpSecService;
@@ -42,6 +44,7 @@
 import android.net.TrafficStats;
 import android.net.util.NetdService;
 import android.os.Binder;
+import android.os.DeadSystemException;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -974,6 +977,13 @@
         return service;
     }
 
+    @NonNull
+    private AppOpsManager getAppOpsManager() {
+        AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+        if(appOps == null) throw new RuntimeException("System Server couldn't get AppOps");
+        return appOps;
+    }
+
     /** @hide */
     @VisibleForTesting
     public IpSecService(Context context, IpSecServiceConfiguration config) {
@@ -1240,7 +1250,9 @@
      */
     @Override
     public synchronized IpSecTunnelInterfaceResponse createTunnelInterface(
-            String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder) {
+            String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder,
+            String callingPackage) {
+        enforceTunnelPermissions(callingPackage);
         checkNotNull(binder, "Null Binder passed to createTunnelInterface");
         checkNotNull(underlyingNetwork, "No underlying network was specified");
         checkInetAddress(localAddr);
@@ -1320,8 +1332,8 @@
      */
     @Override
     public synchronized void addAddressToTunnelInterface(
-            int tunnelResourceId, LinkAddress localAddr) {
-        enforceNetworkStackPermission();
+            int tunnelResourceId, LinkAddress localAddr, String callingPackage) {
+        enforceTunnelPermissions(callingPackage);
         UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
 
         // Get tunnelInterface record; if no such interface is found, will throw
@@ -1352,10 +1364,10 @@
      */
     @Override
     public synchronized void removeAddressFromTunnelInterface(
-            int tunnelResourceId, LinkAddress localAddr) {
-        enforceNetworkStackPermission();
-        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+            int tunnelResourceId, LinkAddress localAddr, String callingPackage) {
+        enforceTunnelPermissions(callingPackage);
 
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
         // Get tunnelInterface record; if no such interface is found, will throw
         // IllegalArgumentException
         TunnelInterfaceRecord tunnelInterfaceInfo =
@@ -1383,7 +1395,9 @@
      * server
      */
     @Override
-    public synchronized void deleteTunnelInterface(int resourceId) throws RemoteException {
+    public synchronized void deleteTunnelInterface(
+            int resourceId, String callingPackage) throws RemoteException {
+        enforceTunnelPermissions(callingPackage);
         UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
         releaseResource(userRecord.mTunnelInterfaceRecords, resourceId);
     }
@@ -1469,7 +1483,6 @@
             case IpSecTransform.MODE_TRANSPORT:
                 break;
             case IpSecTransform.MODE_TUNNEL:
-                enforceNetworkStackPermission();
                 break;
             default:
                 throw new IllegalArgumentException(
@@ -1477,9 +1490,20 @@
         }
     }
 
-    private void enforceNetworkStackPermission() {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.NETWORK_STACK,
-                "IpSecService");
+    private void enforceTunnelPermissions(String callingPackage) {
+        checkNotNull(callingPackage, "Null calling package cannot create IpSec tunnels");
+        switch (getAppOpsManager().noteOp(
+                    AppOpsManager.OP_MANAGE_IPSEC_TUNNELS,
+                    Binder.getCallingUid(), callingPackage)) {
+            case AppOpsManager.MODE_DEFAULT:
+                mContext.enforceCallingOrSelfPermission(
+                        android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService");
+                break;
+            case AppOpsManager.MODE_ALLOWED:
+                return;
+            default:
+                throw new SecurityException("Request to ignore AppOps for non-legacy API");
+        }
     }
 
     private void createOrUpdateTransform(
@@ -1535,8 +1559,12 @@
      * result in all of those sockets becoming unable to send or receive data.
      */
     @Override
-    public synchronized IpSecTransformResponse createTransform(IpSecConfig c, IBinder binder)
-            throws RemoteException {
+    public synchronized IpSecTransformResponse createTransform(
+            IpSecConfig c, IBinder binder, String callingPackage) throws RemoteException {
+        checkNotNull(c);
+        if (c.getMode() == IpSecTransform.MODE_TUNNEL) {
+            enforceTunnelPermissions(callingPackage);
+        }
         checkIpSecConfig(c);
         checkNotNull(binder, "Null Binder passed to createTransform");
         final int resourceId = mNextResourceId++;
@@ -1657,8 +1685,9 @@
      */
     @Override
     public synchronized void applyTunnelModeTransform(
-            int tunnelResourceId, int direction, int transformResourceId) throws RemoteException {
-        enforceNetworkStackPermission();
+            int tunnelResourceId, int direction,
+            int transformResourceId, String callingPackage) throws RemoteException {
+        enforceTunnelPermissions(callingPackage);
         checkDirection(direction);
 
         UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
diff --git a/com/android/server/LocationManagerService.java b/com/android/server/LocationManagerService.java
index 26b83f5..fb5fba0 100644
--- a/com/android/server/LocationManagerService.java
+++ b/com/android/server/LocationManagerService.java
@@ -1344,13 +1344,7 @@
      * @param provider the name of the location provider
      */
     private boolean isAllowedByCurrentUserSettingsLocked(String provider) {
-        if (mEnabledProviders.contains(provider)) {
-            return true;
-        }
-        if (mDisabledProviders.contains(provider)) {
-            return false;
-        }
-        return isLocationProviderEnabledForUser(provider, mCurrentUserId);
+        return isAllowedByUserSettingsLockedForUser(provider, mCurrentUserId);
     }
 
     /**
@@ -1359,13 +1353,33 @@
      * processes belonging to background users.
      *
      * @param provider the name of the location provider
-     * @param uid      the requestor's UID
+     * @param userId   the user id to query
      */
-    private boolean isAllowedByUserSettingsLocked(String provider, int uid) {
+    private boolean isAllowedByUserSettingsLockedForUser(String provider, int userId) {
+        if (mEnabledProviders.contains(provider)) {
+            return true;
+        }
+        if (mDisabledProviders.contains(provider)) {
+            return false;
+        }
+        return isLocationProviderEnabledForUser(provider, userId);
+    }
+
+
+    /**
+     * Returns "true" if access to the specified location provider is allowed by the specified
+     * user's settings. Access to all location providers is forbidden to non-location-provider
+     * processes belonging to background users.
+     *
+     * @param provider the name of the location provider
+     * @param uid      the requestor's UID
+     * @param userId   the user id to query
+     */
+    private boolean isAllowedByUserSettingsLocked(String provider, int uid, int userId) {
         if (!isCurrentProfile(UserHandle.getUserId(uid)) && !isUidALocationProvider(uid)) {
             return false;
         }
-        return isAllowedByCurrentUserSettingsLocked(provider);
+        return isAllowedByUserSettingsLockedForUser(provider, userId);
     }
 
     /**
@@ -1572,7 +1586,8 @@
                         continue;
                     }
                     if (allowedResolutionLevel >= getMinimumResolutionLevelForProviderUse(name)) {
-                        if (enabledOnly && !isAllowedByUserSettingsLocked(name, uid)) {
+                        if (enabledOnly
+                                && !isAllowedByUserSettingsLocked(name, uid, mCurrentUserId)) {
                             continue;
                         }
                         if (criteria != null && !LocationProvider.propertiesMeetCriteria(
@@ -2098,7 +2113,7 @@
             oldRecord.disposeLocked(false);
         }
 
-        boolean isProviderEnabled = isAllowedByUserSettingsLocked(name, uid);
+        boolean isProviderEnabled = isAllowedByUserSettingsLocked(name, uid, mCurrentUserId);
         if (isProviderEnabled) {
             applyRequirementsLocked(name);
         } else {
@@ -2219,7 +2234,7 @@
                 LocationProviderInterface provider = mProvidersByName.get(name);
                 if (provider == null) return null;
 
-                if (!isAllowedByUserSettingsLocked(name, uid)) return null;
+                if (!isAllowedByUserSettingsLocked(name, uid, mCurrentUserId)) return null;
 
                 Location location;
                 if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) {
@@ -2540,6 +2555,173 @@
     }
 
     /**
+     *  Returns the current location enabled/disabled status for a user
+     *
+     *  @param userId the id of the user
+     *  @return true if location is enabled
+     */
+    @Override
+    public boolean isLocationEnabledForUser(int userId) {
+        // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+        checkInteractAcrossUsersPermission(userId);
+
+        long identity = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                final String allowedProviders = Settings.Secure.getStringForUser(
+                        mContext.getContentResolver(),
+                        Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+                        userId);
+                if (allowedProviders == null) {
+                    return false;
+                }
+                final List<String> providerList = Arrays.asList(allowedProviders.split(","));
+                for(String provider : mRealProviders.keySet()) {
+                    if (provider.equals(LocationManager.PASSIVE_PROVIDER)
+                            || provider.equals(LocationManager.FUSED_PROVIDER)) {
+                        continue;
+                    }
+                    if (providerList.contains(provider)) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     *  Enable or disable location for a user
+     *
+     *  @param enabled true to enable location, false to disable location
+     *  @param userId the id of the user
+     */
+    @Override
+    public void setLocationEnabledForUser(boolean enabled, int userId) {
+        mContext.enforceCallingPermission(
+            android.Manifest.permission.WRITE_SECURE_SETTINGS,
+            "Requires WRITE_SECURE_SETTINGS permission");
+
+        // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+        checkInteractAcrossUsersPermission(userId);
+
+        long identity = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                final Set<String> allRealProviders = mRealProviders.keySet();
+                // Update all providers on device plus gps and network provider when disabling
+                // location
+                Set<String> allProvidersSet = new ArraySet<>(allRealProviders.size() + 2);
+                allProvidersSet.addAll(allRealProviders);
+                // When disabling location, disable gps and network provider that could have been
+                // enabled by location mode api.
+                if (enabled == false) {
+                    allProvidersSet.add(LocationManager.GPS_PROVIDER);
+                    allProvidersSet.add(LocationManager.NETWORK_PROVIDER);
+                }
+                if (allProvidersSet.isEmpty()) {
+                    return;
+                }
+                // to ensure thread safety, we write the provider name with a '+' or '-'
+                // and let the SettingsProvider handle it rather than reading and modifying
+                // the list of enabled providers.
+                final String prefix = enabled ? "+" : "-";
+                StringBuilder locationProvidersAllowed = new StringBuilder();
+                for (String provider : allProvidersSet) {
+                    if (provider.equals(LocationManager.PASSIVE_PROVIDER)
+                            || provider.equals(LocationManager.FUSED_PROVIDER)) {
+                        continue;
+                    }
+                    locationProvidersAllowed.append(prefix);
+                    locationProvidersAllowed.append(provider);
+                    locationProvidersAllowed.append(",");
+                }
+                // Remove the trailing comma
+                locationProvidersAllowed.setLength(locationProvidersAllowed.length() - 1);
+                Settings.Secure.putStringForUser(
+                        mContext.getContentResolver(),
+                        Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+                        locationProvidersAllowed.toString(),
+                        userId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     *  Returns the current enabled/disabled status of a location provider and user
+     *
+     *  @param provider name of the provider
+     *  @param userId the id of the user
+     *  @return true if the provider exists and is enabled
+     */
+    @Override
+    public boolean isProviderEnabledForUser(String provider, int userId) {
+        // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+        checkInteractAcrossUsersPermission(userId);
+
+        // Fused provider is accessed indirectly via criteria rather than the provider-based APIs,
+        // so we discourage its use
+        if (LocationManager.FUSED_PROVIDER.equals(provider)) return false;
+
+        int uid = Binder.getCallingUid();
+        synchronized (mLock) {
+            LocationProviderInterface p = mProvidersByName.get(provider);
+            return p != null
+                    && isAllowedByUserSettingsLocked(provider, uid, userId);
+        }
+    }
+
+    /**
+     * Enable or disable a single location provider.
+     *
+     * @param provider name of the provider
+     * @param enabled true to enable the provider. False to disable the provider
+     * @param userId the id of the user to set
+     * @return true if the value was set, false on errors
+     */
+    @Override
+    public boolean setProviderEnabledForUser(String provider, boolean enabled, int userId) {
+        mContext.enforceCallingPermission(
+                android.Manifest.permission.WRITE_SECURE_SETTINGS,
+                "Requires WRITE_SECURE_SETTINGS permission");
+
+        // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+        checkInteractAcrossUsersPermission(userId);
+
+        // Fused provider is accessed indirectly via criteria rather than the provider-based APIs,
+        // so we discourage its use
+        if (LocationManager.FUSED_PROVIDER.equals(provider)) return false;
+
+        long identity = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                // No such provider exists
+                if (!mProvidersByName.containsKey(provider)) return false;
+
+                // If it is a test provider, do not write to Settings.Secure
+                if (mMockProviders.containsKey(provider)) {
+                    setTestProviderEnabled(provider, enabled);
+                    return true;
+                }
+
+                // to ensure thread safety, we write the provider name with a '+' or '-'
+                // and let the SettingsProvider handle it rather than reading and modifying
+                // the list of enabled providers.
+                String providerChange = (enabled ? "+" : "-") + provider;
+                return Settings.Secure.putStringForUser(
+                        mContext.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+                        providerChange, userId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
      * Read location provider status from Settings.Secure
      *
      * @param provider the location provider to query
@@ -2560,6 +2742,23 @@
     }
 
     /**
+     * Method for checking INTERACT_ACROSS_USERS permission if specified user id is not the same as
+     * current user id
+     *
+     * @param userId the user id to get or set value
+     */
+    private void checkInteractAcrossUsersPermission(int userId) {
+        int uid = Binder.getCallingUid();
+        if (UserHandle.getUserId(uid) != userId) {
+            if (ActivityManager.checkComponentPermission(
+                    android.Manifest.permission.INTERACT_ACROSS_USERS, uid, -1, true)
+                    != PERMISSION_GRANTED) {
+                throw new SecurityException("Requires INTERACT_ACROSS_USERS permission");
+            }
+        }
+    }
+
+    /**
      * Returns "true" if the UID belongs to a bound location provider.
      *
      * @param uid the uid
@@ -3076,7 +3275,11 @@
         if (!canCallerAccessMockLocation(opPackageName)) {
             return;
         }
+        setTestProviderEnabled(provider, enabled);
+    }
 
+    /** Enable or disable a test location provider. */
+    private void setTestProviderEnabled(String provider, boolean enabled) {
         synchronized (mLock) {
             MockProvider mockProvider = mMockProviders.get(provider);
             if (mockProvider == null) {
diff --git a/com/android/server/LockGuard.java b/com/android/server/LockGuard.java
index b744917..5ce16c4 100644
--- a/com/android/server/LockGuard.java
+++ b/com/android/server/LockGuard.java
@@ -16,10 +16,14 @@
 
 package com.android.server;
 
+import android.annotation.Nullable;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
 
+import com.android.internal.os.BackgroundThread;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
@@ -60,8 +64,6 @@
 public class LockGuard {
     private static final String TAG = "LockGuard";
 
-    public static final boolean ENABLED = false;
-
     /**
      * Well-known locks ordered by fixed index. Locks with a specific index
      * should never be acquired while holding a lock of a lower index.
@@ -73,8 +75,9 @@
     public static final int INDEX_STORAGE = 4;
     public static final int INDEX_WINDOW = 5;
     public static final int INDEX_ACTIVITY = 6;
+    public static final int INDEX_DPMS = 7;
 
-    private static Object[] sKnownFixed = new Object[INDEX_ACTIVITY + 1];
+    private static Object[] sKnownFixed = new Object[INDEX_DPMS + 1];
 
     private static ArrayMap<Object, LockInfo> sKnown = new ArrayMap<>(0, true);
 
@@ -84,6 +87,9 @@
 
         /** Child locks that can be acquired while this lock is already held */
         public ArraySet<Object> children = new ArraySet<>(0, true);
+
+        /** If true, do wtf instead of a warning log. */
+        public boolean doWtf;
     }
 
     private static LockInfo findOrCreateLockInfo(Object lock) {
@@ -114,9 +120,9 @@
             if (child == null) continue;
 
             if (Thread.holdsLock(child)) {
-                Slog.w(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding "
-                      + lockToString(child) + " while trying to acquire "
-                      + lockToString(lock), new Throwable());
+                doLog(lock, "Calling thread " + Thread.currentThread().getName()
+                        + " is holding " + lockToString(child) + " while trying to acquire "
+                        + lockToString(lock));
                 triggered = true;
             }
         }
@@ -145,13 +151,30 @@
         for (int i = 0; i < index; i++) {
             final Object lock = sKnownFixed[i];
             if (lock != null && Thread.holdsLock(lock)) {
-                Slog.w(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding "
-                        + lockToString(i) + " while trying to acquire "
-                        + lockToString(index), new Throwable());
+
+                // Note in this case sKnownFixed may not contain a lock at the given index,
+                // which is okay and in that case we just don't do a WTF.
+                final Object targetMayBeNull = sKnownFixed[index];
+                doLog(targetMayBeNull, "Calling thread " + Thread.currentThread().getName()
+                        + " is holding " + lockToString(i) + " while trying to acquire "
+                        + lockToString(index));
             }
         }
     }
 
+    private static void doLog(@Nullable Object lock, String message) {
+        if (lock != null && findOrCreateLockInfo(lock).doWtf) {
+
+            // Don't want to call into ActivityManager with any lock held, so let's just call it
+            // from a new thread. We don't want to use known threads (e.g. BackgroundThread) either
+            // because they may be stuck too.
+            final Throwable stackTrace = new RuntimeException(message);
+            new Thread(() -> Slog.wtf(TAG, stackTrace)).start();
+            return;
+        }
+        Slog.w(TAG, message, new Throwable());
+    }
+
     /**
      * Report the given lock with a well-known label.
      */
@@ -165,19 +188,33 @@
      * Report the given lock with a well-known index.
      */
     public static Object installLock(Object lock, int index) {
+        return installLock(lock, index, /*doWtf=*/ false);
+    }
+
+    /**
+     * Report the given lock with a well-known index.
+     */
+    public static Object installLock(Object lock, int index, boolean doWtf) {
         sKnownFixed[index] = lock;
+        final LockInfo info = findOrCreateLockInfo(lock);
+        info.doWtf = doWtf;
+        info.label = "Lock-" + lockToString(index);
         return lock;
     }
 
     public static Object installNewLock(int index) {
+        return installNewLock(index, /*doWtf=*/ false);
+    }
+
+    public static Object installNewLock(int index, boolean doWtf) {
         final Object lock = new Object();
-        installLock(lock, index);
+        installLock(lock, index, doWtf);
         return lock;
     }
 
     private static String lockToString(Object lock) {
         final LockInfo info = sKnown.get(lock);
-        if (info != null) {
+        if (info != null && !TextUtils.isEmpty(info.label)) {
             return info.label;
         } else {
             return "0x" + Integer.toHexString(System.identityHashCode(lock));
@@ -193,6 +230,7 @@
             case INDEX_STORAGE: return "STORAGE";
             case INDEX_WINDOW: return "WINDOW";
             case INDEX_ACTIVITY: return "ACTIVITY";
+            case INDEX_DPMS: return "DPMS";
             default: return Integer.toString(index);
         }
     }
diff --git a/com/android/server/NetworkScoreService.java b/com/android/server/NetworkScoreService.java
index 33f7769..80d7ac9 100644
--- a/com/android/server/NetworkScoreService.java
+++ b/com/android/server/NetworkScoreService.java
@@ -128,6 +128,30 @@
         }
     };
 
+    public static final class Lifecycle extends SystemService {
+        private final NetworkScoreService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+            mService = new NetworkScoreService(context);
+        }
+
+        @Override
+        public void onStart() {
+            Log.i(TAG, "Registering " + Context.NETWORK_SCORE_SERVICE);
+            publishBinderService(Context.NETWORK_SCORE_SERVICE, mService);
+        }
+
+        @Override
+        public void onBootPhase(int phase) {
+            if (phase == PHASE_SYSTEM_SERVICES_READY) {
+                mService.systemReady();
+            } else if (phase == PHASE_BOOT_COMPLETED) {
+                mService.systemRunning();
+            }
+        }
+    }
+
     /**
      * Clears scores when the active scorer package is no longer valid and
      * manages the service connection.
diff --git a/com/android/server/StatLogger.java b/com/android/server/StatLogger.java
deleted file mode 100644
index d85810d..0000000
--- a/com/android/server/StatLogger.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2018 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.server;
-
-import android.os.SystemClock;
-import android.util.Slog;
-import android.util.proto.ProtoOutputStream;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.StatLoggerProto.Event;
-
-import java.io.PrintWriter;
-
-/**
- * Simple class to keep track of the number of times certain events happened and their durations for
- * benchmarking.
- *
- * TODO Update shortcut service to switch to it.
- *
- * @hide
- */
-public class StatLogger {
-    private static final String TAG = "StatLogger";
-
-    private final Object mLock = new Object();
-
-    private final int SIZE;
-
-    @GuardedBy("mLock")
-    private final int[] mCountStats;
-
-    @GuardedBy("mLock")
-    private final long[] mDurationStats;
-
-    private final String[] mLabels;
-
-    public StatLogger(String[] eventLabels) {
-        SIZE = eventLabels.length;
-        mCountStats = new int[SIZE];
-        mDurationStats = new long[SIZE];
-        mLabels = eventLabels;
-    }
-
-    /**
-     * Return the current time in the internal time unit.
-     * Call it before an event happens, and
-     * give it back to the {@link #logDurationStat(int, long)}} after the event.
-     */
-    public long getTime() {
-        return SystemClock.elapsedRealtimeNanos() / 1000;
-    }
-
-    /**
-     * @see {@link #getTime()}
-     */
-    public void logDurationStat(int eventId, long start) {
-        synchronized (mLock) {
-            if (eventId >= 0 && eventId < SIZE) {
-                mCountStats[eventId]++;
-                mDurationStats[eventId] += (getTime() - start);
-            } else {
-                Slog.wtf(TAG, "Invalid event ID: " + eventId);
-            }
-        }
-    }
-
-    @Deprecated
-    public void dump(PrintWriter pw, String prefix) {
-        dump(new IndentingPrintWriter(pw, "  ").setIndent(prefix));
-    }
-
-    public void dump(IndentingPrintWriter pw) {
-        synchronized (mLock) {
-            pw.println("Stats:");
-            pw.increaseIndent();
-            for (int i = 0; i < SIZE; i++) {
-                final int count = mCountStats[i];
-                final double durationMs = mDurationStats[i] / 1000.0;
-                pw.println(String.format("%s: count=%d, total=%.1fms, avg=%.3fms",
-                        mLabels[i], count, durationMs,
-                        (count == 0 ? 0 : ((double) durationMs) / count)));
-            }
-            pw.decreaseIndent();
-        }
-    }
-
-    public void dumpProto(ProtoOutputStream proto, long fieldId) {
-        synchronized (mLock) {
-            final long outer = proto.start(fieldId);
-
-            for (int i = 0; i < mLabels.length; i++) {
-                final long inner = proto.start(StatLoggerProto.EVENTS);
-
-                proto.write(Event.EVENT_ID, i);
-                proto.write(Event.LABEL, mLabels[i]);
-                proto.write(Event.COUNT, mCountStats[i]);
-                proto.write(Event.TOTAL_DURATION_MICROS, mDurationStats[i]);
-
-                proto.end(inner);
-            }
-
-            proto.end(outer);
-        }
-    }
-}
diff --git a/com/android/server/SystemServer.java b/com/android/server/SystemServer.java
index 6f50ee2..5519d22 100644
--- a/com/android/server/SystemServer.java
+++ b/com/android/server/SystemServer.java
@@ -725,7 +725,6 @@
         NetworkStatsService networkStats = null;
         NetworkPolicyManagerService networkPolicy = null;
         ConnectivityService connectivity = null;
-        NetworkScoreService networkScore = null;
         NsdService serviceDiscovery= null;
         WindowManagerService wm = null;
         SerialService serial = null;
@@ -1090,12 +1089,7 @@
             }
 
             traceBeginAndSlog("StartNetworkScoreService");
-            try {
-                networkScore = new NetworkScoreService(context);
-                ServiceManager.addService(Context.NETWORK_SCORE_SERVICE, networkScore);
-            } catch (Throwable e) {
-                reportWtf("starting Network Score Service", e);
-            }
+            mSystemServiceManager.startService(NetworkScoreService.Lifecycle.class);
             traceEnd();
 
             traceBeginAndSlog("StartNetworkStatsService");
@@ -1728,7 +1722,6 @@
         final NetworkStatsService networkStatsF = networkStats;
         final NetworkPolicyManagerService networkPolicyF = networkPolicy;
         final ConnectivityService connectivityF = connectivity;
-        final NetworkScoreService networkScoreF = networkScore;
         final LocationManagerService locationF = location;
         final CountryDetectorService countryDetectorF = countryDetector;
         final NetworkTimeUpdateService networkTimeUpdaterF = networkTimeUpdater;
@@ -1789,13 +1782,6 @@
                 reportWtf("starting System UI", e);
             }
             traceEnd();
-            traceBeginAndSlog("MakeNetworkScoreReady");
-            try {
-                if (networkScoreF != null) networkScoreF.systemReady();
-            } catch (Throwable e) {
-                reportWtf("making Network Score Service ready", e);
-            }
-            traceEnd();
             traceBeginAndSlog("MakeNetworkManagementServiceReady");
             try {
                 if (networkManagementF != null) networkManagementF.systemReady();
@@ -1917,13 +1903,6 @@
             }
             traceEnd();
 
-            traceBeginAndSlog("MakeNetworkScoreServiceReady");
-            try {
-                if (networkScoreF != null) networkScoreF.systemRunning();
-            } catch (Throwable e) {
-                reportWtf("Notifying NetworkScoreService running", e);
-            }
-            traceEnd();
             traceBeginAndSlog("IncidentDaemonReady");
             try {
                 // TODO: Switch from checkService to getService once it's always
diff --git a/com/android/server/ThreadPriorityBooster.java b/com/android/server/ThreadPriorityBooster.java
index cc9ac0d..53e8ce4 100644
--- a/com/android/server/ThreadPriorityBooster.java
+++ b/com/android/server/ThreadPriorityBooster.java
@@ -25,6 +25,8 @@
  */
 public class ThreadPriorityBooster {
 
+    private static final boolean ENABLE_LOCK_GUARD = false;
+
     private volatile int mBoostToPriority;
     private final int mLockGuardIndex;
 
@@ -50,7 +52,7 @@
             }
         }
         state.regionCounter++;
-        if (LockGuard.ENABLED) {
+        if (ENABLE_LOCK_GUARD) {
             LockGuard.guard(mLockGuardIndex);
         }
     }
diff --git a/com/android/server/VibratorService.java b/com/android/server/VibratorService.java
index 752c44a..66c2b07 100644
--- a/com/android/server/VibratorService.java
+++ b/com/android/server/VibratorService.java
@@ -53,6 +53,7 @@
 import android.provider.Settings.SettingNotFoundException;
 import android.util.DebugUtils;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.view.InputDevice;
 import android.media.AudioAttributes;
 
@@ -91,7 +92,7 @@
     private final boolean mAllowPriorityVibrationsInLowPowerMode;
     private final boolean mSupportsAmplitudeControl;
     private final int mDefaultVibrationAmplitude;
-    private final VibrationEffect[] mFallbackEffects;
+    private final SparseArray<VibrationEffect> mFallbackEffects;
     private final WorkSource mTmpWorkSource = new WorkSource();
     private final Handler mH = new Handler();
     private final Object mLock = new Object();
@@ -177,6 +178,7 @@
                 switch (prebaked.getId()) {
                     case VibrationEffect.EFFECT_CLICK:
                     case VibrationEffect.EFFECT_DOUBLE_CLICK:
+                    case VibrationEffect.EFFECT_HEAVY_CLICK:
                     case VibrationEffect.EFFECT_TICK:
                         return true;
                     default:
@@ -293,7 +295,11 @@
                 com.android.internal.R.array.config_clockTickVibePattern);
         VibrationEffect tickEffect = createEffect(tickEffectTimings);
 
-        mFallbackEffects = new VibrationEffect[] { clickEffect, doubleClickEffect, tickEffect };
+        mFallbackEffects = new SparseArray<VibrationEffect>();
+        mFallbackEffects.put(VibrationEffect.EFFECT_CLICK, clickEffect);
+        mFallbackEffects.put(VibrationEffect.EFFECT_DOUBLE_CLICK, doubleClickEffect);
+        mFallbackEffects.put(VibrationEffect.EFFECT_TICK, tickEffect);
+        mFallbackEffects.put(VibrationEffect.EFFECT_HEAVY_CLICK, clickEffect);
     }
 
     private static VibrationEffect createEffect(long[] timings) {
@@ -960,10 +966,7 @@
     }
 
     private VibrationEffect getFallbackEffect(int effectId) {
-        if (effectId < 0 || effectId >= mFallbackEffects.length) {
-            return null;
-        }
-        return mFallbackEffects[effectId];
+        return mFallbackEffects.get(effectId);
     }
 
     /**
diff --git a/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 5c5978a..28aa984 100644
--- a/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -53,7 +53,6 @@
 import android.view.accessibility.IAccessibilityInteractionConnection;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
 
-
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.DumpUtils;
 import com.android.server.accessibility.AccessibilityManagerService.RemoteAccessibilityConnection;
@@ -76,7 +75,7 @@
         implements ServiceConnection, IBinder.DeathRecipient, KeyEventDispatcher.KeyEventFilter,
         FingerprintGestureDispatcher.FingerprintGestureClient {
     private static final boolean DEBUG = false;
-    private static final String LOG_TAG = "AccessibilityClientConnection";
+    private static final String LOG_TAG = "AbstractAccessibilityServiceConnection";
 
     protected final Context mContext;
     protected final SystemSupport mSystemSupport;
diff --git a/com/android/server/accessibility/AccessibilityManagerService.java b/com/android/server/accessibility/AccessibilityManagerService.java
index de112f9..08aa063 100644
--- a/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2721,7 +2721,7 @@
             }
         }
 
-        private AccessibilityWindowInfo populateReportedWindow(WindowInfo window) {
+        private AccessibilityWindowInfo populateReportedWindowLocked(WindowInfo window) {
             final int windowId = findWindowIdLocked(window.token);
             if (windowId < 0) {
                 return null;
@@ -2949,7 +2949,7 @@
                 | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
                 | AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;
 
-        // In Z order
+        // In Z order top to bottom
         public List<AccessibilityWindowInfo> mWindows;
         public SparseArray<AccessibilityWindowInfo> mA11yWindowInfoById = new SparseArray<>();
         public SparseArray<WindowInfo> mWindowInfoById = new SparseArray<>();
@@ -3140,11 +3140,19 @@
                     mAccessibilityFocusedWindowId != INVALID_WINDOW_ID;
             if (windowCount > 0) {
                 for (int i = 0; i < windowCount; i++) {
-                    WindowInfo windowInfo = windows.get(i);
-                    AccessibilityWindowInfo window = (mWindowsForAccessibilityCallback != null)
-                            ? mWindowsForAccessibilityCallback.populateReportedWindow(windowInfo)
-                            : null;
+                    final WindowInfo windowInfo = windows.get(i);
+                    final AccessibilityWindowInfo window;
+                    if (mWindowsForAccessibilityCallback != null) {
+                        window = mWindowsForAccessibilityCallback
+                                .populateReportedWindowLocked(windowInfo);
+                    } else {
+                        window = null;
+                    }
                     if (window != null) {
+
+                        // Flip layers in list to be consistent with AccessibilityService#getWindows
+                        window.setLayer(windowCount - 1 - window.getLayer());
+
                         final int windowId = window.getId();
                         if (window.isFocused()) {
                             mFocusedWindowId = windowId;
@@ -3169,7 +3177,7 @@
                 // active window once we decided which it is.
                 final int accessibilityWindowCount = mWindows.size();
                 for (int i = 0; i < accessibilityWindowCount; i++) {
-                    AccessibilityWindowInfo window = mWindows.get(i);
+                    final AccessibilityWindowInfo window = mWindows.get(i);
                     if (window.getId() == mActiveWindowId) {
                         window.setActive(true);
                     }
@@ -3188,7 +3196,10 @@
             }
 
             if (shouldClearAccessibilityFocus) {
-                clearAccessibilityFocus(mAccessibilityFocusedWindowId);
+                mMainHandler.sendMessage(obtainMessage(
+                        AccessibilityManagerService::clearAccessibilityFocus,
+                        AccessibilityManagerService.this,
+                        box(mAccessibilityFocusedWindowId)));
             }
         }
 
diff --git a/com/android/server/am/ActiveServices.java b/com/android/server/am/ActiveServices.java
index f57e9ac..067566d 100644
--- a/com/android/server/am/ActiveServices.java
+++ b/com/android/server/am/ActiveServices.java
@@ -2283,7 +2283,7 @@
                         + " app=" + app);
             if (app != null && app.thread != null) {
                 try {
-                    app.addPackage(r.appInfo.packageName, r.appInfo.versionCode, mAm.mProcessStats);
+                    app.addPackage(r.appInfo.packageName, r.appInfo.longVersionCode, mAm.mProcessStats);
                     realStartServiceLocked(r, app, execInFg);
                     return null;
                 } catch (TransactionTooLargeException e) {
@@ -2645,6 +2645,8 @@
                 Message msg = mAm.mHandler.obtainMessage(
                         ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG);
                 msg.obj = r.app;
+                msg.getData().putCharSequence(
+                    ActivityManagerService.SERVICE_RECORD_KEY, r.toString());
                 mAm.mHandler.sendMessage(msg);
             }
         }
@@ -3009,7 +3011,7 @@
 
                     mPendingServices.remove(i);
                     i--;
-                    proc.addPackage(sr.appInfo.packageName, sr.appInfo.versionCode,
+                    proc.addPackage(sr.appInfo.packageName, sr.appInfo.longVersionCode,
                             mAm.mProcessStats);
                     realStartServiceLocked(sr, proc, sr.createdFromFg);
                     didSomething = true;
@@ -3563,13 +3565,15 @@
 
         if (app != null) {
             mAm.mAppErrors.appNotResponding(app, null, null, false,
-                    "Context.startForegroundService() did not then call Service.startForeground()");
+                    "Context.startForegroundService() did not then call Service.startForeground(): "
+                        + r);
         }
     }
 
-    void serviceForegroundCrash(ProcessRecord app) {
+    void serviceForegroundCrash(ProcessRecord app, CharSequence serviceRecord) {
         mAm.crashApplication(app.uid, app.pid, app.info.packageName, app.userId,
-                "Context.startForegroundService() did not then call Service.startForeground()");
+                "Context.startForegroundService() did not then call Service.startForeground(): "
+                    + serviceRecord);
     }
 
     void scheduleServiceTimeoutLocked(ProcessRecord proc) {
diff --git a/com/android/server/am/ActivityDisplay.java b/com/android/server/am/ActivityDisplay.java
index 4a8bc87..fac3f92 100644
--- a/com/android/server/am/ActivityDisplay.java
+++ b/com/android/server/am/ActivityDisplay.java
@@ -16,7 +16,6 @@
 
 package com.android.server.am;
 
-import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -44,7 +43,6 @@
 import android.app.ActivityOptions;
 import android.app.WindowConfiguration;
 import android.graphics.Point;
-import android.graphics.Rect;
 import android.util.IntArray;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
@@ -702,57 +700,52 @@
     }
 
     /**
-     * @return the stack currently above the home stack.  Can be null if there is no home stack, or
-     *         the home stack is already on top.
+     * @return the stack currently above the {@param stack}.  Can be null if the {@param stack} is
+     *         already top-most.
      */
-    ActivityStack getStackAboveHome() {
-        if (mHomeStack == null) {
-            // Skip if there is no home stack
-            return null;
-        }
-
-        final int stackIndex = mStacks.indexOf(mHomeStack) + 1;
+    ActivityStack getStackAbove(ActivityStack stack) {
+        final int stackIndex = mStacks.indexOf(stack) + 1;
         return (stackIndex < mStacks.size()) ? mStacks.get(stackIndex) : null;
     }
 
     /**
-     * Adjusts the home stack behind the last visible stack in the display if necessary. Generally
-     * used in conjunction with {@link #moveHomeStackBehindStack}.
+     * Adjusts the {@param stack} behind the last visible stack in the display if necessary.
+     * Generally used in conjunction with {@link #moveStackBehindStack}.
      */
-    void moveHomeStackBehindBottomMostVisibleStack() {
-        if (mHomeStack == null || mHomeStack.shouldBeVisible(null)) {
-            // Skip if there is no home stack, or if it is already visible
+    void moveStackBehindBottomMostVisibleStack(ActivityStack stack) {
+        if (stack.shouldBeVisible(null)) {
+            // Skip if the stack is already visible
             return;
         }
 
-        // Move the home stack to the bottom to not affect the following visibility checks
-        positionChildAtBottom(mHomeStack);
+        // Move the stack to the bottom to not affect the following visibility checks
+        positionChildAtBottom(stack);
 
-        // Find the next position where the homes stack should be placed
+        // Find the next position where the stack should be placed
         final int numStacks = mStacks.size();
         for (int stackNdx = 0; stackNdx < numStacks; stackNdx++) {
-            final ActivityStack stack = mStacks.get(stackNdx);
-            if (stack == mHomeStack) {
+            final ActivityStack s = mStacks.get(stackNdx);
+            if (s == stack) {
                 continue;
             }
-            final int winMode = stack.getWindowingMode();
+            final int winMode = s.getWindowingMode();
             final boolean isValidWindowingMode = winMode == WINDOWING_MODE_FULLSCREEN ||
                     winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-            if (stack.shouldBeVisible(null) && isValidWindowingMode) {
-                // Move the home stack to behind this stack
-                positionChildAt(mHomeStack, Math.max(0, stackNdx - 1));
+            if (s.shouldBeVisible(null) && isValidWindowingMode) {
+                // Move the provided stack to behind this stack
+                positionChildAt(stack, Math.max(0, stackNdx - 1));
                 break;
             }
         }
     }
 
     /**
-     * Moves the home stack behind the given {@param stack} if possible. If {@param stack} is not
-     * currently in the display, then then the home stack is moved to the back. Generally used in
-     * conjunction with {@link #moveHomeStackBehindBottomMostVisibleStack}.
+     * Moves the {@param stack} behind the given {@param behindStack} if possible. If
+     * {@param behindStack} is not currently in the display, then then the stack is moved to the
+     * back. Generally used in conjunction with {@link #moveStackBehindBottomMostVisibleStack}.
      */
-    void moveHomeStackBehindStack(ActivityStack behindStack) {
-        if (behindStack == null || behindStack == mHomeStack) {
+    void moveStackBehindStack(ActivityStack stack, ActivityStack behindStack) {
+        if (behindStack == null || behindStack == stack) {
             return;
         }
 
@@ -760,11 +753,11 @@
         // list, so we need to adjust the insertion index to account for the removed index
         // TODO: Remove this logic when WindowContainer.positionChildAt() is updated to adjust the
         //       position internally
-        final int homeStackIndex = mStacks.indexOf(mHomeStack);
+        final int stackIndex = mStacks.indexOf(stack);
         final int behindStackIndex = mStacks.indexOf(behindStack);
-        final int insertIndex = homeStackIndex <= behindStackIndex
+        final int insertIndex = stackIndex <= behindStackIndex
                 ? behindStackIndex - 1 : behindStackIndex;
-        positionChildAt(mHomeStack, Math.max(0, insertIndex));
+        positionChildAt(stack, Math.max(0, insertIndex));
     }
 
     boolean isSleeping() {
diff --git a/com/android/server/am/ActivityManagerService.java b/com/android/server/am/ActivityManagerService.java
index f97c6d6..47284cb 100644
--- a/com/android/server/am/ActivityManagerService.java
+++ b/com/android/server/am/ActivityManagerService.java
@@ -27,6 +27,7 @@
 import static android.Manifest.permission.READ_FRAME_BUFFER;
 import static android.Manifest.permission.REMOVE_TASKS;
 import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
+import static android.Manifest.permission.STOP_APP_SWITCHES;
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.app.ActivityManager.RESIZE_MODE_PRESERVE_WINDOW;
 import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
@@ -38,6 +39,7 @@
 import static android.app.ActivityThread.PROC_START_SEQ_IDENT;
 import static android.app.AppOpsManager.OP_ASSIST_STRUCTURE;
 import static android.app.AppOpsManager.OP_NONE;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -50,6 +52,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
+import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT;
 import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
 import static android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY;
@@ -116,6 +119,7 @@
 import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES;
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RTL;
+import static android.provider.Settings.Global.HIDE_ERROR_DIALOGS;
 import static android.provider.Settings.Global.NETWORK_ACCESS_TIMEOUT_MS;
 import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
 import static android.provider.Settings.System.FONT_SCALE;
@@ -204,6 +208,8 @@
 import static android.view.WindowManager.TRANSIT_TASK_IN_PLACE;
 import static android.view.WindowManager.TRANSIT_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_TASK_TO_FRONT;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
+import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
 import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
@@ -257,7 +263,6 @@
 import android.app.WindowConfiguration.ActivityType;
 import android.app.WindowConfiguration.WindowingMode;
 import android.app.admin.DevicePolicyCache;
-import android.app.admin.DevicePolicyManager;
 import android.app.assist.AssistContent;
 import android.app.assist.AssistStructure;
 import android.app.backup.IBackupManager;
@@ -762,7 +767,7 @@
     /**
      * The controller for all operations related to locktask.
      */
-    final LockTaskController mLockTaskController;
+    private final LockTaskController mLockTaskController;
 
     final UserController mUserController;
 
@@ -1279,17 +1284,24 @@
 
     private final class FontScaleSettingObserver extends ContentObserver {
         private final Uri mFontScaleUri = Settings.System.getUriFor(FONT_SCALE);
+        private final Uri mHideErrorDialogsUri = Settings.Global.getUriFor(HIDE_ERROR_DIALOGS);
 
         public FontScaleSettingObserver() {
             super(mHandler);
             ContentResolver resolver = mContext.getContentResolver();
             resolver.registerContentObserver(mFontScaleUri, false, this, UserHandle.USER_ALL);
+            resolver.registerContentObserver(mHideErrorDialogsUri, false, this,
+                    UserHandle.USER_ALL);
         }
 
         @Override
         public void onChange(boolean selfChange, Uri uri, @UserIdInt int userId) {
             if (mFontScaleUri.equals(uri)) {
                 updateFontScaleIfNeeded(userId);
+            } else if (mHideErrorDialogsUri.equals(uri)) {
+                synchronized (ActivityManagerService.this) {
+                    updateShouldShowDialogsLocked(getGlobalConfiguration());
+                }
             }
         }
     }
@@ -1918,6 +1930,8 @@
     static final int FIRST_COMPAT_MODE_MSG = 300;
     static final int FIRST_SUPERVISOR_STACK_MSG = 100;
 
+    static final String SERVICE_RECORD_KEY = "servicerecord";
+
     static ServiceThread sKillThread = null;
     static KillHandler sKillHandler = null;
 
@@ -1944,7 +1958,7 @@
     final ActivityManagerConstants mConstants;
 
     // Encapsulates the global setting "hidden_api_blacklist_exemptions"
-    final HiddenApiBlacklist mHiddenApiBlacklist;
+    final HiddenApiSettings mHiddenApiBlacklist;
 
     PackageManagerInternal mPackageManagerInt;
 
@@ -2166,7 +2180,8 @@
                 mServices.serviceForegroundTimeout((ServiceRecord)msg.obj);
             } break;
             case SERVICE_FOREGROUND_CRASH_MSG: {
-                mServices.serviceForegroundCrash((ProcessRecord)msg.obj);
+                mServices.serviceForegroundCrash(
+                    (ProcessRecord) msg.obj, msg.getData().getCharSequence(SERVICE_RECORD_KEY));
             } break;
             case DISPATCH_PENDING_INTENT_CANCEL_MSG: {
                 RemoteCallbackList<IResultReceiver> callbacks
@@ -2878,17 +2893,20 @@
     }
 
     /**
-     * Encapsulates the global setting "hidden_api_blacklist_exemptions", including tracking the
-     * latest value via a content observer.
+     * Encapsulates global settings related to hidden API enforcement behaviour, including tracking
+     * the latest value via a content observer.
      */
-    static class HiddenApiBlacklist extends ContentObserver {
+    static class HiddenApiSettings extends ContentObserver {
 
         private final Context mContext;
         private boolean mBlacklistDisabled;
         private String mExemptionsStr;
         private List<String> mExemptions = Collections.emptyList();
+        private int mLogSampleRate = -1;
+        @HiddenApiEnforcementPolicy private int mPolicyPreP = HIDDEN_API_ENFORCEMENT_DEFAULT;
+        @HiddenApiEnforcementPolicy private int mPolicyP = HIDDEN_API_ENFORCEMENT_DEFAULT;
 
-        public HiddenApiBlacklist(Handler handler, Context context) {
+        public HiddenApiSettings(Handler handler, Context context) {
             super(handler);
             mContext = context;
         }
@@ -2898,6 +2916,18 @@
                     Settings.Global.getUriFor(Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS),
                     false,
                     this);
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.HIDDEN_API_ACCESS_LOG_SAMPLING_RATE),
+                    false,
+                    this);
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.HIDDEN_API_POLICY_PRE_P_APPS),
+                    false,
+                    this);
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.HIDDEN_API_POLICY_P_APPS),
+                    false,
+                    this);
             update();
         }
 
@@ -2917,13 +2947,41 @@
                 }
                 zygoteProcess.setApiBlacklistExemptions(mExemptions);
             }
+            int logSampleRate = Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.HIDDEN_API_ACCESS_LOG_SAMPLING_RATE, -1);
+            if (logSampleRate < 0 || logSampleRate > 0x10000) {
+                logSampleRate = -1;
+            }
+            if (logSampleRate != -1 && logSampleRate != mLogSampleRate) {
+                mLogSampleRate = logSampleRate;
+                zygoteProcess.setHiddenApiAccessLogSampleRate(mLogSampleRate);
+            }
+            mPolicyPreP = getValidEnforcementPolicy(Settings.Global.HIDDEN_API_POLICY_PRE_P_APPS);
+            mPolicyP = getValidEnforcementPolicy(Settings.Global.HIDDEN_API_POLICY_P_APPS);
+        }
 
+        private @HiddenApiEnforcementPolicy int getValidEnforcementPolicy(String settingsKey) {
+            int policy = Settings.Global.getInt(mContext.getContentResolver(), settingsKey,
+                    ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT);
+            if (ApplicationInfo.isValidHiddenApiEnforcementPolicy(policy)) {
+                return policy;
+            } else {
+                return ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT;
+            }
         }
 
         boolean isDisabled() {
             return mBlacklistDisabled;
         }
 
+        @HiddenApiEnforcementPolicy int getPolicyForPrePApps() {
+            return mPolicyPreP;
+        }
+
+        @HiddenApiEnforcementPolicy int getPolicyForPApps() {
+            return mPolicyP;
+        }
+
         public void onChange(boolean selfChange) {
             update();
         }
@@ -3096,7 +3154,7 @@
             }
         };
 
-        mHiddenApiBlacklist = new HiddenApiBlacklist(mHandler, mContext);
+        mHiddenApiBlacklist = new HiddenApiSettings(mHandler, mContext);
 
         Watchdog.getInstance().addMonitor(this);
         Watchdog.getInstance().addThread(mHandler);
@@ -4212,6 +4270,9 @@
             }
 
             if (!disableHiddenApiChecks && !mHiddenApiBlacklist.isDisabled()) {
+                app.info.maybeUpdateHiddenApiEnforcementPolicy(
+                        mHiddenApiBlacklist.getPolicyForPrePApps(),
+                        mHiddenApiBlacklist.getPolicyForPApps());
                 @HiddenApiEnforcementPolicy int policy =
                         app.info.getHiddenApiEnforcementPolicy();
                 int policyBits = (policy << Zygote.API_ENFORCEMENT_POLICY_SHIFT);
@@ -5238,12 +5299,14 @@
     }
 
     @Override
-    public void cancelRecentsAnimation() {
+    public void cancelRecentsAnimation(boolean restoreHomeStackPosition) {
         enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "cancelRecentsAnimation()");
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized (this) {
-                mWindowManager.cancelRecentsAnimation();
+                mWindowManager.cancelRecentsAnimation(restoreHomeStackPosition
+                        ? REORDER_MOVE_TO_ORIGINAL_POSITION
+                        : REORDER_KEEP_IN_PLACE, "cancelRecentsAnimation");
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -7635,7 +7698,7 @@
             // target APIs higher than O MR1. Since access to the serial
             // is now behind a permission we push down the value.
             final String buildSerial = (appInfo.targetSandboxVersion < 2
-                    && appInfo.targetSdkVersion <= Build.VERSION_CODES.O_MR1)
+                    && appInfo.targetSdkVersion < Build.VERSION_CODES.P)
                             ? sTheRealBuildSerial : Build.UNKNOWN;
 
             // Check if this is a secondary process that should be incorporated into some
@@ -12392,6 +12455,10 @@
         return mActivityStartController;
     }
 
+    LockTaskController getLockTaskController() {
+        return mLockTaskController;
+    }
+
     ClientLifecycleManager getLifecycleManager() {
         return mLifecycleManager;
     }
@@ -12951,9 +13018,13 @@
         } catch (RemoteException exc) {
             // Ignore.
         }
+        return isBackgroundRestrictedNoCheck(callingUid, packageName);
+    }
+
+    boolean isBackgroundRestrictedNoCheck(final int uid, final String packageName) {
         final int mode = mAppOpsService.checkOperation(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
-                callingUid, packageName);
-        return (mode != AppOpsManager.MODE_ALLOWED);
+                uid, packageName);
+        return mode != AppOpsManager.MODE_ALLOWED;
     }
 
     @Override
@@ -13335,12 +13406,7 @@
 
     @Override
     public void stopAppSwitches() {
-        if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
-                != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException("viewquires permission "
-                    + android.Manifest.permission.STOP_APP_SWITCHES);
-        }
-
+        enforceCallerIsRecentsOrHasPermission(STOP_APP_SWITCHES, "stopAppSwitches");
         synchronized(this) {
             mAppSwitchesAllowedTime = SystemClock.uptimeMillis()
                     + APP_SWITCH_DELAY_TIME;
@@ -13350,12 +13416,7 @@
     }
 
     public void resumeAppSwitches() {
-        if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
-                != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException("Requires permission "
-                    + android.Manifest.permission.STOP_APP_SWITCHES);
-        }
-
+        enforceCallerIsRecentsOrHasPermission(STOP_APP_SWITCHES, "resumeAppSwitches");
         synchronized(this) {
             // Note that we don't execute any pending app switches... we will
             // let those wait until either the timeout, or the next start
@@ -13382,9 +13443,11 @@
             return true;
         }
 
-        int perm = checkComponentPermission(
-                android.Manifest.permission.STOP_APP_SWITCHES, sourcePid,
-                sourceUid, -1, true);
+        if (mRecentTasks.isCallerRecents(sourceUid)) {
+            return true;
+        }
+
+        int perm = checkComponentPermission(STOP_APP_SWITCHES, sourcePid, sourceUid, -1, true);
         if (perm == PackageManager.PERMISSION_GRANTED) {
             return true;
         }
@@ -13395,9 +13458,7 @@
         // If the actual IPC caller is different from the logical source, then
         // also see if they are allowed to control app switches.
         if (callingUid != -1 && callingUid != sourceUid) {
-            perm = checkComponentPermission(
-                    android.Manifest.permission.STOP_APP_SWITCHES, callingPid,
-                    callingUid, -1, true);
+            perm = checkComponentPermission(STOP_APP_SWITCHES, callingPid, callingUid, -1, true);
             if (perm == PackageManager.PERMISSION_GRANTED) {
                 return true;
             }
@@ -16995,6 +17056,11 @@
                     pw.println("ms");
                 }
                 mUidObservers.finishBroadcast();
+
+                pw.println();
+                pw.println("  ServiceManager statistics:");
+                ServiceManager.sStatLogger.dump(pw, "    ");
+                pw.println();
             }
         }
         pw.println("  mForceBackgroundCheck=" + mForceBackgroundCheck);
@@ -21058,6 +21124,7 @@
             }
         }
 
+        final String action = intent.getAction();
         BroadcastOptions brOptions = null;
         if (bOptions != null) {
             brOptions = new BroadcastOptions(bOptions);
@@ -21078,11 +21145,16 @@
                     throw new SecurityException(msg);
                 }
             }
+            if (brOptions.isDontSendToRestrictedApps()
+                    && isBackgroundRestrictedNoCheck(callingUid, callerPackage)) {
+                Slog.i(TAG, "Not sending broadcast " + action + " - app " + callerPackage
+                        + " has background restrictions");
+                return ActivityManager.START_CANCELED;
+            }
         }
 
         // Verify that protected broadcasts are only being sent by system code,
         // and that system code is only sending protected broadcasts.
-        final String action = intent.getAction();
         final boolean isProtectedBroadcast;
         try {
             isProtectedBroadcast = AppGlobals.getPackageManager().isProtectedBroadcast(action);
@@ -21904,8 +21976,6 @@
         "com.android.frameworks.locationtests",
         "com.android.frameworks.coretests.privacy",
         "com.android.settings.ui",
-        "com.android.perftests.core",
-        "com.android.perftests.multiuser",
     };
 
     public boolean startInstrumentation(ComponentName className,
@@ -22443,7 +22513,7 @@
                 mUserController.getCurrentUserId());
 
         // TODO: If our config changes, should we auto dismiss any currently showing dialogs?
-        mShowDialogs = shouldShowDialogs(mTempConfig);
+        updateShouldShowDialogsLocked(mTempConfig);
 
         AttributeCache ac = AttributeCache.instance();
         if (ac != null) {
@@ -22698,7 +22768,7 @@
      * A thought: SystemUI might also want to get told about this, the Power
      * dialog / global actions also might want different behaviors.
      */
-    private static boolean shouldShowDialogs(Configuration config) {
+    private void updateShouldShowDialogsLocked(Configuration config) {
         final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS
                                    && config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH
                                    && config.navigation == Configuration.NAVIGATION_NONAV);
@@ -22707,7 +22777,9 @@
                 && !(modeType == Configuration.UI_MODE_TYPE_WATCH && Build.IS_USER)
                 && modeType != Configuration.UI_MODE_TYPE_TELEVISION
                 && modeType != Configuration.UI_MODE_TYPE_VR_HEADSET);
-        return inputMethodExists && uiModeSupportsDialogs;
+        final boolean hideDialogsSet = Settings.Global.getInt(mContext.getContentResolver(),
+                HIDE_ERROR_DIALOGS, 0) != 0;
+        mShowDialogs = inputMethodExists && uiModeSupportsDialogs && !hideDialogsSet;
     }
 
     @Override
@@ -26614,7 +26686,7 @@
                 record.waitingForNetwork = false;
                 final long totalTime = SystemClock.uptimeMillis() - startTime;
                 if (totalTime >= mWaitForNetworkTimeoutMs || DEBUG_NETWORK) {
-                    Slog.wtf(TAG_NETWORK, "Total time waited for network rules to get updated: "
+                    Slog.w(TAG_NETWORK, "Total time waited for network rules to get updated: "
                             + totalTime + ". Uid: " + callingUid + " procStateSeq: "
                             + procStateSeq + " UidRec: " + record
                             + " validateUidRec: " + mValidateUids.get(callingUid));
diff --git a/com/android/server/am/ActivityRecord.java b/com/android/server/am/ActivityRecord.java
index 1af4114..f32717a 100644
--- a/com/android/server/am/ActivityRecord.java
+++ b/com/android/server/am/ActivityRecord.java
@@ -93,8 +93,6 @@
 import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_VISIBILITY;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.am.ActivityStack.ActivityState.DESTROYED;
-import static com.android.server.am.ActivityStack.ActivityState.DESTROYING;
 import static com.android.server.am.ActivityStack.ActivityState.INITIALIZING;
 import static com.android.server.am.ActivityStack.ActivityState.PAUSED;
 import static com.android.server.am.ActivityStack.ActivityState.PAUSING;
@@ -154,6 +152,7 @@
 import android.graphics.Bitmap;
 import android.graphics.GraphicBuffer;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Debug;
@@ -780,11 +779,13 @@
      * @param task The new parent {@link TaskRecord}.
      */
     void setTask(TaskRecord task) {
-        setTask(task, false /*reparenting*/);
+        setTask(task /* task */, false /* reparenting */);
     }
 
     /**
      * This method should only be called by {@link TaskRecord#removeActivity(ActivityRecord)}.
+     * @param task          The new parent task.
+     * @param reparenting   Whether we're in the middle of reparenting.
      */
     void setTask(TaskRecord task, boolean reparenting) {
         // Do nothing if the {@link TaskRecord} is the same as the current {@link getTask}.
@@ -792,12 +793,19 @@
             return;
         }
 
-        final ActivityStack stack = getStack();
+        final ActivityStack oldStack = getStack();
+        final ActivityStack newStack = task != null ? task.getStack() : null;
 
-        // If the new {@link TaskRecord} is from a different {@link ActivityStack}, remove this
-        // {@link ActivityRecord} from its current {@link ActivityStack}.
-        if (!reparenting && stack != null && (task == null || stack != task.getStack())) {
-            stack.onActivityRemovedFromStack(this);
+        // Inform old stack (if present) of activity removal and new stack (if set) of activity
+        // addition.
+        if (oldStack != newStack) {
+            if (!reparenting && oldStack != null) {
+                oldStack.onActivityRemovedFromStack(this);
+            }
+
+            if (newStack != null) {
+                newStack.onActivityAddedToStack(this);
+            }
         }
 
         this.task = task;
@@ -1074,8 +1082,15 @@
         // Must reparent first in window manager
         mWindowContainerController.reparent(newTask.getWindowContainerController(), position);
 
+        // Reparenting prevents informing the parent stack of activity removal in the case that
+        // the new stack has the same parent. we must manually signal here if this is not the case.
+        final ActivityStack prevStack = prevTask.getStack();
+
+        if (prevStack != newTask.getStack()) {
+            prevStack.onActivityRemovedFromStack(this);
+        }
         // Remove the activity from the old task and add it to the new task.
-        prevTask.removeActivity(this, true /*reparenting*/);
+        prevTask.removeActivity(this, true /* reparenting */);
 
         newTask.addActivityAtIndex(position, this);
     }
@@ -1199,10 +1214,7 @@
     }
 
     boolean isFocusable() {
-        if (inSplitScreenPrimaryWindowingMode() && mStackSupervisor.mIsDockMinimized) {
-            return false;
-        }
-        return getWindowConfiguration().canReceiveKeys() || isAlwaysFocusable();
+        return mStackSupervisor.isFocusable(this, isAlwaysFocusable());
     }
 
     boolean isResizeable() {
@@ -1590,14 +1602,20 @@
     void pauseKeyDispatchingLocked() {
         if (!keysPaused) {
             keysPaused = true;
-            mWindowContainerController.pauseKeyDispatching();
+
+            if (mWindowContainerController != null) {
+                mWindowContainerController.pauseKeyDispatching();
+            }
         }
     }
 
     void resumeKeyDispatchingLocked() {
         if (keysPaused) {
             keysPaused = false;
-            mWindowContainerController.resumeKeyDispatching();
+
+            if (mWindowContainerController != null) {
+                mWindowContainerController.resumeKeyDispatching();
+            }
         }
     }
 
@@ -2880,7 +2898,7 @@
 
         final ActivityManagerService service = stackSupervisor.mService;
         final ActivityInfo aInfo = stackSupervisor.resolveActivity(intent, resolvedType, 0, null,
-                userId);
+                userId, Binder.getCallingUid());
         if (aInfo == null) {
             throw new XmlPullParserException("restoreActivity resolver error. Intent=" + intent +
                     " resolvedType=" + resolvedType);
diff --git a/com/android/server/am/ActivityStack.java b/com/android/server/am/ActivityStack.java
index 00ebcbd..eb482c1 100644
--- a/com/android/server/am/ActivityStack.java
+++ b/com/android/server/am/ActivityStack.java
@@ -489,13 +489,13 @@
      */
     void onActivityStateChanged(ActivityRecord record, ActivityState state, String reason) {
         if (record == mResumedActivity && state != RESUMED) {
-            clearResumedActivity(reason + " - onActivityStateChanged");
+            setResumedActivity(null, reason + " - onActivityStateChanged");
         }
 
         if (state == RESUMED) {
             if (DEBUG_STACK) Slog.v(TAG_STACK, "set resumed activity to:" + record + " reason:"
                     + reason);
-            mResumedActivity = record;
+            setResumedActivity(record, reason + " - onActivityStateChanged");
             mService.setResumedActivityUncheckLocked(record, reason);
             mStackSupervisor.mRecentTasks.add(record.getTask());
         }
@@ -1077,13 +1077,8 @@
     }
 
     boolean isFocusable() {
-        if (getWindowConfiguration().canReceiveKeys()) {
-            return true;
-        }
-        // The stack isn't focusable. See if its top activity is focusable to force focus on the
-        // stack.
         final ActivityRecord r = topRunningActivityLocked();
-        return r != null && r.isFocusable();
+        return mStackSupervisor.isFocusable(this, r != null && r.isFocusable());
     }
 
     final boolean isAttached() {
@@ -2314,14 +2309,14 @@
         return mResumedActivity;
     }
 
-    /**
-     * Clears reference to currently resumed activity.
-     */
-    private void clearResumedActivity(String reason) {
-        if (DEBUG_STACK) Slog.d(TAG_STACK, "clearResumedActivity: " + mResumedActivity + " reason:"
-                + reason);
+    private void setResumedActivity(ActivityRecord r, String reason) {
+        if (mResumedActivity == r) {
+            return;
+        }
 
-        mResumedActivity = null;
+        if (DEBUG_STACK) Slog.d(TAG_STACK, "setResumedActivity stack:" + this + " + from: "
+                + mResumedActivity + " to:" + r + " reason:" + reason);
+        mResumedActivity = r;
     }
 
     @GuardedBy("mService")
@@ -3544,7 +3539,15 @@
         mService.updateOomAdjLocked();
     }
 
-    final TaskRecord finishTopRunningActivityLocked(ProcessRecord app, String reason) {
+    /**
+     * Finish the topmost activity that belongs to the crashed app. We may also finish the activity
+     * that requested launch of the crashed one to prevent launch-crash loop.
+     * @param app The app that crashed.
+     * @param reason Reason to perform this action.
+     * @return The task that was finished in this stack, {@code null} if top running activity does
+     *         not belong to the crashed app.
+     */
+    final TaskRecord finishTopCrashedActivityLocked(ProcessRecord app, String reason) {
         ActivityRecord r = topRunningActivityLocked();
         TaskRecord finishedTask = null;
         if (r == null || r.app != app) {
@@ -3735,7 +3738,7 @@
                 }
 
                 if (endTask) {
-                    mService.mLockTaskController.clearLockedTask(task);
+                    mService.getLockTaskController().clearLockedTask(task);
                 }
             } else if (!r.isState(PAUSING)) {
                 // If the activity is PAUSING, we will complete the finish once
@@ -4019,14 +4022,20 @@
      * an activity moves away from the stack.
      */
     void onActivityRemovedFromStack(ActivityRecord r) {
-        if (mResumedActivity == r) {
-            clearResumedActivity("onActivityRemovedFromStack");
+        removeTimeoutsForActivityLocked(r);
+
+        if (mResumedActivity != null && mResumedActivity == r) {
+            setResumedActivity(null, "onActivityRemovedFromStack");
         }
-        if (mPausingActivity == r) {
+        if (mPausingActivity != null && mPausingActivity == r) {
             mPausingActivity = null;
         }
+    }
 
-        removeTimeoutsForActivityLocked(r);
+    void onActivityAddedToStack(ActivityRecord r) {
+        if(r.getState() == RESUMED) {
+            setResumedActivity(r, "onActivityAddedToStack");
+        }
     }
 
     /**
@@ -4631,7 +4640,7 @@
 
         // In LockTask mode, moving a locked task to the back of the stack may expose unlocked
         // ones. Therefore we need to check if this operation is allowed.
-        if (!mService.mLockTaskController.canMoveTaskToBack(tr)) {
+        if (!mService.getLockTaskController().canMoveTaskToBack(tr)) {
             return false;
         }
 
@@ -4744,30 +4753,32 @@
         mTmpBounds.clear();
         mTmpInsetBounds.clear();
 
-        for (int i = mTaskHistory.size() - 1; i >= 0; i--) {
-            final TaskRecord task = mTaskHistory.get(i);
-            if (task.isResizeable()) {
-                if (inFreeformWindowingMode()) {
-                    // TODO: Can be removed now since each freeform task is in its own stack.
-                    // For freeform stack we don't adjust the size of the tasks to match that
-                    // of the stack, but we do try to make sure the tasks are still contained
-                    // with the bounds of the stack.
-                    mTmpRect2.set(task.getOverrideBounds());
-                    fitWithinBounds(mTmpRect2, bounds);
-                    task.updateOverrideConfiguration(mTmpRect2);
-                } else {
-                    task.updateOverrideConfiguration(taskBounds, insetBounds);
+        synchronized (mWindowManager.getWindowManagerLock()) {
+            for (int i = mTaskHistory.size() - 1; i >= 0; i--) {
+                final TaskRecord task = mTaskHistory.get(i);
+                if (task.isResizeable()) {
+                    if (inFreeformWindowingMode()) {
+                        // TODO: Can be removed now since each freeform task is in its own stack.
+                        // For freeform stack we don't adjust the size of the tasks to match that
+                        // of the stack, but we do try to make sure the tasks are still contained
+                        // with the bounds of the stack.
+                        mTmpRect2.set(task.getOverrideBounds());
+                        fitWithinBounds(mTmpRect2, bounds);
+                        task.updateOverrideConfiguration(mTmpRect2);
+                    } else {
+                        task.updateOverrideConfiguration(taskBounds, insetBounds);
+                    }
+                }
+
+                mTmpBounds.put(task.taskId, task.getOverrideBounds());
+                if (tempTaskInsetBounds != null) {
+                    mTmpInsetBounds.put(task.taskId, tempTaskInsetBounds);
                 }
             }
 
-            mTmpBounds.put(task.taskId, task.getOverrideBounds());
-            if (tempTaskInsetBounds != null) {
-                mTmpInsetBounds.put(task.taskId, tempTaskInsetBounds);
-            }
+            mWindowContainerController.resize(bounds, mTmpBounds, mTmpInsetBounds);
+            setBounds(bounds);
         }
-
-        mWindowContainerController.resize(bounds, mTmpBounds, mTmpInsetBounds);
-        setBounds(bounds);
     }
 
 
@@ -5076,7 +5087,12 @@
             onActivityRemovedFromStack(record);
         }
 
-        mTaskHistory.remove(task);
+        final boolean removed = mTaskHistory.remove(task);
+
+        if (removed) {
+            EventLog.writeEvent(EventLogTags.AM_REMOVE_TASK, task.taskId, getStackId());
+        }
+
         removeActivitiesFromLRUListLocked(task);
         updateTaskMovement(task, true);
 
diff --git a/com/android/server/am/ActivityStackSupervisor.java b/com/android/server/am/ActivityStackSupervisor.java
index d5dfdcf..cbf30bd 100644
--- a/com/android/server/am/ActivityStackSupervisor.java
+++ b/com/android/server/am/ActivityStackSupervisor.java
@@ -667,6 +667,14 @@
         return mFocusedStack;
     }
 
+    boolean isFocusable(ConfigurationContainer container, boolean alwaysFocusable) {
+        if (container.inSplitScreenPrimaryWindowingMode() && mIsDockMinimized) {
+            return false;
+        }
+
+        return container.getWindowConfiguration().canReceiveKeys() || alwaysFocusable;
+    }
+
     ActivityStack getLastStack() {
         return mLastFocusedStack;
     }
@@ -1264,10 +1272,11 @@
     }
 
     ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId) {
-        return resolveIntent(intent, resolvedType, userId, 0);
+        return resolveIntent(intent, resolvedType, userId, 0, Binder.getCallingUid());
     }
 
-    ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags) {
+    ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags,
+            int filterCallingUid) {
         synchronized (mService) {
             try {
                 Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "resolveIntent");
@@ -1278,7 +1287,7 @@
                     modifiedFlags |= PackageManager.MATCH_INSTANT;
                 }
                 return mService.getPackageManagerInternalLocked().resolveIntent(
-                        intent, resolvedType, modifiedFlags, userId, true);
+                        intent, resolvedType, modifiedFlags, userId, true, filterCallingUid);
 
             } finally {
                 Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
@@ -1287,8 +1296,8 @@
     }
 
     ActivityInfo resolveActivity(Intent intent, String resolvedType, int startFlags,
-            ProfilerInfo profilerInfo, int userId) {
-        final ResolveInfo rInfo = resolveIntent(intent, resolvedType, userId);
+            ProfilerInfo profilerInfo, int userId, int filterCallingUid) {
+        final ResolveInfo rInfo = resolveIntent(intent, resolvedType, userId, 0, filterCallingUid);
         return resolveActivity(intent, rInfo, startFlags, profilerInfo);
     }
 
@@ -1371,12 +1380,13 @@
             mService.updateLruProcessLocked(app, true, null);
             mService.updateOomAdjLocked();
 
+            final LockTaskController lockTaskController = mService.getLockTaskController();
             if (task.mLockTaskAuth == LOCK_TASK_AUTH_LAUNCHABLE
                     || task.mLockTaskAuth == LOCK_TASK_AUTH_LAUNCHABLE_PRIV
                     || (task.mLockTaskAuth == LOCK_TASK_AUTH_WHITELISTED
-                            && mService.mLockTaskController.getLockTaskModeState()
-                            == LOCK_TASK_MODE_LOCKED)) {
-                mService.mLockTaskController.startLockTaskMode(task, false, 0 /* blank UID */);
+                            && lockTaskController.getLockTaskModeState()
+                                    == LOCK_TASK_MODE_LOCKED)) {
+                lockTaskController.startLockTaskMode(task, false, 0 /* blank UID */);
             }
 
             try {
@@ -1579,7 +1589,7 @@
                     // to run in multiple processes, because this is actually
                     // part of the framework so doesn't make sense to track as a
                     // separate apk in the process.
-                    app.addPackage(r.info.packageName, r.info.applicationInfo.versionCode,
+                    app.addPackage(r.info.packageName, r.info.applicationInfo.longVersionCode,
                             mService.mProcessStats);
                 }
                 realStartActivityLocked(r, app, andResume, checkConfig);
@@ -2126,15 +2136,22 @@
         }
     }
 
-    TaskRecord finishTopRunningActivityLocked(ProcessRecord app, String reason) {
+    /**
+     * Finish the topmost activities in all stacks that belong to the crashed app.
+     * @param app The app that crashed.
+     * @param reason Reason to perform this action.
+     * @return The task that was finished in this stack, {@code null} if haven't found any.
+     */
+    TaskRecord finishTopCrashedActivitiesLocked(ProcessRecord app, String reason) {
         TaskRecord finishedTask = null;
         ActivityStack focusedStack = getFocusedStack();
         for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
             final ActivityDisplay display = mActivityDisplays.valueAt(displayNdx);
-            final int numStacks = display.getChildCount();
-            for (int stackNdx = 0; stackNdx < numStacks; ++stackNdx) {
+            // It is possible that request to finish activity might also remove its task and stack,
+            // so we need to be careful with indexes in the loop and check child count every time.
+            for (int stackNdx = 0; stackNdx < display.getChildCount(); ++stackNdx) {
                 final ActivityStack stack = display.getChildAt(stackNdx);
-                TaskRecord t = stack.finishTopRunningActivityLocked(app, reason);
+                final TaskRecord t = stack.finishTopCrashedActivityLocked(app, reason);
                 if (stack == focusedStack || finishedTask == null) {
                     finishedTask = t;
                 }
@@ -2892,7 +2909,7 @@
         if (tr != null) {
             tr.removeTaskActivitiesLocked(pauseImmediately, reason);
             cleanUpRemovedTaskLocked(tr, killProcess, removeFromRecents);
-            mService.mLockTaskController.clearLockedTask(tr);
+            mService.getLockTaskController().clearLockedTask(tr);
             if (tr.isPersistable) {
                 mService.notifyTaskPersisterLocked(null, true);
             }
@@ -3806,7 +3823,7 @@
         pw.print(mRecentTasks.isRecentsComponentHomeActivity(mCurrentUser));
 
         getKeyguardController().dump(pw, prefix);
-        mService.mLockTaskController.dump(pw, prefix);
+        mService.getLockTaskController().dump(pw, prefix);
     }
 
     public void writeToProto(ProtoOutputStream proto, long fieldId) {
diff --git a/com/android/server/am/ActivityStartController.java b/com/android/server/am/ActivityStartController.java
index fb78838..86a3fce 100644
--- a/com/android/server/am/ActivityStartController.java
+++ b/com/android/server/am/ActivityStartController.java
@@ -23,7 +23,6 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.am.ActivityManagerService.ALLOW_FULL_ONLY;
 
-import android.app.ActivityOptions;
 import android.app.IApplicationThread;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -33,7 +32,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.FactoryTest;
 import android.os.Handler;
 import android.os.IBinder;
@@ -322,7 +320,7 @@
 
                     // Collect information about the target of the Intent.
                     ActivityInfo aInfo = mSupervisor.resolveActivity(intent, resolvedTypes[i], 0,
-                            null, userId);
+                            null, userId, realCallingUid);
                     // TODO: New, check if this is correct
                     aInfo = mService.getActivityInfoForUser(aInfo, userId);
 
diff --git a/com/android/server/am/ActivityStartInterceptor.java b/com/android/server/am/ActivityStartInterceptor.java
index b86a8a6..5b6b508 100644
--- a/com/android/server/am/ActivityStartInterceptor.java
+++ b/com/android/server/am/ActivityStartInterceptor.java
@@ -21,6 +21,8 @@
 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
+import static android.app.admin.DevicePolicyManager.EXTRA_RESTRICTION;
+import static android.app.admin.DevicePolicyManager.POLICY_SUSPEND_PACKAGES;
 import static android.content.Context.KEYGUARD_SERVICE;
 import static android.content.Intent.EXTRA_INTENT;
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
@@ -30,8 +32,10 @@
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
 import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED;
 
+import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
+import android.Manifest;
 import android.app.ActivityOptions;
-import android.app.AppGlobals;
 import android.app.KeyguardManager;
 import android.app.admin.DevicePolicyManagerInternal;
 import android.content.Context;
@@ -39,6 +43,7 @@
 import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
 import android.os.Binder;
@@ -49,6 +54,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.HarmfulAppWarningActivity;
+import com.android.internal.app.SuspendedAppActivity;
 import com.android.internal.app.UnlaunchableAppActivity;
 import com.android.server.LocalServices;
 
@@ -149,7 +155,7 @@
         mInTask = inTask;
         mActivityOptions = activityOptions;
 
-        if (interceptSuspendPackageIfNeed()) {
+        if (interceptSuspendedPackageIfNeeded()) {
             // Skip the rest of interceptions as the package is suspended by device admin so
             // no user action can undo this.
             return true;
@@ -202,18 +208,15 @@
         return true;
     }
 
-    private boolean interceptSuspendPackageIfNeed() {
-        // Do not intercept if the admin did not suspend the package
-        if (mAInfo == null || mAInfo.applicationInfo == null ||
-                (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) == 0) {
-            return false;
-        }
+    private boolean interceptSuspendedByAdminPackage() {
         DevicePolicyManagerInternal devicePolicyManager = LocalServices
                 .getService(DevicePolicyManagerInternal.class);
         if (devicePolicyManager == null) {
             return false;
         }
         mIntent = devicePolicyManager.createShowAdminSupportIntent(mUserId, true);
+        mIntent.putExtra(EXTRA_RESTRICTION, POLICY_SUSPEND_PACKAGES);
+
         mCallingPid = mRealCallingPid;
         mCallingUid = mRealCallingUid;
         mResolvedType = null;
@@ -228,6 +231,56 @@
         return true;
     }
 
+    private Intent createSuspendedAppInterceptIntent(String suspendedPackage,
+            String suspendingPackage, String dialogMessage, int userId) {
+        final Intent interceptIntent = new Intent(mServiceContext, SuspendedAppActivity.class)
+                .putExtra(SuspendedAppActivity.EXTRA_SUSPENDED_PACKAGE, suspendedPackage)
+                .putExtra(SuspendedAppActivity.EXTRA_DIALOG_MESSAGE, dialogMessage)
+                .putExtra(SuspendedAppActivity.EXTRA_SUSPENDING_PACKAGE, suspendingPackage)
+                .putExtra(Intent.EXTRA_USER_ID, userId)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                        | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+        final Intent moreDetailsIntent = new Intent(Intent.ACTION_SHOW_SUSPENDED_APP_DETAILS)
+                .setPackage(suspendingPackage);
+        final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS;
+        final ResolveInfo resolvedInfo = mSupervisor.resolveIntent(moreDetailsIntent, null, userId);
+        if (resolvedInfo != null && resolvedInfo.activityInfo != null
+                && requiredPermission.equals(resolvedInfo.activityInfo.permission)) {
+            moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, suspendedPackage)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            interceptIntent.putExtra(SuspendedAppActivity.EXTRA_MORE_DETAILS_INTENT,
+                    moreDetailsIntent);
+        }
+        return interceptIntent;
+    }
+
+    private boolean interceptSuspendedPackageIfNeeded() {
+        // Do not intercept if the package is not suspended
+        if (mAInfo == null || mAInfo.applicationInfo == null ||
+                (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) == 0) {
+            return false;
+        }
+        final PackageManagerInternal pmi = mService.getPackageManagerInternalLocked();
+        if (pmi == null) {
+            return false;
+        }
+        final String suspendedPackage = mAInfo.applicationInfo.packageName;
+        final String suspendingPackage = pmi.getSuspendingPackage(suspendedPackage, mUserId);
+        if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) {
+            return interceptSuspendedByAdminPackage();
+        }
+        final String dialogMessage = pmi.getSuspendedDialogMessage(suspendedPackage, mUserId);
+        mIntent = createSuspendedAppInterceptIntent(suspendedPackage, suspendingPackage,
+                dialogMessage, mUserId);
+        mCallingPid = mRealCallingPid;
+        mCallingUid = mRealCallingUid;
+        mResolvedType = null;
+        mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, 0);
+        mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/);
+        return true;
+    }
+
     private boolean interceptWorkProfileChallengeIfNeeded() {
         final Intent interceptingIntent = interceptWithConfirmCredentialsIfNeeded(mAInfo, mUserId);
         if (interceptingIntent == null) {
@@ -289,9 +342,9 @@
     private boolean interceptHarmfulAppIfNeeded() {
         CharSequence harmfulAppWarning;
         try {
-            harmfulAppWarning = AppGlobals.getPackageManager().getHarmfulAppWarning(
-                    mAInfo.packageName, mUserId);
-        } catch (RemoteException e) {
+            harmfulAppWarning = mService.getPackageManager()
+                    .getHarmfulAppWarning(mAInfo.packageName, mUserId);
+        } catch (RemoteException ex) {
             return false;
         }
 
diff --git a/com/android/server/am/ActivityStarter.java b/com/android/server/am/ActivityStarter.java
index 1b7e1ed..fb89e67 100644
--- a/com/android/server/am/ActivityStarter.java
+++ b/com/android/server/am/ActivityStarter.java
@@ -28,10 +28,10 @@
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE;
 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
@@ -903,14 +903,22 @@
         final int clearTaskFlags = FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK;
         boolean clearedTask = (mLaunchFlags & clearTaskFlags) == clearTaskFlags
                 && mReuseTask != null;
-        if (startedActivityStack.inPinnedWindowingMode()
-                && (result == START_TASK_TO_FRONT || result == START_DELIVERED_TO_TOP
-                || clearedTask)) {
-            // The activity was already running in the pinned stack so it wasn't started, but either
-            // brought to the front or the new intent was delivered to it since it was already in
-            // front. Notify anyone interested in this piece of information.
-            mService.mTaskChangeNotificationController.notifyPinnedActivityRestartAttempt(
-                    clearedTask);
+        if (result == START_TASK_TO_FRONT || result == START_DELIVERED_TO_TOP || clearedTask) {
+            // The activity was already running so it wasn't started, but either brought to the
+            // front or the new intent was delivered to it since it was already in front. Notify
+            // anyone interested in this piece of information.
+            switch (startedActivityStack.getWindowingMode()) {
+                case WINDOWING_MODE_PINNED:
+                    mService.mTaskChangeNotificationController.notifyPinnedActivityRestartAttempt(
+                            clearedTask);
+                    break;
+                case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY:
+                    final ActivityStack homeStack = mSupervisor.mHomeStack;
+                    if (homeStack != null && homeStack.shouldBeVisible(null /* starting */)) {
+                        mService.mWindowManager.showRecentApps();
+                    }
+                    break;
+            }
         }
     }
 
@@ -966,7 +974,8 @@
                 if (profileLockedAndParentUnlockingOrUnlocked) {
                     rInfo = mSupervisor.resolveIntent(intent, resolvedType, userId,
                             PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+                            Binder.getCallingUid());
                 }
             }
         }
@@ -1147,9 +1156,10 @@
             // If we are not able to proceed, disassociate the activity from the task. Leaving an
             // activity in an incomplete state can lead to issues, such as performing operations
             // without a window container.
-            if (!ActivityManager.isStartResultSuccessful(result)
-                    && mStartActivity.getTask() != null) {
-                mStartActivity.getTask().removeActivity(mStartActivity);
+            final ActivityStack stack = mStartActivity.getStack();
+            if (!ActivityManager.isStartResultSuccessful(result) && stack != null) {
+                stack.finishActivityLocked(mStartActivity, RESULT_CANCELED,
+                        null /* intentResultData */, "startActivity", true /* oomAdj */);
             }
             mService.mWindowManager.continueSurfaceLayout();
         }
@@ -1199,7 +1209,7 @@
             // When the flags NEW_TASK and CLEAR_TASK are set, then the task gets reused but
             // still needs to be a lock task mode violation since the task gets cleared out and
             // the device would otherwise leave the locked task.
-            if (mService.mLockTaskController.isLockTaskModeViolation(reusedActivity.getTask(),
+            if (mService.getLockTaskController().isLockTaskModeViolation(reusedActivity.getTask(),
                     (mLaunchFlags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))
                             == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))) {
                 Slog.e(TAG, "startActivityUnchecked: Attempt to violate Lock Task Mode");
@@ -2011,7 +2021,7 @@
             mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
         }
 
-        if (mService.mLockTaskController.isLockTaskModeViolation(mStartActivity.getTask())) {
+        if (mService.getLockTaskController().isLockTaskModeViolation(mStartActivity.getTask())) {
             Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
             return START_RETURN_LOCK_TASK_MODE_VIOLATION;
         }
@@ -2034,7 +2044,7 @@
     }
 
     private int setTaskFromSourceRecord() {
-        if (mService.mLockTaskController.isLockTaskModeViolation(mSourceRecord.getTask())) {
+        if (mService.getLockTaskController().isLockTaskModeViolation(mSourceRecord.getTask())) {
             Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
             return START_RETURN_LOCK_TASK_MODE_VIOLATION;
         }
@@ -2128,7 +2138,7 @@
     private int setTaskFromInTask() {
         // The caller is asking that the new activity be started in an explicit
         // task it has provided to us.
-        if (mService.mLockTaskController.isLockTaskModeViolation(mInTask)) {
+        if (mService.getLockTaskController().isLockTaskModeViolation(mInTask)) {
             Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
             return START_RETURN_LOCK_TASK_MODE_VIOLATION;
         }
diff --git a/com/android/server/am/AppErrors.java b/com/android/server/am/AppErrors.java
index d5bb7ed..bd1000a 100644
--- a/com/android/server/am/AppErrors.java
+++ b/com/android/server/am/AppErrors.java
@@ -742,8 +742,8 @@
             }
             mService.mStackSupervisor.resumeFocusedStackTopActivityLocked();
         } else {
-            TaskRecord affectedTask =
-                    mService.mStackSupervisor.finishTopRunningActivityLocked(app, reason);
+            final TaskRecord affectedTask =
+                    mService.mStackSupervisor.finishTopCrashedActivitiesLocked(app, reason);
             if (data != null) {
                 data.task = affectedTask;
             }
diff --git a/com/android/server/am/AppWarnings.java b/com/android/server/am/AppWarnings.java
index ab1d7bf..ea0251e 100644
--- a/com/android/server/am/AppWarnings.java
+++ b/com/android/server/am/AppWarnings.java
@@ -122,8 +122,9 @@
             return;
         }
 
-        if (ActivityManager.isRunningInTestHarness()
-                && !mAlwaysShowUnsupportedCompileSdkWarningActivities.contains(r.realActivity)) {
+        // TODO(b/77862563): temp. fix while P is being finalized.  To be reverted
+        if (/*ActivityManager.isRunningInTestHarness()
+                &&*/ !mAlwaysShowUnsupportedCompileSdkWarningActivities.contains(r.realActivity)) {
             // Don't show warning if we are running in a test harness and we don't have to always
             // show for this activity.
             return;
diff --git a/com/android/server/am/BatteryStatsService.java b/com/android/server/am/BatteryStatsService.java
index 8ecd93e..9c2b1a5 100644
--- a/com/android/server/am/BatteryStatsService.java
+++ b/com/android/server/am/BatteryStatsService.java
@@ -19,16 +19,10 @@
 import android.app.ActivityManager;
 import android.app.job.JobProtoEnums;
 import android.bluetooth.BluetoothActivityEnergyInfo;
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.hardware.usb.UsbManager;
 import android.net.wifi.WifiActivityEnergyInfo;
-import android.os.PowerManager.ServiceType;
-import android.os.PowerSaveState;
 import android.os.BatteryStats;
 import android.os.BatteryStatsInternal;
 import android.os.Binder;
@@ -37,18 +31,18 @@
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFormatException;
+import android.os.PowerManager.ServiceType;
 import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
 import android.os.Process;
-import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManagerInternal;
 import android.os.WorkSource;
-import android.os.WorkSource.WorkChain;
 import android.os.connectivity.CellularBatteryStats;
-import android.os.connectivity.WifiBatteryStats;
 import android.os.connectivity.GpsBatteryStats;
+import android.os.connectivity.WifiBatteryStats;
 import android.os.health.HealthStatsParceler;
 import android.os.health.HealthStatsWriter;
 import android.os.health.UidHealthStats;
@@ -57,6 +51,7 @@
 import android.telephony.SignalStrength;
 import android.telephony.TelephonyManager;
 import android.util.Slog;
+import android.util.StatsLog;
 
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.os.BatteryStatsHelper;
@@ -65,7 +60,6 @@
 import com.android.internal.os.RpmStats;
 import com.android.internal.util.DumpUtils;
 import com.android.server.LocalServices;
-import android.util.StatsLog;
 
 import java.io.File;
 import java.io.FileDescriptor;
@@ -701,13 +695,6 @@
         }
     }
 
-    public void noteUsbConnectionState(boolean connected) {
-        enforceCallingPermission();
-        synchronized (mStats) {
-            mStats.noteUsbConnectionStateLocked(connected);
-        }
-    }
-
     public void notePhoneSignalStrength(SignalStrength signalStrength) {
         enforceCallingPermission();
         synchronized (mStats) {
@@ -1164,35 +1151,6 @@
                 Binder.getCallingPid(), Binder.getCallingUid(), null);
     }
 
-    public final static class UsbConnectionReceiver extends BroadcastReceiver {
-        private static final String TAG = UsbConnectionReceiver.class.getSimpleName();
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
-                final Intent usbState = context.registerReceiver(null, new IntentFilter(UsbManager.ACTION_USB_STATE));
-                if (usbState != null) {
-                    handleUsbState(usbState);
-                }
-            } else if (UsbManager.ACTION_USB_STATE.equals(action)) {
-                handleUsbState(intent);
-            }
-        }
-        private void handleUsbState(Intent intent) {
-            IBatteryStats bs = getService();
-            if (bs == null) {
-                Slog.w(TAG, "Could not access batterystats");
-                return;
-            }
-            boolean connected = intent.getExtras().getBoolean(UsbManager.USB_CONNECTED);
-            try {
-                bs.noteUsbConnectionState(connected);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Could not access batterystats: ", e);
-            }
-        }
-    }
-
     final class WakeupReasonThread extends Thread {
         private static final int MAX_REASON_SIZE = 512;
         private CharsetDecoder mDecoder;
diff --git a/com/android/server/am/ProcessRecord.java b/com/android/server/am/ProcessRecord.java
index 03acb84..b7fde1d 100644
--- a/com/android/server/am/ProcessRecord.java
+++ b/com/android/server/am/ProcessRecord.java
@@ -496,7 +496,7 @@
         uid = _uid;
         userId = UserHandle.getUserId(_uid);
         processName = _processName;
-        pkgList.put(_info.packageName, new ProcessStats.ProcessStateHolder(_info.versionCode));
+        pkgList.put(_info.packageName, new ProcessStats.ProcessStateHolder(_info.longVersionCode));
         maxAdj = ProcessList.UNKNOWN_ADJ;
         curRawAdj = setRawAdj = ProcessList.INVALID_ADJ;
         curAdj = setAdj = verifiedAdj = ProcessList.INVALID_ADJ;
@@ -521,7 +521,7 @@
                 origBase.makeInactive();
             }
             baseProcessTracker = tracker.getProcessStateLocked(info.packageName, uid,
-                    info.versionCode, processName);
+                    info.longVersionCode, processName);
             baseProcessTracker.makeActive();
             for (int i=0; i<pkgList.size(); i++) {
                 ProcessStats.ProcessStateHolder holder = pkgList.valueAt(i);
@@ -529,7 +529,7 @@
                     holder.state.makeInactive();
                 }
                 holder.state = tracker.getProcessStateLocked(pkgList.keyAt(i), uid,
-                        info.versionCode, processName);
+                        info.longVersionCode, processName);
                 if (holder.state != baseProcessTracker) {
                     holder.state.makeActive();
                 }
@@ -828,9 +828,9 @@
                 }
                 pkgList.clear();
                 ProcessState ps = tracker.getProcessStateLocked(
-                        info.packageName, uid, info.versionCode, processName);
+                        info.packageName, uid, info.longVersionCode, processName);
                 ProcessStats.ProcessStateHolder holder = new ProcessStats.ProcessStateHolder(
-                        info.versionCode);
+                        info.longVersionCode);
                 holder.state = ps;
                 pkgList.put(info.packageName, holder);
                 if (ps != baseProcessTracker) {
@@ -839,7 +839,7 @@
             }
         } else if (N != 1) {
             pkgList.clear();
-            pkgList.put(info.packageName, new ProcessStats.ProcessStateHolder(info.versionCode));
+            pkgList.put(info.packageName, new ProcessStats.ProcessStateHolder(info.longVersionCode));
         }
     }
 
diff --git a/com/android/server/am/RecentTasks.java b/com/android/server/am/RecentTasks.java
index 1d305fb..a20452b 100644
--- a/com/android/server/am/RecentTasks.java
+++ b/com/android/server/am/RecentTasks.java
@@ -523,7 +523,7 @@
         }
         for (int i = mTasks.size() - 1; i >= 0; --i) {
             final TaskRecord tr = mTasks.get(i);
-            if (tr.userId == userId && !mService.mLockTaskController.isTaskWhitelisted(tr)) {
+            if (tr.userId == userId && !mService.getLockTaskController().isTaskWhitelisted(tr)) {
                 remove(tr);
             }
         }
@@ -534,8 +534,8 @@
             final TaskRecord tr = mTasks.get(i);
             final String taskPackageName =
                     tr.getBaseIntent().getComponent().getPackageName();
-            if (tr.userId != userId) return;
-            if (!taskPackageName.equals(packageName)) return;
+            if (tr.userId != userId) continue;
+            if (!taskPackageName.equals(packageName)) continue;
 
             mService.mStackSupervisor.removeTaskByIdLocked(tr.taskId, true, REMOVE_FROM_RECENTS,
                     "remove-package-task");
@@ -1156,7 +1156,7 @@
         }
 
         // If we're in lock task mode, ignore the root task
-        if (task == mService.mLockTaskController.getRootTask()) {
+        if (task == mService.getLockTaskController().getRootTask()) {
             return false;
         }
 
@@ -1255,7 +1255,7 @@
         for (int i = 0; i < recentsCount; i++) {
             final TaskRecord tr = mTasks.get(i);
             if (task != tr) {
-                if (!task.hasCompatibleActivityType(tr)) {
+                if (!task.hasCompatibleActivityType(tr) || task.userId != tr.userId) {
                     continue;
                 }
                 final Intent trIntent = tr.intent;
diff --git a/com/android/server/am/RecentsAnimation.java b/com/android/server/am/RecentsAnimation.java
index 9df321c..06b5e20 100644
--- a/com/android/server/am/RecentsAnimation.java
+++ b/com/android/server/am/RecentsAnimation.java
@@ -18,20 +18,25 @@
 
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
 import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
 import static android.view.WindowManager.TRANSIT_NONE;
 import static com.android.server.am.ActivityStackSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_TOP;
 
 import android.app.ActivityOptions;
 import android.content.ComponentName;
 import android.content.Intent;
-import android.os.Handler;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.util.Slog;
 import android.view.IRecentsAnimationRunner;
+import com.android.server.wm.RecentsAnimationController;
 import com.android.server.wm.RecentsAnimationController.RecentsAnimationCallbacks;
 import com.android.server.wm.WindowManagerService;
 
@@ -41,22 +46,28 @@
  */
 class RecentsAnimation implements RecentsAnimationCallbacks {
     private static final String TAG = RecentsAnimation.class.getSimpleName();
+    // TODO (b/73188263): Reset debugging flags
+    private static final boolean DEBUG = true;
 
     private final ActivityManagerService mService;
     private final ActivityStackSupervisor mStackSupervisor;
     private final ActivityStartController mActivityStartController;
     private final WindowManagerService mWindowManager;
     private final UserController mUserController;
+    private final ActivityDisplay mDefaultDisplay;
     private final int mCallingPid;
 
-    // The stack to restore the home stack behind when the animation is finished
-    private ActivityStack mRestoreHomeBehindStack;
+    private int mTargetActivityType;
+
+    // The stack to restore the target stack behind when the animation is finished
+    private ActivityStack mRestoreTargetBehindStack;
 
     RecentsAnimation(ActivityManagerService am, ActivityStackSupervisor stackSupervisor,
             ActivityStartController activityStartController, WindowManagerService wm,
             UserController userController, int callingPid) {
         mService = am;
         mStackSupervisor = stackSupervisor;
+        mDefaultDisplay = stackSupervisor.getDefaultDisplay();
         mActivityStartController = activityStartController;
         mWindowManager = wm;
         mUserController = userController;
@@ -65,25 +76,44 @@
 
     void startRecentsActivity(Intent intent, IRecentsAnimationRunner recentsAnimationRunner,
             ComponentName recentsComponent, int recentsUid) {
+        if (DEBUG) Slog.d(TAG, "startRecentsActivity(): intent=" + intent);
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "RecentsAnimation#startRecentsActivity");
 
         if (!mWindowManager.canStartRecentsAnimation()) {
             notifyAnimationCancelBeforeStart(recentsAnimationRunner);
+            if (DEBUG) Slog.d(TAG, "Can't start recents animation, nextAppTransition="
+                        + mWindowManager.getPendingAppTransition());
             return;
         }
 
-        // If the existing home activity is already on top, then cancel
-        ActivityRecord homeActivity = mStackSupervisor.getHomeActivity();
-        final boolean hasExistingHomeActivity = homeActivity != null;
-        if (hasExistingHomeActivity) {
-            final ActivityDisplay display = homeActivity.getDisplay();
-            mRestoreHomeBehindStack = display.getStackAboveHome();
-            if (mRestoreHomeBehindStack == null) {
+        // If the activity is associated with the recents stack, then try and get that first
+        mTargetActivityType = intent.getComponent() != null
+                && recentsComponent.equals(intent.getComponent())
+                        ? ACTIVITY_TYPE_RECENTS
+                        : ACTIVITY_TYPE_HOME;
+        final ActivityStack targetStack = mDefaultDisplay.getStack(WINDOWING_MODE_UNDEFINED,
+                mTargetActivityType);
+        ActivityRecord targetActivity = targetStack != null
+                ? targetStack.getTopActivity()
+                : null;
+        final boolean hasExistingActivity = targetActivity != null;
+        if (hasExistingActivity) {
+            final ActivityDisplay display = targetActivity.getDisplay();
+            mRestoreTargetBehindStack = display.getStackAbove(targetStack);
+            if (mRestoreTargetBehindStack == null) {
                 notifyAnimationCancelBeforeStart(recentsAnimationRunner);
+                if (DEBUG) Slog.d(TAG, "No stack above target stack=" + targetStack);
                 return;
             }
         }
 
+        // Send launch hint if we are actually launching the target. If it's already visible
+        // (shouldn't happen in general) we don't need to send it.
+        if (targetActivity == null || !targetActivity.visible) {
+            mStackSupervisor.sendPowerHintForLaunchStartIfNeeded(true /* forceSend */,
+                    targetActivity);
+        }
+
         mStackSupervisor.getActivityMetricsLogger().notifyActivityLaunching();
 
         mService.setRunningRemoteAnimation(mCallingPid, true);
@@ -91,48 +121,57 @@
         mWindowManager.deferSurfaceLayout();
         try {
             final ActivityDisplay display;
-            if (hasExistingHomeActivity) {
-                // Move the home activity into place for the animation if it is not already top most
-                display = homeActivity.getDisplay();
-                display.moveHomeStackBehindBottomMostVisibleStack();
+            if (hasExistingActivity) {
+                // Move the recents activity into place for the animation if it is not top most
+                display = targetActivity.getDisplay();
+                display.moveStackBehindBottomMostVisibleStack(targetStack);
+                if (DEBUG) Slog.d(TAG, "Moved stack=" + targetStack + " behind stack="
+                            + display.getStackAbove(targetStack));
             } else {
-                // No home activity
-                final ActivityOptions opts = ActivityOptions.makeBasic();
-                opts.setLaunchActivityType(ACTIVITY_TYPE_HOME);
-                opts.setAvoidMoveToFront();
+                // No recents activity
+                ActivityOptions options = ActivityOptions.makeBasic();
+                options.setLaunchActivityType(mTargetActivityType);
+                options.setAvoidMoveToFront();
                 intent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION);
 
                 mActivityStartController
-                        .obtainStarter(intent, "startRecentsActivity_noHomeActivity")
+                        .obtainStarter(intent, "startRecentsActivity_noTargetActivity")
                         .setCallingUid(recentsUid)
                         .setCallingPackage(recentsComponent.getPackageName())
-                        .setActivityOptions(SafeActivityOptions.fromBundle(opts.toBundle()))
+                        .setActivityOptions(SafeActivityOptions.fromBundle(options.toBundle()))
                         .setMayWait(mUserController.getCurrentUserId())
                         .execute();
                 mWindowManager.prepareAppTransition(TRANSIT_NONE, false);
 
-                homeActivity = mStackSupervisor.getHomeActivity();
-                display = homeActivity.getDisplay();
+                targetActivity = mDefaultDisplay.getStack(WINDOWING_MODE_UNDEFINED,
+                        mTargetActivityType).getTopActivity();
+                display = targetActivity.getDisplay();
 
                 // TODO: Maybe wait for app to draw in this particular case?
+
+                if (DEBUG) Slog.d(TAG, "Started intent=" + intent);
             }
 
-            // Mark the home activity as launch-behind to bump its visibility for the
+            // Mark the target activity as launch-behind to bump its visibility for the
             // duration of the gesture that is driven by the recents component
-            homeActivity.mLaunchTaskBehind = true;
+            targetActivity.mLaunchTaskBehind = true;
 
             // Fetch all the surface controls and pass them to the client to get the animation
             // started
-            mWindowManager.cancelRecentsAnimation();
-            mWindowManager.initializeRecentsAnimation(recentsAnimationRunner, this,
-                    display.mDisplayId, mStackSupervisor.mRecentTasks.getRecentTaskIds());
+            mWindowManager.cancelRecentsAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION,
+                    "startRecentsActivity");
+            mWindowManager.initializeRecentsAnimation(mTargetActivityType, recentsAnimationRunner,
+                    this, display.mDisplayId, mStackSupervisor.mRecentTasks.getRecentTaskIds());
 
             // If we updated the launch-behind state, update the visibility of the activities after
             // we fetch the visible tasks to be controlled by the animation
             mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, PRESERVE_WINDOWS);
 
             mStackSupervisor.getActivityMetricsLogger().notifyActivityLaunched(START_TASK_TO_FRONT,
-                    homeActivity);
+                    targetActivity);
+        } catch (Exception e) {
+            Slog.e(TAG, "Failed to start recents activity", e);
+            throw e;
         } finally {
             mWindowManager.continueSurfaceLayout();
             Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
@@ -140,10 +179,19 @@
     }
 
     @Override
-    public void onAnimationFinished(boolean moveHomeToTop) {
+    public void onAnimationFinished(@RecentsAnimationController.ReorderMode int reorderMode) {
         synchronized (mService) {
+            if (DEBUG) Slog.d(TAG, "onAnimationFinished(): controller="
+                        + mWindowManager.getRecentsAnimationController()
+                        + " reorderMode=" + reorderMode);
             if (mWindowManager.getRecentsAnimationController() == null) return;
 
+            // Just to be sure end the launch hint in case the target activity was never launched.
+            // However, if we're keeping the activity and making it visible, we can leave it on.
+            if (reorderMode != REORDER_KEEP_IN_PLACE) {
+                mStackSupervisor.sendPowerHintForLaunchEndIfNeeded();
+            }
+
             mService.setRunningRemoteAnimation(mCallingPid, false);
 
             mWindowManager.inSurfaceTransaction(() -> {
@@ -151,26 +199,50 @@
                         "RecentsAnimation#onAnimationFinished_inSurfaceTransaction");
                 mWindowManager.deferSurfaceLayout();
                 try {
-                    mWindowManager.cleanupRecentsAnimation(moveHomeToTop);
+                    mWindowManager.cleanupRecentsAnimation(reorderMode);
 
-                    // Move the home stack to the front
-                    final ActivityRecord homeActivity = mStackSupervisor.getHomeActivity();
-                    if (homeActivity == null) {
+                    final ActivityStack targetStack = mDefaultDisplay.getStack(
+                            WINDOWING_MODE_UNDEFINED, mTargetActivityType);
+                    final ActivityRecord targetActivity = targetStack.getTopActivity();
+                    if (DEBUG) Slog.d(TAG, "onAnimationFinished(): targetStack=" + targetStack
+                            + " targetActivity=" + targetActivity
+                            + " mRestoreTargetBehindStack=" + mRestoreTargetBehindStack);
+                    if (targetActivity == null) {
                         return;
                     }
 
                     // Restore the launched-behind state
-                    homeActivity.mLaunchTaskBehind = false;
+                    targetActivity.mLaunchTaskBehind = false;
 
-                    if (moveHomeToTop) {
-                        // Bring the home stack to the front
-                        final ActivityStack homeStack = homeActivity.getStack();
-                        mStackSupervisor.mNoAnimActivities.add(homeActivity);
-                        homeStack.moveToFront("RecentsAnimation.onAnimationFinished()");
+                    if (reorderMode == REORDER_MOVE_TO_TOP) {
+                        // Bring the target stack to the front
+                        mStackSupervisor.mNoAnimActivities.add(targetActivity);
+                        targetStack.moveToFront("RecentsAnimation.onAnimationFinished()");
+                        if (DEBUG) {
+                            final ActivityStack topStack = getTopNonAlwaysOnTopStack();
+                            if (topStack != targetStack) {
+                                Slog.w(TAG, "Expected target stack=" + targetStack
+                                        + " to be top most but found stack=" + topStack);
+                            }
+                        }
+                    } else if (reorderMode == REORDER_MOVE_TO_ORIGINAL_POSITION){
+                        // Restore the target stack to its previous position
+                        final ActivityDisplay display = targetActivity.getDisplay();
+                        display.moveStackBehindStack(targetStack, mRestoreTargetBehindStack);
+                        if (DEBUG) {
+                            final ActivityStack aboveTargetStack =
+                                    mDefaultDisplay.getStackAbove(targetStack);
+                            if (mRestoreTargetBehindStack != null
+                                    && aboveTargetStack != mRestoreTargetBehindStack) {
+                                Slog.w(TAG, "Expected target stack=" + targetStack
+                                        + " to restored behind stack=" + mRestoreTargetBehindStack
+                                        + " but it is behind stack=" + aboveTargetStack);
+                            }
+                        }
                     } else {
-                        // Restore the home stack to its previous position
-                        final ActivityDisplay display = homeActivity.getDisplay();
-                        display.moveHomeStackBehindStack(mRestoreHomeBehindStack);
+                        // Keep target stack in place, nothing changes, so ignore the transition
+                        // logic below
+                        return;
                     }
 
                     mWindowManager.prepareAppTransition(TRANSIT_NONE, false);
@@ -180,6 +252,15 @@
                     // No reason to wait for the pausing activity in this case, as the hiding of
                     // surfaces needs to be done immediately.
                     mWindowManager.executeAppTransition();
+
+                    // After reordering the stacks, reset the minimized state. At this point, either
+                    // the target activity is now top-most and we will stay minimized (if in
+                    // split-screen), or we will have returned to the app, and the minimized state
+                    // should be reset
+                    mWindowManager.checkSplitScreenMinimizedChanged(true /* animate */);
+                } catch (Exception e) {
+                    Slog.e(TAG, "Failed to clean up recents activity", e);
+                    throw e;
                 } finally {
                     mWindowManager.continueSurfaceLayout();
                     Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
@@ -198,4 +279,18 @@
             Slog.e(TAG, "Failed to cancel recents animation before start", e);
         }
     }
+
+    /**
+     * @return The top stack that is not always-on-top.
+     */
+    private ActivityStack getTopNonAlwaysOnTopStack() {
+        for (int i = mDefaultDisplay.getChildCount() - 1; i >= 0; i--) {
+            final ActivityStack s = mDefaultDisplay.getChildAt(i);
+            if (s.getWindowConfiguration().isAlwaysOnTop()) {
+                continue;
+            }
+            return s;
+        }
+        return null;
+    }
 }
diff --git a/com/android/server/am/SafeActivityOptions.java b/com/android/server/am/SafeActivityOptions.java
index ac6f01f..2de7527 100644
--- a/com/android/server/am/SafeActivityOptions.java
+++ b/com/android/server/am/SafeActivityOptions.java
@@ -210,7 +210,7 @@
         // Check if someone tries to launch an unwhitelisted activity into LockTask mode.
         final boolean lockTaskMode = options.getLockTaskMode();
         if (aInfo != null && lockTaskMode
-                && !supervisor.mService.mLockTaskController.isPackageWhitelisted(
+                && !supervisor.mService.getLockTaskController().isPackageWhitelisted(
                         UserHandle.getUserId(callingUid), aInfo.packageName)) {
             final String msg = "Permission Denial: starting " + getIntentString(intent)
                     + " from " + callerApp + " (pid=" + callingPid
diff --git a/com/android/server/am/TaskRecord.java b/com/android/server/am/TaskRecord.java
index 034cb2e..0e418ad 100644
--- a/com/android/server/am/TaskRecord.java
+++ b/com/android/server/am/TaskRecord.java
@@ -451,7 +451,7 @@
     }
 
     void removeWindowContainer() {
-        mService.mLockTaskController.clearLockedTask(this);
+        mService.getLockTaskController().clearLockedTask(this);
         mWindowContainerController.removeContainer();
         if (!getWindowConfiguration().persistTaskBounds()) {
             // Reset current bounds for task whose bounds shouldn't be persisted so it uses
@@ -927,7 +927,26 @@
         if (stack != null && !stack.isInStackLocked(this)) {
             throw new IllegalStateException("Task must be added as a Stack child first.");
         }
+        final ActivityStack oldStack = mStack;
         mStack = stack;
+
+        // If the new {@link TaskRecord} is from a different {@link ActivityStack}, remove this
+        // {@link ActivityRecord} from its current {@link ActivityStack}.
+
+        if (oldStack != mStack) {
+            for (int i = getChildCount() - 1; i >= 0; --i) {
+                final ActivityRecord activity = getChildAt(i);
+
+                if (oldStack != null) {
+                    oldStack.onActivityRemovedFromStack(activity);
+                }
+
+                if (mStack != null) {
+                    stack.onActivityAddedToStack(activity);
+                }
+            }
+        }
+
         onParentChanged();
     }
 
@@ -1232,6 +1251,7 @@
 
         index = Math.min(size, index);
         mActivities.add(index, r);
+
         updateEffectiveIntent();
         if (r.isPersistable()) {
             mService.notifyTaskPersisterLocked(this, false);
@@ -1257,7 +1277,7 @@
      * @return true if this was the last activity in the task.
      */
     boolean removeActivity(ActivityRecord r) {
-        return removeActivity(r, false /*reparenting*/);
+        return removeActivity(r, false /* reparenting */);
     }
 
     boolean removeActivity(ActivityRecord r, boolean reparenting) {
@@ -1266,7 +1286,7 @@
                     "Activity=" + r + " does not belong to task=" + this);
         }
 
-        r.setTask(null /*task*/, reparenting);
+        r.setTask(null /* task */, reparenting /* reparenting */);
 
         if (mActivities.remove(r) && r.fullscreen) {
             // Was previously in list.
@@ -1446,9 +1466,10 @@
         }
 
         final String pkg = (realActivity != null) ? realActivity.getPackageName() : null;
+        final LockTaskController lockTaskController = mService.getLockTaskController();
         switch (r.lockTaskLaunchMode) {
             case LOCK_TASK_LAUNCH_MODE_DEFAULT:
-                mLockTaskAuth = mService.mLockTaskController.isPackageWhitelisted(userId, pkg)
+                mLockTaskAuth = lockTaskController.isPackageWhitelisted(userId, pkg)
                         ? LOCK_TASK_AUTH_WHITELISTED : LOCK_TASK_AUTH_PINNABLE;
                 break;
 
@@ -1461,7 +1482,7 @@
                 break;
 
             case LOCK_TASK_LAUNCH_MODE_IF_WHITELISTED:
-                mLockTaskAuth = mService.mLockTaskController.isPackageWhitelisted(userId, pkg)
+                mLockTaskAuth = lockTaskController.isPackageWhitelisted(userId, pkg)
                         ? LOCK_TASK_AUTH_LAUNCHABLE : LOCK_TASK_AUTH_PINNABLE;
                 break;
         }
diff --git a/com/android/server/am/UserController.java b/com/android/server/am/UserController.java
index a294334..fecb934 100644
--- a/com/android/server/am/UserController.java
+++ b/com/android/server/am/UserController.java
@@ -1184,11 +1184,6 @@
             Slog.w(TAG, "No user info for user #" + targetUserId);
             return false;
         }
-        if (!targetUserInfo.isDemo() && UserManager.isDeviceInDemoMode(mInjector.getContext())) {
-            Slog.w(TAG, "Cannot switch to non-demo user #" + targetUserId
-                    + " when device is in demo mode");
-            return false;
-        }
         if (!targetUserInfo.supportsSwitchTo()) {
             Slog.w(TAG, "Cannot switch to User #" + targetUserId + ": not supported");
             return false;
@@ -2220,7 +2215,7 @@
 
         protected void clearAllLockedTasks(String reason) {
             synchronized (mService) {
-                mService.mLockTaskController.clearLockedTasks(reason);
+                mService.getLockTaskController().clearLockedTasks(reason);
             }
         }
 
diff --git a/com/android/server/audio/AudioService.java b/com/android/server/audio/AudioService.java
index c8b6b50..8212463 100644
--- a/com/android/server/audio/AudioService.java
+++ b/com/android/server/audio/AudioService.java
@@ -320,13 +320,13 @@
         0,  // STREAM_SYSTEM
         0,  // STREAM_RING
         0,  // STREAM_MUSIC
-        0,  // STREAM_ALARM
+        1,  // STREAM_ALARM
         0,  // STREAM_NOTIFICATION
         0,  // STREAM_BLUETOOTH_SCO
         0,  // STREAM_SYSTEM_ENFORCED
         0,  // STREAM_DTMF
         0,  // STREAM_TTS
-        0   // STREAM_ACCESSIBILITY
+        1   // STREAM_ACCESSIBILITY
     };
 
     /* mStreamVolumeAlias[] indicates for each stream if it uses the volume settings
@@ -588,7 +588,7 @@
             AudioSystem.DEVICE_OUT_HDMI_ARC |
             AudioSystem.DEVICE_OUT_SPDIF |
             AudioSystem.DEVICE_OUT_AUX_LINE;
-    int mFullVolumeDevices = AudioSystem.DEVICE_OUT_HEARING_AID;
+    int mFullVolumeDevices = 0;
 
     private final boolean mMonitorRotation;
 
@@ -1208,6 +1208,8 @@
                             System.VOLUME_SETTINGS_INT[a11yStreamAlias];
                     mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].setAllIndexes(
                             mStreamStates[a11yStreamAlias], caller);
+                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].refreshRange(
+                            mStreamVolumeAlias[AudioSystem.STREAM_ACCESSIBILITY]);
                 }
             }
             if (sIndependentA11yVolume) {
@@ -1389,7 +1391,15 @@
     }
 
     private int rescaleIndex(int index, int srcStream, int dstStream) {
-        return (index * mStreamStates[dstStream].getMaxIndex() + mStreamStates[srcStream].getMaxIndex() / 2) / mStreamStates[srcStream].getMaxIndex();
+        final int rescaled =
+                (index * mStreamStates[dstStream].getMaxIndex()
+                        + mStreamStates[srcStream].getMaxIndex() / 2)
+                / mStreamStates[srcStream].getMaxIndex();
+        if (rescaled < mStreamStates[dstStream].getMinIndex()) {
+            return mStreamStates[dstStream].getMinIndex();
+        } else {
+            return rescaled;
+        }
     }
 
     ///////////////////////////////////////////////////////////////////////////
@@ -4602,8 +4612,8 @@
     //  4       VolumeStreamState.class
     public class VolumeStreamState {
         private final int mStreamType;
-        private final int mIndexMin;
-        private final int mIndexMax;
+        private int mIndexMin;
+        private int mIndexMax;
 
         private boolean mIsMuted;
         private String mVolumeIndexSettingName;
@@ -4755,6 +4765,8 @@
                 index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
             } else if ((device & mFullVolumeDevices) != 0) {
                 index = (mIndexMax + 5)/10;
+            } else if ((device & AudioSystem.DEVICE_OUT_HEARING_AID) != 0) {
+                index = (mIndexMax + 5)/10;
             } else {
                 index = (getIndex(device) + 5)/10;
             }
@@ -4775,6 +4787,8 @@
                             index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
                         } else if ((device & mFullVolumeDevices) != 0) {
                             index = (mIndexMax + 5)/10;
+                        } else if ((device & AudioSystem.DEVICE_OUT_HEARING_AID) != 0) {
+                            index = (mIndexMax + 5)/10;
                         } else {
                             index = (mIndexMap.valueAt(i) + 5)/10;
                         }
@@ -4889,6 +4903,24 @@
         }
 
         /**
+         * Updates the min/max index values from another stream. Use this when changing the alias
+         * for the current stream type.
+         * @param sourceStreamType
+         */
+        // must be sync'd on mSettingsLock before VolumeStreamState.class
+        @GuardedBy("VolumeStreamState.class")
+        public void refreshRange(int sourceStreamType) {
+            mIndexMin = MIN_STREAM_VOLUME[sourceStreamType] * 10;
+            mIndexMax = MAX_STREAM_VOLUME[sourceStreamType] * 10;
+            // verify all current volumes are within bounds
+            for (int i = 0 ; i < mIndexMap.size(); i++) {
+                final int device = mIndexMap.keyAt(i);
+                final int index = mIndexMap.valueAt(i);
+                mIndexMap.put(device, getValidIndex(index));
+            }
+        }
+
+        /**
          * Copies all device/index pairs from the given VolumeStreamState after initializing
          * them with the volume for DEVICE_OUT_DEFAULT. No-op if the source VolumeStreamState
          * has the same stream type as this instance.
diff --git a/com/android/server/autofill/Session.java b/com/android/server/autofill/Session.java
index e14584f..06707da 100644
--- a/com/android/server/autofill/Session.java
+++ b/com/android/server/autofill/Session.java
@@ -99,6 +99,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -1400,6 +1401,7 @@
          *   the current values of all fields in the screen.
          */
         if (saveInfo == null) {
+            if (sVerbose) Slog.w(TAG, "showSaveLocked(): no saveInfo from service");
             return true;
         }
 
@@ -1970,8 +1972,8 @@
                     return;
                 }
 
-                if (value != null && !value.equals(viewState.getCurrentValue())) {
-                    if (value.isEmpty()
+                if (!Objects.equals(value, viewState.getCurrentValue())) {
+                    if ((value == null || value.isEmpty())
                             && viewState.getCurrentValue() != null
                             && viewState.getCurrentValue().isText()
                             && viewState.getCurrentValue().getTextValue() != null
@@ -1992,18 +1994,26 @@
                     // Must check if this update was caused by autofilling the view, in which
                     // case we just update the value, but not the UI.
                     final AutofillValue filledValue = viewState.getAutofilledValue();
-                    if (value.equals(filledValue)) {
+                    if (filledValue != null && filledValue.equals(value)) {
+                        if (sVerbose) {
+                            Slog.v(TAG, "ignoring autofilled change on id " + id);
+                        }
                         return;
                     }
                     // Update the internal state...
                     viewState.setState(ViewState.STATE_CHANGED);
 
                     //..and the UI
-                    if (value.isText()) {
-                        getUiForShowing().filterFillUi(value.getTextValue().toString(), this);
+                    final String filterText;
+                    if (value == null || !value.isText()) {
+                        filterText = null;
                     } else {
-                        getUiForShowing().filterFillUi(null, this);
+                        final CharSequence text = value.getTextValue();
+                        // Text should never be null, but it doesn't hurt to check to avoid a
+                        // system crash...
+                        filterText = (text == null) ? null : text.toString();
                     }
+                    getUiForShowing().filterFillUi(filterText, this);
                 }
                 break;
             case ACTION_VIEW_ENTERED:
diff --git a/com/android/server/autofill/ViewState.java b/com/android/server/autofill/ViewState.java
index 9210de2..2a76055 100644
--- a/com/android/server/autofill/ViewState.java
+++ b/com/android/server/autofill/ViewState.java
@@ -237,12 +237,7 @@
         }
         pw.print(prefix); pw.print("state:" ); pw.println(getStateAsString());
         if (mResponse != null) {
-            pw.print(prefix); pw.print("response:");
-            if (sVerbose) {
-                pw.println(mResponse);
-            } else {
-                pw.print("id=");pw.println(mResponse.getRequestId());
-            }
+            pw.print(prefix); pw.print("response id:");pw.println(mResponse.getRequestId());
         }
         if (mCurrentValue != null) {
             pw.print(prefix); pw.print("currentValue:" ); pw.println(mCurrentValue);
diff --git a/com/android/server/autofill/ui/FillUi.java b/com/android/server/autofill/ui/FillUi.java
index 7c0671f..d29ca05 100644
--- a/com/android/server/autofill/ui/FillUi.java
+++ b/com/android/server/autofill/ui/FillUi.java
@@ -656,6 +656,8 @@
         private final WindowManager mWm;
         private final View mContentView;
         private boolean mShowing;
+        // Used on dump only
+        private WindowManager.LayoutParams mShowParams;
 
         /**
          * Constructor.
@@ -672,16 +674,13 @@
          * Shows the window.
          */
         public void show(WindowManager.LayoutParams params) {
+            mShowParams = params;
             if (sVerbose) {
                 Slog.v(TAG, "show(): showing=" + mShowing + ", params=" + paramsToString(params));
             }
             try {
-                // Okay here is a bit of voodoo - we want to show the window as system
-                // controlled one so it covers app windows - adjust the params accordingly.
-                params.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
-                params.token = null;
                 params.packageName = "android";
-                params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+                params.setTitle("Autofill UI"); // Title is set for debugging purposes
                 if (!mShowing) {
                     params.accessibilityTitle = mContentView.getContext()
                             .getString(R.string.autofill_picker_accessibility_title);
@@ -760,6 +759,9 @@
             pw.println();
             pw.print(prefix2); pw.print("showing: "); pw.println(mWindow.mShowing);
             pw.print(prefix2); pw.print("view: "); pw.println(mWindow.mContentView);
+            if (mWindow.mShowParams != null) {
+                pw.print(prefix2); pw.print("params: "); pw.println(mWindow.mShowParams);
+            }
             pw.print(prefix2); pw.print("screen coordinates: ");
             if (mWindow.mContentView == null) {
                 pw.println("N/A");
diff --git a/com/android/server/backup/BackupUtils.java b/com/android/server/backup/BackupUtils.java
index d817534..96c5621 100644
--- a/com/android/server/backup/BackupUtils.java
+++ b/com/android/server/backup/BackupUtils.java
@@ -20,6 +20,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
 import android.util.Slog;
 
 import com.android.internal.util.ArrayUtils;
@@ -55,16 +56,16 @@
             return false;
         }
 
-        Signature[][] deviceHistorySigs = target.signingCertificateHistory;
-        if (ArrayUtils.isEmpty(deviceHistorySigs)) {
-            Slog.w(TAG, "signingCertificateHistory is empty, app was either unsigned or the flag" +
+        SigningInfo signingInfo = target.signingInfo;
+        if (signingInfo == null) {
+            Slog.w(TAG, "signingInfo is empty, app was either unsigned or the flag" +
                     " PackageManager#GET_SIGNING_CERTIFICATES was not specified");
             return false;
         }
 
         if (DEBUG) {
             Slog.v(TAG, "signaturesMatch(): stored=" + storedSigHashes
-                    + " device=" + deviceHistorySigs);
+                    + " device=" + signingInfo.getApkContentsSigners());
         }
 
         final int nStored = storedSigHashes.size();
@@ -78,8 +79,9 @@
         } else {
             // the app couldn't have rotated keys, since it was signed with multiple sigs - do
             // a check to see if we find a match for all stored sigs
-            // since app hasn't rotated key, we only need to check with deviceHistorySigs[0]
-            ArrayList<byte[]> deviceHashes = hashSignatureArray(deviceHistorySigs[0]);
+            // since app hasn't rotated key, we only need to check with current signers
+            ArrayList<byte[]> deviceHashes =
+                    hashSignatureArray(signingInfo.getApkContentsSigners());
             int nDevice = deviceHashes.size();
             // ensure that each stored sig matches an on-device sig
             for (int i = 0; i < nStored; i++) {
diff --git a/com/android/server/backup/KeyValueAdbRestoreEngine.java b/com/android/server/backup/KeyValueAdbRestoreEngine.java
index a2de8e7..fbec5cb 100644
--- a/com/android/server/backup/KeyValueAdbRestoreEngine.java
+++ b/com/android/server/backup/KeyValueAdbRestoreEngine.java
@@ -64,8 +64,7 @@
         try {
             File restoreData = prepareRestoreData(mInfo, mInFD);
 
-            // TODO: version ?
-            invokeAgentForAdbRestore(mAgent, mInfo, restoreData, 0);
+            invokeAgentForAdbRestore(mAgent, mInfo, restoreData);
         } catch (IOException e) {
             e.printStackTrace();
         }
@@ -83,8 +82,8 @@
         return sortedDataName;
     }
 
-    private void invokeAgentForAdbRestore(IBackupAgent agent, FileMetadata info, File restoreData,
-            int versionCode) throws IOException {
+    private void invokeAgentForAdbRestore(IBackupAgent agent, FileMetadata info, File restoreData)
+            throws IOException {
         String pkg = info.packageName;
         File newStateName = new File(mDataDir, pkg + ".new");
         try {
@@ -95,9 +94,9 @@
 
             if (DEBUG) {
                 Slog.i(TAG, "Starting restore of package " + pkg + " for version code "
-                        + versionCode);
+                        + info.version);
             }
-            agent.doRestore(backupData, versionCode, newState, mToken,
+            agent.doRestore(backupData, info.version, newState, mToken,
                     mBackupManagerService.getBackupManagerBinder());
         } catch (IOException e) {
             Slog.e(TAG, "Exception opening file. " + e);
diff --git a/com/android/server/backup/PackageManagerBackupAgent.java b/com/android/server/backup/PackageManagerBackupAgent.java
index dc28cd1..e4ce62d 100644
--- a/com/android/server/backup/PackageManagerBackupAgent.java
+++ b/com/android/server/backup/PackageManagerBackupAgent.java
@@ -27,6 +27,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.util.Slog;
@@ -240,12 +241,13 @@
                         PackageManager.GET_SIGNING_CERTIFICATES);
                 homeInstaller = mPackageManager.getInstallerPackageName(home.getPackageName());
                 homeVersion = homeInfo.getLongVersionCode();
-                Signature[][] signingHistory = homeInfo.signingCertificateHistory;
-                if (signingHistory == null || signingHistory.length == 0) {
-                    Slog.e(TAG, "Home app has no signing history");
+                SigningInfo signingInfo = homeInfo.signingInfo;
+                if (signingInfo == null) {
+                    Slog.e(TAG, "Home app has no signing information");
                 } else {
                     // retrieve the newest sigs to back up
-                    Signature[] homeInfoSignatures = signingHistory[signingHistory.length - 1];
+                    // TODO (b/73988180) use entire signing history in case of rollbacks
+                    Signature[] homeInfoSignatures = signingInfo.getApkContentsSigners();
                     homeSigHashes = BackupUtils.hashSignatureArray(homeInfoSignatures);
                 }
             } catch (NameNotFoundException e) {
@@ -334,8 +336,8 @@
                         }
                     }
 
-                    Signature[][] signingHistory = info.signingCertificateHistory;
-                    if (signingHistory == null || signingHistory.length == 0) {
+                    SigningInfo signingInfo = info.signingInfo;
+                    if (signingInfo == null) {
                         Slog.w(TAG, "Not backing up package " + packName
                                 + " since it appears to have no signatures.");
                         continue;
@@ -358,7 +360,7 @@
                         outputBufferStream.writeInt(info.versionCode);
                     }
                     // retrieve the newest sigs to back up
-                    Signature[] infoSignatures = signingHistory[signingHistory.length - 1];
+                    Signature[] infoSignatures = signingInfo.getApkContentsSigners();
                     writeSignatureHashArray(outputBufferStream,
                             BackupUtils.hashSignatureArray(infoSignatures));
 
diff --git a/com/android/server/backup/restore/PerformAdbRestoreTask.java b/com/android/server/backup/restore/PerformAdbRestoreTask.java
index 77163d3..0c99b44 100644
--- a/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -99,6 +99,7 @@
     private FullBackupObbConnection mObbConnection = null;
     private ParcelFileDescriptor[] mPipes = null;
     private byte[] mWidgetData = null;
+    private long mAppVersion;
 
     private long mBytes;
     private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
@@ -476,6 +477,9 @@
                 if (info.path.equals(BACKUP_MANIFEST_FILENAME)) {
                     Signature[] signatures = tarBackupReader.readAppManifestAndReturnSignatures(
                             info);
+                    // readAppManifestAndReturnSignatures() will have extracted the version from
+                    // the manifest, so we save it to use in key-value restore later.
+                    mAppVersion = info.version;
                     PackageManagerInternal pmi = LocalServices.getService(
                             PackageManagerInternal.class);
                     RestorePolicy restorePolicy = tarBackupReader.chooseRestorePolicy(
@@ -667,6 +671,8 @@
                                     Slog.d(TAG, "Restoring key-value file for " + pkg
                                             + " : " + info.path);
                                 }
+                                // Set the version saved from manifest entry.
+                                info.version = mAppVersion;
                                 KeyValueAdbRestoreEngine restoreEngine =
                                         new KeyValueAdbRestoreEngine(
                                                 mBackupManagerService,
diff --git a/com/android/server/backup/transport/TransportClient.java b/com/android/server/backup/transport/TransportClient.java
index fa881a9..e4dcb25 100644
--- a/com/android/server/backup/transport/TransportClient.java
+++ b/com/android/server/backup/transport/TransportClient.java
@@ -439,8 +439,17 @@
         synchronized (mStateLock) {
             log(Priority.ERROR, "Service disconnected: client UNUSABLE");
             setStateLocked(State.UNUSABLE, null);
-            // After unbindService() no calls back to mConnection
-            mContext.unbindService(mConnection);
+            try {
+                // After unbindService() no calls back to mConnection
+                mContext.unbindService(mConnection);
+            } catch (IllegalArgumentException e) {
+                // TODO: Investigate why this is happening
+                // We're UNUSABLE, so any calls to mConnection will be no-op, so it's safe to
+                // swallow this one
+                log(
+                        Priority.WARN,
+                        "Exception trying to unbind onServiceDisconnected(): " + e.getMessage());
+            }
         }
     }
 
diff --git a/com/android/server/backup/utils/AppBackupUtils.java b/com/android/server/backup/utils/AppBackupUtils.java
index 5518374..c39cceb 100644
--- a/com/android/server/backup/utils/AppBackupUtils.java
+++ b/com/android/server/backup/utils/AppBackupUtils.java
@@ -27,6 +27,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
 import android.os.Process;
 import android.util.Slog;
 
@@ -203,15 +204,16 @@
             return false;
         }
 
-        Signature[][] deviceHistorySigs = target.signingCertificateHistory;
-        if (ArrayUtils.isEmpty(deviceHistorySigs)) {
-            Slog.w(TAG, "signingCertificateHistory is empty, app was either unsigned or the flag" +
+        SigningInfo signingInfo = target.signingInfo;
+        if (signingInfo == null) {
+            Slog.w(TAG, "signingInfo is empty, app was either unsigned or the flag" +
                     " PackageManager#GET_SIGNING_CERTIFICATES was not specified");
             return false;
         }
 
         if (DEBUG) {
-            Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + " device=" + deviceHistorySigs);
+            Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + " device="
+                    + signingInfo.getApkContentsSigners());
         }
 
         final int nStored = storedSigs.length;
@@ -225,8 +227,8 @@
         } else {
             // the app couldn't have rotated keys, since it was signed with multiple sigs - do
             // a check to see if we find a match for all stored sigs
-            // since app hasn't rotated key, we only need to check with deviceHistorySigs[0]
-            Signature[] deviceSigs = deviceHistorySigs[0];
+            // since app hasn't rotated key, we only need to check with its current signers
+            Signature[] deviceSigs = signingInfo.getApkContentsSigners();
             int nDevice = deviceSigs.length;
 
             // ensure that each stored sig matches an on-device sig
diff --git a/com/android/server/backup/utils/FullBackupUtils.java b/com/android/server/backup/utils/FullBackupUtils.java
index 994d5a9..a3d5601 100644
--- a/com/android/server/backup/utils/FullBackupUtils.java
+++ b/com/android/server/backup/utils/FullBackupUtils.java
@@ -22,6 +22,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.util.Slog;
@@ -106,12 +107,13 @@
         printer.println(withApk ? "1" : "0");
 
         // write the signature block
-        Signature[][] signingHistory = pkg.signingCertificateHistory;
-        if (signingHistory == null) {
+        SigningInfo signingInfo = pkg.signingInfo;
+        if (signingInfo == null) {
             printer.println("0");
         } else {
             // retrieve the newest sigs to write
-            Signature[] signatures = signingHistory[signingHistory.length - 1];
+            // TODO (b/73988180) use entire signing history in case of rollbacks
+            Signature[] signatures = signingInfo.getApkContentsSigners();
             printer.println(Integer.toString(signatures.length));
             for (Signature sig : signatures) {
                 printer.println(sig.toCharsString());
diff --git a/com/android/server/connectivity/DnsManager.java b/com/android/server/connectivity/DnsManager.java
index 36f5a6c..7aaac06 100644
--- a/com/android/server/connectivity/DnsManager.java
+++ b/com/android/server/connectivity/DnsManager.java
@@ -34,27 +34,28 @@
 import android.net.Network;
 import android.net.NetworkUtils;
 import android.net.Uri;
+import android.net.dns.ResolvUtil;
 import android.os.Binder;
 import android.os.INetworkManagementService;
-import android.os.Handler;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.system.GaiException;
-import android.system.OsConstants;
-import android.system.StructAddrinfo;
 import android.text.TextUtils;
+import android.util.Pair;
 import android.util.Slog;
 
 import com.android.server.connectivity.MockableSystemProperties;
 
-import libcore.io.Libcore;
-
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
+import java.util.Set;
 import java.util.StringJoiner;
 
 
@@ -64,10 +65,56 @@
  * This class it NOT designed for concurrent access. Furthermore, all non-static
  * methods MUST be called from ConnectivityService's thread.
  *
+ * [ Private DNS ]
+ * The code handling Private DNS is spread across several components, but this
+ * seems like the least bad place to collect all the observations.
+ *
+ * Private DNS handling and updating occurs in response to several different
+ * events. Each is described here with its corresponding intended handling.
+ *
+ * [A] Event: A new network comes up.
+ * Mechanics:
+ *     [1] ConnectivityService gets notifications from NetworkAgents.
+ *     [2] in updateNetworkInfo(), the first time the NetworkAgent goes into
+ *         into CONNECTED state, the Private DNS configuration is retrieved,
+ *         programmed, and strict mode hostname resolution (if applicable) is
+ *         enqueued in NetworkAgent's NetworkMonitor, via a call to
+ *         handlePerNetworkPrivateDnsConfig().
+ *     [3] Re-resolution of strict mode hostnames that fail to return any
+ *         IP addresses happens inside NetworkMonitor; it sends itself a
+ *         delayed CMD_EVALUATE_PRIVATE_DNS message in a simple backoff
+ *         schedule.
+ *     [4] Successfully resolved hostnames are sent to ConnectivityService
+ *         inside an EVENT_PRIVATE_DNS_CONFIG_RESOLVED message. The resolved
+ *         IP addresses are programmed into netd via:
+ *
+ *             updatePrivateDns() -> updateDnses()
+ *
+ *         both of which make calls into DnsManager.
+ *     [5] Upon a successful hostname resolution NetworkMonitor initiates a
+ *         validation attempt in the form of a lookup for a one-time hostname
+ *         that uses Private DNS.
+ *
+ * [B] Event: Private DNS settings are changed.
+ * Mechanics:
+ *     [1] ConnectivityService gets notifications from its SettingsObserver.
+ *     [2] handlePrivateDnsSettingsChanged() is called, which calls
+ *         handlePerNetworkPrivateDnsConfig() and the process proceeds
+ *         as if from A.3 above.
+ *
+ * [C] Event: An application calls ConnectivityManager#reportBadNetwork().
+ * Mechanics:
+ *     [1] NetworkMonitor is notified and initiates a reevaluation, which
+ *         always bypasses Private DNS.
+ *     [2] Once completed, NetworkMonitor checks if strict mode is in operation
+ *         and if so enqueues another evaluation of Private DNS, as if from
+ *         step A.5 above.
+ *
  * @hide
  */
 public class DnsManager {
     private static final String TAG = DnsManager.class.getSimpleName();
+    private static final PrivateDnsConfig PRIVATE_DNS_OFF = new PrivateDnsConfig();
 
     /* Defaults for resolver parameters. */
     private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
@@ -126,35 +173,104 @@
     }
 
     public static PrivateDnsConfig tryBlockingResolveOf(Network network, String name) {
-        final StructAddrinfo hints = new StructAddrinfo();
-        // Unnecessary, but expressly no AI_ADDRCONFIG.
-        hints.ai_flags = 0;
-        // Fetch all IP addresses at once to minimize re-resolution.
-        hints.ai_family = OsConstants.AF_UNSPEC;
-        hints.ai_socktype = OsConstants.SOCK_DGRAM;
-
         try {
-            final InetAddress[] ips = Libcore.os.android_getaddrinfo(name, hints, network.netId);
-            if (ips != null && ips.length > 0) {
-                return new PrivateDnsConfig(name, ips);
-            }
-        } catch (GaiException ignored) {}
-
-        return null;
+            final InetAddress[] ips = ResolvUtil.blockingResolveAllLocally(network, name);
+            return new PrivateDnsConfig(name, ips);
+        } catch (UnknownHostException uhe) {
+            return new PrivateDnsConfig(name, null);
+        }
     }
 
     public static Uri[] getPrivateDnsSettingsUris() {
-        final Uri[] uris = new Uri[2];
-        uris[0] = Settings.Global.getUriFor(PRIVATE_DNS_MODE);
-        uris[1] = Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER);
-        return uris;
+        return new Uri[]{
+            Settings.Global.getUriFor(PRIVATE_DNS_MODE),
+            Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER),
+        };
+    }
+
+    public static class PrivateDnsValidationUpdate {
+        final public int netId;
+        final public InetAddress ipAddress;
+        final public String hostname;
+        final public boolean validated;
+
+        public PrivateDnsValidationUpdate(int netId, InetAddress ipAddress,
+                String hostname, boolean validated) {
+            this.netId = netId;
+            this.ipAddress = ipAddress;
+            this.hostname = hostname;
+            this.validated = validated;
+        }
+    }
+
+    private static class PrivateDnsValidationStatuses {
+        enum ValidationStatus {
+            IN_PROGRESS,
+            FAILED,
+            SUCCEEDED
+        }
+
+        // Validation statuses of <hostname, ipAddress> pairs for a single netId
+        private Map<Pair<String, InetAddress>, ValidationStatus> mValidationMap;
+
+        private PrivateDnsValidationStatuses() {
+            mValidationMap = new HashMap<>();
+        }
+
+        private boolean hasValidatedServer() {
+            for (ValidationStatus status : mValidationMap.values()) {
+                if (status == ValidationStatus.SUCCEEDED) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void updateTrackedDnses(String[] ipAddresses, String hostname) {
+            Set<Pair<String, InetAddress>> latestDnses = new HashSet<>();
+            for (String ipAddress : ipAddresses) {
+                try {
+                    latestDnses.add(new Pair(hostname,
+                            InetAddress.parseNumericAddress(ipAddress)));
+                } catch (IllegalArgumentException e) {}
+            }
+            // Remove <hostname, ipAddress> pairs that should not be tracked.
+            for (Iterator<Map.Entry<Pair<String, InetAddress>, ValidationStatus>> it =
+                    mValidationMap.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry<Pair<String, InetAddress>, ValidationStatus> entry = it.next();
+                if (!latestDnses.contains(entry.getKey())) {
+                    it.remove();
+                }
+            }
+            // Add new <hostname, ipAddress> pairs that should be tracked.
+            for (Pair<String, InetAddress> p : latestDnses) {
+                if (!mValidationMap.containsKey(p)) {
+                    mValidationMap.put(p, ValidationStatus.IN_PROGRESS);
+                }
+            }
+        }
+
+        private void updateStatus(PrivateDnsValidationUpdate update) {
+            Pair<String, InetAddress> p = new Pair(update.hostname,
+                    update.ipAddress);
+            if (!mValidationMap.containsKey(p)) {
+                return;
+            }
+            if (update.validated) {
+                mValidationMap.put(p, ValidationStatus.SUCCEEDED);
+            } else {
+                mValidationMap.put(p, ValidationStatus.FAILED);
+            }
+        }
     }
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
     private final INetworkManagementService mNMS;
     private final MockableSystemProperties mSystemProperties;
+    // TODO: Replace these Maps with SparseArrays.
     private final Map<Integer, PrivateDnsConfig> mPrivateDnsMap;
+    private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
 
     private int mNumDnsEntries;
     private int mSampleValidity;
@@ -170,6 +286,7 @@
         mNMS = nms;
         mSystemProperties = sp;
         mPrivateDnsMap = new HashMap<>();
+        mPrivateDnsValidationMap = new HashMap<>();
 
         // TODO: Create and register ContentObservers to track every setting
         // used herein, posting messages to respond to changes.
@@ -181,6 +298,7 @@
 
     public void removeNetwork(Network network) {
         mPrivateDnsMap.remove(network.netId);
+        mPrivateDnsValidationMap.remove(network.netId);
     }
 
     public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
@@ -190,6 +308,40 @@
                 : mPrivateDnsMap.remove(network.netId);
     }
 
+    public void updatePrivateDnsStatus(int netId, LinkProperties lp) {
+        // Use the PrivateDnsConfig data pushed to this class instance
+        // from ConnectivityService.
+        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+                PRIVATE_DNS_OFF);
+
+        final boolean useTls = privateDnsCfg.useTls;
+        final boolean strictMode = privateDnsCfg.inStrictMode();
+        final String tlsHostname = strictMode ? privateDnsCfg.hostname : "";
+
+        if (strictMode) {
+            lp.setUsePrivateDns(true);
+            lp.setPrivateDnsServerName(tlsHostname);
+        } else if (useTls) {
+            // We are in opportunistic mode. Private DNS should be used if there
+            // is a known DNS-over-TLS validated server.
+            boolean validated = mPrivateDnsValidationMap.containsKey(netId) &&
+                    mPrivateDnsValidationMap.get(netId).hasValidatedServer();
+            lp.setUsePrivateDns(validated);
+            lp.setPrivateDnsServerName(null);
+        } else {
+            // Private DNS is disabled.
+            lp.setUsePrivateDns(false);
+            lp.setPrivateDnsServerName(null);
+        }
+    }
+
+    public void updatePrivateDnsValidation(PrivateDnsValidationUpdate update) {
+        final PrivateDnsValidationStatuses statuses =
+                mPrivateDnsValidationMap.get(update.netId);
+        if (statuses == null) return;
+        statuses.updateStatus(update);
+    }
+
     public void setDnsConfigurationForNetwork(
             int netId, LinkProperties lp, boolean isDefaultNetwork) {
         final String[] assignedServers = NetworkUtils.makeStrings(lp.getDnsServers());
@@ -203,12 +355,13 @@
         // NetworkMonitor to decide which networks need validation and runs the
         // blocking calls to resolve Private DNS strict mode hostnames.
         //
-        // At this time we do attempt to enable Private DNS on non-Internet
+        // At this time we do not attempt to enable Private DNS on non-Internet
         // networks like IMS.
-        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.get(netId);
+        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+                PRIVATE_DNS_OFF);
 
-        final boolean useTls = (privateDnsCfg != null) && privateDnsCfg.useTls;
-        final boolean strictMode = (privateDnsCfg != null) && privateDnsCfg.inStrictMode();
+        final boolean useTls = privateDnsCfg.useTls;
+        final boolean strictMode = privateDnsCfg.inStrictMode();
         final String tlsHostname = strictMode ? privateDnsCfg.hostname : "";
         final String[] tlsServers =
                 strictMode ? NetworkUtils.makeStrings(
@@ -218,6 +371,17 @@
                 : useTls ? assignedServers  // Opportunistic
                 : new String[0];            // Off
 
+        // Prepare to track the validation status of the DNS servers in the
+        // resolver config when private DNS is in opportunistic or strict mode.
+        if (useTls) {
+            if (!mPrivateDnsValidationMap.containsKey(netId)) {
+                mPrivateDnsValidationMap.put(netId, new PrivateDnsValidationStatuses());
+            }
+            mPrivateDnsValidationMap.get(netId).updateTrackedDnses(tlsServers, tlsHostname);
+        } else {
+            mPrivateDnsValidationMap.remove(netId);
+        }
+
         Slog.d(TAG, String.format("setDnsConfigurationForNetwork(%d, %s, %s, %s, %s, %s)",
                 netId, Arrays.toString(assignedServers), Arrays.toString(domainStrs),
                 Arrays.toString(params), tlsHostname, Arrays.toString(tlsServers)));
diff --git a/com/android/server/connectivity/MultipathPolicyTracker.java b/com/android/server/connectivity/MultipathPolicyTracker.java
index 4eb1930..6fa999c 100644
--- a/com/android/server/connectivity/MultipathPolicyTracker.java
+++ b/com/android/server/connectivity/MultipathPolicyTracker.java
@@ -20,35 +20,63 @@
 import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkPolicy.LIMIT_DISABLED;
+import static android.net.NetworkPolicy.WARNING_DISABLED;
+import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
 
 import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
+import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
 
 import android.app.usage.NetworkStatsManager;
 import android.app.usage.NetworkStatsManager.UsageCallback;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
+import android.net.NetworkPolicy;
 import android.net.NetworkPolicyManager;
 import android.net.NetworkRequest;
 import android.net.NetworkStats;
 import android.net.NetworkTemplate;
 import android.net.StringNetworkSpecifier;
+import android.os.BestClock;
 import android.os.Handler;
+import android.os.SystemClock;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.Settings;
 import android.telephony.TelephonyManager;
+import android.util.DataUnit;
 import android.util.DebugUtils;
+import android.util.Pair;
+import android.util.Range;
 import android.util.Slog;
 
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
 import com.android.server.net.NetworkPolicyManagerInternal;
-import com.android.server.net.NetworkPolicyManagerService;
 import com.android.server.net.NetworkStatsManagerInternal;
 
-import java.util.Calendar;
+import java.time.Clock;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Iterator;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Manages multipath data budgets.
@@ -69,6 +97,13 @@
 
     private final Context mContext;
     private final Handler mHandler;
+    private final Clock mClock;
+    private final Dependencies mDeps;
+    private final ContentResolver mResolver;
+    private final ConfigChangeReceiver mConfigChangeReceiver;
+
+    @VisibleForTesting
+    final ContentObserver mSettingsObserver;
 
     private ConnectivityManager mCM;
     private NetworkPolicyManager mNPM;
@@ -77,12 +112,32 @@
     private NetworkCallback mMobileNetworkCallback;
     private NetworkPolicyManager.Listener mPolicyListener;
 
-    // STOPSHIP: replace this with a configurable mechanism.
-    private static final long DEFAULT_DAILY_MULTIPATH_QUOTA = 2_500_000;
+
+    /**
+     * Divider to calculate opportunistic quota from user-set data limit or warning: 5% of user-set
+     * limit.
+     */
+    private static final int OPQUOTA_USER_SETTING_DIVIDER = 20;
+
+    public static class Dependencies {
+        public Clock getClock() {
+            return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
+                    Clock.systemUTC());
+        }
+    }
 
     public MultipathPolicyTracker(Context ctx, Handler handler) {
+        this(ctx, handler, new Dependencies());
+    }
+
+    public MultipathPolicyTracker(Context ctx, Handler handler, Dependencies deps) {
         mContext = ctx;
         mHandler = handler;
+        mClock = deps.getClock();
+        mDeps = deps;
+        mResolver = mContext.getContentResolver();
+        mSettingsObserver = new SettingsObserver(mHandler);
+        mConfigChangeReceiver = new ConfigChangeReceiver();
         // Because we are initialized by the ConnectivityService constructor, we can't touch any
         // connectivity APIs. Service initialization is done in start().
     }
@@ -94,6 +149,14 @@
 
         registerTrackMobileCallback();
         registerNetworkPolicyListener();
+        final Uri defaultSettingUri =
+                Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
+        mResolver.registerContentObserver(defaultSettingUri, false, mSettingsObserver);
+
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        mContext.registerReceiverAsUser(
+                mConfigChangeReceiver, UserHandle.ALL, intentFilter, null, mHandler);
     }
 
     public void shutdown() {
@@ -103,6 +166,8 @@
             t.shutdown();
         }
         mMultipathTrackers.clear();
+        mResolver.unregisterContentObserver(mSettingsObserver);
+        mContext.unregisterReceiver(mConfigChangeReceiver);
     }
 
     // Called on an arbitrary binder thread.
@@ -128,9 +193,11 @@
         private long mMultipathBudget;
         private final NetworkTemplate mNetworkTemplate;
         private final UsageCallback mUsageCallback;
+        private NetworkCapabilities mNetworkCapabilities;
 
         public MultipathTracker(Network network, NetworkCapabilities nc) {
             this.network = network;
+            this.mNetworkCapabilities = new NetworkCapabilities(nc);
             try {
                 subId = Integer.parseInt(
                         ((StringNetworkSpecifier) nc.getNetworkSpecifier()).toString());
@@ -167,42 +234,111 @@
             updateMultipathBudget();
         }
 
-        private long getDailyNonDefaultDataUsage() {
-            Calendar start = Calendar.getInstance();
-            Calendar end = (Calendar) start.clone();
-            start.set(Calendar.HOUR_OF_DAY, 0);
-            start.set(Calendar.MINUTE, 0);
-            start.set(Calendar.SECOND, 0);
-            start.set(Calendar.MILLISECOND, 0);
+        public void setNetworkCapabilities(NetworkCapabilities nc) {
+            mNetworkCapabilities = new NetworkCapabilities(nc);
+        }
 
+        // TODO: calculate with proper timezone information
+        private long getDailyNonDefaultDataUsage() {
+            final ZonedDateTime end =
+                    ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault());
+            final ZonedDateTime start = end.truncatedTo(ChronoUnit.DAYS);
+
+            final long bytes = getNetworkTotalBytes(
+                    start.toInstant().toEpochMilli(),
+                    end.toInstant().toEpochMilli());
+            if (DBG) Slog.d(TAG, "Non-default data usage: " + bytes);
+            return bytes;
+        }
+
+        private long getNetworkTotalBytes(long start, long end) {
             try {
-                final long bytes = LocalServices.getService(NetworkStatsManagerInternal.class)
-                        .getNetworkTotalBytes(mNetworkTemplate, start.getTimeInMillis(),
-                                end.getTimeInMillis());
-                if (DBG) Slog.d(TAG, "Non-default data usage: " + bytes);
-                return bytes;
+                return LocalServices.getService(NetworkStatsManagerInternal.class)
+                        .getNetworkTotalBytes(mNetworkTemplate, start, end);
             } catch (RuntimeException e) {
                 Slog.w(TAG, "Failed to get data usage: " + e);
                 return -1;
             }
         }
 
+        private NetworkIdentity getTemplateMatchingNetworkIdentity(NetworkCapabilities nc) {
+            return new NetworkIdentity(
+                    ConnectivityManager.TYPE_MOBILE,
+                    0 /* subType, unused for template matching */,
+                    subscriberId,
+                    null /* networkId, unused for matching mobile networks */,
+                    !nc.hasCapability(NET_CAPABILITY_NOT_ROAMING),
+                    !nc.hasCapability(NET_CAPABILITY_NOT_METERED),
+                    false /* defaultNetwork, templates should have DEFAULT_NETWORK_ALL */);
+        }
+
+        private long getRemainingDailyBudget(long limitBytes,
+                Range<ZonedDateTime> cycle) {
+            final long start = cycle.getLower().toInstant().toEpochMilli();
+            final long end = cycle.getUpper().toInstant().toEpochMilli();
+            final long totalBytes = getNetworkTotalBytes(start, end);
+            final long remainingBytes = totalBytes == -1 ? 0 : Math.max(0, limitBytes - totalBytes);
+            // 1 + ((end - now - 1) / millisInDay with integers is equivalent to:
+            // ceil((double)(end - now) / millisInDay)
+            final long remainingDays =
+                    1 + ((end - mClock.millis() - 1) / TimeUnit.DAYS.toMillis(1));
+
+            return remainingBytes / Math.max(1, remainingDays);
+        }
+
+        private long getUserPolicyOpportunisticQuotaBytes() {
+            // Keep the most restrictive applicable policy
+            long minQuota = Long.MAX_VALUE;
+            final NetworkIdentity identity = getTemplateMatchingNetworkIdentity(
+                    mNetworkCapabilities);
+
+            final NetworkPolicy[] policies = mNPM.getNetworkPolicies();
+            for (NetworkPolicy policy : policies) {
+                if (policy.hasCycle() && policy.template.matches(identity)) {
+                    final long cycleStart = policy.cycleIterator().next().getLower()
+                            .toInstant().toEpochMilli();
+                    // Prefer user-defined warning, otherwise use hard limit
+                    final long activeWarning = getActiveWarning(policy, cycleStart);
+                    final long policyBytes = (activeWarning == WARNING_DISABLED)
+                            ? getActiveLimit(policy, cycleStart)
+                            : activeWarning;
+
+                    if (policyBytes != LIMIT_DISABLED && policyBytes != WARNING_DISABLED) {
+                        final long policyBudget = getRemainingDailyBudget(policyBytes,
+                                policy.cycleIterator().next());
+                        minQuota = Math.min(minQuota, policyBudget);
+                    }
+                }
+            }
+
+            if (minQuota == Long.MAX_VALUE) {
+                return OPPORTUNISTIC_QUOTA_UNKNOWN;
+            }
+
+            return minQuota / OPQUOTA_USER_SETTING_DIVIDER;
+        }
+
         void updateMultipathBudget() {
             long quota = LocalServices.getService(NetworkPolicyManagerInternal.class)
                     .getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH);
             if (DBG) Slog.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes");
 
-            if (quota == NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN) {
-                // STOPSHIP: replace this with a configurable mechanism.
-                quota = DEFAULT_DAILY_MULTIPATH_QUOTA;
+            // Fallback to user settings-based quota if not available from phone plan
+            if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
+                quota = getUserPolicyOpportunisticQuotaBytes();
+                if (DBG) Slog.d(TAG, "Opportunistic quota from user policy: " + quota + " bytes");
+            }
+
+            if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
+                quota = getDefaultDailyMultipathQuotaBytes();
                 if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes");
             }
 
+            // TODO: re-register if day changed: budget may have run out but should be refreshed.
             if (haveMultipathBudget() && quota == mQuota) {
-                // If we already have a usage callback pending , there's no need to re-register it
+                // If there is already a usage callback pending , there's no need to re-register it
                 // if the quota hasn't changed. The callback will simply fire as expected when the
-                // budget is spent. Also: if we re-register the callback when we're below the
-                // UsageCallback's minimum value of 2MB, we'll overshoot the budget.
+                // budget is spent.
                 if (DBG) Slog.d(TAG, "Quota still " + quota + ", not updating.");
                 return;
             }
@@ -212,7 +348,17 @@
             // ourselves any budget to work with.
             final long usage = getDailyNonDefaultDataUsage();
             final long budget = (usage == -1) ? 0 : Math.max(0, quota - usage);
-            if (budget > 0) {
+
+            // Only consider budgets greater than MIN_THRESHOLD_BYTES, otherwise the callback will
+            // fire late, after data usage went over budget. Also budget should be 0 if remaining
+            // data is close to 0.
+            // This is necessary because the usage callback does not accept smaller thresholds.
+            // Because it snaps everything to MIN_THRESHOLD_BYTES, the lesser of the two evils is
+            // to snap to 0 here.
+            // This will only be called if the total quota for the day changed, not if usage changed
+            // since last time, so even if this is called very often the budget will not snap to 0
+            // as soon as there are less than 2MB left for today.
+            if (budget > NetworkStatsManager.MIN_THRESHOLD_BYTES) {
                 if (DBG) Slog.d(TAG, "Setting callback for " + budget +
                         " bytes on network " + network);
                 registerUsageCallback(budget);
@@ -262,11 +408,38 @@
         }
     }
 
+    private static long getActiveWarning(NetworkPolicy policy, long cycleStart) {
+        return policy.lastWarningSnooze < cycleStart
+                ? policy.warningBytes
+                : WARNING_DISABLED;
+    }
+
+    private static long getActiveLimit(NetworkPolicy policy, long cycleStart) {
+        return policy.lastLimitSnooze < cycleStart
+                ? policy.limitBytes
+                : LIMIT_DISABLED;
+    }
+
     // Only ever updated on the handler thread. Accessed from other binder threads to retrieve
     // the tracker for a specific network.
     private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers =
             new ConcurrentHashMap<>();
 
+    private long getDefaultDailyMultipathQuotaBytes() {
+        final String setting = Settings.Global.getString(mContext.getContentResolver(),
+                NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
+        if (setting != null) {
+            try {
+                return Long.parseLong(setting);
+            } catch(NumberFormatException e) {
+                // fall through
+            }
+        }
+
+        return mContext.getResources().getInteger(
+                R.integer.config_networkDefaultDailyMultipathQuotaBytes);
+    }
+
     // TODO: this races with app code that might respond to onAvailable() by immediately calling
     // getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly
     // invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its
@@ -281,6 +454,7 @@
             public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
                 MultipathTracker existing = mMultipathTrackers.get(network);
                 if (existing != null) {
+                    existing.setNetworkCapabilities(nc);
                     existing.updateMultipathBudget();
                     return;
                 }
@@ -307,6 +481,15 @@
         mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler);
     }
 
+    /**
+     * Update multipath budgets for all trackers. To be called on the mHandler thread.
+     */
+    private void updateAllMultipathBudgets() {
+        for (MultipathTracker t : mMultipathTrackers.values()) {
+            t.updateMultipathBudget();
+        }
+    }
+
     private void maybeUnregisterTrackMobileCallback() {
         if (mMobileNetworkCallback != null) {
             mCM.unregisterNetworkCallback(mMobileNetworkCallback);
@@ -319,11 +502,7 @@
             @Override
             public void onMeteredIfacesChanged(String[] meteredIfaces) {
                 // Dispatched every time opportunistic quota is recalculated.
-                mHandler.post(() -> {
-                    for (MultipathTracker t : mMultipathTrackers.values()) {
-                        t.updateMultipathBudget();
-                    }
-                });
+                mHandler.post(() -> updateAllMultipathBudgets());
             }
         };
         mNPM.registerListener(mPolicyListener);
@@ -333,6 +512,35 @@
         mNPM.unregisterListener(mPolicyListener);
     }
 
+    private final class SettingsObserver extends ContentObserver {
+        public SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Slog.wtf(TAG, "Should never be reached.");
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (!Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES)
+                    .equals(uri)) {
+                Slog.wtf(TAG, "Unexpected settings observation: " + uri);
+            }
+            if (DBG) Slog.d(TAG, "Settings change: updating budgets.");
+            updateAllMultipathBudgets();
+        }
+    }
+
+    private final class ConfigChangeReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DBG) Slog.d(TAG, "Configuration change: updating budgets.");
+            updateAllMultipathBudgets();
+        }
+    }
+
     public void dump(IndentingPrintWriter pw) {
         // Do not use in production. Access to class data is only safe on the handler thrad.
         pw.println("MultipathPolicyTracker:");
diff --git a/com/android/server/connectivity/NetworkMonitor.java b/com/android/server/connectivity/NetworkMonitor.java
index 8a2e71c..2845383 100644
--- a/com/android/server/connectivity/NetworkMonitor.java
+++ b/com/android/server/connectivity/NetworkMonitor.java
@@ -34,6 +34,7 @@
 import android.net.ProxyInfo;
 import android.net.TrafficStats;
 import android.net.Uri;
+import android.net.dns.ResolvUtil;
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
 import android.net.metrics.ValidationProbeEvent;
@@ -64,6 +65,7 @@
 import com.android.internal.util.Protocol;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
@@ -77,6 +79,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Random;
+import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -165,7 +168,7 @@
      * Force evaluation even if it has succeeded in the past.
      * arg1 = UID responsible for requesting this reeval.  Will be billed for data.
      */
-    public static final int CMD_FORCE_REEVALUATION = BASE + 8;
+    private static final int CMD_FORCE_REEVALUATION = BASE + 8;
 
     /**
      * Message to self indicating captive portal app finished.
@@ -205,9 +208,15 @@
      * Private DNS. If a DNS resolution is required, e.g. for DNS-over-TLS in
      * strict mode, then an event is sent back to ConnectivityService with the
      * result of the resolution attempt.
+     *
+     * A separate message is used to trigger (re)evaluation of the Private DNS
+     * configuration, so that the message can be handled as needed in different
+     * states, including being ignored until after an ongoing captive portal
+     * validation phase is completed.
      */
     private static final int CMD_PRIVATE_DNS_SETTINGS_CHANGED = BASE + 13;
     public static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = BASE + 14;
+    private static final int CMD_EVALUATE_PRIVATE_DNS = BASE + 15;
 
     // Start mReevaluateDelayMs at this value and double.
     private static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
@@ -215,6 +224,7 @@
     // Before network has been evaluated this many times, ignore repeated reevaluate requests.
     private static final int IGNORE_REEVALUATE_ATTEMPTS = 5;
     private int mReevaluateToken = 0;
+    private static final int NO_UID = 0;
     private static final int INVALID_UID = -1;
     private int mUidResponsibleForReeval = INVALID_UID;
     // Stop blaming UID that requested re-evaluation after this many attempts.
@@ -224,6 +234,8 @@
 
     private static final int NUM_VALIDATION_LOG_LINES = 20;
 
+    private String mPrivateDnsProviderHostname = "";
+
     public static boolean isValidationRequired(
             NetworkCapabilities dfltNetCap, NetworkCapabilities nc) {
         // TODO: Consider requiring validation for DUN networks.
@@ -261,13 +273,12 @@
 
     public boolean systemReady = false;
 
-    private DnsManager.PrivateDnsConfig mPrivateDnsCfg = null;
-
     private final State mDefaultState = new DefaultState();
     private final State mValidatedState = new ValidatedState();
     private final State mMaybeNotifyState = new MaybeNotifyState();
     private final State mEvaluatingState = new EvaluatingState();
     private final State mCaptivePortalState = new CaptivePortalState();
+    private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState();
 
     private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null;
 
@@ -293,6 +304,10 @@
         // Add suffix indicating which NetworkMonitor we're talking about.
         super(TAG + networkAgentInfo.name());
 
+        // Logs with a tag of the form given just above, e.g.
+        //     <timestamp>   862  2402 D NetworkMonitor/NetworkAgentInfo [WIFI () - 100]: ...
+        setDbg(VDBG);
+
         mContext = context;
         mMetricsLog = logger;
         mConnectivityServiceHandler = handler;
@@ -305,10 +320,11 @@
         mDefaultRequest = defaultRequest;
 
         addState(mDefaultState);
-        addState(mValidatedState, mDefaultState);
         addState(mMaybeNotifyState, mDefaultState);
             addState(mEvaluatingState, mMaybeNotifyState);
             addState(mCaptivePortalState, mMaybeNotifyState);
+        addState(mEvaluatingPrivateDnsState, mDefaultState);
+        addState(mValidatedState, mDefaultState);
         setInitialState(mDefaultState);
 
         mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
@@ -321,6 +337,17 @@
         start();
     }
 
+    public void forceReevaluation(int responsibleUid) {
+        sendMessage(CMD_FORCE_REEVALUATION, responsibleUid, 0);
+    }
+
+    public void notifyPrivateDnsSettingsChanged(PrivateDnsConfig newCfg) {
+        // Cancel any outstanding resolutions.
+        removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
+        // Send the update to the proper thread.
+        sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
+    }
+
     @Override
     protected void log(String s) {
         if (DBG) Log.d(TAG + "/" + mNetworkAgentInfo.name(), s);
@@ -349,6 +376,12 @@
                 mDefaultRequest.networkCapabilities, mNetworkAgentInfo.networkCapabilities);
     }
 
+
+    private void notifyNetworkTestResultInvalid(Object obj) {
+        mConnectivityServiceHandler.sendMessage(obtainMessage(
+                EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, obj));
+    }
+
     // DefaultState is the parent of all States.  It exists only to handle CMD_* messages but
     // does not entail any real state (hence no enter() or exit() routines).
     private class DefaultState extends State {
@@ -392,41 +425,66 @@
 
                     switch (message.arg1) {
                         case APP_RETURN_DISMISSED:
-                            sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0);
+                            sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0);
                             break;
                         case APP_RETURN_WANTED_AS_IS:
                             mDontDisplaySigninNotification = true;
                             // TODO: Distinguish this from a network that actually validates.
-                            // Displaying the "!" on the system UI icon may still be a good idea.
-                            transitionTo(mValidatedState);
+                            // Displaying the "x" on the system UI icon may still be a good idea.
+                            transitionTo(mEvaluatingPrivateDnsState);
                             break;
                         case APP_RETURN_UNWANTED:
                             mDontDisplaySigninNotification = true;
                             mUserDoesNotWant = true;
-                            mConnectivityServiceHandler.sendMessage(obtainMessage(
-                                    EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID,
-                                    mNetId, null));
+                            notifyNetworkTestResultInvalid(null);
                             // TODO: Should teardown network.
                             mUidResponsibleForReeval = 0;
                             transitionTo(mEvaluatingState);
                             break;
                     }
                     return HANDLED;
-                case CMD_PRIVATE_DNS_SETTINGS_CHANGED:
-                    if (isValidationRequired()) {
-                        // This performs a blocking DNS resolution of the
-                        // strict mode hostname, if required.
-                        resolvePrivateDnsConfig((DnsManager.PrivateDnsConfig) message.obj);
-                        if ((mPrivateDnsCfg != null) && mPrivateDnsCfg.inStrictMode()) {
-                            mConnectivityServiceHandler.sendMessage(obtainMessage(
-                                    EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0, mNetId,
-                                    new DnsManager.PrivateDnsConfig(mPrivateDnsCfg)));
-                        }
+                case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
+                    final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
+                    if (!isValidationRequired() || cfg == null || !cfg.inStrictMode()) {
+                        // No DNS resolution required.
+                        //
+                        // We don't force any validation in opportunistic mode
+                        // here. Opportunistic mode nameservers are validated
+                        // separately within netd.
+                        //
+                        // Reset Private DNS settings state.
+                        mPrivateDnsProviderHostname = "";
+                        break;
                     }
-                    return HANDLED;
+
+                    mPrivateDnsProviderHostname = cfg.hostname;
+
+                    // DNS resolutions via Private DNS strict mode block for a
+                    // few seconds (~4.2) checking for any IP addresses to
+                    // arrive and validate. Initiating a (re)evaluation now
+                    // should not significantly alter the validation outcome.
+                    //
+                    // No matter what: enqueue a validation request; one of
+                    // three things can happen with this request:
+                    //     [1] ignored (EvaluatingState or CaptivePortalState)
+                    //     [2] transition to EvaluatingPrivateDnsState
+                    //         (DefaultState and ValidatedState)
+                    //     [3] handled (EvaluatingPrivateDnsState)
+                    //
+                    // The Private DNS configuration to be evaluated will:
+                    //     [1] be skipped (not in strict mode), or
+                    //     [2] validate (huzzah), or
+                    //     [3] encounter some problem (invalid hostname,
+                    //         no resolved IP addresses, IPs unreachable,
+                    //         port 853 unreachable, port 853 is not running a
+                    //         DNS-over-TLS server, et cetera).
+                    sendMessage(CMD_EVALUATE_PRIVATE_DNS);
+                    break;
+                }
                 default:
-                    return HANDLED;
+                    break;
             }
+            return HANDLED;
         }
     }
 
@@ -440,7 +498,7 @@
             maybeLogEvaluationResult(
                     networkEventType(validationStage(), EvaluationResult.VALIDATED));
             mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
-                    NETWORK_TEST_RESULT_VALID, mNetId, mPrivateDnsCfg));
+                    NETWORK_TEST_RESULT_VALID, mNetId, null));
             mValidations++;
         }
 
@@ -449,10 +507,14 @@
             switch (message.what) {
                 case CMD_NETWORK_CONNECTED:
                     transitionTo(mValidatedState);
-                    return HANDLED;
+                    break;
+                case CMD_EVALUATE_PRIVATE_DNS:
+                    transitionTo(mEvaluatingPrivateDnsState);
+                    break;
                 default:
                     return NOT_HANDLED;
             }
+            return HANDLED;
         }
     }
 
@@ -569,11 +631,11 @@
                 case CMD_REEVALUATE:
                     if (message.arg1 != mReevaluateToken || mUserDoesNotWant)
                         return HANDLED;
-                    // Don't bother validating networks that don't satisify the default request.
+                    // Don't bother validating networks that don't satisfy the default request.
                     // This includes:
                     //  - VPNs which can be considered explicitly desired by the user and the
                     //    user's desire trumps whether the network validates.
-                    //  - Networks that don't provide internet access.  It's unclear how to
+                    //  - Networks that don't provide Internet access.  It's unclear how to
                     //    validate such networks.
                     //  - Untrusted networks.  It's unsafe to prompt the user to sign-in to
                     //    such networks and the user didn't express interest in connecting to
@@ -588,7 +650,6 @@
                     //    expensive metered network, or unwanted leaking of the User Agent string.
                     if (!isValidationRequired()) {
                         validationLog("Network would not satisfy default request, not validating");
-                        mPrivateDnsCfg = null;
                         transitionTo(mValidatedState);
                         return HANDLED;
                     }
@@ -601,20 +662,18 @@
                     // if this is found to cause problems.
                     CaptivePortalProbeResult probeResult = isCaptivePortal();
                     if (probeResult.isSuccessful()) {
-                        resolvePrivateDnsConfig();
-                        transitionTo(mValidatedState);
+                        // Transit EvaluatingPrivateDnsState to get to Validated
+                        // state (even if no Private DNS validation required).
+                        transitionTo(mEvaluatingPrivateDnsState);
                     } else if (probeResult.isPortal()) {
-                        mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
-                                NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.redirectUrl));
+                        notifyNetworkTestResultInvalid(probeResult.redirectUrl);
                         mLastPortalProbeResult = probeResult;
                         transitionTo(mCaptivePortalState);
                     } else {
                         final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
                         sendMessageDelayed(msg, mReevaluateDelayMs);
                         logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
-                        mConnectivityServiceHandler.sendMessage(obtainMessage(
-                                EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId,
-                                probeResult.redirectUrl));
+                        notifyNetworkTestResultInvalid(probeResult.redirectUrl);
                         if (mAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
                             // Don't continue to blame UID forever.
                             TrafficStats.clearThreadStatsUid();
@@ -700,6 +759,110 @@
         }
     }
 
+    private class EvaluatingPrivateDnsState extends State {
+        private int mPrivateDnsReevalDelayMs;
+        private PrivateDnsConfig mPrivateDnsConfig;
+
+        @Override
+        public void enter() {
+            mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
+            mPrivateDnsConfig = null;
+            sendMessage(CMD_EVALUATE_PRIVATE_DNS);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_EVALUATE_PRIVATE_DNS:
+                    if (inStrictMode()) {
+                        if (!isStrictModeHostnameResolved()) {
+                            resolveStrictModeHostname();
+
+                            if (isStrictModeHostnameResolved()) {
+                                notifyPrivateDnsConfigResolved();
+                            } else {
+                                handlePrivateDnsEvaluationFailure();
+                                break;
+                            }
+                        }
+
+                        // Look up a one-time hostname, to bypass caching.
+                        //
+                        // Note that this will race with ConnectivityService
+                        // code programming the DNS-over-TLS server IP addresses
+                        // into netd (if invoked, above). If netd doesn't know
+                        // the IP addresses yet, or if the connections to the IP
+                        // addresses haven't yet been validated, netd will block
+                        // for up to a few seconds before failing the lookup.
+                        if (!sendPrivateDnsProbe()) {
+                            handlePrivateDnsEvaluationFailure();
+                            break;
+                        }
+                    }
+
+                    // All good!
+                    transitionTo(mValidatedState);
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+
+        private boolean inStrictMode() {
+            return !TextUtils.isEmpty(mPrivateDnsProviderHostname);
+        }
+
+        private boolean isStrictModeHostnameResolved() {
+            return (mPrivateDnsConfig != null) &&
+                   mPrivateDnsConfig.hostname.equals(mPrivateDnsProviderHostname) &&
+                   (mPrivateDnsConfig.ips.length > 0);
+        }
+
+        private void resolveStrictModeHostname() {
+            try {
+                // Do a blocking DNS resolution using the network-assigned nameservers.
+                mPrivateDnsConfig = new PrivateDnsConfig(
+                        mPrivateDnsProviderHostname,
+                        mNetwork.getAllByName(mPrivateDnsProviderHostname));
+            } catch (UnknownHostException uhe) {
+                mPrivateDnsConfig = null;
+            }
+        }
+
+        private void notifyPrivateDnsConfigResolved() {
+            mConnectivityServiceHandler.sendMessage(obtainMessage(
+                    EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0, mNetId, mPrivateDnsConfig));
+        }
+
+        private void handlePrivateDnsEvaluationFailure() {
+            notifyNetworkTestResultInvalid(null);
+
+            // Queue up a re-evaluation with backoff.
+            //
+            // TODO: Consider abandoning this state after a few attempts and
+            // transitioning back to EvaluatingState, to perhaps give ourselves
+            // the opportunity to (re)detect a captive portal or something.
+            sendMessageDelayed(CMD_EVALUATE_PRIVATE_DNS, mPrivateDnsReevalDelayMs);
+            mPrivateDnsReevalDelayMs *= 2;
+            if (mPrivateDnsReevalDelayMs > MAX_REEVALUATE_DELAY_MS) {
+                mPrivateDnsReevalDelayMs = MAX_REEVALUATE_DELAY_MS;
+            }
+        }
+
+        private boolean sendPrivateDnsProbe() {
+            // q.v. system/netd/server/dns/DnsTlsTransport.cpp
+            final String ONE_TIME_HOSTNAME_SUFFIX = "-dnsotls-ds.metric.gstatic.com";
+            final String host = UUID.randomUUID().toString().substring(0, 8) +
+                    ONE_TIME_HOSTNAME_SUFFIX;
+            try {
+                final InetAddress[] ips = mNetworkAgentInfo.network().getAllByName(host);
+                return (ips != null && ips.length > 0);
+            } catch (UnknownHostException uhe) {}
+            return false;
+        }
+    }
+
     // Limits the list of IP addresses returned by getAllByName or tried by openConnection to at
     // most one per address family. This ensures we only wait up to 20 seconds for TCP connections
     // to complete, regardless of how many IP addresses a host has.
@@ -710,7 +873,9 @@
 
         @Override
         public InetAddress[] getAllByName(String host) throws UnknownHostException {
-            List<InetAddress> addrs = Arrays.asList(super.getAllByName(host));
+            // Always bypass Private DNS.
+            final List<InetAddress> addrs = Arrays.asList(
+                    ResolvUtil.blockingResolveAllLocally(this, host));
 
             // Ensure the address family of the first address is tried first.
             LinkedHashMap<Class, InetAddress> addressByFamily = new LinkedHashMap<>();
@@ -1065,44 +1230,6 @@
         return null;
     }
 
-    public void notifyPrivateDnsSettingsChanged(DnsManager.PrivateDnsConfig newCfg) {
-        // Cancel any outstanding resolutions.
-        removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
-        // Send the update to the proper thread.
-        sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
-    }
-
-    private void resolvePrivateDnsConfig() {
-        resolvePrivateDnsConfig(DnsManager.getPrivateDnsConfig(mContext.getContentResolver()));
-    }
-
-    private void resolvePrivateDnsConfig(DnsManager.PrivateDnsConfig cfg) {
-        // Nothing to do.
-        if (cfg == null) {
-            mPrivateDnsCfg = null;
-            return;
-        }
-
-        // No DNS resolution required.
-        if (!cfg.inStrictMode()) {
-            mPrivateDnsCfg = cfg;
-            return;
-        }
-
-        if ((mPrivateDnsCfg != null) && mPrivateDnsCfg.inStrictMode() &&
-                (mPrivateDnsCfg.ips.length > 0) && mPrivateDnsCfg.hostname.equals(cfg.hostname)) {
-            // We have already resolved this strict mode hostname. Assume that
-            // Private DNS services won't be changing serving IP addresses very
-            // frequently and save ourselves one re-resolve.
-            return;
-        }
-
-        mPrivateDnsCfg = cfg;
-        final DnsManager.PrivateDnsConfig resolvedCfg = DnsManager.tryBlockingResolveOf(
-                mNetwork, mPrivateDnsCfg.hostname);
-        if (resolvedCfg != null) mPrivateDnsCfg = resolvedCfg;
-    }
-
     /**
      * @param responseReceived - whether or not we received a valid HTTP response to our request.
      * If false, isCaptivePortal and responseTimestampMs are ignored
diff --git a/com/android/server/connectivity/Tethering.java b/com/android/server/connectivity/Tethering.java
index d37dd18..df6a6f8 100644
--- a/com/android/server/connectivity/Tethering.java
+++ b/com/android/server/connectivity/Tethering.java
@@ -249,6 +249,7 @@
                 "CarrierConfigChangeListener", mContext, smHandler, filter,
                 (Intent ignored) -> {
                     mLog.log("OBSERVED carrier config change");
+                    updateConfiguration();
                     reevaluateSimCardProvisioning();
                 });
         // TODO: Remove SimChangeListener altogether. For now, we retain it
@@ -261,28 +262,35 @@
                 });
 
         mStateReceiver = new StateReceiver();
-        filter = new IntentFilter();
+
+        // Load tethering configuration.
+        updateConfiguration();
+
+        startStateMachineUpdaters();
+    }
+
+    private void startStateMachineUpdaters() {
+        mCarrierConfigChange.startListening();
+
+        final Handler handler = mTetherMasterSM.getHandler();
+        IntentFilter filter = new IntentFilter();
         filter.addAction(UsbManager.ACTION_USB_STATE);
         filter.addAction(CONNECTIVITY_ACTION);
         filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
         filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
-        mContext.registerReceiver(mStateReceiver, filter, null, smHandler);
+        mContext.registerReceiver(mStateReceiver, filter, null, handler);
 
         filter = new IntentFilter();
         filter.addAction(Intent.ACTION_MEDIA_SHARED);
         filter.addAction(Intent.ACTION_MEDIA_UNSHARED);
         filter.addDataScheme("file");
-        mContext.registerReceiver(mStateReceiver, filter, null, smHandler);
+        mContext.registerReceiver(mStateReceiver, filter, null, handler);
 
-        UserManagerInternal userManager = LocalServices.getService(UserManagerInternal.class);
-
-        // this check is useful only for some unit tests; example: ConnectivityServiceTest
-        if (userManager != null) {
-            userManager.addUserRestrictionsListener(new TetheringUserRestrictionListener(this));
+        final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+        // This check is useful only for some unit tests; example: ConnectivityServiceTest.
+        if (umi != null) {
+            umi.addUserRestrictionsListener(new TetheringUserRestrictionListener(this));
         }
-
-        // load device config info
-        updateConfiguration();
     }
 
     private WifiManager getWifiManager() {
@@ -384,17 +392,15 @@
      */
     @VisibleForTesting
     protected boolean isTetherProvisioningRequired() {
-        String[] provisionApp = mContext.getResources().getStringArray(
-                com.android.internal.R.array.config_mobile_hotspot_provision_app);
+        final TetheringConfiguration cfg = mConfig;
         if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
-                || provisionApp == null) {
+                || cfg.provisioningApp.length == 0) {
             return false;
         }
-
         if (carrierConfigAffirmsEntitlementCheckNotRequired()) {
             return false;
         }
-        return (provisionApp.length == 2);
+        return (cfg.provisioningApp.length == 2);
     }
 
     // The logic here is aimed solely at confirming that a CarrierConfig exists
@@ -417,20 +423,6 @@
         return !isEntitlementCheckRequired;
     }
 
-    // Used by the SIM card change observation code.
-    // TODO: De-duplicate above code.
-    private boolean hasMobileHotspotProvisionApp() {
-        try {
-            if (!mContext.getResources().getString(com.android.internal.R.string.
-                    config_mobile_hotspot_provision_app_no_ui).isEmpty()) {
-                Log.d(TAG, "re-evaluate provisioning");
-                return true;
-            }
-        } catch (Resources.NotFoundException e) {}
-        Log.d(TAG, "no prov-check needed for new SIM");
-        return false;
-    }
-
     /**
      * Enables or disables tethering for the given type. This should only be called once
      * provisioning has succeeded or is not necessary. It will also schedule provisioning rechecks
@@ -1187,7 +1179,7 @@
     }
 
     private void reevaluateSimCardProvisioning() {
-        if (!hasMobileHotspotProvisionApp()) return;
+        if (!mConfig.hasMobileHotspotProvisionApp()) return;
         if (carrierConfigAffirmsEntitlementCheckNotRequired()) return;
 
         ArrayList<Integer> tethered = new ArrayList<>();
@@ -1546,7 +1538,6 @@
                     return;
                 }
 
-                mCarrierConfigChange.startListening();
                 mSimChange.startListening();
                 mUpstreamNetworkMonitor.start();
 
@@ -1564,7 +1555,6 @@
                 mOffload.stop();
                 mUpstreamNetworkMonitor.stop();
                 mSimChange.stopListening();
-                mCarrierConfigChange.stopListening();
                 notifyDownstreamsOfNewUpstreamIface(null);
                 handleNewUpstreamNetworkState(null);
             }
diff --git a/com/android/server/connectivity/tethering/TetheringConfiguration.java b/com/android/server/connectivity/tethering/TetheringConfiguration.java
index 09bce7f..454c579 100644
--- a/com/android/server/connectivity/tethering/TetheringConfiguration.java
+++ b/com/android/server/connectivity/tethering/TetheringConfiguration.java
@@ -21,14 +21,23 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static com.android.internal.R.array.config_mobile_hotspot_provision_app;
+import static com.android.internal.R.array.config_tether_bluetooth_regexs;
+import static com.android.internal.R.array.config_tether_dhcp_range;
+import static com.android.internal.R.array.config_tether_usb_regexs;
+import static com.android.internal.R.array.config_tether_upstream_types;
+import static com.android.internal.R.array.config_tether_wifi_regexs;
+import static com.android.internal.R.string.config_mobile_hotspot_provision_app_no_ui;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
-import android.telephony.TelephonyManager;
 import android.net.util.SharedLog;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.R;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -51,6 +60,8 @@
 public class TetheringConfiguration {
     private static final String TAG = TetheringConfiguration.class.getSimpleName();
 
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
     @VisibleForTesting
     public static final int DUN_NOT_REQUIRED = 0;
     public static final int DUN_REQUIRED = 1;
@@ -79,18 +90,18 @@
     public final String[] dhcpRanges;
     public final String[] defaultIPv4DNS;
 
+    public final String[] provisioningApp;
+    public final String provisioningAppNoUi;
+
     public TetheringConfiguration(Context ctx, SharedLog log) {
         final SharedLog configLog = log.forSubComponent("config");
 
-        tetherableUsbRegexs = ctx.getResources().getStringArray(
-                com.android.internal.R.array.config_tether_usb_regexs);
+        tetherableUsbRegexs = getResourceStringArray(ctx, config_tether_usb_regexs);
         // TODO: Evaluate deleting this altogether now that Wi-Fi always passes
         // us an interface name. Careful consideration needs to be given to
         // implications for Settings and for provisioning checks.
-        tetherableWifiRegexs = ctx.getResources().getStringArray(
-                com.android.internal.R.array.config_tether_wifi_regexs);
-        tetherableBluetoothRegexs = ctx.getResources().getStringArray(
-                com.android.internal.R.array.config_tether_bluetooth_regexs);
+        tetherableWifiRegexs = getResourceStringArray(ctx, config_tether_wifi_regexs);
+        tetherableBluetoothRegexs = getResourceStringArray(ctx, config_tether_bluetooth_regexs);
 
         dunCheck = checkDunRequired(ctx);
         configLog.log("DUN check returned: " + dunCheckString(dunCheck));
@@ -101,6 +112,9 @@
         dhcpRanges = getDhcpRanges(ctx);
         defaultIPv4DNS = copy(DEFAULT_IPV4_DNS);
 
+        provisioningApp = getResourceStringArray(ctx, config_mobile_hotspot_provision_app);
+        provisioningAppNoUi = getProvisioningAppNoUi(ctx);
+
         configLog.log(toString());
     }
 
@@ -116,6 +130,10 @@
         return matchesDownstreamRegexs(iface, tetherableBluetoothRegexs);
     }
 
+    public boolean hasMobileHotspotProvisionApp() {
+        return !TextUtils.isEmpty(provisioningAppNoUi);
+    }
+
     public void dump(PrintWriter pw) {
         dumpStringArray(pw, "tetherableUsbRegexs", tetherableUsbRegexs);
         dumpStringArray(pw, "tetherableWifiRegexs", tetherableWifiRegexs);
@@ -129,6 +147,10 @@
 
         dumpStringArray(pw, "dhcpRanges", dhcpRanges);
         dumpStringArray(pw, "defaultIPv4DNS", defaultIPv4DNS);
+
+        dumpStringArray(pw, "provisioningApp", provisioningApp);
+        pw.print("provisioningAppNoUi: ");
+        pw.println(provisioningAppNoUi);
     }
 
     public String toString() {
@@ -140,6 +162,8 @@
         sj.add(String.format("isDunRequired:%s", isDunRequired));
         sj.add(String.format("preferredUpstreamIfaceTypes:%s",
                 makeString(preferredUpstreamNames(preferredUpstreamIfaceTypes))));
+        sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
+        sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
         return String.format("TetheringConfiguration{%s}", sj.toString());
     }
 
@@ -159,6 +183,7 @@
     }
 
     private static String makeString(String[] strings) {
+        if (strings == null) return "null";
         final StringJoiner sj = new StringJoiner(",", "[", "]");
         for (String s : strings) sj.add(s);
         return sj.toString();
@@ -195,8 +220,7 @@
     }
 
     private static Collection<Integer> getUpstreamIfaceTypes(Context ctx, int dunCheck) {
-        final int ifaceTypes[] = ctx.getResources().getIntArray(
-                com.android.internal.R.array.config_tether_upstream_types);
+        final int ifaceTypes[] = ctx.getResources().getIntArray(config_tether_upstream_types);
         final ArrayList<Integer> upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length);
         for (int i : ifaceTypes) {
             switch (i) {
@@ -247,14 +271,30 @@
     }
 
     private static String[] getDhcpRanges(Context ctx) {
-        final String[] fromResource = ctx.getResources().getStringArray(
-                com.android.internal.R.array.config_tether_dhcp_range);
+        final String[] fromResource = getResourceStringArray(ctx, config_tether_dhcp_range);
         if ((fromResource.length > 0) && (fromResource.length % 2 == 0)) {
             return fromResource;
         }
         return copy(DHCP_DEFAULT_RANGE);
     }
 
+    private static String getProvisioningAppNoUi(Context ctx) {
+        try {
+            return ctx.getResources().getString(config_mobile_hotspot_provision_app_no_ui);
+        } catch (Resources.NotFoundException e) {
+            return "";
+        }
+    }
+
+    private static String[] getResourceStringArray(Context ctx, int resId) {
+        try {
+            final String[] strArray = ctx.getResources().getStringArray(resId);
+            return (strArray != null) ? strArray : EMPTY_STRING_ARRAY;
+        } catch (Resources.NotFoundException e404) {
+            return EMPTY_STRING_ARRAY;
+        }
+    }
+
     private static String[] copy(String[] strarray) {
         return Arrays.copyOf(strarray, strarray.length);
     }
diff --git a/com/android/server/content/ContentService.java b/com/android/server/content/ContentService.java
index b7bbb3b..e3d0bdd 100644
--- a/com/android/server/content/ContentService.java
+++ b/com/android/server/content/ContentService.java
@@ -79,7 +79,7 @@
  */
 public final class ContentService extends IContentService.Stub {
     static final String TAG = "ContentService";
-    static final boolean DEBUG = true;
+    static final boolean DEBUG = false;
 
     public static class Lifecycle extends SystemService {
         private ContentService mService;
diff --git a/com/android/server/devicepolicy/ClockworkDevicePolicyManagerWrapperService.java b/com/android/server/devicepolicy/ClockworkDevicePolicyManagerWrapperService.java
new file mode 100644
index 0000000..3d3d0a9
--- /dev/null
+++ b/com/android/server/devicepolicy/ClockworkDevicePolicyManagerWrapperService.java
@@ -0,0 +1,1397 @@
+package com.android.server.devicepolicy;
+
+import static android.app.admin.DevicePolicyManager.CODE_DEVICE_ADMIN_NOT_SUPPORTED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.IApplicationThread;
+import android.app.IServiceConnection;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.NetworkEvent;
+import android.app.admin.PasswordMetrics;
+import android.app.admin.SystemUpdateInfo;
+import android.app.admin.SystemUpdatePolicy;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.StringParceledListSlice;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A thin wrapper around {@link DevicePolicyManagerService} for granual enabling and disabling of
+ * management functionality on Wear.
+ */
+public class ClockworkDevicePolicyManagerWrapperService extends BaseIDevicePolicyManager {
+
+    private static final String NOT_SUPPORTED_MESSAGE = "The operation is not supported on Wear.";
+
+    private DevicePolicyManagerService mDpmsDelegate;
+
+    /**
+     * If true, throw {@link UnsupportedOperationException} when unsupported setter methods are
+     * called. Otherwise make the unsupported methods no-op.
+     *
+     * It should be normally set to false. Enable throwing of the exception when needed for debug
+     * purposes.
+     */
+    private final boolean mThrowUnsupportedException;
+
+    public ClockworkDevicePolicyManagerWrapperService(Context context) {
+        this(context, false);
+    }
+
+    public ClockworkDevicePolicyManagerWrapperService(
+            Context context, boolean throwUnsupportedException) {
+        mDpmsDelegate = new DevicePolicyManagerService(new ClockworkInjector(context));
+
+        if (Build.TYPE.equals("userdebug") || Build.TYPE.equals("eng")) {
+            mThrowUnsupportedException = true;
+        } else {
+            mThrowUnsupportedException = throwUnsupportedException;
+        }
+    }
+
+    static class ClockworkInjector extends DevicePolicyManagerService.Injector {
+
+        ClockworkInjector(Context context) {
+            super(context);
+        }
+
+        @Override
+        public boolean hasFeature() {
+            return getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+        }
+    }
+
+    @Override
+    void systemReady(int phase) {
+        mDpmsDelegate.systemReady(phase);
+    }
+
+    @Override
+    void handleStartUser(int userId) {
+        mDpmsDelegate.handleStartUser(userId);
+    }
+
+    @Override
+    void handleUnlockUser(int userId) {
+        mDpmsDelegate.handleUnlockUser(userId);
+    }
+
+    @Override
+    void handleStopUser(int userId) {
+        mDpmsDelegate.handleStopUser(userId);
+    }
+
+    @Override
+    public void setPasswordQuality(ComponentName who, int quality, boolean parent) {
+        mDpmsDelegate.setPasswordQuality(who, quality, parent);
+    }
+
+    @Override
+    public int getPasswordQuality(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordQuality(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordMinimumLength(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordMinimumLength(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordMinimumLength(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordMinimumLength(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordMinimumUpperCase(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordMinimumUpperCase(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordMinimumUpperCase(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordMinimumUpperCase(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordMinimumLowerCase(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordMinimumLowerCase(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordMinimumLowerCase(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordMinimumLowerCase(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordMinimumLetters(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordMinimumLetters(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordMinimumLetters(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordMinimumLetters(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordMinimumNumeric(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordMinimumNumeric(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordMinimumNumeric(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordMinimumNumeric(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordMinimumSymbols(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordMinimumSymbols(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordMinimumSymbols(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordMinimumSymbols(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordMinimumNonLetter(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordMinimumNonLetter(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordMinimumNonLetter(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordMinimumNonLetter(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordHistoryLength(ComponentName who, int length, boolean parent) {
+        mDpmsDelegate.setPasswordHistoryLength(who, length, parent);
+    }
+
+    @Override
+    public int getPasswordHistoryLength(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordHistoryLength(who, userHandle, parent);
+    }
+
+    @Override
+    public void setPasswordExpirationTimeout(ComponentName who, long timeout, boolean parent) {
+        mDpmsDelegate.setPasswordExpirationTimeout(who, timeout, parent);
+    }
+
+    @Override
+    public long getPasswordExpirationTimeout(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordExpirationTimeout(who, userHandle, parent);
+    }
+
+    @Override
+    public long getPasswordExpiration(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getPasswordExpiration(who, userHandle, parent);
+    }
+
+    @Override
+    public boolean isActivePasswordSufficient(int userHandle, boolean parent) {
+        return mDpmsDelegate.isActivePasswordSufficient(userHandle, parent);
+    }
+
+    @Override
+    public boolean isProfileActivePasswordSufficientForParent(int userHandle) {
+        return false;
+    }
+
+    @Override
+    public int getCurrentFailedPasswordAttempts(int userHandle, boolean parent) {
+        return mDpmsDelegate.getCurrentFailedPasswordAttempts(userHandle, parent);
+    }
+
+    @Override
+    public int getProfileWithMinimumFailedPasswordsForWipe(int userHandle, boolean parent) {
+        return UserHandle.USER_NULL;
+    }
+
+    @Override
+    public void setMaximumFailedPasswordsForWipe(ComponentName who, int num, boolean parent) {
+        mDpmsDelegate.setMaximumFailedPasswordsForWipe(who, num, parent);
+    }
+
+    @Override
+    public int getMaximumFailedPasswordsForWipe(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getMaximumFailedPasswordsForWipe(who, userHandle, parent);
+    }
+
+    @Override
+    public boolean resetPassword(String passwordOrNull, int flags) throws RemoteException {
+        return mDpmsDelegate.resetPassword(passwordOrNull, flags);
+    }
+
+    @Override
+    public void setMaximumTimeToLock(ComponentName who, long timeMs, boolean parent) {
+        mDpmsDelegate.setMaximumTimeToLock(who, timeMs, parent);
+    }
+
+    @Override
+    public long getMaximumTimeToLock(ComponentName who, int userHandle, boolean parent) {
+        return mDpmsDelegate.getMaximumTimeToLock(who, userHandle, parent);
+    }
+
+    @Override
+    public void setRequiredStrongAuthTimeout(ComponentName who, long timeoutMs, boolean parent) {
+        mDpmsDelegate.setRequiredStrongAuthTimeout(who, timeoutMs, parent);
+    }
+
+    @Override
+    public long getRequiredStrongAuthTimeout(ComponentName who, int userId, boolean parent) {
+        return mDpmsDelegate.getRequiredStrongAuthTimeout(who, userId, parent);
+    }
+
+    @Override
+    public void lockNow(int flags, boolean parent) {
+        mDpmsDelegate.lockNow(flags, parent);
+    }
+
+    @Override
+    public ComponentName setGlobalProxy(ComponentName who, String proxySpec, String exclusionList) {
+        maybeThrowUnsupportedOperationException();
+        return null;
+    }
+
+    @Override
+    public ComponentName getGlobalProxyAdmin(int userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return null;
+    }
+
+    @Override
+    public void setRecommendedGlobalProxy(ComponentName who, ProxyInfo proxyInfo) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public int setStorageEncryption(ComponentName who, boolean encrypt) {
+        maybeThrowUnsupportedOperationException();
+        return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED;
+    }
+
+    @Override
+    public boolean getStorageEncryption(ComponentName who, int userHandle) {
+        return false;
+    }
+
+    @Override
+    public int getStorageEncryptionStatus(String callerPackage, int userHandle) {
+        // Ok to return current status even though setting encryption is not supported in Wear.
+        return mDpmsDelegate.getStorageEncryptionStatus(callerPackage, userHandle);
+    }
+
+    @Override
+    public boolean requestBugreport(ComponentName who) {
+        return mDpmsDelegate.requestBugreport(who);
+    }
+
+    @Override
+    public void setCameraDisabled(ComponentName who, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getCameraDisabled(ComponentName who, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public void setScreenCaptureDisabled(ComponentName who, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getScreenCaptureDisabled(ComponentName who, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public void setKeyguardDisabledFeatures(ComponentName who, int which, boolean parent) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public int getKeyguardDisabledFeatures(ComponentName who, int userHandle, boolean parent) {
+        return 0;
+    }
+
+    @Override
+    public void setActiveAdmin(ComponentName adminReceiver, boolean refreshing, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isAdminActive(ComponentName adminReceiver, int userHandle) {
+        return false;
+    }
+
+    @Override
+    public List<ComponentName> getActiveAdmins(int userHandle) {
+        return null;
+    }
+
+    @Override
+    public boolean packageHasActiveAdmins(String packageName, int userHandle) {
+        return false;
+    }
+
+    @Override
+    public void getRemoveWarning(ComponentName comp, RemoteCallback result, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void removeActiveAdmin(ComponentName adminReceiver, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void forceRemoveActiveAdmin(ComponentName adminReceiver, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasGrantedPolicy(ComponentName adminReceiver, int policyId, int userHandle) {
+        return false;
+    }
+
+    @Override
+    public void setActivePasswordState(PasswordMetrics metrics, int userHandle) {
+        mDpmsDelegate.setActivePasswordState(metrics, userHandle);
+    }
+
+    @Override
+    public void reportPasswordChanged(@UserIdInt int userId) {
+        mDpmsDelegate.reportPasswordChanged(userId);
+    }
+
+    @Override
+    public void reportFailedPasswordAttempt(int userHandle) {
+        mDpmsDelegate.reportFailedPasswordAttempt(userHandle);
+    }
+
+    @Override
+    public void reportSuccessfulPasswordAttempt(int userHandle) {
+        mDpmsDelegate.reportSuccessfulPasswordAttempt(userHandle);
+    }
+
+    @Override
+    public void reportFailedFingerprintAttempt(int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void reportSuccessfulFingerprintAttempt(int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void reportKeyguardDismissed(int userHandle) {
+        mDpmsDelegate.reportKeyguardDismissed(userHandle);
+    }
+
+    @Override
+    public void reportKeyguardSecured(int userHandle) {
+        mDpmsDelegate.reportKeyguardSecured(userHandle);
+    }
+
+    @Override
+    public boolean setDeviceOwner(ComponentName admin, String ownerName, int userId) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean hasDeviceOwner() {
+        return false;
+    }
+
+    @Override
+    public ComponentName getDeviceOwnerComponent(boolean callingUserOnly) {
+        return null;
+    }
+
+    @Override
+    public String getDeviceOwnerName() {
+        return null;
+    }
+
+    @Override
+    public void clearDeviceOwner(String packageName) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public int getDeviceOwnerUserId() {
+        return UserHandle.USER_NULL;
+    }
+
+    @Override
+    public boolean setProfileOwner(ComponentName who, String ownerName, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public ComponentName getProfileOwner(int userHandle) {
+        return null;
+    }
+
+    @Override
+    public String getProfileOwnerName(int userHandle) {
+        return null;
+    }
+
+    @Override
+    public void setProfileEnabled(ComponentName who) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setProfileName(ComponentName who, String profileName) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void clearProfileOwner(ComponentName who) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasUserSetupCompleted() {
+        return mDpmsDelegate.hasUserSetupCompleted();
+    }
+
+    @Override
+    public void setDeviceOwnerLockScreenInfo(ComponentName who, CharSequence info) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public CharSequence getDeviceOwnerLockScreenInfo() {
+        return null;
+    }
+
+    @Override
+    public String[] setPackagesSuspended(
+            ComponentName who, String callerPackage, String[] packageNames, boolean suspended) {
+        maybeThrowUnsupportedOperationException();
+        return packageNames;
+    }
+
+    @Override
+    public boolean isPackageSuspended(ComponentName who, String callerPackage, String packageName) {
+        return false;
+    }
+
+    @Override
+    public boolean installCaCert(ComponentName admin, String callerPackage, byte[] certBuffer)
+            throws RemoteException {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public void uninstallCaCerts(ComponentName admin, String callerPackage, String[] aliases) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void enforceCanManageCaCerts(ComponentName who, String callerPackage) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean approveCaCert(String alias, int userId, boolean appproval) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean isCaCertApproved(String alias, int userId) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean installKeyPair(ComponentName who, String callerPackage, byte[] privKey,
+            byte[] cert, byte[] chain, String alias, boolean requestAccess,
+            boolean isUserSelectable) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean removeKeyPair(ComponentName who, String callerPackage, String alias) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public void choosePrivateKeyAlias(int uid, Uri uri, String alias, IBinder response) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setDelegatedScopes(ComponentName who, String delegatePackage,
+            List<String> scopes) throws SecurityException {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    @NonNull
+    public List<String> getDelegatedScopes(ComponentName who, String delegatePackage)
+            throws SecurityException {
+        return Collections.EMPTY_LIST;
+    }
+
+    @NonNull
+    public List<String> getDelegatePackages(ComponentName who, String scope)
+            throws SecurityException {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public void setCertInstallerPackage(ComponentName who, String installerPackage) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public String getCertInstallerPackage(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public boolean setAlwaysOnVpnPackage(ComponentName admin, String vpnPackage, boolean lockdown) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public String getAlwaysOnVpnPackage(ComponentName admin) {
+        return null;
+    }
+
+    @Override
+    public void wipeDataWithReason(int flags, String wipeReasonForUser) {
+        mDpmsDelegate.wipeDataWithReason(flags, wipeReasonForUser);
+    }
+
+
+    @Override
+    public void addPersistentPreferredActivity(
+            ComponentName who, IntentFilter filter, ComponentName activity) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void clearPackagePersistentPreferredActivities(ComponentName who, String packageName) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setApplicationRestrictions(ComponentName who, String callerPackage,
+            String packageName, Bundle settings) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public Bundle getApplicationRestrictions(ComponentName who, String callerPackage,
+            String packageName) {
+        return null;
+    }
+
+    @Override
+    public boolean setApplicationRestrictionsManagingPackage(
+            ComponentName admin, String packageName) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public String getApplicationRestrictionsManagingPackage(ComponentName admin) {
+        return null;
+    }
+
+    @Override
+    public boolean isCallerApplicationRestrictionsManagingPackage(String callerPackage) {
+        return false;
+    }
+
+    @Override
+    public void setRestrictionsProvider(ComponentName who, ComponentName permissionProvider) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public ComponentName getRestrictionsProvider(int userHandle) {
+        return null;
+    }
+
+    @Override
+    public void setUserRestriction(ComponentName who, String key, boolean enabledFromThisOwner) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public Bundle getUserRestrictions(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public void addCrossProfileIntentFilter(ComponentName who, IntentFilter filter, int flags) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void clearCrossProfileIntentFilters(ComponentName who) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setPermittedCrossProfileNotificationListeners(
+            ComponentName who, List<String> packageList) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public List<String> getPermittedCrossProfileNotificationListeners(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public boolean isNotificationListenerServicePermitted(String packageName, int userId) {
+        return true;
+    }
+
+    @Override
+    public void setCrossProfileCallerIdDisabled(ComponentName who, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getCrossProfileCallerIdDisabled(ComponentName who) {
+        return false;
+    }
+
+    @Override
+    public boolean getCrossProfileCallerIdDisabledForUser(int userId) {
+        return false;
+    }
+
+    @Override
+    public void setCrossProfileContactsSearchDisabled(ComponentName who, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getCrossProfileContactsSearchDisabled(ComponentName who) {
+        return false;
+    }
+
+    @Override
+    public boolean getCrossProfileContactsSearchDisabledForUser(int userId) {
+        return false;
+    }
+
+    @Override
+    public boolean addCrossProfileWidgetProvider(ComponentName admin, String packageName) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean removeCrossProfileWidgetProvider(ComponentName admin, String packageName) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public List<String> getCrossProfileWidgetProviders(ComponentName admin) {
+        return null;
+    }
+
+    @Override
+    public boolean setPermittedAccessibilityServices(ComponentName who, List packageList) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public List getPermittedAccessibilityServices(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public List getPermittedAccessibilityServicesForUser(int userId) {
+        return null;
+    }
+
+    @Override
+    public boolean isAccessibilityServicePermittedByAdmin(
+            ComponentName who, String packageName, int userHandle) {
+        return true;
+    }
+
+    @Override
+    public boolean setPermittedInputMethods(ComponentName who, List packageList) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public List getPermittedInputMethods(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public List getPermittedInputMethodsForCurrentUser() {
+        return null;
+    }
+
+    @Override
+    public boolean isInputMethodPermittedByAdmin(
+            ComponentName who, String packageName, int userHandle) {
+        return true;
+    }
+
+    @Override
+    public boolean setApplicationHidden(ComponentName who, String callerPackage, String packageName,
+            boolean hidden) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean isApplicationHidden(ComponentName who, String callerPackage,
+            String packageName) {
+        return false;
+    }
+
+    @Override
+    public UserHandle createAndManageUser(ComponentName admin, String name,
+            ComponentName profileOwner, PersistableBundle adminExtras, int flags) {
+        maybeThrowUnsupportedOperationException();
+        return null;
+    }
+
+    @Override
+    public boolean removeUser(ComponentName who, UserHandle userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean switchUser(ComponentName who, UserHandle userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public int startUserInBackground(ComponentName who, UserHandle userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return UserManager.USER_OPERATION_ERROR_UNKNOWN;
+    }
+
+    @Override
+    public int stopUser(ComponentName who, UserHandle userHandle) {
+        maybeThrowUnsupportedOperationException();
+        return UserManager.USER_OPERATION_ERROR_UNKNOWN;
+    }
+
+    @Override
+    public int logoutUser(ComponentName who) {
+        maybeThrowUnsupportedOperationException();
+        return UserManager.USER_OPERATION_ERROR_UNKNOWN;
+    }
+
+    @Override
+    public List<UserHandle> getSecondaryUsers(ComponentName who) {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public boolean isEphemeralUser(ComponentName who) {
+        return false;
+    }
+
+    @Override
+    public void enableSystemApp(ComponentName who, String callerPackage, String packageName) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public int enableSystemAppWithIntent(ComponentName who, String callerPackage, Intent intent) {
+        maybeThrowUnsupportedOperationException();
+        return 0;
+    }
+
+    @Override
+    public boolean installExistingPackage(ComponentName who, String callerPackage,
+            String packageName) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public void setAccountManagementDisabled(
+            ComponentName who, String accountType, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getAccountTypesWithManagementDisabled() {
+        return null;
+    }
+
+    @Override
+    public String[] getAccountTypesWithManagementDisabledAsUser(int userId) {
+        return null;
+    }
+
+    @Override
+    public void setLockTaskPackages(ComponentName who, String[] packages) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getLockTaskPackages(ComponentName who) {
+        return new String[0];
+    }
+
+    @Override
+    public boolean isLockTaskPermitted(String pkg) {
+        return false;
+    }
+
+    public void setLockTaskFeatures(ComponentName admin, int flags) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    public int getLockTaskFeatures(ComponentName admin) {
+        return 0;
+    }
+
+    @Override
+    public void setGlobalSetting(ComponentName who, String setting, String value) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setTime(ComponentName who, long millis) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean setTimeZone(ComponentName who, String timeZone) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public void setSecureSetting(ComponentName who, String setting, String value) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setMasterVolumeMuted(ComponentName who, boolean on) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isMasterVolumeMuted(ComponentName who) {
+        return false;
+    }
+
+    @Override
+    public void notifyLockTaskModeChanged(boolean isEnabled, String pkg, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setUninstallBlocked(ComponentName who, String callerPackage, String packageName,
+            boolean uninstallBlocked) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isUninstallBlocked(ComponentName who, String packageName) {
+        return false;
+    }
+
+    @Override
+    public void startManagedQuickContact(String actualLookupKey, long actualContactId,
+            boolean isContactIdIgnored, long actualDirectoryId, Intent originalIntent) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setBluetoothContactSharingDisabled(ComponentName who, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getBluetoothContactSharingDisabled(ComponentName who) {
+        return false;
+    }
+
+    @Override
+    public boolean getBluetoothContactSharingDisabledForUser(int userId) {
+        return false;
+    }
+
+    @Override
+    public void setTrustAgentConfiguration(ComponentName admin, ComponentName agent,
+            PersistableBundle args, boolean parent) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public List<PersistableBundle> getTrustAgentConfiguration(ComponentName admin,
+            ComponentName agent, int userHandle, boolean parent) {
+        return new ArrayList<PersistableBundle>();
+    }
+
+    @Override
+    public void setAutoTimeRequired(ComponentName who, boolean required) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getAutoTimeRequired() {
+        return false;
+    }
+
+    @Override
+    public void setForceEphemeralUsers(ComponentName who, boolean forceEphemeralUsers) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getForceEphemeralUsers(ComponentName who) {
+        return false;
+    }
+
+    @Override
+    public boolean isRemovingAdmin(ComponentName adminReceiver, int userHandle) {
+        return false;
+    }
+
+    @Override
+    public void setUserIcon(ComponentName who, Bitmap icon) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public Intent createAdminSupportIntent(String restriction) {
+        return null;
+    }
+
+    @Override
+    public void setSystemUpdatePolicy(ComponentName who, SystemUpdatePolicy policy) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public SystemUpdatePolicy getSystemUpdatePolicy() {
+        return null;
+    }
+
+    @Override
+    public boolean setKeyguardDisabled(ComponentName who, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean setStatusBarDisabled(ComponentName who, boolean disabled) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean getDoNotAskCredentialsOnBoot() {
+        return false;
+    }
+
+    @Override
+    public void notifyPendingSystemUpdate(@Nullable SystemUpdateInfo info) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public SystemUpdateInfo getPendingSystemUpdate(ComponentName admin) {
+        maybeThrowUnsupportedOperationException();
+        return null;
+    }
+
+    @Override
+    public void setPermissionPolicy(ComponentName admin, String callerPackage, int policy)
+            throws RemoteException {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public int getPermissionPolicy(ComponentName admin) throws RemoteException {
+        return mDpmsDelegate.getPermissionPolicy(admin);
+    }
+
+    @Override
+    public boolean setPermissionGrantState(ComponentName admin, String callerPackage,
+            String packageName, String permission, int grantState) throws RemoteException {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public int getPermissionGrantState(ComponentName admin, String callerPackage,
+            String packageName, String permission) throws RemoteException {
+        return mDpmsDelegate.getPermissionGrantState(admin, callerPackage, packageName, permission);
+    }
+
+    @Override
+    public boolean isProvisioningAllowed(String action, String packageName) {
+        return false;
+    }
+
+    @Override
+    public int checkProvisioningPreCondition(String action, String packageName) {
+        return CODE_DEVICE_ADMIN_NOT_SUPPORTED;
+    }
+
+    @Override
+    public void setKeepUninstalledPackages(
+            ComponentName who, String callerPackage, List<String> packageList) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getKeepUninstalledPackages(ComponentName who, String callerPackage) {
+        return null;
+    }
+
+    @Override
+    public boolean isManagedProfile(ComponentName admin) {
+        return mDpmsDelegate.isManagedProfile(admin);
+    }
+
+    @Override
+    public boolean isSystemOnlyUser(ComponentName admin) {
+        return mDpmsDelegate.isSystemOnlyUser(admin);
+    }
+
+    @Override
+    public String getWifiMacAddress(ComponentName admin) {
+        return mDpmsDelegate.getWifiMacAddress(admin);
+    }
+
+    @Override
+    public void reboot(ComponentName admin) {
+        mDpmsDelegate.reboot(admin);
+    }
+
+    @Override
+    public void setShortSupportMessage(ComponentName who, CharSequence message) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public CharSequence getShortSupportMessage(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public void setLongSupportMessage(ComponentName who, CharSequence message) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public CharSequence getLongSupportMessage(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public CharSequence getShortSupportMessageForUser(ComponentName who, int userHandle) {
+        return null;
+    }
+
+    @Override
+    public CharSequence getLongSupportMessageForUser(ComponentName who, int userHandle) {
+        return null;
+    }
+
+    @Override
+    public boolean isSeparateProfileChallengeAllowed(int userHandle) {
+        return false;
+    }
+
+    @Override
+    public void setOrganizationColor(ComponentName who, int color) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setOrganizationColorForUser(int color, int userId) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public int getOrganizationColor(ComponentName who) {
+        return Color.parseColor("#00796B");
+    }
+
+    @Override
+    public int getOrganizationColorForUser(int userHandle) {
+        return Color.parseColor("#00796B");
+    }
+
+    @Override
+    public void setOrganizationName(ComponentName who, CharSequence text) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public CharSequence getOrganizationName(ComponentName who) {
+        return null;
+    }
+
+    @Override
+    public CharSequence getDeviceOwnerOrganizationName() {
+        return null;
+    }
+
+    @Override
+    public CharSequence getOrganizationNameForUser(int userHandle) {
+        return null;
+    }
+
+    @Override
+    public int getUserProvisioningState() {
+        return DevicePolicyManager.STATE_USER_UNMANAGED;
+    }
+
+    @Override
+    public void setUserProvisioningState(int newState, int userHandle) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setAffiliationIds(ComponentName admin, List<String> ids) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getAffiliationIds(ComponentName admin) {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public boolean isAffiliatedUser() {
+        return false;
+    }
+
+    @Override
+    public void setSecurityLoggingEnabled(ComponentName admin, boolean enabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isSecurityLoggingEnabled(ComponentName admin) {
+        return false;
+    }
+
+    @Override
+    public ParceledListSlice retrieveSecurityLogs(ComponentName admin) {
+        return null;
+    }
+
+    @Override
+    public ParceledListSlice retrievePreRebootSecurityLogs(ComponentName admin) {
+        return null;
+    }
+
+    @Override
+    public boolean isUninstallInQueue(String packageName) {
+        return false;
+    }
+
+    @Override
+    public void uninstallPackageWithActiveAdmins(String packageName) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isDeviceProvisioned() {
+        return mDpmsDelegate.isDeviceProvisioned();
+    }
+
+    @Override
+    public boolean isDeviceProvisioningConfigApplied() {
+        return false;
+    }
+
+    @Override
+    public void setDeviceProvisioningConfigApplied() {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void forceUpdateUserSetupComplete() {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public void setBackupServiceEnabled(ComponentName admin, boolean enabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isBackupServiceEnabled(ComponentName admin) {
+        return false;
+    }
+
+    @Override
+    public boolean bindDeviceAdminServiceAsUser(
+            @NonNull ComponentName admin, @NonNull IApplicationThread caller,
+            @Nullable IBinder activtityToken, @NonNull Intent serviceIntent,
+            @NonNull IServiceConnection connection, int flags, @UserIdInt int targetUserId) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public @NonNull List<UserHandle> getBindDeviceAdminTargetUsers(@NonNull ComponentName admin) {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public void setNetworkLoggingEnabled(ComponentName admin, boolean enabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isNetworkLoggingEnabled(ComponentName admin) {
+        return false;
+    }
+
+    @Override
+    public List<NetworkEvent> retrieveNetworkLogs(ComponentName admin, long batchToken) {
+        return null;
+    }
+
+    @Override
+    public long getLastSecurityLogRetrievalTime() {
+        return -1;
+    }
+
+    @Override
+    public long getLastBugReportRequestTime() {
+        return -1;
+    }
+
+    @Override
+    public long getLastNetworkLogRetrievalTime() {
+        return -1;
+    }
+
+    @Override
+    public boolean setResetPasswordToken(ComponentName admin, byte[] token) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean clearResetPasswordToken(ComponentName admin) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean isResetPasswordTokenActive(ComponentName admin) {
+        return false;
+    }
+
+    @Override
+    public boolean resetPasswordWithToken(ComponentName admin, String passwordOrNull, byte[] token,
+            int flags) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean isCurrentInputMethodSetByOwner() {
+        return false;
+    }
+
+    @Override
+    public StringParceledListSlice getOwnerInstalledCaCerts(@NonNull UserHandle user) {
+        return new StringParceledListSlice(new ArrayList<>(new ArraySet<>()));
+    }
+
+    @Override
+    public void clearApplicationUserData(ComponentName admin, String packageName,
+            IPackageDataObserver callback) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public synchronized void setLogoutEnabled(ComponentName admin, boolean enabled) {
+        maybeThrowUnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isLogoutEnabled() {
+        return false;
+    }
+
+    @Override
+    public List<String> getDisallowedSystemApps(ComponentName admin, int userId,
+            String provisioningAction) throws RemoteException {
+        return null;
+    }
+
+    @Override
+    public boolean setMandatoryBackupTransport(
+            ComponentName admin, ComponentName backupTransportComponent) {
+        maybeThrowUnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public ComponentName getMandatoryBackupTransport() {
+        return null;
+    }
+
+    private void maybeThrowUnsupportedOperationException() throws UnsupportedOperationException {
+        if (mThrowUnsupportedException) {
+            throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE);
+        }
+    }
+}
diff --git a/com/android/server/devicepolicy/DevicePolicyManagerService.java b/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 90e8a9c..e07b89f 100644
--- a/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -108,7 +108,6 @@
 import android.app.admin.SecurityLog.SecurityEvent;
 import android.app.admin.SystemUpdateInfo;
 import android.app.admin.SystemUpdatePolicy;
-import android.app.backup.BackupManager;
 import android.app.backup.IBackupManager;
 import android.app.backup.ISelectBackupTransportCallback;
 import android.app.trust.TrustManager;
@@ -220,6 +219,8 @@
 import com.android.internal.util.XmlUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.LocalServices;
+import com.android.server.LockGuard;
+import com.android.internal.util.StatLogger;
 import com.android.server.SystemServerInitThreadPool;
 import com.android.server.SystemService;
 import com.android.server.devicepolicy.DevicePolicyManagerService.ActiveAdmin.TrustAgentInfo;
@@ -462,21 +463,60 @@
      * Whether or not device admin feature is supported. If it isn't return defaults for all
      * public methods.
      */
-    boolean mHasFeature;
+    final boolean mHasFeature;
 
     /**
      * Whether or not this device is a watch.
      */
-    boolean mIsWatch;
+    final boolean mIsWatch;
 
     private final CertificateMonitor mCertificateMonitor;
     private final SecurityLogMonitor mSecurityLogMonitor;
+
+    @GuardedBy("getLockObject()")
     private NetworkLogger mNetworkLogger;
 
     private final AtomicBoolean mRemoteBugreportServiceIsActive = new AtomicBoolean();
     private final AtomicBoolean mRemoteBugreportSharingAccepted = new AtomicBoolean();
 
-    private SetupContentObserver mSetupContentObserver;
+    private final SetupContentObserver mSetupContentObserver;
+
+    private static boolean ENABLE_LOCK_GUARD = Build.IS_ENG
+            || (SystemProperties.getInt("debug.dpm.lock_guard", 0) == 1);
+
+    interface Stats {
+        int LOCK_GUARD_GUARD = 0;
+
+        int COUNT = LOCK_GUARD_GUARD + 1;
+    }
+
+    private final StatLogger mStatLogger = new StatLogger(new String[] {
+            "LockGuard.guard()",
+    });
+
+    private final Object mLockDoNoUseDirectly = LockGuard.installNewLock(
+            LockGuard.INDEX_DPMS, /* doWtf=*/ true);
+
+    final Object getLockObject() {
+        if (ENABLE_LOCK_GUARD) {
+            final long start = mStatLogger.getTime();
+            LockGuard.guard(LockGuard.INDEX_DPMS);
+            mStatLogger.logDurationStat(Stats.LOCK_GUARD_GUARD, start);
+        }
+        return mLockDoNoUseDirectly;
+    }
+
+    /**
+     * Check if the current thread holds the DPMS lock, and if not, do a WTF.
+     *
+     * (Doing this check too much may be costly, so don't call it in a hot path.)
+     */
+    final void ensureLocked() {
+        if (Thread.holdsLock(mLockDoNoUseDirectly)) {
+            return;
+        }
+        Slog.wtfStack(LOG_TAG, "Not holding DPMS lock.");
+    }
 
     @VisibleForTesting
     final TransferOwnershipMetadataManager mTransferOwnershipMetadataManager;
@@ -627,8 +667,10 @@
         }
     }
 
+    @GuardedBy("getLockObject()")
     final SparseArray<DevicePolicyData> mUserData = new SparseArray<>();
-    @GuardedBy("DevicePolicyManagerService.this")
+
+    @GuardedBy("getLockObject()")
     final SparseArray<PasswordMetrics> mUserPasswordMetrics = new SparseArray<>();
 
     final Handler mHandler;
@@ -649,7 +691,7 @@
              */
             if (Intent.ACTION_USER_STARTED.equals(action)
                     && userHandle == mOwners.getDeviceOwnerUserId()) {
-                synchronized (DevicePolicyManagerService.this) {
+                synchronized (getLockObject()) {
                     if (isNetworkLoggingEnabledInternalLocked()) {
                         setNetworkLoggingActiveInternal(true);
                     }
@@ -684,14 +726,14 @@
 
             if (Intent.ACTION_USER_ADDED.equals(action)) {
                 sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_ADDED, userHandle);
-                synchronized (DevicePolicyManagerService.this) {
+                synchronized (getLockObject()) {
                     // It might take a while for the user to become affiliated. Make security
                     // and network logging unavailable in the meantime.
                     maybePauseDeviceWideLoggingLocked();
                 }
             } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
                 sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_REMOVED, userHandle);
-                synchronized (DevicePolicyManagerService.this) {
+                synchronized (getLockObject()) {
                     // Check whether the user is affiliated, *before* removing its data.
                     boolean isRemovedUserAffiliated = isUserAffiliatedWithDeviceLocked(userHandle);
                     removeUserData(userHandle);
@@ -705,7 +747,7 @@
                 }
             } else if (Intent.ACTION_USER_STARTED.equals(action)) {
                 sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STARTED, userHandle);
-                synchronized (DevicePolicyManagerService.this) {
+                synchronized (getLockObject()) {
                     maybeSendAdminEnabledBroadcastLocked(userHandle);
                     // Reset the policy data
                     mUserData.remove(userHandle);
@@ -716,7 +758,7 @@
             } else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
                 sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_SWITCHED, userHandle);
             } else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
-                synchronized (DevicePolicyManagerService.this) {
+                synchronized (getLockObject()) {
                     maybeSendAdminEnabledBroadcastLocked(userHandle);
                 }
             } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
@@ -741,7 +783,7 @@
         }
 
         private void sendDeviceOwnerUserCommand(String action, int userHandle) {
-            synchronized (DevicePolicyManagerService.this) {
+            synchronized (getLockObject()) {
                 ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
                 if (deviceOwner != null) {
                     Bundle extras = new Bundle();
@@ -1662,7 +1704,7 @@
                     + " for user " + userHandle);
         }
         DevicePolicyData policy = getUserData(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             for (int i = policy.mAdminList.size() - 1; i >= 0; i--) {
                 ActiveAdmin aa = policy.mAdminList.get(i);
                 try {
@@ -2091,6 +2133,7 @@
 
         if (!mHasFeature) {
             // Skip the rest of the initialization
+            mSetupContentObserver = null;
             return;
         }
 
@@ -2132,7 +2175,7 @@
      */
     @NonNull
     DevicePolicyData getUserData(int userHandle) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policy = mUserData.get(userHandle);
             if (policy == null) {
                 policy = new DevicePolicyData(userHandle);
@@ -2173,7 +2216,7 @@
     }
 
     void removeUserData(int userHandle) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (userHandle == UserHandle.USER_SYSTEM) {
                 Slog.w(LOG_TAG, "Tried to remove device policy file for user 0! Ignoring.");
                 return;
@@ -2199,7 +2242,7 @@
     }
 
     void loadOwners() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             mOwners.load();
             setDeviceOwnerSystemPropertyLocked();
             findOwnerComponentIfNecessaryLocked();
@@ -2223,7 +2266,7 @@
 
     /** Apply default restrictions that haven't been applied to profile owners yet. */
     private void maybeSetDefaultProfileOwnerUserRestrictions() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             for (final int userId : mOwners.getProfileOwnerKeys()) {
                 final ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userId);
                 // The following restrictions used to be applied to managed profiles by different
@@ -2513,6 +2556,7 @@
     }
 
     ActiveAdmin getActiveAdminUncheckedLocked(ComponentName who, int userHandle) {
+        ensureLocked();
         ActiveAdmin admin = getUserData(userHandle).mAdminMap.get(who);
         if (admin != null
                 && who.getPackageName().equals(admin.info.getActivityInfo().packageName)
@@ -2523,6 +2567,7 @@
     }
 
     ActiveAdmin getActiveAdminUncheckedLocked(ComponentName who, int userHandle, boolean parent) {
+        ensureLocked();
         if (parent) {
             enforceManagedProfile(userHandle, "call APIs on the parent profile");
         }
@@ -2535,6 +2580,7 @@
 
     ActiveAdmin getActiveAdminForCallerLocked(ComponentName who, int reqPolicy)
             throws SecurityException {
+        ensureLocked();
         final int callingUid = mInjector.binderGetCallingUid();
 
         ActiveAdmin result = getActiveAdminWithPolicyForUidLocked(who, reqPolicy, callingUid);
@@ -2565,6 +2611,7 @@
 
     ActiveAdmin getActiveAdminForCallerLocked(ComponentName who, int reqPolicy, boolean parent)
             throws SecurityException {
+        ensureLocked();
         if (parent) {
             enforceManagedProfile(mInjector.userHandleGetCallingUserId(),
                     "call APIs on the parent profile");
@@ -2577,6 +2624,7 @@
      * the admin's uid matches the uid.
      */
     private ActiveAdmin getActiveAdminForUidLocked(ComponentName who, int uid) {
+        ensureLocked();
         final int userId = UserHandle.getUserId(uid);
         final DevicePolicyData policy = getUserData(userId);
         ActiveAdmin admin = policy.mAdminMap.get(who);
@@ -2591,6 +2639,7 @@
 
     private ActiveAdmin getActiveAdminWithPolicyForUidLocked(ComponentName who, int reqPolicy,
             int uid) {
+        ensureLocked();
         // Try to find an admin which can use reqPolicy
         final int userId = UserHandle.getUserId(uid);
         final DevicePolicyData policy = getUserData(userId);
@@ -2620,6 +2669,7 @@
     @VisibleForTesting
     boolean isActiveAdminWithPolicyForUserLocked(ActiveAdmin admin, int reqPolicy,
             int userId) {
+        ensureLocked();
         final boolean ownsDevice = isDeviceOwner(admin.info.getComponent(), userId);
         final boolean ownsProfile = isProfileOwner(admin.info.getComponent(), userId);
 
@@ -3319,14 +3369,14 @@
         updateUserSetupCompleteAndPaired();
 
         List<String> packageList;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             packageList = getKeepUninstalledPackagesLocked();
         }
         if (packageList != null) {
             mInjector.getPackageManagerInternal().setKeepUninstalledPackages(packageList);
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             if (deviceOwner != null) {
                 // Push the force-ephemeral-users policy to the user manager.
@@ -3379,7 +3429,7 @@
 
     private void ensureDeviceOwnerUserStarted() {
         final int userId;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!mOwners.hasDeviceOwner()) {
                 return;
             }
@@ -3436,7 +3486,7 @@
         // before reboot
         Set<Integer> usersWithProfileOwners;
         Set<Integer> usersWithData;
-        synchronized(this) {
+        synchronized (getLockObject()) {
             usersWithProfileOwners = mOwners.getProfileOwnerKeys();
             usersWithData = new ArraySet<>();
             for (int i = 0; i < mUserData.size(); i++) {
@@ -3460,7 +3510,7 @@
         final Bundle adminExtras = new Bundle();
         adminExtras.putParcelable(Intent.EXTRA_USER, UserHandle.of(userHandle));
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final long now = System.currentTimeMillis();
 
             List<ActiveAdmin> admins = getActiveAdminsForLockscreenPoliciesLocked(
@@ -3496,7 +3546,7 @@
         }
         enforceManageUsers();
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final DevicePolicyData policy = getUserData(userHandle.getIdentifier());
 
             boolean changed = false;
@@ -3515,7 +3565,7 @@
         if (!mHasFeature) {
             return Collections.<String> emptySet();
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final DevicePolicyData policy = getUserData(userHandle.getIdentifier());
             return policy.mAcceptedCaCertificates;
         }
@@ -3542,7 +3592,7 @@
         DevicePolicyData policy = getUserData(userHandle);
         DeviceAdminInfo info = findAdmin(adminReceiver, userHandle,
                 /* throwForMissingPermission= */ true);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             checkActiveAdminPrecondition(adminReceiver, info, policy);
             long ident = mInjector.binderClearCallingIdentity();
             try {
@@ -3592,7 +3642,7 @@
     }
 
     private void pushActiveAdminPackages() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final List<UserInfo> users = mUserManager.getUsers();
             for (int i = users.size() - 1; i >= 0; --i) {
                 final int userId = users.get(i).id;
@@ -3603,7 +3653,7 @@
     }
 
     private void pushAllMeteredRestrictedPackages() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final List<UserInfo> users = mUserManager.getUsers();
             for (int i = users.size() - 1; i >= 0; --i) {
                 final int userId = users.get(i).id;
@@ -3681,7 +3731,7 @@
             return false;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return getActiveAdminUncheckedLocked(adminReceiver, userHandle) != null;
         }
     }
@@ -3692,7 +3742,7 @@
             return false;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policyData = getUserData(userHandle);
             return policyData.mRemovingAdmins.contains(adminReceiver);
         }
@@ -3704,7 +3754,7 @@
             return false;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin administrator = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
             if (administrator == null) {
                 throw new SecurityException("No active admin " + adminReceiver);
@@ -3721,7 +3771,7 @@
         }
 
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policy = getUserData(userHandle);
             final int N = policy.mAdminList.size();
             if (N <= 0) {
@@ -3741,7 +3791,7 @@
             return false;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policy = getUserData(userHandle);
             final int N = policy.mAdminList.size();
             for (int i=0; i<N; i++) {
@@ -3762,7 +3812,7 @@
         enforceShell("forceRemoveActiveAdmin");
         long ident = mInjector.binderClearCallingIdentity();
         try {
-            synchronized (this)  {
+            synchronized (getLockObject()) {
                 if (!isAdminTestOnlyLocked(adminReceiver, userHandle)) {
                     throw new SecurityException("Attempt to remove non-test admin "
                             + adminReceiver + " " + userHandle);
@@ -3842,7 +3892,7 @@
         }
         enforceFullCrossUsersPermission(userHandle);
         enforceUserUnlocked(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
             if (admin == null) {
                 return;
@@ -3884,7 +3934,7 @@
         validateQualityConstant(quality);
 
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -3903,7 +3953,6 @@
      * be the correct one upon boot.
      * This should be called whenever the password or the admin policies have changed.
      */
-    @GuardedBy("DevicePolicyManagerService.this")
     private void updatePasswordValidityCheckpointLocked(int userHandle, boolean parent) {
         final int credentialOwner = getCredentialOwner(userHandle, parent);
         DevicePolicyData policy = getUserData(credentialOwner);
@@ -3926,7 +3975,7 @@
             return PASSWORD_QUALITY_UNSPECIFIED;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             int mode = PASSWORD_QUALITY_UNSPECIFIED;
 
             if (who != null) {
@@ -3998,7 +4047,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4023,7 +4072,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             if (ap.passwordHistoryLength != length) {
@@ -4052,7 +4101,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         Preconditions.checkArgumentNonnegative(timeout, "Timeout must be >= 0 ms");
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD, parent);
             // Calling this API automatically bumps the expiration date
@@ -4086,7 +4135,7 @@
             return 0L;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             long timeout = 0L;
 
             if (who != null) {
@@ -4114,7 +4163,7 @@
         final int userId = UserHandle.getCallingUserId();
         List<String> changedProviders = null;
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(admin,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (activeAdmin.crossProfileWidgetProviders == null) {
@@ -4141,7 +4190,7 @@
         final int userId = UserHandle.getCallingUserId();
         List<String> changedProviders = null;
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(admin,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (activeAdmin.crossProfileWidgetProviders == null
@@ -4165,7 +4214,7 @@
 
     @Override
     public List<String> getCrossProfileWidgetProviders(ComponentName admin) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(admin,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (activeAdmin.crossProfileWidgetProviders == null
@@ -4211,7 +4260,7 @@
             return 0L;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return getPasswordExpirationLocked(who, userHandle, parent);
         }
     }
@@ -4223,7 +4272,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4245,7 +4294,7 @@
     public void setPasswordMinimumLowerCase(ComponentName who, int length, boolean parent) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4270,7 +4319,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4295,7 +4344,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4320,7 +4369,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4345,7 +4394,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
             final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4372,7 +4421,7 @@
             return 0;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (who != null) {
                 final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
                 return admin != null ? getter.apply(admin) : 0;
@@ -4404,7 +4453,7 @@
         enforceFullCrossUsersPermission(userHandle);
         enforceUserUnlocked(userHandle, parent);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // This API can only be called by an active device admin,
             // so try to retrieve it to check that the caller is one.
             getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
@@ -4435,7 +4484,7 @@
         enforceFullCrossUsersPermission(userHandle);
         enforceManagedProfile(userHandle, "call APIs refering to the parent profile");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final int targetUser = getProfileParentId(userHandle);
             enforceUserUnlocked(targetUser, false);
             int credentialOwner = getCredentialOwner(userHandle, false);
@@ -4502,7 +4551,7 @@
     @Override
     public int getCurrentFailedPasswordAttempts(int userHandle, boolean parent) {
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!isCallerWithSystemUid()) {
                 // This API can only be called by an active device admin,
                 // so try to retrieve it to check that the caller is one.
@@ -4523,7 +4572,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // This API can only be called by an active device admin,
             // so try to retrieve it to check that the caller is one.
             getActiveAdminForCallerLocked(
@@ -4548,7 +4597,7 @@
             return 0;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = (who != null)
                     ? getActiveAdminUncheckedLocked(who, userHandle, parent)
                     : getAdminWithMinimumFailedPasswordsForWipeLocked(userHandle, parent);
@@ -4562,7 +4611,7 @@
             return UserHandle.USER_NULL;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getAdminWithMinimumFailedPasswordsForWipeLocked(
                     userHandle, parent);
             return admin != null ? admin.getUserHandle().getIdentifier() : UserHandle.USER_NULL;
@@ -4623,7 +4672,7 @@
 
     /* PO or DO could do an untrusted reset in certain conditions. */
     private boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // An active DO or PO might be able to fo an untrusted credential reset
             for (final ActiveAdmin admin : getUserData(userId).mAdminList) {
                 if (!isActiveAdminWithPolicyForUserLocked(admin,
@@ -4649,7 +4698,7 @@
             enforceNotManagedProfile(userHandle, "clear the active password");
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // If caller has PO (or DO) it can change the password, so see if that's the case first.
             ActiveAdmin admin = getActiveAdminWithPolicyForUidLocked(
                     null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, callingUid);
@@ -4719,7 +4768,7 @@
     private boolean resetPasswordInternal(String password, long tokenHandle, byte[] token,
             int flags, int callingUid, int userHandle) {
         int quality;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             quality = getPasswordQuality(null, userHandle, /* parent */ false);
             if (quality == DevicePolicyManager.PASSWORD_QUALITY_MANAGED) {
                 quality = PASSWORD_QUALITY_UNSPECIFIED;
@@ -4829,7 +4878,7 @@
                 mLockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW,
                         UserHandle.USER_ALL);
             }
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 int newOwner = requireEntry ? callingUid : -1;
                 if (policy.mPasswordOwner != newOwner) {
                     policy.mPasswordOwner = newOwner;
@@ -4852,7 +4901,7 @@
     }
 
     private void setDoNotAskCredentialsOnBoot() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
             if (!policyData.doNotAskCredentialsOnBoot) {
                 policyData.doNotAskCredentialsOnBoot = true;
@@ -4865,7 +4914,7 @@
     public boolean getDoNotAskCredentialsOnBoot() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.QUERY_DO_NOT_ASK_CREDENTIALS_ON_BOOT, null);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
             return policyData.doNotAskCredentialsOnBoot;
         }
@@ -4878,7 +4927,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_FORCE_LOCK, parent);
             if (ap.maximumTimeToUnlock != timeMs) {
@@ -4952,7 +5001,7 @@
             return 0;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (who != null) {
                 final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
                 return admin != null ? admin.maximumTimeToUnlock : 0;
@@ -4994,7 +5043,7 @@
         }
 
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent);
             if (ap.strongAuthUnlockTimeout != timeoutMs) {
@@ -5015,7 +5064,7 @@
             return DevicePolicyManager.DEFAULT_STRONG_AUTH_TIMEOUT_MS;
         }
         enforceFullCrossUsersPermission(userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (who != null) {
                 ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userId, parent);
                 return admin != null ? admin.strongAuthUnlockTimeout : 0;
@@ -5053,7 +5102,7 @@
         }
 
         final int callingUserId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // This API can only be called by an active device admin,
             // so try to retrieve it to check that the caller is one.
             final ActiveAdmin admin = getActiveAdminForCallerLocked(
@@ -5122,7 +5171,7 @@
     }
 
     private void enforceProfileOrDeviceOwner(ComponentName who) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
         }
     }
@@ -5130,7 +5179,7 @@
     @Override
     public boolean approveCaCert(String alias, int userId, boolean approval) {
         enforceManageUsers();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             Set<String> certs = getUserData(userId).mAcceptedCaCertificates;
             boolean changed = (approval ? certs.add(alias) : certs.remove(alias));
             if (!changed) {
@@ -5145,7 +5194,7 @@
     @Override
     public boolean isCaCertApproved(String alias, int userId) {
         enforceManageUsers();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return getUserData(userId).mAcceptedCaCertificates.contains(alias);
         }
     }
@@ -5157,7 +5206,7 @@
                 isSecure |= mLockPatternUtils.isSecure(getProfileParentId(userInfo.id));
             }
             if (!isSecure) {
-                synchronized (this) {
+                synchronized (getLockObject()) {
                     getUserData(userInfo.id).mAcceptedCaCertificates.clear();
                     saveSettingsLocked(userInfo.id);
                 }
@@ -5188,7 +5237,7 @@
             mInjector.binderRestoreCallingIdentity(id);
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getUserData(userHandle.getIdentifier()).mOwnerInstalledCaCerts.add(alias);
             saveSettingsLocked(userHandle.getIdentifier());
         }
@@ -5210,7 +5259,7 @@
             mInjector.binderRestoreCallingIdentity(id);
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (getUserData(userId).mOwnerInstalledCaCerts.removeAll(Arrays.asList(aliases))) {
                 saveSettingsLocked(userId);
             }
@@ -5295,7 +5344,7 @@
             }
         } else {
             // Caller provided - check it is the device owner.
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             }
         }
@@ -5555,7 +5604,7 @@
 
         // Retrieve the user ID of the calling process.
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure calling process is device/profile owner.
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             // Ensure the delegate is installed (skip this for DELEGATION_CERT_INSTALL in pre-N).
@@ -5616,7 +5665,7 @@
         // Retrieve the user ID of the calling process.
         final int callingUid = mInjector.binderGetCallingUid();
         final int userId = UserHandle.getUserId(callingUid);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure calling process is device/profile owner.
             if (who != null) {
                 getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -5659,7 +5708,7 @@
 
         // Retrieve the user ID of the calling process.
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure calling process is device/profile owner.
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             final DevicePolicyData policy = getUserData(userId);
@@ -5698,7 +5747,7 @@
         // Retrieve the UID and user ID of the calling process.
         final int callingUid = mInjector.binderGetCallingUid();
         final int userId = UserHandle.getUserId(callingUid);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Retrieve user policy data.
             final DevicePolicyData policy = getUserData(userId);
             // Retrieve the list of delegation scopes granted to callerPackage.
@@ -5737,7 +5786,7 @@
             String scope) {
         // If a ComponentName is given ensure it is a device or profile owner according to policy.
         if (who != null) {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 getActiveAdminForCallerLocked(who, reqPolicy);
             }
         // If no ComponentName is given ensure calling process has scope delegation.
@@ -5756,7 +5805,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
 
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized(this) {
+        synchronized (getLockObject()) {
             // Ensure calling process is device/profile owner.
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             final DevicePolicyData policy = getUserData(userId);
@@ -5887,7 +5936,7 @@
         enforceFullCrossUsersPermission(mInjector.userHandleGetCallingUserId());
 
         final ActiveAdmin admin;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             admin = getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_WIPE_DATA);
         }
         String internalReason = "DevicePolicyManager.wipeDataWithReason() from "
@@ -5970,7 +6019,7 @@
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BIND_DEVICE_ADMIN, null);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminUncheckedLocked(comp, userHandle);
             if (admin == null) {
                 result.sendResult(null);
@@ -6009,7 +6058,7 @@
         }
 
         validateQualityConstant(metrics.quality);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             mUserPasswordMetrics.put(userHandle, metrics);
         }
     }
@@ -6033,7 +6082,7 @@
 
         long ident = mInjector.binderClearCallingIdentity();
         try {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 policy.mFailedPasswordAttempts = 0;
                 updatePasswordValidityCheckpointLocked(userId, /* parent */ false);
                 updatePasswordExpirationsLocked(userId);
@@ -6086,7 +6135,7 @@
         ActiveAdmin strictestAdmin = null;
         final long ident = mInjector.binderClearCallingIdentity();
         try {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 DevicePolicyData policy = getUserData(userHandle);
                 policy.mFailedPasswordAttempts++;
                 saveSettingsLocked(userHandle);
@@ -6146,7 +6195,7 @@
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BIND_DEVICE_ADMIN, null);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policy = getUserData(userHandle);
             if (policy.mFailedPasswordAttempts != 0 || policy.mPasswordOwner >= 0) {
                 long ident = mInjector.binderClearCallingIdentity();
@@ -6221,7 +6270,7 @@
         if (!mHasFeature) {
             return null;
         }
-        synchronized(this) {
+        synchronized (getLockObject()) {
             Preconditions.checkNotNull(who, "ComponentName is null");
 
             // Only check if system user has set global proxy. We don't allow other users to set it.
@@ -6276,7 +6325,7 @@
             return null;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized(this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
             // Scan through active admins and find if anyone has already
             // set the global proxy.
@@ -6296,7 +6345,7 @@
 
     @Override
     public void setRecommendedGlobalProxy(ComponentName who, ProxyInfo proxyInfo) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
         long token = mInjector.binderClearCallingIdentity();
@@ -6365,7 +6414,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Check for permissions
             // Only system user can set storage encryption
             if (userHandle != UserHandle.USER_SYSTEM) {
@@ -6416,7 +6465,7 @@
             return false;
         }
         enforceFullCrossUsersPermission(userHandle);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Check for permissions if a particular caller is specified
             if (who != null) {
                 // When checking for a single caller, status is based on caller's request
@@ -6519,7 +6568,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (ap.disableScreenCapture != disabled) {
@@ -6539,7 +6588,7 @@
         if (!mHasFeature) {
             return false;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (who != null) {
                 ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
                 return (admin != null) ? admin.disableScreenCapture : false;
@@ -6581,7 +6630,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (admin.requireAutoTime != required) {
@@ -6609,7 +6658,7 @@
         if (!mHasFeature) {
             return false;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             if (deviceOwner != null && deviceOwner.requireAutoTime) {
                 // If the device owner enforces auto time, we don't need to check the PO's
@@ -6640,7 +6689,7 @@
                     "Cannot force ephemeral users on systems without split system user.");
         }
         boolean removeAllUsers = false;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin deviceOwner =
                     getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             if (deviceOwner.forceEphemeralUsers != forceEphemeralUsers) {
@@ -6666,7 +6715,7 @@
             return false;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin deviceOwner =
                     getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             return deviceOwner.forceEphemeralUsers;
@@ -6674,7 +6723,7 @@
     }
 
     private void ensureDeviceOwnerAndAllUsersAffiliated(ComponentName who) throws SecurityException {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             if (!areAllUsersAffiliatedWithDeviceLocked()) {
                 throw new SecurityException("Not all users are affiliated.");
@@ -6701,7 +6750,7 @@
         }
 
         final long currentTime = System.currentTimeMillis();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
             if (currentTime > policyData.mLastBugReportRequestTime) {
                 policyData.mLastBugReportRequestTime = currentTime;
@@ -6736,7 +6785,7 @@
     void sendDeviceOwnerCommand(String action, Bundle extras) {
         int deviceOwnerUserId;
         ComponentName deviceOwnerComponent;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             deviceOwnerUserId = mOwners.getDeviceOwnerUserId();
             deviceOwnerComponent = mOwners.getDeviceOwnerComponent();
         }
@@ -6765,13 +6814,17 @@
         mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
     }
 
-    private synchronized String getDeviceOwnerRemoteBugreportUri() {
-        return mOwners.getDeviceOwnerRemoteBugreportUri();
+    private String getDeviceOwnerRemoteBugreportUri() {
+        synchronized (getLockObject()) {
+            return mOwners.getDeviceOwnerRemoteBugreportUri();
+        }
     }
 
-    private synchronized void setDeviceOwnerRemoteBugreportUriAndHash(String bugreportUri,
+    private void setDeviceOwnerRemoteBugreportUriAndHash(String bugreportUri,
             String bugreportHash) {
-        mOwners.setDeviceOwnerRemoteBugreportUriAndHash(bugreportUri, bugreportHash);
+        synchronized (getLockObject()) {
+            mOwners.setDeviceOwnerRemoteBugreportUriAndHash(bugreportUri, bugreportHash);
+        }
     }
 
     private void registerRemoteBugreportReceivers() {
@@ -6833,7 +6886,7 @@
         mRemoteBugreportSharingAccepted.set(true);
         String bugreportUriString = null;
         String bugreportHash = null;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             bugreportUriString = getDeviceOwnerRemoteBugreportUri();
             bugreportHash = mOwners.getDeviceOwnerRemoteBugreportHash();
         }
@@ -6870,7 +6923,7 @@
             Uri bugreportUri = Uri.parse(bugreportUriString);
             pfd = mContext.getContentResolver().openFileDescriptor(bugreportUri, "r");
 
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 Intent intent = new Intent(DeviceAdminReceiver.ACTION_BUGREPORT_SHARE);
                 intent.setComponent(mOwners.getDeviceOwnerComponent());
                 intent.setDataAndType(bugreportUri, RemoteBugreportUtils.BUGREPORT_MIMETYPE);
@@ -6911,7 +6964,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA);
             if (ap.disableCamera != disabled) {
@@ -6937,7 +6990,7 @@
         if (!mHasFeature) {
             return false;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (who != null) {
                 ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
                 return (admin != null) ? admin.disableCamera : false;
@@ -6978,7 +7031,7 @@
                 which = which & PROFILE_KEYGUARD_FEATURES;
             }
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES, parent);
             if (ap.disabledKeyguardFeatures != which) {
@@ -7005,7 +7058,7 @@
         enforceFullCrossUsersPermission(userHandle);
         final long ident = mInjector.binderClearCallingIdentity();
         try {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 if (who != null) {
                     ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
                     return (admin != null) ? admin.disabledKeyguardFeatures : 0;
@@ -7053,7 +7106,7 @@
         }
         Preconditions.checkNotNull(packageList, "packageList is null");
         final int userHandle = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO or a keep uninstalled packages delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
                     DELEGATION_KEEP_UNINSTALLED_PACKAGES);
@@ -7074,7 +7127,7 @@
             return null;
         }
         // TODO In split system user mode, allow apps on user 0 to query the list
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO or a keep uninstalled packages delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
                     DELEGATION_KEEP_UNINSTALLED_PACKAGES);
@@ -7099,7 +7152,7 @@
         }
         final boolean hasIncompatibleAccountsOrNonAdb =
                 hasIncompatibleAccountsOrNonAdbNoLock(userId, admin);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             enforceCanSetDeviceOwnerLocked(admin, userId, hasIncompatibleAccountsOrNonAdb);
             final ActiveAdmin activeAdmin = getActiveAdminUncheckedLocked(admin, userId);
             if (activeAdmin == null
@@ -7168,7 +7221,7 @@
     }
 
     public boolean isDeviceOwner(ComponentName who, int userId) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return mOwners.hasDeviceOwner()
                     && mOwners.getDeviceOwnerUserId() == userId
                     && mOwners.getDeviceOwnerComponent().equals(who);
@@ -7176,7 +7229,7 @@
     }
 
     private boolean isDeviceOwnerPackage(String packageName, int userId) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return mOwners.hasDeviceOwner()
                     && mOwners.getDeviceOwnerUserId() == userId
                     && mOwners.getDeviceOwnerPackageName().equals(packageName);
@@ -7184,7 +7237,7 @@
     }
 
     private boolean isProfileOwnerPackage(String packageName, int userId) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return mOwners.hasProfileOwner(userId)
                     && mOwners.getProfileOwnerPackage(userId).equals(packageName);
         }
@@ -7203,7 +7256,7 @@
         if (!callingUserOnly) {
             enforceManageUsers();
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!mOwners.hasDeviceOwner()) {
                 return null;
             }
@@ -7221,7 +7274,7 @@
             return UserHandle.USER_NULL;
         }
         enforceManageUsers();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return mOwners.hasDeviceOwner() ? mOwners.getDeviceOwnerUserId() : UserHandle.USER_NULL;
         }
     }
@@ -7236,7 +7289,7 @@
             return null;
         }
         enforceManageUsers();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!mOwners.hasDeviceOwner()) {
                 return null;
             }
@@ -7250,6 +7303,7 @@
     /** Returns the active device owner or {@code null} if there is no device owner. */
     @VisibleForTesting
     ActiveAdmin getDeviceOwnerAdminLocked() {
+        ensureLocked();
         ComponentName component = mOwners.getDeviceOwnerComponent();
         if (component == null) {
             return null;
@@ -7280,7 +7334,7 @@
         } catch (NameNotFoundException e) {
             throw new SecurityException(e);
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ComponentName deviceOwnerComponent = mOwners.getDeviceOwnerComponent();
             final int deviceOwnerUserId = mOwners.getDeviceOwnerUserId();
             if (!mOwners.hasDeviceOwner()
@@ -7368,7 +7422,7 @@
         }
         final boolean hasIncompatibleAccountsOrNonAdb =
                 hasIncompatibleAccountsOrNonAdbNoLock(userHandle, who);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             enforceCanSetProfileOwnerLocked(who, userHandle, hasIncompatibleAccountsOrNonAdb);
 
             final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
@@ -7414,7 +7468,7 @@
         final int userId = mInjector.userHandleGetCallingUserId();
         enforceNotManagedProfile(userId, "clear profile owner");
         enforceUserUnlocked(userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Check if this is the profile owner who is calling
             final ActiveAdmin admin =
                     getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -7457,7 +7511,7 @@
             return;
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             long token = mInjector.binderClearCallingIdentity();
             try {
@@ -7546,7 +7600,7 @@
                       + "device or profile owner is set.");
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             boolean transitionCheckNeeded = true;
 
             // Calling identity/permission checks.
@@ -7616,7 +7670,7 @@
             return;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Check if this is the profile owner who is calling
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             final int userId = UserHandle.getCallingUserId();
@@ -7664,7 +7718,7 @@
             return null;
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return mOwners.getProfileOwnerComponent(userHandle);
         }
     }
@@ -7881,7 +7935,7 @@
     }
 
     private void enforceDeviceOwnerOrManageUsers() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (getActiveAdminWithPolicyForUidLocked(null, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
                     mInjector.binderGetCallingUid()) != null) {
                 return;
@@ -7891,7 +7945,7 @@
     }
 
     private void enforceProfileOwnerOrSystemUser() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (getActiveAdminWithPolicyForUidLocked(null,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, mInjector.binderGetCallingUid())
                             != null) {
@@ -7904,7 +7958,7 @@
 
     private void enforceProfileOwnerOrFullCrossUsersPermission(int userId) {
         if (userId == mInjector.userHandleGetCallingUserId()) {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 if (getActiveAdminWithPolicyForUidLocked(null,
                         DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, mInjector.binderGetCallingUid())
                                 != null) {
@@ -8017,7 +8071,7 @@
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             pw.println("Current Device Policy Manager state:");
 
             mOwners.dump("  ", pw);
@@ -8048,6 +8102,8 @@
             pw.println();
             mConstants.dump("  ", pw);
             pw.println();
+            mStatLogger.dump(pw, "  ");
+            pw.println();
             pw.println("  Encryption Status: " + getEncryptionStatusName(getEncryptionStatus()));
         }
     }
@@ -8076,7 +8132,7 @@
             ComponentName activity) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             long id = mInjector.binderClearCallingIdentity();
@@ -8095,7 +8151,7 @@
     public void clearPackagePersistentPreferredActivities(ComponentName who, String packageName) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             long id = mInjector.binderClearCallingIdentity();
@@ -8113,7 +8169,7 @@
     @Override
     public void setDefaultSmsApplication(ComponentName admin, String packageName) {
         Preconditions.checkNotNull(admin, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
         mInjector.binderWithCleanCallingIdentity(() ->
@@ -8167,7 +8223,7 @@
         Preconditions.checkNotNull(admin, "admin is null");
         Preconditions.checkNotNull(agent, "agent is null");
         final int userHandle = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(admin,
                     DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES, parent);
             ap.trustAgentInfos.put(agent.flattenToString(), new TrustAgentInfo(args));
@@ -8184,7 +8240,7 @@
         Preconditions.checkNotNull(agent, "agent null");
         enforceFullCrossUsersPermission(userHandle);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final String componentName = agent.flattenToString();
             if (admin != null) {
                 final ActiveAdmin ap = getActiveAdminUncheckedLocked(admin, userHandle, parent);
@@ -8234,7 +8290,7 @@
     @Override
     public void setRestrictionsProvider(ComponentName who, ComponentName permissionProvider) {
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             int userHandle = UserHandle.getCallingUserId();
@@ -8246,7 +8302,7 @@
 
     @Override
     public ComponentName getRestrictionsProvider(int userHandle) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!isCallerWithSystemUid()) {
                 throw new SecurityException("Only the system can query the permission provider");
             }
@@ -8259,7 +8315,7 @@
     public void addCrossProfileIntentFilter(ComponentName who, IntentFilter filter, int flags) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         int callingUserId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             long id = mInjector.binderClearCallingIdentity();
@@ -8290,7 +8346,7 @@
     public void clearCrossProfileIntentFilters(ComponentName who) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         int callingUserId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             long id = mInjector.binderClearCallingIdentity();
             try {
@@ -8397,7 +8453,7 @@
             }
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             admin.permittedAccessiblityServices = packageList;
@@ -8413,7 +8469,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.permittedAccessiblityServices;
@@ -8425,7 +8481,8 @@
         if (!mHasFeature) {
             return null;
         }
-        synchronized (this) {
+        enforceManageUsers();
+        synchronized (getLockObject()) {
             List<String> result = null;
             // If we have multiple profiles we return the intersection of the
             // permitted lists. This can happen in cases where we have a device
@@ -8492,7 +8549,7 @@
             throw new SecurityException(
                     "Only the system can query if an accessibility service is disabled by admin");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
             if (admin == null) {
                 return false;
@@ -8570,7 +8627,7 @@
             }
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             admin.permittedInputMethods = packageList;
@@ -8586,7 +8643,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.permittedInputMethods;
@@ -8606,7 +8663,7 @@
         }
 
         int userId = currentUser.id;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             List<String> result = null;
             // If we have multiple profiles we return the intersection of the
             // permitted lists. This can happen in cases where we have a device
@@ -8666,7 +8723,7 @@
             throw new SecurityException(
                     "Only the system can query if an input method is disabled by admin");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
             if (admin == null) {
                 return false;
@@ -8692,7 +8749,7 @@
             return false;
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             admin.permittedNotificationListeners = packageList;
@@ -8708,7 +8765,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.permittedNotificationListeners;
@@ -8726,7 +8783,7 @@
             throw new SecurityException(
                     "Only the system can query if a notification listener service is permitted");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userId);
             if (profileOwner == null || profileOwner.permittedNotificationListeners == null) {
                 return true;
@@ -8782,7 +8839,7 @@
 
         // Create user.
         UserHandle user = null;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
 
             final int callingUid = mInjector.binderGetCallingUid();
@@ -8871,7 +8928,7 @@
             final String ownerName = getProfileOwnerName(Process.myUserHandle().getIdentifier());
             setProfileOwner(profileOwner, ownerName, userHandle);
 
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 DevicePolicyData policyData = getUserData(userHandle);
                 policyData.mInitBundle = adminExtras;
                 policyData.mAdminBroadcastPending = true;
@@ -8902,7 +8959,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         Preconditions.checkNotNull(userHandle, "UserHandle is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -8941,7 +8998,7 @@
     public boolean switchUser(ComponentName who, UserHandle userHandle) {
         Preconditions.checkNotNull(who, "ComponentName is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
 
             long id = mInjector.binderClearCallingIdentity();
@@ -8965,7 +9022,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         Preconditions.checkNotNull(userHandle, "UserHandle is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -9000,7 +9057,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         Preconditions.checkNotNull(userHandle, "UserHandle is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -9018,7 +9075,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
 
         final int callingUserId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (!isUserAffiliatedWithDeviceLocked(callingUserId)) {
                 throw new SecurityException("Admin " + who +
@@ -9070,7 +9127,7 @@
     @Override
     public List<UserHandle> getSecondaryUsers(ComponentName who) {
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -9094,7 +9151,7 @@
     @Override
     public boolean isEphemeralUser(ComponentName who) {
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
         }
 
@@ -9129,7 +9186,7 @@
     public String[] setPackagesSuspended(ComponentName who, String callerPackage,
             String[] packageNames, boolean suspended) {
         int callingUserId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or a package access delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_PACKAGE_ACCESS);
@@ -9137,7 +9194,7 @@
             long id = mInjector.binderClearCallingIdentity();
             try {
                 return mIPackageManager.setPackagesSuspendedAsUser(
-                        packageNames, suspended, null, null, "android", callingUserId);
+                        packageNames, suspended, null, null, null, "android", callingUserId);
             } catch (RemoteException re) {
                 // Shouldn't happen.
                 Slog.e(LOG_TAG, "Failed talking to the package manager", re);
@@ -9151,7 +9208,7 @@
     @Override
     public boolean isPackageSuspended(ComponentName who, String callerPackage, String packageName) {
         int callingUserId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or a package access delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_PACKAGE_ACCESS);
@@ -9177,7 +9234,7 @@
         }
 
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin activeAdmin =
                     getActiveAdminForCallerLocked(who,
                             DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -9216,7 +9273,7 @@
     }
 
     private void pushUserRestrictions(int userId) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final boolean isDeviceOwner = mOwners.isDeviceOwnerUserId(userId);
             final Bundle userRestrictions;
             // Whether device owner enforces camera restriction.
@@ -9263,7 +9320,7 @@
             return null;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return activeAdmin.userRestrictions;
@@ -9274,7 +9331,7 @@
     public boolean setApplicationHidden(ComponentName who, String callerPackage, String packageName,
             boolean hidden) {
         int callingUserId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or a package access delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_PACKAGE_ACCESS);
@@ -9297,7 +9354,7 @@
     public boolean isApplicationHidden(ComponentName who, String callerPackage,
             String packageName) {
         int callingUserId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or a package access delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_PACKAGE_ACCESS);
@@ -9318,7 +9375,7 @@
 
     @Override
     public void enableSystemApp(ComponentName who, String callerPackage, String packageName) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or an enable system app delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_ENABLE_SYSTEM_APP);
@@ -9359,7 +9416,7 @@
 
     @Override
     public int enableSystemAppWithIntent(ComponentName who, String callerPackage, Intent intent) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or an enable system app delegate.
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_ENABLE_SYSTEM_APP);
@@ -9421,7 +9478,7 @@
     @Override
     public boolean installExistingPackage(ComponentName who, String callerPackage,
             String packageName) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a PO or an install existing package delegate
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_INSTALL_EXISTING_PACKAGE);
@@ -9458,7 +9515,7 @@
             return;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (disabled) {
@@ -9481,7 +9538,7 @@
         if (!mHasFeature) {
             return null;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policy = getUserData(userId);
             final int N = policy.mAdminList.size();
             ArraySet<String> resultSet = new ArraySet<>();
@@ -9497,7 +9554,7 @@
     public void setUninstallBlocked(ComponentName who, String callerPackage, String packageName,
             boolean uninstallBlocked) {
         final int userId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or a block uninstall delegate
             enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_BLOCK_UNINSTALL);
@@ -9521,7 +9578,7 @@
         // when the package is a system app, or when it is an active device admin.
         final int userId = UserHandle.getCallingUserId();
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (who != null) {
                 getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             }
@@ -9545,7 +9602,7 @@
             return;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (admin.disableCallerId != disabled) {
@@ -9561,7 +9618,7 @@
             return false;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.disableCallerId;
@@ -9571,7 +9628,7 @@
     @Override
     public boolean getCrossProfileCallerIdDisabledForUser(int userId) {
         enforceCrossUsersPermission(userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
             return (admin != null) ? admin.disableCallerId : false;
         }
@@ -9583,7 +9640,7 @@
             return;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (admin.disableContactsSearch != disabled) {
@@ -9599,7 +9656,7 @@
             return false;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.disableContactsSearch;
@@ -9609,7 +9666,7 @@
     @Override
     public boolean getCrossProfileContactsSearchDisabledForUser(int userId) {
         enforceCrossUsersPermission(userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
             return (admin != null) ? admin.disableContactsSearch : false;
         }
@@ -9624,7 +9681,7 @@
 
         final long ident = mInjector.binderClearCallingIdentity();
         try {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 final int managedUserId = getManagedUserId(callingUserId);
                 if (managedUserId < 0) {
                     return;
@@ -9682,7 +9739,7 @@
             return;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (admin.disableBluetoothContactSharing != disabled) {
@@ -9698,7 +9755,7 @@
             return false;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.disableBluetoothContactSharing;
@@ -9710,7 +9767,7 @@
         // TODO: Should there be a check to make sure this relationship is
         // within a profile group?
         // enforceSystemProcess("getCrossProfileCallerIdDisabled can only be called by system");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
             return (admin != null) ? admin.disableBluetoothContactSharing : false;
         }
@@ -9722,7 +9779,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         Preconditions.checkNotNull(packages, "packages is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             enforceCanCallLockTaskLocked(who);
             final int userHandle = mInjector.userHandleGetCallingUserId();
             setLockTaskPackagesLocked(userHandle, new ArrayList<>(Arrays.asList(packages)));
@@ -9743,7 +9800,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
 
         final int userHandle = mInjector.binderGetCallingUserHandle().getIdentifier();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             enforceCanCallLockTaskLocked(who);
             final List<String> packages = getUserData(userHandle).mLockTaskPackages;
             return packages.toArray(new String[packages.size()]);
@@ -9753,7 +9810,7 @@
     @Override
     public boolean isLockTaskPermitted(String pkg) {
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return getUserData(userHandle).mLockTaskPackages.contains(pkg);
         }
     }
@@ -9772,7 +9829,7 @@
             "Cannot use LOCK_TASK_FEATURE_NOTIFICATIONS without LOCK_TASK_FEATURE_HOME");
 
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             enforceCanCallLockTaskLocked(who);
             setLockTaskFeaturesLocked(userHandle, flags);
         }
@@ -9789,7 +9846,7 @@
     public int getLockTaskFeatures(ComponentName who) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             enforceCanCallLockTaskLocked(who);
             return getUserData(userHandle).mLockTaskFeatures;
         }
@@ -9828,7 +9885,7 @@
         if (!isCallerWithSystemUid()) {
             throw new SecurityException("notifyLockTaskModeChanged can only be called by system");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final DevicePolicyData policy = getUserData(userHandle);
 
             if (policy.mStatusBarDisabled) {
@@ -9858,7 +9915,7 @@
     public void setGlobalSetting(ComponentName who, String setting, String value) {
         Preconditions.checkNotNull(who, "ComponentName is null");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
 
             // Some settings are no supported any more. However we do not want to throw a
@@ -9897,7 +9954,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         Preconditions.checkStringNotEmpty(setting, "String setting is null or empty");
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             if (!SYSTEM_SETTINGS_WHITELIST.contains(setting)) {
@@ -9942,7 +9999,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         int callingUserId = mInjector.userHandleGetCallingUserId();
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             if (isDeviceOwner(who, callingUserId)) {
@@ -10002,7 +10059,7 @@
     @Override
     public void setMasterVolumeMuted(ComponentName who, boolean on) {
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             setUserRestriction(who, UserManager.DISALLOW_UNMUTE_DEVICE, on);
         }
@@ -10011,7 +10068,7 @@
     @Override
     public boolean isMasterVolumeMuted(ComponentName who) {
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             AudioManager audioManager =
@@ -10022,7 +10079,7 @@
 
     @Override
     public void setUserIcon(ComponentName who, Bitmap icon) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             Preconditions.checkNotNull(who, "ComponentName is null");
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
@@ -10040,7 +10097,7 @@
     public boolean setKeyguardDisabled(ComponentName who, boolean disabled) {
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (!isUserAffiliatedWithDeviceLocked(userId)) {
                 throw new SecurityException("Admin " + who +
@@ -10070,7 +10127,7 @@
     @Override
     public boolean setStatusBarDisabled(ComponentName who, boolean disabled) {
         int userId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (!isUserAffiliatedWithDeviceLocked(userId)) {
                 throw new SecurityException("Admin " + who +
@@ -10139,7 +10196,7 @@
                 DevicePolicyData policy = getUserData(userHandle);
                 if (!policy.mUserSetupComplete) {
                     policy.mUserSetupComplete = true;
-                    synchronized (this) {
+                    synchronized (getLockObject()) {
                         saveSettingsLocked(userHandle);
                     }
                 }
@@ -10149,7 +10206,7 @@
                 DevicePolicyData policy = getUserData(userHandle);
                 if (!policy.mPaired) {
                     policy.mPaired = true;
-                    synchronized (this) {
+                    synchronized (getLockObject()) {
                         saveSettingsLocked(userHandle);
                     }
                 }
@@ -10166,7 +10223,7 @@
         private final Uri mDefaultImeChanged = Settings.Secure.getUriFor(
                 Settings.Secure.DEFAULT_INPUT_METHOD);
 
-        @GuardedBy("DevicePolicyManagerService.this")
+        @GuardedBy("getLockObject()")
         private Set<Integer> mUserIdsWithPendingChangesByOwner = new ArraySet<>();
 
         public SetupContentObserver(Handler handler) {
@@ -10182,7 +10239,7 @@
             mInjector.registerContentObserver(mDefaultImeChanged, false, this, UserHandle.USER_ALL);
         }
 
-        @GuardedBy("DevicePolicyManagerService.this")
+        @GuardedBy("getLockObject()")
         private void addPendingChangeByOwnerLocked(int userId) {
             mUserIdsWithPendingChangesByOwner.add(userId);
         }
@@ -10192,13 +10249,13 @@
             if (mUserSetupComplete.equals(uri) || (mIsWatch && mPaired.equals(uri))) {
                 updateUserSetupCompleteAndPaired();
             } else if (mDeviceProvisioned.equals(uri)) {
-                synchronized (DevicePolicyManagerService.this) {
+                synchronized (getLockObject()) {
                     // Set PROPERTY_DEVICE_OWNER_PRESENT, for the SUW case where setting the property
                     // is delayed until device is marked as provisioned.
                     setDeviceOwnerSystemPropertyLocked();
                 }
             } else if (mDefaultImeChanged.equals(uri)) {
-                synchronized (DevicePolicyManagerService.this) {
+                synchronized (getLockObject()) {
                     if (mUserIdsWithPendingChangesByOwner.contains(userId)) {
                         // This change notification was triggered by the owner changing the current
                         // IME. Ignore it.
@@ -10220,7 +10277,7 @@
 
         @Override
         public List<String> getCrossProfileWidgetProviders(int profileId) {
-            synchronized (DevicePolicyManagerService.this) {
+            synchronized (getLockObject()) {
                 if (mOwners == null) {
                     return Collections.emptyList();
                 }
@@ -10244,7 +10301,7 @@
         @Override
         public void addOnCrossProfileWidgetProvidersChangeListener(
                 OnCrossProfileWidgetProvidersChangeListener listener) {
-            synchronized (DevicePolicyManagerService.this) {
+            synchronized (getLockObject()) {
                 if (mWidgetProviderListeners == null) {
                     mWidgetProviderListeners = new ArrayList<>();
                 }
@@ -10256,14 +10313,14 @@
 
         @Override
         public boolean isActiveAdminWithPolicy(int uid, int reqPolicy) {
-            synchronized(DevicePolicyManagerService.this) {
+            synchronized (getLockObject()) {
                 return getActiveAdminWithPolicyForUidLocked(null, reqPolicy, uid) != null;
             }
         }
 
         private void notifyCrossProfileProvidersChanged(int userId, List<String> packages) {
             final List<OnCrossProfileWidgetProvidersChangeListener> listeners;
-            synchronized (DevicePolicyManagerService.this) {
+            synchronized (getLockObject()) {
                 listeners = new ArrayList<>(mWidgetProviderListeners);
             }
             final int listenerCount = listeners.size();
@@ -10352,7 +10409,7 @@
 
         @Override
         public void reportSeparateProfileChallengeChanged(@UserIdInt int userId) {
-            synchronized (DevicePolicyManagerService.this) {
+            synchronized (getLockObject()) {
                 updateMaximumTimeToLockLocked(userId);
             }
         }
@@ -10364,7 +10421,7 @@
 
         @Override
         public CharSequence getPrintingDisabledReasonForUser(@UserIdInt int userId) {
-            synchronized (DevicePolicyManagerService.this) {
+            synchronized (getLockObject()) {
                 DevicePolicyData policy = getUserData(userId);
                 if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_PRINTING,
                         UserHandle.of(userId))) {
@@ -10426,7 +10483,7 @@
         if (DevicePolicyManager.POLICY_DISABLE_CAMERA.equals(restriction) ||
                 DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE.equals(restriction) ||
                 DevicePolicyManager.POLICY_MANDATORY_BACKUPS.equals(restriction)) {
-            synchronized(this) {
+            synchronized (getLockObject()) {
                 final DevicePolicyData policy = getUserData(userId);
                 final int N = policy.mAdminList.size();
                 for (int i = 0; i < N; i++) {
@@ -10485,7 +10542,7 @@
             policy.validateAgainstPreviousFreezePeriod(record.first, record.second,
                     LocalDate.now());
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             if (policy == null) {
                 mOwners.clearSystemUpdatePolicy();
@@ -10502,7 +10559,7 @@
 
     @Override
     public SystemUpdatePolicy getSystemUpdatePolicy() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             SystemUpdatePolicy policy =  mOwners.getSystemUpdatePolicy();
             if (policy != null && !policy.isValid()) {
                 Slog.w(LOG_TAG, "Stored system update policy is invalid, return null instead.");
@@ -10535,7 +10592,7 @@
      */
     private void updateSystemUpdateFreezePeriodsRecord(boolean saveIfChanged) {
         Slog.d(LOG_TAG, "updateSystemUpdateFreezePeriodsRecord");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final SystemUpdatePolicy policy = mOwners.getSystemUpdatePolicy();
             if (policy == null) {
                 return;
@@ -10579,7 +10636,7 @@
     @Override
     public void clearSystemUpdatePolicyFreezePeriodRecord() {
         enforceShell("clearSystemUpdatePolicyFreezePeriodRecord");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Print out current record to help diagnosed CTS failures
             Slog.i(LOG_TAG, "Clear freeze period record: "
                     + mOwners.getSystemUpdateFreezePeriodRecordAsString());
@@ -10597,7 +10654,7 @@
      */
     @VisibleForTesting
     boolean isCallerDeviceOwner(int callerUid) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!mOwners.hasDeviceOwner()) {
                 return false;
             }
@@ -10643,7 +10700,7 @@
 
         final long ident = mInjector.binderClearCallingIdentity();
         try {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 // Broadcast to device owner first if there is one.
                 if (mOwners.hasDeviceOwner()) {
                     final UserHandle deviceOwnerUser =
@@ -10663,7 +10720,7 @@
             }
             // Send broadcasts to corresponding profile owners if any.
             for (final int userId : runningUserIds) {
-                synchronized (this) {
+                synchronized (getLockObject()) {
                     final ComponentName profileOwnerPackage =
                             mOwners.getProfileOwnerComponent(userId);
                     if (profileOwnerPackage != null) {
@@ -10689,7 +10746,7 @@
     public void setPermissionPolicy(ComponentName admin, String callerPackage, int policy)
             throws RemoteException {
         int userId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or a permission grant state delegate.
             enforceCanManageScope(admin, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_PERMISSION_GRANT);
@@ -10704,7 +10761,7 @@
     @Override
     public int getPermissionPolicy(ComponentName admin) throws RemoteException {
         int userId = UserHandle.getCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData userPolicy = getUserData(userId);
             return userPolicy.mPermissionPolicy;
         }
@@ -10714,7 +10771,7 @@
     public boolean setPermissionGrantState(ComponentName admin, String callerPackage,
             String packageName, String permission, int grantState) throws RemoteException {
         UserHandle user = mInjector.binderGetCallingUserHandle();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // Ensure the caller is a DO/PO or a permission grant state delegate.
             enforceCanManageScope(admin, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
                     DELEGATION_PERMISSION_GRANT);
@@ -10772,7 +10829,7 @@
             enforceCanManageScope(admin, callerPackage,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, DELEGATION_PERMISSION_GRANT);
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             long ident = mInjector.binderClearCallingIdentity();
             try {
                 int granted = mIPackageManager.checkPermission(permission,
@@ -10910,7 +10967,7 @@
     }
 
     private int checkDeviceOwnerProvisioningPreCondition(int deviceOwnerUserId) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             // hasIncompatibleAccountsOrNonAdb doesn't matter since the caller is not adb.
             return checkDeviceOwnerProvisioningPreConditionLocked(/* owner unknown */ null,
                     deviceOwnerUserId, /* isAdb= */ false,
@@ -10977,7 +11034,7 @@
      * Return device owner or profile owner set on a given user.
      */
     private @Nullable ComponentName getOwnerComponent(int userId) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (mOwners.getDeviceOwnerUserId() == userId) {
                 return mOwners.getDeviceOwnerComponent();
             }
@@ -11028,7 +11085,7 @@
     @Override
     public String getWifiMacAddress(ComponentName admin) {
         // Make sure caller has DO.
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -11068,7 +11125,7 @@
 
     @Override
     public boolean isSystemOnlyUser(ComponentName admin) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
         final int callingUserId = mInjector.userHandleGetCallingUserId();
@@ -11079,7 +11136,7 @@
     public void reboot(ComponentName admin) {
         Preconditions.checkNotNull(admin);
         // Make sure caller has DO.
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
         long ident = mInjector.binderClearCallingIdentity();
@@ -11101,7 +11158,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForUidLocked(who,
                     mInjector.binderGetCallingUid());
             if (!TextUtils.equals(admin.shortSupportMessage, message)) {
@@ -11117,7 +11174,7 @@
             return null;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForUidLocked(who,
                     mInjector.binderGetCallingUid());
             return admin.shortSupportMessage;
@@ -11131,7 +11188,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForUidLocked(who,
                     mInjector.binderGetCallingUid());
             if (!TextUtils.equals(admin.longSupportMessage, message)) {
@@ -11147,7 +11204,7 @@
             return null;
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForUidLocked(who,
                     mInjector.binderGetCallingUid());
             return admin.longSupportMessage;
@@ -11163,7 +11220,7 @@
         if (!isCallerWithSystemUid()) {
             throw new SecurityException("Only the system can query support message for user");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
             if (admin != null) {
                 return admin.shortSupportMessage;
@@ -11181,7 +11238,7 @@
         if (!isCallerWithSystemUid()) {
             throw new SecurityException("Only the system can query support message for user");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
             if (admin != null) {
                 return admin.longSupportMessage;
@@ -11198,7 +11255,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = mInjector.userHandleGetCallingUserId();
         enforceManagedProfile(userHandle, "set organization color");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             admin.organizationColor = color;
@@ -11214,7 +11271,7 @@
         enforceFullCrossUsersPermission(userId);
         enforceManageUsers();
         enforceManagedProfile(userId, "set organization color");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
             admin.organizationColor = color;
             saveSettingsLocked(userId);
@@ -11228,7 +11285,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         enforceManagedProfile(mInjector.userHandleGetCallingUserId(), "get organization color");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.organizationColor;
@@ -11242,7 +11299,7 @@
         }
         enforceFullCrossUsersPermission(userHandle);
         enforceManagedProfile(userHandle, "get organization color");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userHandle);
             return (profileOwner != null)
                     ? profileOwner.organizationColor
@@ -11258,7 +11315,7 @@
         Preconditions.checkNotNull(who, "ComponentName is null");
         final int userHandle = mInjector.userHandleGetCallingUserId();
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             if (!TextUtils.equals(admin.organizationName, text)) {
@@ -11276,7 +11333,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null");
         enforceManagedProfile(mInjector.userHandleGetCallingUserId(), "get organization name");
-        synchronized(this) {
+        synchronized (getLockObject()) {
             ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.organizationName;
@@ -11289,7 +11346,7 @@
             return null;
         }
         enforceDeviceOwnerOrManageUsers();
-        synchronized(this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin deviceOwnerAdmin = getDeviceOwnerAdminLocked();
             return deviceOwnerAdmin == null ? null : deviceOwnerAdmin.organizationName;
         }
@@ -11302,7 +11359,7 @@
         }
         enforceFullCrossUsersPermission(userHandle);
         enforceManagedProfile(userHandle, "get organization name");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userHandle);
             return (profileOwner != null)
                     ? profileOwner.organizationName
@@ -11318,7 +11375,7 @@
         if (!mHasFeature) {
             return packageNames;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             final int callingUserId = mInjector.userHandleGetCallingUserId();
@@ -11367,7 +11424,7 @@
         if (!mHasFeature) {
             return new ArrayList<>();
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
                     DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return admin.meteredDisabledPackages == null
@@ -11387,7 +11444,7 @@
             throw new SecurityException(
                     "Only the system can query restricted pkgs for a specific user");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userId);
             if (admin != null && admin.meteredDisabledPackages != null) {
                 return admin.meteredDisabledPackages.contains(packageName);
@@ -11429,7 +11486,7 @@
 
         final Set<String> affiliationIds = new ArraySet<>(ids);
         final int callingUserId = mInjector.userHandleGetCallingUserId();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             getUserData(callingUserId).mAffiliationIds = affiliationIds;
             saveSettingsLocked(callingUserId);
@@ -11456,7 +11513,7 @@
         }
 
         Preconditions.checkNotNull(admin);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             return new ArrayList<String>(
                     getUserData(mInjector.userHandleGetCallingUserId()).mAffiliationIds);
@@ -11469,7 +11526,7 @@
             return false;
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return isUserAffiliatedWithDeviceLocked(mInjector.userHandleGetCallingUserId());
         }
     }
@@ -11530,7 +11587,7 @@
         }
         Preconditions.checkNotNull(admin);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             if (enabled == mInjector.securityLogGetLoggingEnabledProperty()) {
                 return;
@@ -11551,7 +11608,7 @@
             return false;
         }
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!isCallerWithSystemUid()) {
                 Preconditions.checkNotNull(admin);
                 getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -11560,12 +11617,14 @@
         }
     }
 
-    private synchronized void recordSecurityLogRetrievalTime() {
-        final long currentTime = System.currentTimeMillis();
-        DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
-        if (currentTime > policyData.mLastSecurityLogRetrievalTime) {
-            policyData.mLastSecurityLogRetrievalTime = currentTime;
-            saveSettingsLocked(UserHandle.USER_SYSTEM);
+    private void recordSecurityLogRetrievalTime() {
+        synchronized (getLockObject()) {
+            final long currentTime = System.currentTimeMillis();
+            DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
+            if (currentTime > policyData.mLastSecurityLogRetrievalTime) {
+                policyData.mLastSecurityLogRetrievalTime = currentTime;
+                saveSettingsLocked(UserHandle.USER_SYSTEM);
+            }
         }
     }
 
@@ -11646,7 +11705,7 @@
         enforceCanManageDeviceAdmin();
         final int userId = mInjector.userHandleGetCallingUserId();
         Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return mPackagesToRemove.contains(packageUserPair);
         }
     }
@@ -11672,7 +11731,7 @@
         }
 
         final Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             mPackagesToRemove.add(packageUserPair);
         }
 
@@ -11707,7 +11766,7 @@
     @Override
     public boolean isDeviceProvisioned() {
         enforceManageUsers();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return getUserDataUnchecked(UserHandle.USER_SYSTEM).mUserSetupComplete;
         }
     }
@@ -11734,7 +11793,7 @@
 
     private void startUninstallIntent(final String packageName, final int userId) {
         final Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (!mPackagesToRemove.contains(packageUserPair)) {
                 // Do nothing if uninstall was not requested or was already started.
                 return;
@@ -11769,7 +11828,7 @@
      * @param userHandle The user for which this admin has to be removed.
      */
     private void removeAdminArtifacts(final ComponentName adminReceiver, final int userHandle) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
             if (admin == null) {
                 return;
@@ -11799,7 +11858,7 @@
     @Override
     public void setDeviceProvisioningConfigApplied() {
         enforceManageUsers();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
             policy.mDeviceProvisioningConfigApplied = true;
             saveSettingsLocked(UserHandle.USER_SYSTEM);
@@ -11809,7 +11868,7 @@
     @Override
     public boolean isDeviceProvisioningConfigApplied() {
         enforceManageUsers();
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
             return policy.mDeviceProvisioningConfigApplied;
         }
@@ -11835,7 +11894,7 @@
                 Settings.Secure.USER_SETUP_COMPLETE, 0, userId) != 0;
         DevicePolicyData policy = getUserData(userId);
         policy.mUserSetupComplete = isUserCompleted;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             saveSettingsLocked(userId);
         }
     }
@@ -11850,7 +11909,7 @@
             return;
         }
         Preconditions.checkNotNull(admin);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(
                     admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             if (!enabled) {
@@ -11879,7 +11938,7 @@
         if (!mHasFeature) {
             return true;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             try {
                 getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
                 IBackupManager ibm = mInjector.getIBackupManager();
@@ -11898,7 +11957,7 @@
             return false;
         }
         Preconditions.checkNotNull(admin);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -11944,18 +12003,20 @@
         return success.get();
     }
 
-    synchronized private void saveMandatoryBackupTransport(
+    private void saveMandatoryBackupTransport(
             ComponentName admin, int callingUid, ComponentName backupTransportComponent) {
-        ActiveAdmin activeAdmin =
-                getActiveAdminWithPolicyForUidLocked(
-                        admin,
-                        DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
-                        callingUid);
-        if (!Objects.equals(backupTransportComponent,
-                activeAdmin.mandatoryBackupTransport)) {
-            activeAdmin.mandatoryBackupTransport =
-                    backupTransportComponent;
-            saveSettingsLocked(UserHandle.USER_SYSTEM);
+        synchronized (getLockObject()) {
+            ActiveAdmin activeAdmin =
+                    getActiveAdminWithPolicyForUidLocked(
+                            admin,
+                            DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
+                            callingUid);
+            if (!Objects.equals(backupTransportComponent,
+                    activeAdmin.mandatoryBackupTransport)) {
+                activeAdmin.mandatoryBackupTransport =
+                        backupTransportComponent;
+                saveSettingsLocked(UserHandle.USER_SYSTEM);
+            }
         }
     }
 
@@ -11964,7 +12025,7 @@
         if (!mHasFeature) {
             return null;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin activeAdmin = getDeviceOwnerAdminLocked();
             return activeAdmin == null ? null : activeAdmin.mandatoryBackupTransport;
         }
@@ -11995,7 +12056,7 @@
         }
 
         final String targetPackage;
-        synchronized (this) {
+        synchronized (getLockObject()) {
             targetPackage = getOwnerPackageNameForUserLocked(targetUserId);
         }
 
@@ -12032,7 +12093,7 @@
         }
         Preconditions.checkNotNull(admin);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
             final int callingUserId = mInjector.userHandleGetCallingUserId();
@@ -12106,7 +12167,7 @@
             if (accounts.length == 0) {
                 return false;
             }
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 if (owner == null || !isAdminTestOnlyLocked(owner, userId)) {
                     Log.w(LOG_TAG,
                             "Non test-only owner can't be installed with existing accounts.");
@@ -12158,50 +12219,54 @@
     }
 
     @Override
-    public synchronized void setNetworkLoggingEnabled(ComponentName admin, boolean enabled) {
+    public void setNetworkLoggingEnabled(ComponentName admin, boolean enabled) {
         if (!mHasFeature) {
             return;
         }
-        Preconditions.checkNotNull(admin);
-        getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+        synchronized (getLockObject()) {
+            Preconditions.checkNotNull(admin);
+            getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
 
-        if (enabled == isNetworkLoggingEnabledInternalLocked()) {
-            // already in the requested state
-            return;
-        }
-        ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
-        deviceOwner.isNetworkLoggingEnabled = enabled;
-        if (!enabled) {
-            deviceOwner.numNetworkLoggingNotifications = 0;
-            deviceOwner.lastNetworkLoggingNotificationTimeMs = 0;
-        }
-        saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+            if (enabled == isNetworkLoggingEnabledInternalLocked()) {
+                // already in the requested state
+                return;
+            }
+            ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+            deviceOwner.isNetworkLoggingEnabled = enabled;
+            if (!enabled) {
+                deviceOwner.numNetworkLoggingNotifications = 0;
+                deviceOwner.lastNetworkLoggingNotificationTimeMs = 0;
+            }
+            saveSettingsLocked(mInjector.userHandleGetCallingUserId());
 
-        setNetworkLoggingActiveInternal(enabled);
+            setNetworkLoggingActiveInternal(enabled);
+        }
     }
 
-    private synchronized void setNetworkLoggingActiveInternal(boolean active) {
-        final long callingIdentity = mInjector.binderClearCallingIdentity();
-        try {
-            if (active) {
-                mNetworkLogger = new NetworkLogger(this, mInjector.getPackageManagerInternal());
-                if (!mNetworkLogger.startNetworkLogging()) {
+    private void setNetworkLoggingActiveInternal(boolean active) {
+        synchronized (getLockObject()) {
+            final long callingIdentity = mInjector.binderClearCallingIdentity();
+            try {
+                if (active) {
+                    mNetworkLogger = new NetworkLogger(this, mInjector.getPackageManagerInternal());
+                    if (!mNetworkLogger.startNetworkLogging()) {
+                        mNetworkLogger = null;
+                        Slog.wtf(LOG_TAG, "Network logging could not be started due to the logging"
+                                + " service not being available yet.");
+                    }
+                    maybePauseDeviceWideLoggingLocked();
+                    sendNetworkLoggingNotificationLocked();
+                } else {
+                    if (mNetworkLogger != null && !mNetworkLogger.stopNetworkLogging()) {
+                        Slog.wtf(LOG_TAG, "Network logging could not be stopped due to the logging"
+                                + " service not being available yet.");
+                    }
                     mNetworkLogger = null;
-                    Slog.wtf(LOG_TAG, "Network logging could not be started due to the logging"
-                            + " service not being available yet.");
+                    mInjector.getNotificationManager().cancel(SystemMessage.NOTE_NETWORK_LOGGING);
                 }
-                maybePauseDeviceWideLoggingLocked();
-                sendNetworkLoggingNotificationLocked();
-            } else {
-                if (mNetworkLogger != null && !mNetworkLogger.stopNetworkLogging()) {
-                    Slog.wtf(LOG_TAG, "Network logging could not be stopped due to the logging"
-                            + " service not being available yet.");
-                }
-                mNetworkLogger = null;
-                mInjector.getNotificationManager().cancel(SystemMessage.NOTE_NETWORK_LOGGING);
+            } finally {
+                mInjector.binderRestoreCallingIdentity(callingIdentity);
             }
-        } finally {
-            mInjector.binderRestoreCallingIdentity(callingIdentity);
         }
     }
 
@@ -12248,7 +12313,7 @@
         if (!mHasFeature) {
             return false;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             enforceDeviceOwnerOrManageUsers();
             return isNetworkLoggingEnabledInternalLocked();
         }
@@ -12274,7 +12339,7 @@
         Preconditions.checkNotNull(admin);
         ensureDeviceOwnerAndAllUsersAffiliated(admin);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             if (mNetworkLogger == null
                     || !isNetworkLoggingEnabledInternalLocked()) {
                 return null;
@@ -12399,7 +12464,7 @@
         if (token == null || token.length < 32) {
             throw new IllegalArgumentException("token must be at least 32-byte long");
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final int userHandle = mInjector.userHandleGetCallingUserId();
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
@@ -12424,7 +12489,7 @@
         if (!mHasFeature) {
             return false;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final int userHandle = mInjector.userHandleGetCallingUserId();
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
@@ -12447,7 +12512,7 @@
 
     @Override
     public boolean isResetPasswordTokenActive(ComponentName admin) {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final int userHandle = mInjector.userHandleGetCallingUserId();
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
@@ -12469,7 +12534,7 @@
     public boolean resetPasswordWithToken(ComponentName admin, String passwordOrNull, byte[] token,
             int flags) {
         Preconditions.checkNotNull(token);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final int userHandle = mInjector.userHandleGetCallingUserId();
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
 
@@ -12495,7 +12560,7 @@
     public StringParceledListSlice getOwnerInstalledCaCerts(@NonNull UserHandle user) {
         final int userId = user.getIdentifier();
         enforceProfileOwnerOrFullCrossUsersPermission(userId);
-        synchronized (this) {
+        synchronized (getLockObject()) {
             return new StringParceledListSlice(
                     new ArrayList<>(getUserData(userId).mOwnerInstalledCaCerts));
         }
@@ -12507,7 +12572,7 @@
         Preconditions.checkNotNull(admin, "ComponentName is null");
         Preconditions.checkNotNull(packageName, "packageName is null");
         Preconditions.checkNotNull(callback, "callback is null");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
         }
         final int userId = UserHandle.getCallingUserId();
@@ -12535,13 +12600,13 @@
     }
 
     @Override
-    public synchronized void setLogoutEnabled(ComponentName admin, boolean enabled) {
+    public void setLogoutEnabled(ComponentName admin, boolean enabled) {
         if (!mHasFeature) {
             return;
         }
         Preconditions.checkNotNull(admin);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin deviceOwner =
                     getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
 
@@ -12559,7 +12624,7 @@
         if (!mHasFeature) {
             return false;
         }
-        synchronized (this) {
+        synchronized (getLockObject()) {
             ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             return (deviceOwner != null) && deviceOwner.isLogoutEnabled;
         }
@@ -12607,7 +12672,7 @@
 
         final long id = mInjector.binderClearCallingIdentity();
         try {
-            synchronized (this) {
+            synchronized (getLockObject()) {
                 /*
                 * We must ensure the whole process is atomic to prevent the device from ending up
                 * in an invalid state (e.g. no active admin). This could happen if the device
@@ -12713,7 +12778,7 @@
         final String startUserSessionMessageString =
                 startUserSessionMessage != null ? startUserSessionMessage.toString() : null;
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin deviceOwner =
                     getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
 
@@ -12738,7 +12803,7 @@
         final String endUserSessionMessageString =
                 endUserSessionMessage != null ? endUserSessionMessage.toString() : null;
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin deviceOwner =
                     getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
 
@@ -12760,7 +12825,7 @@
         }
         Preconditions.checkNotNull(admin);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin deviceOwner =
                     getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             return deviceOwner.startUserSessionMessage;
@@ -12774,7 +12839,7 @@
         }
         Preconditions.checkNotNull(admin);
 
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final ActiveAdmin deviceOwner =
                     getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
             return deviceOwner.endUserSessionMessage;
@@ -12788,7 +12853,7 @@
     @Override
     @Nullable
     public PersistableBundle getTransferOwnershipBundle() {
-        synchronized (this) {
+        synchronized (getLockObject()) {
             final int callingUserId = mInjector.userHandleGetCallingUserId();
             getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             final File bundleFile = new File(
@@ -12817,7 +12882,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null in addOverrideApn");
         Preconditions.checkNotNull(apnSetting, "ApnSetting is null in addOverrideApn");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -12848,7 +12913,7 @@
         }
         Preconditions.checkNotNull(who, "ComponentName is null in updateOverrideApn");
         Preconditions.checkNotNull(apnSetting, "ApnSetting is null in updateOverrideApn");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -12871,7 +12936,7 @@
             return false;
         }
         Preconditions.checkNotNull(who, "ComponentName is null in removeOverrideApn");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -12899,7 +12964,7 @@
             return Collections.emptyList();
         }
         Preconditions.checkNotNull(who, "ComponentName is null in getOverrideApns");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -12937,7 +13002,7 @@
             return;
         }
         Preconditions.checkNotNull(who, "ComponentName is null in setOverrideApnEnabled");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
@@ -12962,7 +13027,7 @@
             return false;
         }
         Preconditions.checkNotNull(who, "ComponentName is null in isOverrideApnEnabled");
-        synchronized (this) {
+        synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
         }
 
diff --git a/com/android/server/display/AutomaticBrightnessController.java b/com/android/server/display/AutomaticBrightnessController.java
index 5e1afeb..f403953 100644
--- a/com/android/server/display/AutomaticBrightnessController.java
+++ b/com/android/server/display/AutomaticBrightnessController.java
@@ -169,16 +169,6 @@
     // Use -1 if there is no current auto-brightness value available.
     private int mScreenAutoBrightness = -1;
 
-    // The screen auto-brightness adjustment factor in the range -1 (dimmer) to 1 (brighter)
-    private float mScreenAutoBrightnessAdjustment = 0.0f;
-
-    // The maximum range of gamma adjustment possible using the screen
-    // auto-brightness adjustment setting.
-    private float mScreenAutoBrightnessAdjustmentMaxGamma;
-
-    // The last screen auto-brightness gamma.  (For printing in dump() only.)
-    private float mLastScreenAutoBrightnessGamma = 1.0f;
-
     // The current display policy. This is useful, for example,  for knowing when we're dozing,
     // where the light sensor may not be available.
     private int mDisplayPolicy = DisplayPowerRequest.POLICY_OFF;
@@ -186,10 +176,8 @@
     // True if we are collecting a brightness adjustment sample, along with some data
     // for the initial state of the sample.
     private boolean mBrightnessAdjustmentSamplePending;
-    private float mBrightnessAdjustmentSampleOldAdjustment;
     private float mBrightnessAdjustmentSampleOldLux;
     private int mBrightnessAdjustmentSampleOldBrightness;
-    private float mBrightnessAdjustmentSampleOldGamma;
 
     // When the short term model is invalidated, we don't necessarily reset it (i.e. clear the
     // user's adjustment) immediately, but wait for a drastic enough change in the ambient light.
@@ -200,12 +188,11 @@
     private float SHORT_TERM_MODEL_THRESHOLD_RATIO = 0.6f;
 
     public AutomaticBrightnessController(Callbacks callbacks, Looper looper,
-            SensorManager sensorManager, BrightnessMappingStrategy mapper, int lightSensorWarmUpTime,
-            int brightnessMin, int brightnessMax, float dozeScaleFactor,
+            SensorManager sensorManager, BrightnessMappingStrategy mapper,
+            int lightSensorWarmUpTime, int brightnessMin, int brightnessMax, float dozeScaleFactor,
             int lightSensorRate, int initialLightSensorRate, long brighteningLightDebounceConfig,
             long darkeningLightDebounceConfig, boolean resetAmbientLuxAfterWarmUpConfig,
-            int ambientLightHorizon, float autoBrightnessAdjustmentMaxGamma,
-            HysteresisLevels dynamicHysteresis) {
+            int ambientLightHorizon, HysteresisLevels dynamicHysteresis) {
         mCallbacks = callbacks;
         mSensorManager = sensorManager;
         mBrightnessMapper = mapper;
@@ -221,7 +208,6 @@
         mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
         mAmbientLightHorizon = ambientLightHorizon;
         mWeightingIntercept = ambientLightHorizon;
-        mScreenAutoBrightnessAdjustmentMaxGamma = autoBrightnessAdjustmentMaxGamma;
         mDynamicHysteresis = dynamicHysteresis;
         mShortTermModelValid = true;
         mShortTermModelAnchor = -1;
@@ -236,6 +222,9 @@
     }
 
     public int getAutomaticScreenBrightness() {
+        if (!mAmbientLuxValid) {
+            return -1;
+        }
         if (mDisplayPolicy == DisplayPowerRequest.POLICY_DOZE) {
             return (int) (mScreenAutoBrightness * mDozeScaleFactor);
         }
@@ -243,7 +232,7 @@
     }
 
     public float getAutomaticScreenBrightnessAdjustment() {
-        return mScreenAutoBrightnessAdjustment;
+        return mBrightnessMapper.getAutoBrightnessAdjustment();
     }
 
     public void configure(boolean enable, @Nullable BrightnessConfiguration configuration,
@@ -257,7 +246,9 @@
         boolean dozing = (displayPolicy == DisplayPowerRequest.POLICY_DOZE);
         boolean changed = setBrightnessConfiguration(configuration);
         changed |= setDisplayPolicy(displayPolicy);
-        changed |= setScreenAutoBrightnessAdjustment(adjustment);
+        if (userChangedAutoBrightnessAdjustment) {
+            changed |= setAutoBrightnessAdjustment(adjustment);
+        }
         if (userChangedBrightness && enable) {
             // Update the brightness curve with the new user control point. It's critical this
             // happens after we update the autobrightness adjustment since it may reset it.
@@ -322,9 +313,6 @@
         if (DEBUG) {
             Slog.d(TAG, "ShortTermModel: anchor=" + mShortTermModelAnchor);
         }
-        // Reset the brightness adjustment so that the next time we're queried for brightness we
-        // return the value the user set.
-        mScreenAutoBrightnessAdjustment = 0.0f;
         return true;
     }
 
@@ -361,6 +349,7 @@
         pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
         pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
         pw.println("  mAmbientLux=" + mAmbientLux);
+        pw.println("  mAmbientLuxValid=" + mAmbientLuxValid);
         pw.println("  mAmbientLightHorizon=" + mAmbientLightHorizon);
         pw.println("  mBrighteningLuxThreshold=" + mBrighteningLuxThreshold);
         pw.println("  mDarkeningLuxThreshold=" + mDarkeningLuxThreshold);
@@ -369,10 +358,6 @@
         pw.println("  mRecentLightSamples=" + mRecentLightSamples);
         pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
         pw.println("  mScreenAutoBrightness=" + mScreenAutoBrightness);
-        pw.println("  mScreenAutoBrightnessAdjustment=" + mScreenAutoBrightnessAdjustment);
-        pw.println("  mScreenAutoBrightnessAdjustmentMaxGamma="
-                + mScreenAutoBrightnessAdjustmentMaxGamma);
-        pw.println("  mLastScreenAutoBrightnessGamma=" + mLastScreenAutoBrightnessGamma);
         pw.println("  mDisplayPolicy=" + mDisplayPolicy);
         pw.println("  mShortTermModelAnchor=" + mShortTermModelAnchor);
 
@@ -429,8 +414,8 @@
         if (lightSensorRate != mCurrentLightSensorRate) {
             if (DEBUG) {
                 Slog.d(TAG, "adjustLightSensorRate: " +
-                       "previousRate=" + mCurrentLightSensorRate + ", " +
-                       "currentRate=" + lightSensorRate);
+                        "previousRate=" + mCurrentLightSensorRate + ", " +
+                        "currentRate=" + lightSensorRate);
             }
             mCurrentLightSensorRate = lightSensorRate;
             mSensorManager.unregisterListener(mLightSensorListener);
@@ -439,12 +424,8 @@
         }
     }
 
-    private boolean setScreenAutoBrightnessAdjustment(float adjustment) {
-        if (adjustment != mScreenAutoBrightnessAdjustment) {
-            mScreenAutoBrightnessAdjustment = adjustment;
-            return true;
-        }
-        return false;
+    private boolean setAutoBrightnessAdjustment(float adjustment) {
+        return mBrightnessMapper.setAutoBrightnessAdjustment(adjustment);
     }
 
     private void setAmbientLux(float lux) {
@@ -466,12 +447,14 @@
             final float maxAmbientLux =
                 mShortTermModelAnchor + mShortTermModelAnchor * SHORT_TERM_MODEL_THRESHOLD_RATIO;
             if (minAmbientLux < mAmbientLux && mAmbientLux < maxAmbientLux) {
-                Slog.d(TAG, "ShortTermModel: re-validate user data, ambient lux is " +
-                       minAmbientLux + " < " + mAmbientLux + " < " + maxAmbientLux);
+                if (DEBUG) {
+                    Slog.d(TAG, "ShortTermModel: re-validate user data, ambient lux is " +
+                            minAmbientLux + " < " + mAmbientLux + " < " + maxAmbientLux);
+                }
                 mShortTermModelValid = true;
             } else {
                 Slog.d(TAG, "ShortTermModel: reset data, ambient lux is " + mAmbientLux +
-                       "(" + minAmbientLux + ", " + maxAmbientLux + ")");
+                        "(" + minAmbientLux + ", " + maxAmbientLux + ")");
                 resetShortTermModel();
             }
         }
@@ -498,9 +481,9 @@
             }
         }
         if (DEBUG) {
-            Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=("
-                   + mAmbientLightRingBuffer.getTime(endIndex) + ", "
-                   + mAmbientLightRingBuffer.getLux(endIndex) + ")");
+            Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
+                    mAmbientLightRingBuffer.getTime(endIndex) + ", " +
+                    mAmbientLightRingBuffer.getLux(endIndex) + ")");
         }
         float sum = 0;
         float totalWeight = 0;
@@ -517,8 +500,8 @@
             float lux = mAmbientLightRingBuffer.getLux(i);
             if (DEBUG) {
                 Slog.d(TAG, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
-                       "lux=" + lux + ", " +
-                       "weight=" + weight);
+                        "lux=" + lux + ", " +
+                        "weight=" + weight);
             }
             totalWeight += weight;
             sum += lux * weight;
@@ -526,8 +509,8 @@
         }
         if (DEBUG) {
             Slog.d(TAG, "calculateAmbientLux: " +
-                   "totalWeight=" + totalWeight + ", " +
-                   "newAmbientLux=" + (sum / totalWeight));
+                    "totalWeight=" + totalWeight + ", " +
+                    "newAmbientLux=" + (sum / totalWeight));
         }
         return sum / totalWeight;
     }
@@ -581,8 +564,8 @@
             if (time < timeWhenSensorWarmedUp) {
                 if (DEBUG) {
                     Slog.d(TAG, "updateAmbientLux: Sensor not  ready yet: " +
-                           "time=" + time + ", " +
-                           "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
+                            "time=" + time + ", " +
+                            "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
                 }
                 mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX,
                         timeWhenSensorWarmedUp);
@@ -621,10 +604,10 @@
             setAmbientLux(fastAmbientLux);
             if (DEBUG) {
                 Slog.d(TAG, "updateAmbientLux: " +
-                       ((fastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " +
-                       "mBrighteningLuxThreshold=" + mBrighteningLuxThreshold + ", " +
-                       "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " +
-                       "mAmbientLux=" + mAmbientLux);
+                        ((fastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " +
+                        "mBrighteningLuxThreshold=" + mBrighteningLuxThreshold + ", " +
+                        "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " +
+                        "mAmbientLux=" + mAmbientLux);
             }
             updateAutoBrightness(true);
             nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
@@ -641,7 +624,7 @@
                 nextTransitionTime > time ? nextTransitionTime : time + mNormalLightSensorRate;
         if (DEBUG) {
             Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for " +
-                   nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
+                    nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
         }
         mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime);
     }
@@ -652,40 +635,17 @@
         }
 
         float value = mBrightnessMapper.getBrightness(mAmbientLux);
-        float gamma = 1.0f;
-
-        if (USE_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT
-                && mScreenAutoBrightnessAdjustment != 0.0f) {
-            final float adjGamma = MathUtils.pow(mScreenAutoBrightnessAdjustmentMaxGamma,
-                    Math.min(1.0f, Math.max(-1.0f, -mScreenAutoBrightnessAdjustment)));
-            gamma *= adjGamma;
-            if (DEBUG) {
-                Slog.d(TAG, "updateAutoBrightness: adjGamma=" + adjGamma);
-            }
-        }
-
-        if (gamma != 1.0f) {
-            final float in = value;
-            value = MathUtils.pow(value, gamma);
-            if (DEBUG) {
-                Slog.d(TAG, "updateAutoBrightness: " +
-                       "gamma=" + gamma + ", " +
-                       "in=" + in + ", " +
-                       "out=" + value);
-            }
-        }
 
         int newScreenAutoBrightness =
                 clampScreenBrightness(Math.round(value * PowerManager.BRIGHTNESS_ON));
         if (mScreenAutoBrightness != newScreenAutoBrightness) {
             if (DEBUG) {
                 Slog.d(TAG, "updateAutoBrightness: " +
-                       "mScreenAutoBrightness=" + mScreenAutoBrightness + ", " +
-                       "newScreenAutoBrightness=" + newScreenAutoBrightness);
+                        "mScreenAutoBrightness=" + mScreenAutoBrightness + ", " +
+                        "newScreenAutoBrightness=" + newScreenAutoBrightness);
             }
 
             mScreenAutoBrightness = newScreenAutoBrightness;
-            mLastScreenAutoBrightnessGamma = gamma;
             if (sendUpdate) {
                 mCallbacks.updateBrightness();
             }
@@ -700,10 +660,8 @@
     private void prepareBrightnessAdjustmentSample() {
         if (!mBrightnessAdjustmentSamplePending) {
             mBrightnessAdjustmentSamplePending = true;
-            mBrightnessAdjustmentSampleOldAdjustment = mScreenAutoBrightnessAdjustment;
             mBrightnessAdjustmentSampleOldLux = mAmbientLuxValid ? mAmbientLux : -1;
             mBrightnessAdjustmentSampleOldBrightness = mScreenAutoBrightness;
-            mBrightnessAdjustmentSampleOldGamma = mLastScreenAutoBrightnessGamma;
         } else {
             mHandler.removeMessages(MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE);
         }
@@ -725,22 +683,16 @@
             if (mAmbientLuxValid && mScreenAutoBrightness >= 0) {
                 if (DEBUG) {
                     Slog.d(TAG, "Auto-brightness adjustment changed by user: " +
-                           "adj=" + mScreenAutoBrightnessAdjustment + ", " +
-                           "lux=" + mAmbientLux + ", " +
-                           "brightness=" + mScreenAutoBrightness + ", " +
-                           "gamma=" + mLastScreenAutoBrightnessGamma + ", " +
-                           "ring=" + mAmbientLightRingBuffer);
+                            "lux=" + mAmbientLux + ", " +
+                            "brightness=" + mScreenAutoBrightness + ", " +
+                            "ring=" + mAmbientLightRingBuffer);
                 }
 
                 EventLog.writeEvent(EventLogTags.AUTO_BRIGHTNESS_ADJ,
-                        mBrightnessAdjustmentSampleOldAdjustment,
                         mBrightnessAdjustmentSampleOldLux,
                         mBrightnessAdjustmentSampleOldBrightness,
-                        mBrightnessAdjustmentSampleOldGamma,
-                        mScreenAutoBrightnessAdjustment,
                         mAmbientLux,
-                        mScreenAutoBrightness,
-                        mLastScreenAutoBrightnessGamma);
+                        mScreenAutoBrightness);
             }
         }
     }
diff --git a/com/android/server/display/BrightnessMappingStrategy.java b/com/android/server/display/BrightnessMappingStrategy.java
index 4313d17..f7439b9 100644
--- a/com/android/server/display/BrightnessMappingStrategy.java
+++ b/com/android/server/display/BrightnessMappingStrategy.java
@@ -28,6 +28,7 @@
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.utils.Plog;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -41,11 +42,13 @@
  */
 public abstract class BrightnessMappingStrategy {
     private static final String TAG = "BrightnessMappingStrategy";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
 
     private static final float LUX_GRAD_SMOOTHING = 0.25f;
     private static final float MAX_GRAD = 1.0f;
 
+    private static final Plog PLOG = Plog.createSystemPlog(TAG);
+
     @Nullable
     public static BrightnessMappingStrategy create(Resources resources) {
         float[] luxLevels = getLuxLevels(resources.getIntArray(
@@ -54,6 +57,9 @@
                 com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
         float[] brightnessLevelsNits = getFloatArray(resources.obtainTypedArray(
                 com.android.internal.R.array.config_autoBrightnessDisplayValuesNits));
+        float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
+                com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
+                1, 1);
 
         float[] nitsRange = getFloatArray(resources.obtainTypedArray(
                 com.android.internal.R.array.config_screenBrightnessNits));
@@ -68,14 +74,16 @@
                     com.android.internal.R.integer.config_screenBrightnessSettingMaximum);
             if (backlightRange[0] > minimumBacklight
                     || backlightRange[backlightRange.length - 1] < maximumBacklight) {
-                Slog.w(TAG, "Screen brightness mapping does not cover whole range of available"
-                        + " backlight values, autobrightness functionality may be impaired.");
+                Slog.w(TAG, "Screen brightness mapping does not cover whole range of available " +
+                        "backlight values, autobrightness functionality may be impaired.");
             }
             BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
             builder.setCurve(luxLevels, brightnessLevelsNits);
-            return new PhysicalMappingStrategy(builder.build(), nitsRange, backlightRange);
+            return new PhysicalMappingStrategy(builder.build(), nitsRange, backlightRange,
+                    autoBrightnessAdjustmentMaxGamma);
         } else if (isValidMapping(luxLevels, brightnessLevelsBacklight)) {
-            return new SimpleMappingStrategy(luxLevels, brightnessLevelsBacklight);
+            return new SimpleMappingStrategy(luxLevels, brightnessLevelsBacklight,
+                    autoBrightnessAdjustmentMaxGamma);
         } else {
             return null;
         }
@@ -173,6 +181,26 @@
     public abstract float getBrightness(float lux);
 
     /**
+     * Returns the current auto-brightness adjustment.
+     *
+     * The returned adjustment is a value in the range [-1.0, 1.0] such that
+     * {@code config_autoBrightnessAdjustmentMaxGamma<sup>-adjustment</sup>} is used to gamma
+     * correct the brightness curve.
+     */
+    public abstract float getAutoBrightnessAdjustment();
+
+    /**
+     * Sets the auto-brightness adjustment.
+     *
+     * @param adjustment The desired auto-brightness adjustment.
+     * @return Whether the auto-brightness adjustment has changed.
+     *
+     * @Deprecated The auto-brightness adjustment should not be set directly, but rather inferred
+     * from user data points.
+     */
+    public abstract boolean setAutoBrightnessAdjustment(float adjustment);
+
+    /**
      * Converts the provided backlight value to nits if possible.
      *
      * Returns -1.0f if there's no available mapping for the backlight to nits.
@@ -200,12 +228,13 @@
      */
     public abstract void clearUserDataPoints();
 
-    /** @return true if there are any short term adjustments applied to the curve */
+    /** @return True if there are any short term adjustments applied to the curve. */
     public abstract boolean hasUserDataPoints();
 
-    /** @return true if the current brightness config is the default one */
+    /** @return True if the current brightness configuration is the default one. */
     public abstract boolean isDefaultConfig();
 
+    /** @return The default brightness configuration. */
     public abstract BrightnessConfiguration getDefaultConfig();
 
     public abstract void dump(PrintWriter pw);
@@ -216,22 +245,8 @@
         return (float) brightness / PowerManager.BRIGHTNESS_ON;
     }
 
-    private static Spline createSpline(float[] x, float[] y) {
-        Spline spline = Spline.createSpline(x, y);
-        if (DEBUG) {
-            Slog.d(TAG, "Spline: " + spline);
-            for (float v = 1f; v < x[x.length - 1] * 1.25f; v *= 1.25f) {
-                Slog.d(TAG, String.format("  %7.1f: %7.1f", v, spline.interpolate(v)));
-            }
-        }
-        return spline;
-    }
-
     private static Pair<float[], float[]> insertControlPoint(
             float[] luxLevels, float[] brightnessLevels, float lux, float brightness) {
-        if (DEBUG) {
-            Slog.d(TAG, "Inserting new control point at (" + lux + ", " + brightness + ")");
-        }
         final int idx = findInsertionPoint(luxLevels, lux);
         final float[] newLuxLevels;
         final float[] newBrightnessLevels;
@@ -274,9 +289,7 @@
 
     private static void smoothCurve(float[] lux, float[] brightness, int idx) {
         if (DEBUG) {
-            Slog.d(TAG, "smoothCurve(lux=" + Arrays.toString(lux)
-                    + ", brightness=" + Arrays.toString(brightness)
-                    + ", idx=" + idx + ")");
+            PLOG.logCurve("unsmoothed curve", lux, brightness);
         }
         float prevLux = lux[idx];
         float prevBrightness = brightness[idx];
@@ -294,7 +307,6 @@
             prevBrightness = newBrightness;
             brightness[i] = newBrightness;
         }
-
         // Smooth curve for data points below the newly introduced point
         prevLux = lux[idx];
         prevBrightness = brightness[idx];
@@ -312,8 +324,7 @@
             brightness[i] = newBrightness;
         }
         if (DEBUG) {
-            Slog.d(TAG, "Smoothed Curve: lux=" + Arrays.toString(lux)
-                    + ", brightness=" + Arrays.toString(brightness));
+            PLOG.logCurve("smoothed curve", lux, brightness);
         }
     }
 
@@ -323,6 +334,72 @@
                     - MathUtils.log(prevLux + LUX_GRAD_SMOOTHING)));
     }
 
+    private static float inferAutoBrightnessAdjustment(float maxGamma,
+            float desiredBrightness, float currentBrightness) {
+        float adjustment = 0;
+        float gamma = Float.NaN;
+        // Extreme edge cases: use a simpler heuristic, as proper gamma correction around the edges
+        // affects the curve rather drastically.
+        if (currentBrightness <= 0.1f || currentBrightness >= 0.9f) {
+            adjustment = (desiredBrightness - currentBrightness) * 2;
+        // Edge case: darkest adjustment possible.
+        } else if (desiredBrightness == 0) {
+            adjustment = -1;
+        // Edge case: brightest adjustment possible.
+        } else if (desiredBrightness == 1) {
+            adjustment = +1;
+        } else {
+            // current^gamma = desired => gamma = log[current](desired)
+            gamma = MathUtils.log(desiredBrightness) / MathUtils.log(currentBrightness);
+            // max^-adjustment = gamma => adjustment = -log[max](gamma)
+            adjustment = -MathUtils.log(gamma) / MathUtils.log(maxGamma);
+        }
+        adjustment = MathUtils.constrain(adjustment, -1, +1);
+        if (DEBUG) {
+            Slog.d(TAG, "inferAutoBrightnessAdjustment: " + maxGamma + "^" + -adjustment + "=" +
+                    MathUtils.pow(maxGamma, -adjustment) + " == " + gamma);
+            Slog.d(TAG, "inferAutoBrightnessAdjustment: " + currentBrightness + "^" + gamma + "=" +
+                    MathUtils.pow(currentBrightness, gamma) + " == " + desiredBrightness);
+        }
+        return adjustment;
+    }
+
+    private static Pair<float[], float[]> getAdjustedCurve(float[] lux, float[] brightness,
+            float userLux, float userBrightness, float adjustment, float maxGamma) {
+        float[] newLux = lux;
+        float[] newBrightness = Arrays.copyOf(brightness, brightness.length);
+        if (DEBUG) {
+            PLOG.logCurve("unadjusted curve", newLux, newBrightness);
+        }
+        adjustment = MathUtils.constrain(adjustment, -1, 1);
+        float gamma = MathUtils.pow(maxGamma, -adjustment);
+        if (DEBUG) {
+            Slog.d(TAG, "getAdjustedCurve: " + maxGamma + "^" + -adjustment + "=" +
+                    MathUtils.pow(maxGamma, -adjustment) + " == " + gamma);
+        }
+        if (gamma != 1) {
+            for (int i = 0; i < newBrightness.length; i++) {
+                newBrightness[i] = MathUtils.pow(newBrightness[i], gamma);
+            }
+        }
+        if (DEBUG) {
+            PLOG.logCurve("gamma adjusted curve", newLux, newBrightness);
+        }
+        if (userLux != -1) {
+            Pair<float[], float[]> curve = insertControlPoint(newLux, newBrightness, userLux,
+                    userBrightness);
+            newLux = curve.first;
+            newBrightness = curve.second;
+            if (DEBUG) {
+                PLOG.logCurve("gamma and user adjusted curve", newLux, newBrightness);
+                // This is done for comparison.
+                curve = insertControlPoint(lux, brightness, userLux, userBrightness);
+                PLOG.logCurve("user adjusted curve", curve.first ,curve.second);
+            }
+        }
+        return Pair.create(newLux, newBrightness);
+    }
+
     /**
      * A {@link BrightnessMappingStrategy} that maps from ambient room brightness directly to the
      * backlight of the display.
@@ -337,10 +414,12 @@
         private final float[] mBrightness;
 
         private Spline mSpline;
+        private float mMaxGamma;
+        private float mAutoBrightnessAdjustment;
         private float mUserLux;
         private float mUserBrightness;
 
-        public SimpleMappingStrategy(float[] lux, int[] brightness) {
+        public SimpleMappingStrategy(float[] lux, int[] brightness, float maxGamma) {
             Preconditions.checkArgument(lux.length != 0 && brightness.length != 0,
                     "Lux and brightness arrays must not be empty!");
             Preconditions.checkArgument(lux.length == brightness.length,
@@ -357,9 +436,14 @@
                 mBrightness[i] = normalizeAbsoluteBrightness(brightness[i]);
             }
 
-            mSpline = createSpline(mLux, mBrightness);
+            mMaxGamma = maxGamma;
+            mAutoBrightnessAdjustment = 0;
             mUserLux = -1;
             mUserBrightness = -1;
+            if (DEBUG) {
+                PLOG.start("simple mapping strategy");
+            }
+            computeSpline();
         }
 
         @Override
@@ -373,27 +457,65 @@
         }
 
         @Override
+        public float getAutoBrightnessAdjustment() {
+            return mAutoBrightnessAdjustment;
+        }
+
+        @Override
+        public boolean setAutoBrightnessAdjustment(float adjustment) {
+            adjustment = MathUtils.constrain(adjustment, -1, 1);
+            if (adjustment == mAutoBrightnessAdjustment) {
+                return false;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "setAutoBrightnessAdjustment: " + mAutoBrightnessAdjustment + " => " +
+                        adjustment);
+                PLOG.start("auto-brightness adjustment");
+            }
+            mAutoBrightnessAdjustment = adjustment;
+            computeSpline();
+            return true;
+        }
+
+        @Override
         public float convertToNits(int backlight) {
             return -1.0f;
         }
 
         @Override
         public void addUserDataPoint(float lux, float brightness) {
+            float unadjustedBrightness = getUnadjustedBrightness(lux);
             if (DEBUG){
-                Slog.d(TAG, "addUserDataPoint(lux=" + lux + ", brightness=" + brightness + ")");
+                Slog.d(TAG, "addUserDataPoint: (" + lux + "," + brightness + ")");
+                PLOG.start("add user data point")
+                        .logPoint("user data point", lux, brightness)
+                        .logPoint("current brightness", lux, unadjustedBrightness);
             }
-            Pair<float[], float[]> curve = insertControlPoint(mLux, mBrightness, lux, brightness);
-            mSpline = createSpline(curve.first, curve.second);
+            float adjustment = inferAutoBrightnessAdjustment(mMaxGamma,
+                    brightness /* desiredBrightness */,
+                    unadjustedBrightness /* currentBrightness */);
+            if (DEBUG) {
+                Slog.d(TAG, "addUserDataPoint: " + mAutoBrightnessAdjustment + " => " +
+                        adjustment);
+            }
+            mAutoBrightnessAdjustment = adjustment;
             mUserLux = lux;
             mUserBrightness = brightness;
+            computeSpline();
         }
 
         @Override
         public void clearUserDataPoints() {
             if (mUserLux != -1) {
-                mSpline = createSpline(mLux, mBrightness);
+                if (DEBUG) {
+                    Slog.d(TAG, "clearUserDataPoints: " + mAutoBrightnessAdjustment + " => 0");
+                    PLOG.start("clear user data points")
+                            .logPoint("user data point", mUserLux, mUserBrightness);
+                }
+                mAutoBrightnessAdjustment = 0;
                 mUserLux = -1;
                 mUserBrightness = -1;
+                computeSpline();
             }
         }
 
@@ -408,15 +530,30 @@
         }
 
         @Override
-        public BrightnessConfiguration getDefaultConfig() { return null; }
+        public BrightnessConfiguration getDefaultConfig() {
+            return null;
+        }
 
         @Override
         public void dump(PrintWriter pw) {
             pw.println("SimpleMappingStrategy");
             pw.println("  mSpline=" + mSpline);
+            pw.println("  mMaxGamma=" + mMaxGamma);
+            pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
             pw.println("  mUserLux=" + mUserLux);
             pw.println("  mUserBrightness=" + mUserBrightness);
         }
+
+        private void computeSpline() {
+            Pair<float[], float[]> curve = getAdjustedCurve(mLux, mBrightness, mUserLux,
+                    mUserBrightness, mAutoBrightnessAdjustment, mMaxGamma);
+            mSpline = Spline.createSpline(curve.first, curve.second);
+        }
+
+        private float getUnadjustedBrightness(float lux) {
+            Spline spline = Spline.createSpline(mLux, mBrightness);
+            return spline.interpolate(lux);
+        }
     }
 
     /** A {@link BrightnessMappingStrategy} that maps from ambient room brightness to the physical
@@ -445,11 +582,13 @@
         // a brightness in nits.
         private Spline mBacklightToNitsSpline;
 
+        private float mMaxGamma;
+        private float mAutoBrightnessAdjustment;
         private float mUserLux;
         private float mUserBrightness;
 
-        public PhysicalMappingStrategy(BrightnessConfiguration config,
-                float[] nits, int[] backlight) {
+        public PhysicalMappingStrategy(BrightnessConfiguration config, float[] nits,
+                                       int[] backlight, float maxGamma) {
             Preconditions.checkArgument(nits.length != 0 && backlight.length != 0,
                     "Nits and backlight arrays must not be empty!");
             Preconditions.checkArgument(nits.length == backlight.length,
@@ -459,6 +598,8 @@
             Preconditions.checkArrayElementsInRange(backlight,
                     PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON, "backlight");
 
+            mMaxGamma = maxGamma;
+            mAutoBrightnessAdjustment = 0;
             mUserLux = -1;
             mUserBrightness = -1;
 
@@ -469,11 +610,15 @@
                 normalizedBacklight[i] = normalizeAbsoluteBrightness(backlight[i]);
             }
 
-            mNitsToBacklightSpline = createSpline(nits, normalizedBacklight);
-            mBacklightToNitsSpline = createSpline(normalizedBacklight, nits);
+            mNitsToBacklightSpline = Spline.createSpline(nits, normalizedBacklight);
+            mBacklightToNitsSpline = Spline.createSpline(normalizedBacklight, nits);
 
             mDefaultConfig = config;
-            setBrightnessConfiguration(config);
+            if (DEBUG) {
+                PLOG.start("physical mapping strategy");
+            }
+            mConfig = config;
+            computeSpline();
         }
 
         @Override
@@ -484,10 +629,11 @@
             if (config.equals(mConfig)) {
                 return false;
             }
-
-            Pair<float[], float[]> curve = config.getCurve();
-            mBrightnessSpline = createSpline(curve.first /*lux*/, curve.second /*nits*/);
+            if (DEBUG) {
+                PLOG.start("brightness configuration");
+            }
             mConfig = config;
+            computeSpline();
             return true;
         }
 
@@ -499,31 +645,65 @@
         }
 
         @Override
+        public float getAutoBrightnessAdjustment() {
+            return mAutoBrightnessAdjustment;
+        }
+
+        @Override
+        public boolean setAutoBrightnessAdjustment(float adjustment) {
+            adjustment = MathUtils.constrain(adjustment, -1, 1);
+            if (adjustment == mAutoBrightnessAdjustment) {
+                return false;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "setAutoBrightnessAdjustment: " + mAutoBrightnessAdjustment + " => " +
+                        adjustment);
+                PLOG.start("auto-brightness adjustment");
+            }
+            mAutoBrightnessAdjustment = adjustment;
+            computeSpline();
+            return true;
+        }
+
+        @Override
         public float convertToNits(int backlight) {
             return mBacklightToNitsSpline.interpolate(normalizeAbsoluteBrightness(backlight));
         }
 
         @Override
-        public void addUserDataPoint(float lux, float backlight) {
+        public void addUserDataPoint(float lux, float brightness) {
+            float unadjustedBrightness = getUnadjustedBrightness(lux);
             if (DEBUG){
-                Slog.d(TAG, "addUserDataPoint(lux=" + lux + ", backlight=" + backlight + ")");
+                Slog.d(TAG, "addUserDataPoint: (" + lux + "," + brightness + ")");
+                PLOG.start("add user data point")
+                        .logPoint("user data point", lux, brightness)
+                        .logPoint("current brightness", lux, unadjustedBrightness);
             }
-            float brightness = mBacklightToNitsSpline.interpolate(backlight);
-            Pair<float[], float[]> defaultCurve = mConfig.getCurve();
-            Pair<float[], float[]> newCurve =
-                    insertControlPoint(defaultCurve.first, defaultCurve.second, lux, brightness);
-            mBrightnessSpline = createSpline(newCurve.first, newCurve.second);
+            float adjustment = inferAutoBrightnessAdjustment(mMaxGamma,
+                    brightness /* desiredBrightness */,
+                    unadjustedBrightness /* currentBrightness */);
+            if (DEBUG) {
+                Slog.d(TAG, "addUserDataPoint: " + mAutoBrightnessAdjustment + " => " +
+                        adjustment);
+            }
+            mAutoBrightnessAdjustment = adjustment;
             mUserLux = lux;
             mUserBrightness = brightness;
+            computeSpline();
         }
 
         @Override
         public void clearUserDataPoints() {
             if (mUserLux != -1) {
-                Pair<float[], float[]> defaultCurve = mConfig.getCurve();
-                mBrightnessSpline = createSpline(defaultCurve.first, defaultCurve.second);
+                if (DEBUG) {
+                    Slog.d(TAG, "clearUserDataPoints: " + mAutoBrightnessAdjustment + " => 0");
+                    PLOG.start("clear user data points")
+                            .logPoint("user data point", mUserLux, mUserBrightness);
+                }
+                mAutoBrightnessAdjustment = 0;
                 mUserLux = -1;
                 mUserBrightness = -1;
+                computeSpline();
             }
         }
 
@@ -538,7 +718,9 @@
         }
 
         @Override
-        public BrightnessConfiguration getDefaultConfig() { return mDefaultConfig; }
+        public BrightnessConfiguration getDefaultConfig() {
+            return mDefaultConfig;
+        }
 
         @Override
         public void dump(PrintWriter pw) {
@@ -546,8 +728,35 @@
             pw.println("  mConfig=" + mConfig);
             pw.println("  mBrightnessSpline=" + mBrightnessSpline);
             pw.println("  mNitsToBacklightSpline=" + mNitsToBacklightSpline);
+            pw.println("  mMaxGamma=" + mMaxGamma);
+            pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
             pw.println("  mUserLux=" + mUserLux);
             pw.println("  mUserBrightness=" + mUserBrightness);
         }
+
+        private void computeSpline() {
+            Pair<float[], float[]> defaultCurve = mConfig.getCurve();
+            float[] defaultLux = defaultCurve.first;
+            float[] defaultNits = defaultCurve.second;
+            float[] defaultBacklight = new float[defaultNits.length];
+            for (int i = 0; i < defaultBacklight.length; i++) {
+                defaultBacklight[i] = mNitsToBacklightSpline.interpolate(defaultNits[i]);
+            }
+            Pair<float[], float[]> curve = getAdjustedCurve(defaultLux, defaultBacklight, mUserLux,
+                    mUserBrightness, mAutoBrightnessAdjustment, mMaxGamma);
+            float[] lux = curve.first;
+            float[] backlight = curve.second;
+            float[] nits = new float[backlight.length];
+            for (int i = 0; i < nits.length; i++) {
+                nits[i] = mBacklightToNitsSpline.interpolate(backlight[i]);
+            }
+            mBrightnessSpline = Spline.createSpline(lux, nits);
+        }
+
+        private float getUnadjustedBrightness(float lux) {
+            Pair<float[], float[]> curve = mConfig.getCurve();
+            Spline spline = Spline.createSpline(curve.first, curve.second);
+            return mNitsToBacklightSpline.interpolate(spline.interpolate(lux));
+        }
     }
 }
diff --git a/com/android/server/display/BrightnessTracker.java b/com/android/server/display/BrightnessTracker.java
index 88df195..905c7e3 100644
--- a/com/android/server/display/BrightnessTracker.java
+++ b/com/android/server/display/BrightnessTracker.java
@@ -686,10 +686,15 @@
     }
 
     public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(int userId) {
-        ArrayList<AmbientBrightnessDayStats> stats = mAmbientBrightnessStatsTracker.getUserStats(
-                userId);
-        return (stats != null) ? new ParceledListSlice<>(stats) : new ParceledListSlice<>(
-                Collections.EMPTY_LIST);
+        if (mAmbientBrightnessStatsTracker != null) {
+            ArrayList<AmbientBrightnessDayStats> stats =
+                    mAmbientBrightnessStatsTracker.getUserStats(
+                            userId);
+            if (stats != null) {
+                return new ParceledListSlice<>(stats);
+            }
+        }
+        return ParceledListSlice.emptyList();
     }
 
     // Not allowed to keep the SensorEvent so used to copy the data we care about.
diff --git a/com/android/server/display/ColorDisplayService.java b/com/android/server/display/ColorDisplayService.java
index b3d309d..37035c3 100644
--- a/com/android/server/display/ColorDisplayService.java
+++ b/com/android/server/display/ColorDisplayService.java
@@ -300,6 +300,11 @@
         dtm.setColorMode(mode, mIsActivated ? mMatrixNight : MATRIX_IDENTITY);
     }
 
+    @Override
+    public void onAccessibilityTransformChanged(boolean state) {
+        onDisplayColorModeChanged(mController.getColorMode());
+    }
+
     /**
      * Set coefficients based on native mode. Use DisplayTransformManager#isNativeModeEnabled while
      * setting is stable; when setting is changing, pass native mode selection directly.
diff --git a/com/android/server/display/DisplayManagerService.java b/com/android/server/display/DisplayManagerService.java
index c7ae1f4..0f531a8 100644
--- a/com/android/server/display/DisplayManagerService.java
+++ b/com/android/server/display/DisplayManagerService.java
@@ -36,11 +36,13 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Point;
 import android.hardware.SensorManager;
 import android.hardware.display.AmbientBrightnessDayStats;
 import android.hardware.display.BrightnessChangeEvent;
 import android.hardware.display.BrightnessConfiguration;
+import android.hardware.display.Curve;
 import android.hardware.display.DisplayManagerGlobal;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayViewport;
@@ -61,16 +63,21 @@
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.ServiceManager;
+import android.os.ShellCallback;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.IntArray;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.Spline;
 import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.Surface;
@@ -273,6 +280,11 @@
 
     private final Injector mInjector;
 
+    // The minimum brightness curve, which guarantess that any brightness curve that dips below it
+    // is rejected by the system.
+    private final Curve mMinimumBrightnessCurve;
+    private final Spline mMinimumBrightnessSpline;
+
     public DisplayManagerService(Context context) {
         this(context, new Injector());
     }
@@ -286,8 +298,15 @@
         mUiHandler = UiThread.getHandler();
         mDisplayAdapterListener = new DisplayAdapterListener();
         mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false);
+        Resources resources = mContext.getResources();
         mDefaultDisplayDefaultColorMode = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_defaultDisplayDefaultColorMode);
+        float[] lux = getFloatArray(resources.obtainTypedArray(
+                com.android.internal.R.array.config_minimumBrightnessCurveLux));
+        float[] nits = getFloatArray(resources.obtainTypedArray(
+                com.android.internal.R.array.config_minimumBrightnessCurveNits));
+        mMinimumBrightnessCurve = new Curve(lux, nits);
+        mMinimumBrightnessSpline = Spline.createSpline(lux, nits);
 
         PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
@@ -1029,9 +1048,15 @@
         }
     }
 
+    @VisibleForTesting
+    Curve getMinimumBrightnessCurveInternal() {
+        return mMinimumBrightnessCurve;
+    }
+
     private void setBrightnessConfigurationForUserInternal(
-            @NonNull BrightnessConfiguration c, @UserIdInt int userId,
+            @Nullable BrightnessConfiguration c, @UserIdInt int userId,
             @Nullable String packageName) {
+        validateBrightnessConfiguration(c);
         final int userSerial = getUserManager().getUserSerialNumber(userId);
         synchronized (mSyncRoot) {
             try {
@@ -1046,6 +1071,28 @@
         }
     }
 
+    @VisibleForTesting
+    void validateBrightnessConfiguration(BrightnessConfiguration config) {
+        if (config == null) {
+            return;
+        }
+        if (isBrightnessConfigurationTooDark(config)) {
+            throw new IllegalArgumentException("brightness curve is too dark");
+        }
+    }
+
+    private boolean isBrightnessConfigurationTooDark(BrightnessConfiguration config) {
+        Pair<float[], float[]> curve = config.getCurve();
+        float[] lux = curve.first;
+        float[] nits = curve.second;
+        for (int i = 0; i < lux.length; i++) {
+            if (nits[i] < mMinimumBrightnessSpline.interpolate(lux[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void loadBrightnessConfiguration() {
         synchronized (mSyncRoot) {
             final int userSerial = getUserManager().getUserSerialNumber(mCurrentUserId);
@@ -1366,6 +1413,16 @@
         }
     }
 
+    private static float[] getFloatArray(TypedArray array) {
+        int length = array.length();
+        float[] floatArray = new float[length];
+        for (int i = 0; i < length; i++) {
+            floatArray[i] = array.getFloat(i, Float.NaN);
+        }
+        array.recycle();
+        return floatArray;
+    }
+
     /**
      * This is the object that everything in the display manager locks on.
      * We make it an inner class within the {@link DisplayManagerService} to so that it is
@@ -1983,6 +2040,39 @@
             }
         }
 
+        @Override // Binder call
+        public void onShellCommand(FileDescriptor in, FileDescriptor out,
+                FileDescriptor err, String[] args, ShellCallback callback,
+                ResultReceiver resultReceiver) {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                DisplayManagerShellCommand command = new DisplayManagerShellCommand(this);
+                command.exec(this, in, out, err, args, callback, resultReceiver);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override // Binder call
+        public Curve getMinimumBrightnessCurve() {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                return getMinimumBrightnessCurveInternal();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        void setBrightness(int brightness) {
+            Settings.System.putIntForUser(mContext.getContentResolver(),
+                    Settings.System.SCREEN_BRIGHTNESS, brightness, UserHandle.USER_CURRENT);
+        }
+
+        void resetBrightnessConfiguration() {
+            setBrightnessConfigurationForUserInternal(null, mContext.getUserId(),
+                    mContext.getPackageName());
+        }
+
         private boolean validatePackageName(int uid, String packageName) {
             if (packageName != null) {
                 String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
diff --git a/com/android/server/display/DisplayManagerShellCommand.java b/com/android/server/display/DisplayManagerShellCommand.java
new file mode 100644
index 0000000..27cad1e
--- /dev/null
+++ b/com/android/server/display/DisplayManagerShellCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2018 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.server.display;
+
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.lang.NumberFormatException;
+
+class DisplayManagerShellCommand extends ShellCommand {
+    private static final String TAG = "DisplayManagerShellCommand";
+
+    private final DisplayManagerService.BinderService mService;
+
+    DisplayManagerShellCommand(DisplayManagerService.BinderService service) {
+        mService = service;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+        final PrintWriter pw = getOutPrintWriter();
+        switch(cmd) {
+            case "set-brightness":
+                return setBrightness();
+            case "reset-brightness-configuration":
+                return resetBrightnessConfiguration();
+            default:
+                return handleDefaultCommands(cmd);
+        }
+    }
+
+    @Override
+    public void onHelp() {
+        final PrintWriter pw = getOutPrintWriter();
+        pw.println("Display manager commands:");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println();
+        pw.println("  set-brightness BRIGHTNESS");
+        pw.println("    Sets the current brightness to BRIGHTNESS (a number between 0 and 1).");
+        pw.println("  reset-brightness-configuration");
+        pw.println("    Reset the brightness to its default configuration.");
+        pw.println();
+        Intent.printIntentArgsHelp(pw , "");
+    }
+
+    private int setBrightness() {
+        String brightnessText = getNextArg();
+        if (brightnessText == null) {
+            getErrPrintWriter().println("Error: no brightness specified");
+            return 1;
+        }
+        float brightness = -1;
+        try {
+            brightness = Float.parseFloat(brightnessText);
+        } catch (NumberFormatException e) {
+        }
+        if (brightness < 0 || brightness > 1) {
+            getErrPrintWriter().println("Error: brightness should be a number between 0 and 1");
+            return 1;
+        }
+        mService.setBrightness((int) brightness * 255);
+        return 0;
+    }
+
+    private int resetBrightnessConfiguration() {
+        mService.resetBrightnessConfiguration();
+        return 0;
+    }
+}
diff --git a/com/android/server/display/DisplayPowerController.java b/com/android/server/display/DisplayPowerController.java
index 1784ef1..5f4c8ef 100644
--- a/com/android/server/display/DisplayPowerController.java
+++ b/com/android/server/display/DisplayPowerController.java
@@ -424,9 +424,6 @@
                     com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
             int ambientLightHorizon = resources.getInteger(
                     com.android.internal.R.integer.config_autoBrightnessAmbientLightHorizon);
-            float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
-                    com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
-                    1, 1);
 
             int lightSensorWarmUpTimeConfig = resources.getInteger(
                     com.android.internal.R.integer.config_lightSensorWarmupTime);
@@ -450,7 +447,7 @@
                         mScreenBrightnessRangeMaximum, dozeScaleFactor, lightSensorRate,
                         initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
                         autoBrightnessResetAmbientLuxAfterWarmUp, ambientLightHorizon,
-                        autoBrightnessAdjustmentMaxGamma, dynamicHysteresis);
+                        dynamicHysteresis);
             } else {
                 mUseSoftwareAutoBrightnessConfig = false;
             }
diff --git a/com/android/server/display/LocalDisplayAdapter.java b/com/android/server/display/LocalDisplayAdapter.java
index 5ca9abc..b9a279a 100644
--- a/com/android/server/display/LocalDisplayAdapter.java
+++ b/com/android/server/display/LocalDisplayAdapter.java
@@ -436,6 +436,10 @@
                                 com.android.internal.R.bool.config_localDisplaysMirrorContent)) {
                         mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
                     }
+
+                    if (res.getBoolean(com.android.internal.R.bool.config_localDisplaysPrivate)) {
+                        mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE;
+                    }
                 }
             }
             return mInfo;
diff --git a/com/android/server/display/utils/Plog.java b/com/android/server/display/utils/Plog.java
new file mode 100644
index 0000000..3a18387
--- /dev/null
+++ b/com/android/server/display/utils/Plog.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2018 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.server.display.utils;
+
+
+import java.lang.StringBuilder;
+import java.lang.System;
+
+import android.util.Slog;
+
+/**
+ * A utility to log multiple points and curves in a structured way so they can be easily consumed
+ * by external tooling
+ *
+ * To start a plot, call {@link Plog.start} with the plot's title; to add a point to it, call
+ * {@link Plog.logPoint} with the point name (that will appear in the legend) and coordinates; and
+ * to log a curve, call {@link Plog.logCurve} with its name and points.
+ */
+public abstract class Plog {
+    // A unique identifier used to group points and curves that belong on the same plot.
+    private long mId;
+
+    /**
+     * Returns a Plog instance that emits messages to the system log.
+     *
+     * @param tag The tag of the emitted messages in the system log.
+     * @return A plog instance that emits messages to the system log.
+     */
+    public static Plog createSystemPlog(String tag) {
+        return new SystemPlog(tag);
+    }
+
+    /**
+     * Start a new plot.
+     *
+     * @param title The plot title.
+     * @return The Plog instance (for chaining).
+     */
+    public Plog start(String title) {
+        mId = System.currentTimeMillis();
+        write(formatTitle(title));
+        return this;
+    }
+
+    /**
+     * Adds a point to the current plot.
+     *
+     * @param name The point name (that will appear in the legend).
+     * @param x The point x coordinate.
+     * @param y The point y coordinate.
+     * @return The Plog instance (for chaining).
+     */
+    public Plog logPoint(String name, float x, float y) {
+        write(formatPoint(name, x, y));
+        return this;
+    }
+
+    /**
+     * Adds a curve to the current plot.
+     *
+     * @param name The curve name (that will appear in the legend).
+     * @param xs The curve x coordinates.
+     * @param ys The curve y coordinates.
+     * @return The Plog instance (for chaining).
+     */
+    public Plog logCurve(String name, float[] xs, float[] ys) {
+        write(formatCurve(name, xs, ys));
+        return this;
+    }
+
+    private String formatTitle(String title) {
+        return "title: " + title;
+    }
+
+    private String formatPoint(String name, float x, float y) {
+        return "point: " + name + ": (" + x + "," + y + ")";
+    }
+
+    private String formatCurve(String name, float[] xs, float[] ys) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("curve: " + name + ": [");
+        int n = xs.length <= ys.length ? xs.length : ys.length;
+        for (int i = 0; i < n; i++) {
+            sb.append("(" + xs[i] + "," + ys[i] + "),");
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    private void write(String message) {
+        emit("[PLOG " + mId + "] " + message);
+    }
+
+    /**
+     * Emits a message (depending on the concrete Plog implementation).
+     *
+     * @param message The message.
+     */
+    protected abstract void emit(String message);
+
+    /**
+     * A Plog that emits messages to the system log.
+     */
+    public static class SystemPlog extends Plog {
+        // The tag of the emitted messages in the system log.
+        private final String mTag;
+
+        /**
+         * Returns a Plog instance that emits messages to the system log.
+         *
+         * @param tag The tag of the emitted messages in the system log.
+         * @return A Plog instance that emits messages to the system log.
+         */
+        public SystemPlog(String tag) {
+            mTag = tag;
+        }
+
+        /**
+         * Emits a message to the system log.
+         *
+         * @param message The message.
+         */
+        protected void emit(String message) {
+            Slog.d(mTag, message);
+        }
+    }
+}
diff --git a/com/android/server/fingerprint/AuthenticationClient.java b/com/android/server/fingerprint/AuthenticationClient.java
index 8be2c9e..afd1a94 100644
--- a/com/android/server/fingerprint/AuthenticationClient.java
+++ b/com/android/server/fingerprint/AuthenticationClient.java
@@ -18,8 +18,8 @@
 
 import android.content.Context;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
-import android.hardware.biometrics.BiometricDialog;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricPromptReceiver;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
@@ -46,8 +46,8 @@
     public static final int LOCKOUT_PERMANENT = 2;
 
     // Callback mechanism received from the client
-    // (BiometricDialog -> FingerprintManager -> FingerprintService -> AuthenticationClient)
-    private IBiometricDialogReceiver mDialogReceiverFromClient;
+    // (BiometricPrompt -> FingerprintManager -> FingerprintService -> AuthenticationClient)
+    private IBiometricPromptReceiver mDialogReceiverFromClient;
     private Bundle mBundle;
     private IStatusBarService mStatusBarService;
     private boolean mInLockout;
@@ -55,13 +55,13 @@
     protected boolean mDialogDismissed;
 
     // Receives events from SystemUI and handles them before forwarding them to FingerprintDialog
-    protected IBiometricDialogReceiver mDialogReceiver = new IBiometricDialogReceiver.Stub() {
+    protected IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
         @Override // binder call
         public void onDialogDismissed(int reason) {
             if (mBundle != null && mDialogReceiverFromClient != null) {
                 try {
                     mDialogReceiverFromClient.onDialogDismissed(reason);
-                    if (reason == BiometricDialog.DISMISSED_REASON_USER_CANCEL) {
+                    if (reason == BiometricPrompt.DISMISSED_REASON_USER_CANCEL) {
                         onError(FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED,
                                 0 /* vendorCode */);
                     }
@@ -88,7 +88,7 @@
     public AuthenticationClient(Context context, long halDeviceId, IBinder token,
             IFingerprintServiceReceiver receiver, int targetUserId, int groupId, long opId,
             boolean restricted, String owner, Bundle bundle,
-            IBiometricDialogReceiver dialogReceiver, IStatusBarService statusBarService) {
+            IBiometricPromptReceiver dialogReceiver, IStatusBarService statusBarService) {
         super(context, halDeviceId, token, receiver, targetUserId, groupId, restricted, owner);
         mOpId = opId;
         mBundle = bundle;
@@ -299,7 +299,7 @@
             // If the user already cancelled authentication (via some interaction with the
             // dialog, we do not need to hide it since it's already hidden.
             // If the device is in lockout, don't hide the dialog - it will automatically hide
-            // after BiometricDialog.HIDE_DIALOG_DELAY
+            // after BiometricPrompt.HIDE_DIALOG_DELAY
             if (mBundle != null && !mDialogDismissed && !mInLockout) {
                 try {
                     mStatusBarService.hideFingerprintDialog();
diff --git a/com/android/server/fingerprint/FingerprintService.java b/com/android/server/fingerprint/FingerprintService.java
index 92d3772..4a1beb1 100644
--- a/com/android/server/fingerprint/FingerprintService.java
+++ b/com/android/server/fingerprint/FingerprintService.java
@@ -38,7 +38,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.IBiometricPromptReceiver;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprintClientCallback;
 import android.hardware.fingerprint.Fingerprint;
@@ -230,10 +230,11 @@
                 }
                 List<ActivityManager.RunningTaskInfo> runningTasks = mActivityManager.getTasks(1);
                 if (!runningTasks.isEmpty()) {
-                    if (runningTasks.get(0).topActivity.getPackageName()
-                            != mCurrentClient.getOwnerString()) {
+                    final String topPackage = runningTasks.get(0).topActivity.getPackageName();
+                    if (!topPackage.contentEquals(mCurrentClient.getOwnerString())) {
                         mCurrentClient.stop(false /* initiatedByClient */);
-                        Slog.e(TAG, "Stopping background authentication");
+                        Slog.e(TAG, "Stopping background authentication, top: " + topPackage
+                                + " currentClient: " + mCurrentClient.getOwnerString());
                     }
                 }
             } catch (RemoteException e) {
@@ -849,7 +850,7 @@
 
     private void startAuthentication(IBinder token, long opId, int callingUserId, int groupId,
                 IFingerprintServiceReceiver receiver, int flags, boolean restricted,
-                String opPackageName, Bundle bundle, IBiometricDialogReceiver dialogReceiver) {
+                String opPackageName, Bundle bundle, IBiometricPromptReceiver dialogReceiver) {
         updateActiveGroup(groupId, opPackageName);
 
         if (DEBUG) Slog.v(TAG, "startAuthentication(" + opPackageName + ")");
@@ -1160,7 +1161,7 @@
         public void authenticate(final IBinder token, final long opId, final int groupId,
                 final IFingerprintServiceReceiver receiver, final int flags,
                 final String opPackageName, final Bundle bundle,
-                final IBiometricDialogReceiver dialogReceiver) {
+                final IBiometricPromptReceiver dialogReceiver) {
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
             final int callingUserId = UserHandle.getCallingUserId();
diff --git a/com/android/server/input/InputWindowHandle.java b/com/android/server/input/InputWindowHandle.java
index 3d6f7ad..720eaaa 100644
--- a/com/android/server/input/InputWindowHandle.java
+++ b/com/android/server/input/InputWindowHandle.java
@@ -92,7 +92,7 @@
     public int inputFeatures;
 
     // Display this input is on.
-    public final int displayId;
+    public int displayId;
 
     private native void nativeDispose();
 
diff --git a/com/android/server/job/JobSchedulerService.java b/com/android/server/job/JobSchedulerService.java
index 66817fa..736aa46 100644
--- a/com/android/server/job/JobSchedulerService.java
+++ b/com/android/server/job/JobSchedulerService.java
@@ -127,7 +127,7 @@
  * Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}.
  * @hide
  */
-public final class JobSchedulerService extends com.android.server.SystemService
+public class JobSchedulerService extends com.android.server.SystemService
         implements StateChangedListener, JobCompletedListener {
     public static final String TAG = "JobScheduler";
     public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -781,6 +781,10 @@
         }
     };
 
+    public Context getTestableContext() {
+        return getContext();
+    }
+
     public Object getLock() {
         return mLock;
     }
diff --git a/com/android/server/job/controllers/ConnectivityController.java b/com/android/server/job/controllers/ConnectivityController.java
index 8365fd2..0c66c5b 100644
--- a/com/android/server/job/controllers/ConnectivityController.java
+++ b/com/android/server/job/controllers/ConnectivityController.java
@@ -30,12 +30,12 @@
 import android.net.NetworkPolicyManager;
 import android.net.NetworkRequest;
 import android.net.TrafficStats;
-import android.os.Process;
 import android.os.UserHandle;
 import android.text.format.DateUtils;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
@@ -46,6 +46,7 @@
 import com.android.server.job.JobServiceContext;
 import com.android.server.job.StateControllerProto;
 
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -63,7 +64,6 @@
 
     private final ConnectivityManager mConnManager;
     private final NetworkPolicyManager mNetPolicyManager;
-    private boolean mConnected;
 
     @GuardedBy("mLock")
     private final ArraySet<JobStatus> mTrackedJobs = new ArraySet<>();
@@ -74,9 +74,11 @@
         mConnManager = mContext.getSystemService(ConnectivityManager.class);
         mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
 
-        mConnected = false;
+        // We're interested in all network changes; internally we match these
+        // network changes against the active network for each UID with jobs.
+        final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+        mConnManager.registerNetworkCallback(request, mNetworkCallback);
 
-        mConnManager.registerDefaultNetworkCallback(mNetworkCallback);
         mNetPolicyManager.registerListener(mNetPolicyListener);
     }
 
@@ -198,14 +200,18 @@
     }
 
     private boolean updateConstraintsSatisfied(JobStatus jobStatus) {
+        final Network network = mConnManager.getActiveNetworkForUid(jobStatus.getSourceUid());
+        final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network);
+        return updateConstraintsSatisfied(jobStatus, network, capabilities);
+    }
+
+    private boolean updateConstraintsSatisfied(JobStatus jobStatus, Network network,
+            NetworkCapabilities capabilities) {
         // TODO: consider matching against non-active networks
 
-        final int jobUid = jobStatus.getSourceUid();
         final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
-
-        final Network network = mConnManager.getActiveNetworkForUid(jobUid, ignoreBlocked);
-        final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, jobUid, ignoreBlocked);
-        final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network);
+        final NetworkInfo info = mConnManager.getNetworkInfoForUid(network,
+                jobStatus.getSourceUid(), ignoreBlocked);
 
         final boolean connected = (info != null) && info.isConnected();
         final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants);
@@ -218,12 +224,6 @@
         // using non-default routes.
         jobStatus.network = network;
 
-        // Track system-uid connected/validated as a general reportable proxy for the
-        // overall state of connectivity constraint satisfiability.
-        if (jobUid == Process.SYSTEM_UID) {
-            mConnected = connected;
-        }
-
         if (DEBUG) {
             Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged")
                     + " for " + jobStatus + ": connected=" + connected
@@ -233,18 +233,48 @@
     }
 
     /**
-     * Update all jobs tracked by this controller.
+     * Update any jobs tracked by this controller that match given filters.
      *
-     * @param uid only update jobs belonging to this UID, or {@code -1} to
+     * @param filterUid only update jobs belonging to this UID, or {@code -1} to
      *            update all tracked jobs.
+     * @param filterNetwork only update jobs that would use this
+     *            {@link Network}, or {@code null} to update all tracked jobs.
      */
-    private void updateTrackedJobs(int uid) {
+    private void updateTrackedJobs(int filterUid, Network filterNetwork) {
         synchronized (mLock) {
+            // Since this is a really hot codepath, temporarily cache any
+            // answers that we get from ConnectivityManager.
+            final SparseArray<Network> uidToNetwork = new SparseArray<>();
+            final SparseArray<NetworkCapabilities> networkToCapabilities = new SparseArray<>();
+
             boolean changed = false;
-            for (int i = mTrackedJobs.size()-1; i >= 0; i--) {
+            for (int i = mTrackedJobs.size() - 1; i >= 0; i--) {
                 final JobStatus js = mTrackedJobs.valueAt(i);
-                if (uid == -1 || uid == js.getSourceUid()) {
-                    changed |= updateConstraintsSatisfied(js);
+                final int uid = js.getSourceUid();
+
+                final boolean uidMatch = (filterUid == -1 || filterUid == uid);
+                if (uidMatch) {
+                    Network network = uidToNetwork.get(uid);
+                    if (network == null) {
+                        network = mConnManager.getActiveNetworkForUid(uid);
+                        uidToNetwork.put(uid, network);
+                    }
+
+                    // Update either when we have a network match, or when the
+                    // job hasn't yet been evaluated against the currently
+                    // active network; typically when we just lost a network.
+                    final boolean networkMatch = (filterNetwork == null
+                            || Objects.equals(filterNetwork, network));
+                    final boolean forceUpdate = !Objects.equals(js.network, network);
+                    if (networkMatch || forceUpdate) {
+                        final int netId = network != null ? network.netId : -1;
+                        NetworkCapabilities capabilities = networkToCapabilities.get(netId);
+                        if (capabilities == null) {
+                            capabilities = mConnManager.getNetworkCapabilities(network);
+                            networkToCapabilities.put(netId, capabilities);
+                        }
+                        changed |= updateConstraintsSatisfied(js, network, capabilities);
+                    }
                 }
             }
             if (changed) {
@@ -273,19 +303,19 @@
 
     private final NetworkCallback mNetworkCallback = new NetworkCallback() {
         @Override
-        public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
+        public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
             if (DEBUG) {
-                Slog.v(TAG, "onCapabilitiesChanged() : " + networkCapabilities);
+                Slog.v(TAG, "onCapabilitiesChanged: " + network);
             }
-            updateTrackedJobs(-1);
+            updateTrackedJobs(-1, network);
         }
 
         @Override
         public void onLost(Network network) {
             if (DEBUG) {
-                Slog.v(TAG, "Network lost");
+                Slog.v(TAG, "onLost: " + network);
             }
-            updateTrackedJobs(-1);
+            updateTrackedJobs(-1, network);
         }
     };
 
@@ -293,25 +323,9 @@
         @Override
         public void onUidRulesChanged(int uid, int uidRules) {
             if (DEBUG) {
-                Slog.v(TAG, "Uid rules changed for " + uid);
+                Slog.v(TAG, "onUidRulesChanged: " + uid);
             }
-            updateTrackedJobs(uid);
-        }
-
-        @Override
-        public void onRestrictBackgroundChanged(boolean restrictBackground) {
-            if (DEBUG) {
-                Slog.v(TAG, "Background restriction change to " + restrictBackground);
-            }
-            updateTrackedJobs(-1);
-        }
-
-        @Override
-        public void onUidPoliciesChanged(int uid, int uidPolicies) {
-            if (DEBUG) {
-                Slog.v(TAG, "Uid policy changed for " + uid);
-            }
-            updateTrackedJobs(uid);
+            updateTrackedJobs(uid, null);
         }
     };
 
@@ -319,9 +333,6 @@
     @Override
     public void dumpControllerStateLocked(IndentingPrintWriter pw,
             Predicate<JobStatus> predicate) {
-        pw.println("System connected: " + mConnected);
-        pw.println();
-
         for (int i = 0; i < mTrackedJobs.size(); i++) {
             final JobStatus js = mTrackedJobs.valueAt(i);
             if (predicate.test(js)) {
@@ -343,8 +354,6 @@
         final long token = proto.start(fieldId);
         final long mToken = proto.start(StateControllerProto.CONNECTIVITY);
 
-        proto.write(StateControllerProto.ConnectivityController.IS_CONNECTED, mConnected);
-
         for (int i = 0; i < mTrackedJobs.size(); i++) {
             final JobStatus js = mTrackedJobs.valueAt(i);
             if (!predicate.test(js)) {
diff --git a/com/android/server/job/controllers/StateController.java b/com/android/server/job/controllers/StateController.java
index 495109d..c2be283 100644
--- a/com/android/server/job/controllers/StateController.java
+++ b/com/android/server/job/controllers/StateController.java
@@ -41,7 +41,7 @@
     StateController(JobSchedulerService service) {
         mService = service;
         mStateChangedListener = service;
-        mContext = service.getContext();
+        mContext = service.getTestableContext();
         mLock = service.getLock();
         mConstants = service.getConstants();
     }
diff --git a/com/android/server/location/ExponentialBackOff.java b/com/android/server/location/ExponentialBackOff.java
new file mode 100644
index 0000000..8c77b21
--- /dev/null
+++ b/com/android/server/location/ExponentialBackOff.java
@@ -0,0 +1,32 @@
+package com.android.server.location;
+
+/**
+ * A simple implementation of exponential backoff.
+ */
+class ExponentialBackOff {
+    private static final int MULTIPLIER = 2;
+    private final long mInitIntervalMillis;
+    private final long mMaxIntervalMillis;
+    private long mCurrentIntervalMillis;
+
+    ExponentialBackOff(long initIntervalMillis, long maxIntervalMillis) {
+        mInitIntervalMillis = initIntervalMillis;
+        mMaxIntervalMillis = maxIntervalMillis;
+
+        mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
+    }
+
+    long nextBackoffMillis() {
+        if (mCurrentIntervalMillis > mMaxIntervalMillis) {
+            return mMaxIntervalMillis;
+        }
+
+        mCurrentIntervalMillis *= MULTIPLIER;
+        return mCurrentIntervalMillis;
+    }
+
+    void reset() {
+        mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
+    }
+}
+
diff --git a/com/android/server/location/GnssLocationProvider.java b/com/android/server/location/GnssLocationProvider.java
index 5955c9c..58bca19 100644
--- a/com/android/server/location/GnssLocationProvider.java
+++ b/com/android/server/location/GnssLocationProvider.java
@@ -76,14 +76,18 @@
 import android.telephony.gsm.GsmCellLocation;
 import android.text.TextUtils;
 import android.util.Log;
-import android.util.NtpTrustedTime;
-import com.android.internal.app.IAppOpsService;
+
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.location.GpsNetInitiatedHandler;
 import com.android.internal.location.GpsNetInitiatedHandler.GpsNiNotification;
 import com.android.internal.location.ProviderProperties;
 import com.android.internal.location.ProviderRequest;
 import com.android.internal.location.gnssmetrics.GnssMetrics;
+import com.android.server.location.GnssSatelliteBlacklistHelper.GnssSatelliteBlacklistCallback;
+import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback;
+
+import libcore.io.IoUtils;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -93,21 +97,19 @@
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
 
-import libcore.io.IoUtils;
-
 /**
  * A GNSS implementation of LocationProvider used by LocationManager.
  *
  * {@hide}
  */
-public class GnssLocationProvider implements LocationProviderInterface {
+public class GnssLocationProvider implements LocationProviderInterface, InjectNtpTimeCallback,
+        GnssSatelliteBlacklistCallback {
 
     private static final String TAG = "GnssLocationProvider";
 
@@ -208,7 +210,6 @@
     private static final int UPDATE_LOCATION = 7;  // Handle external location from network listener
     private static final int ADD_LISTENER = 8;
     private static final int REMOVE_LISTENER = 9;
-    private static final int INJECT_NTP_TIME_FINISHED = 10;
     private static final int DOWNLOAD_XTRA_DATA_FINISHED = 11;
     private static final int SUBSCRIPTION_OR_SIM_CHANGED = 12;
     private static final int INITIALIZE_HANDLER = 13;
@@ -310,7 +311,7 @@
         }
     }
 
-    private Object mLock = new Object();
+    private final Object mLock = new Object();
 
     // current status
     private int mStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
@@ -329,9 +330,6 @@
     // Typical hot TTTF is ~5 seconds, so 10 seconds seems sane.
     private static final int GPS_POLLING_THRESHOLD_INTERVAL = 10 * 1000;
 
-    // how often to request NTP time, in milliseconds
-    // current setting 24 hours
-    private static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
     // how long to wait if we have a network error in NTP or XTRA downloading
     // the initial value of the exponential backoff
     // current setting - 5 minutes
@@ -344,8 +342,8 @@
     // Timeout when holding wakelocks for downloading XTRA data.
     private static final long DOWNLOAD_XTRA_DATA_TIMEOUT_MS = 60 * 1000;
 
-    private BackOff mNtpBackOff = new BackOff(RETRY_INTERVAL, MAX_RETRY_INTERVAL);
-    private BackOff mXtraBackOff = new BackOff(RETRY_INTERVAL, MAX_RETRY_INTERVAL);
+    private final ExponentialBackOff mXtraBackOff = new ExponentialBackOff(RETRY_INTERVAL,
+            MAX_RETRY_INTERVAL);
 
     // true if we are enabled, protected by this
     private boolean mEnabled;
@@ -357,12 +355,8 @@
 
     // flags to trigger NTP or XTRA data download when network becomes available
     // initialized to true so we do NTP and XTRA when the network comes up after booting
-    private int mInjectNtpTimePending = STATE_PENDING_NETWORK;
     private int mDownloadXtraDataPending = STATE_PENDING_NETWORK;
 
-    // set to true if the GPS engine requested on-demand NTP time requests
-    private boolean mOnDemandTimeInjection;
-
     // true if GPS is navigating
     private boolean mNavigating;
 
@@ -417,14 +411,15 @@
     private boolean mSuplEsEnabled = false;
 
     private final Context mContext;
-    private final NtpTrustedTime mNtpTime;
     private final ILocationManager mILocationManager;
     private final LocationExtras mLocationExtras = new LocationExtras();
     private final GnssStatusListenerHelper mListenerHelper;
+    private final GnssSatelliteBlacklistHelper mGnssSatelliteBlacklistHelper;
     private final GnssMeasurementsProvider mGnssMeasurementsProvider;
     private final GnssNavigationMessageProvider mGnssNavigationMessageProvider;
     private final LocationChangeListener mNetworkLocationListener = new NetworkLocationListener();
     private final LocationChangeListener mFusedLocationListener = new FusedLocationListener();
+    private final NtpTimeHelper mNtpTimeHelper;
 
     // Handler for processing events
     private Handler mHandler;
@@ -517,9 +512,7 @@
             new ConnectivityManager.NetworkCallback() {
                 @Override
                 public void onAvailable(Network network) {
-                    if (mInjectNtpTimePending == STATE_PENDING_NETWORK) {
-                        requestUtcTime();
-                    }
+                    mNtpTimeHelper.onNetworkAvailable();
                     if (mDownloadXtraDataPending == STATE_PENDING_NETWORK) {
                         if (mSupportsXtra) {
                             // Download only if supported, (prevents an unneccesary on-boot
@@ -588,6 +581,16 @@
                 }
             };
 
+    /**
+     * Implements {@link GnssSatelliteBlacklistCallback#onUpdateSatelliteBlacklist}.
+     */
+    @Override
+    public void onUpdateSatelliteBlacklist(int[] constellations, int[] svids) {
+        mHandler.post(()->{
+            native_set_satellite_blacklist(constellations, svids);
+        });
+    }
+
     private void subscriptionOrSimChanged(Context context) {
         if (DEBUG) Log.d(TAG, "received SIM related action: ");
         TelephonyManager phone = (TelephonyManager)
@@ -762,7 +765,6 @@
     public GnssLocationProvider(Context context, ILocationManager ilocationManager,
             Looper looper) {
         mContext = context;
-        mNtpTime = NtpTrustedTime.getInstance(context);
         mILocationManager = ilocationManager;
 
         // Create a wake lock
@@ -880,6 +882,11 @@
             }
         };
         mGnssMetrics = new GnssMetrics(mBatteryStats);
+
+        mNtpTimeHelper = new NtpTimeHelper(mContext, looper, this);
+        mGnssSatelliteBlacklistHelper = new GnssSatelliteBlacklistHelper(mContext,
+                looper, this);
+        mHandler.post(mGnssSatelliteBlacklistHelper::updateSatelliteBlacklist);
     }
 
     /**
@@ -895,6 +902,15 @@
         return PROPERTIES;
     }
 
+
+    /**
+     * Implements {@link InjectNtpTimeCallback#injectTime}
+     */
+    @Override
+    public void injectTime(long time, long timeReference, int uncertainty) {
+        native_inject_time(time, timeReference, uncertainty);
+    }
+
     private void handleUpdateNetworkState(Network network) {
         // retrieve NetworkInfo for this UID
         NetworkInfo info = mConnMgr.getNetworkInfo(network);
@@ -1014,79 +1030,7 @@
                 Log.e(TAG, "Invalid status to release SUPL connection: " + agpsDataConnStatus);
         }
     }
-
-    private void handleInjectNtpTime() {
-        if (mInjectNtpTimePending == STATE_DOWNLOADING) {
-            // already downloading data
-            return;
-        }
-        if (!isDataNetworkConnected()) {
-            // try again when network is up
-            mInjectNtpTimePending = STATE_PENDING_NETWORK;
-            return;
-        }
-        mInjectNtpTimePending = STATE_DOWNLOADING;
-
-        // hold wake lock while task runs
-        mWakeLock.acquire();
-        Log.i(TAG, "WakeLock acquired by handleInjectNtpTime()");
-        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
-            @Override
-            public void run() {
-                long delay;
-
-                // force refresh NTP cache when outdated
-                boolean refreshSuccess = true;
-                if (mNtpTime.getCacheAge() >= NTP_INTERVAL) {
-                    refreshSuccess = mNtpTime.forceRefresh();
-                }
-
-                // only update when NTP time is fresh
-                if (mNtpTime.getCacheAge() < NTP_INTERVAL) {
-                    long time = mNtpTime.getCachedNtpTime();
-                    long timeReference = mNtpTime.getCachedNtpTimeReference();
-                    long certainty = mNtpTime.getCacheCertainty();
-
-                    if (DEBUG) {
-                        long now = System.currentTimeMillis();
-                        Log.d(TAG, "NTP server returned: "
-                                + time + " (" + new Date(time)
-                                + ") reference: " + timeReference
-                                + " certainty: " + certainty
-                                + " system time offset: " + (time - now));
-                    }
-
-                    native_inject_time(time, timeReference, (int) certainty);
-                    delay = NTP_INTERVAL;
-                    mNtpBackOff.reset();
-                } else {
-                    Log.e(TAG, "requestTime failed");
-                    delay = mNtpBackOff.nextBackoffMillis();
-                }
-
-                sendMessage(INJECT_NTP_TIME_FINISHED, 0, null);
-
-                if (DEBUG) {
-                    String message = String.format(
-                            "onDemandTimeInjection=%s, refreshSuccess=%s, delay=%s",
-                            mOnDemandTimeInjection,
-                            refreshSuccess,
-                            delay);
-                    Log.d(TAG, message);
-                }
-                if (mOnDemandTimeInjection || !refreshSuccess) {
-                    // send delayed message for next NTP injection
-                    // since this is delayed and not urgent we do not hold a wake lock here
-                    mHandler.sendEmptyMessageDelayed(INJECT_NTP_TIME, delay);
-                }
-
-                // release wake lock held by task
-                mWakeLock.release();
-                Log.i(TAG, "WakeLock released by handleInjectNtpTime()");
-            }
-        });
-    }
-
+    
     private void handleRequestLocation(boolean independentFromGnss) {
         if (isRequestLocationRateLimited()) {
             if (DEBUG) {
@@ -2006,7 +1950,7 @@
                 mEngineCapabilities = capabilities;
 
                 if (hasCapability(GPS_CAPABILITY_ON_DEMAND_TIME)) {
-                    mOnDemandTimeInjection = true;
+                    mNtpTimeHelper.enablePeriodicTimeInjection();
                     requestUtcTime();
                 }
 
@@ -2467,7 +2411,7 @@
                     handleReleaseSuplConnection(msg.arg1);
                     break;
                 case INJECT_NTP_TIME:
-                    handleInjectNtpTime();
+                    mNtpTimeHelper.retrieveAndInjectNtpTime();
                     break;
                 case REQUEST_LOCATION:
                     handleRequestLocation((boolean) msg.obj);
@@ -2475,9 +2419,6 @@
                 case DOWNLOAD_XTRA_DATA:
                     handleDownloadXtraData();
                     break;
-                case INJECT_NTP_TIME_FINISHED:
-                    mInjectNtpTimePending = STATE_IDLE;
-                    break;
                 case DOWNLOAD_XTRA_DATA_FINISHED:
                     mDownloadXtraDataPending = STATE_IDLE;
                     break;
@@ -2808,8 +2749,6 @@
                 return "REQUEST_LOCATION";
             case DOWNLOAD_XTRA_DATA:
                 return "DOWNLOAD_XTRA_DATA";
-            case INJECT_NTP_TIME_FINISHED:
-                return "INJECT_NTP_TIME_FINISHED";
             case DOWNLOAD_XTRA_DATA_FINISHED:
                 return "DOWNLOAD_XTRA_DATA_FINISHED";
             case UPDATE_LOCATION:
@@ -2856,36 +2795,6 @@
         pw.append(s);
     }
 
-    /**
-     * A simple implementation of exponential backoff.
-     */
-    private static final class BackOff {
-        private static final int MULTIPLIER = 2;
-        private final long mInitIntervalMillis;
-        private final long mMaxIntervalMillis;
-        private long mCurrentIntervalMillis;
-
-        public BackOff(long initIntervalMillis, long maxIntervalMillis) {
-            mInitIntervalMillis = initIntervalMillis;
-            mMaxIntervalMillis = maxIntervalMillis;
-
-            mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
-        }
-
-        public long nextBackoffMillis() {
-            if (mCurrentIntervalMillis > mMaxIntervalMillis) {
-                return mMaxIntervalMillis;
-            }
-
-            mCurrentIntervalMillis *= MULTIPLIER;
-            return mCurrentIntervalMillis;
-        }
-
-        public void reset() {
-            mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
-        }
-    }
-
     // preallocated to avoid memory allocation in reportNmea()
     private byte[] mNmeaBuffer = new byte[120];
 
@@ -3008,6 +2917,8 @@
 
     private static native boolean native_set_emergency_supl_pdn(int emergencySuplPdn);
 
+    private static native boolean native_set_satellite_blacklist(int[] constellations, int[] svIds);
+
     // GNSS Batching
     private static native int native_get_batch_size();
 
@@ -3022,3 +2933,4 @@
     private static native void native_cleanup_batching();
 
 }
+
diff --git a/com/android/server/location/GnssSatelliteBlacklistHelper.java b/com/android/server/location/GnssSatelliteBlacklistHelper.java
new file mode 100644
index 0000000..77951aa
--- /dev/null
+++ b/com/android/server/location/GnssSatelliteBlacklistHelper.java
@@ -0,0 +1,102 @@
+package com.android.server.location;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Detects blacklist change and updates the blacklist.
+ */
+class GnssSatelliteBlacklistHelper {
+
+    private static final String TAG = "GnssBlacklistHelper";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final String BLACKLIST_DELIMITER = ",";
+
+    private final Context mContext;
+    private final GnssSatelliteBlacklistCallback mCallback;
+
+    interface GnssSatelliteBlacklistCallback {
+        void onUpdateSatelliteBlacklist(int[] constellations, int[] svids);
+    }
+
+    GnssSatelliteBlacklistHelper(Context context, Looper looper,
+            GnssSatelliteBlacklistCallback callback) {
+        mContext = context;
+        mCallback = callback;
+        ContentObserver contentObserver = new ContentObserver(new Handler(looper)) {
+            @Override
+            public void onChange(boolean selfChange) {
+                updateSatelliteBlacklist();
+            }
+        };
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(
+                        Settings.Global.GNSS_SATELLITE_BLACKLIST),
+                true,
+                contentObserver, UserHandle.USER_ALL);
+    }
+
+    void updateSatelliteBlacklist() {
+        ContentResolver resolver = mContext.getContentResolver();
+        String blacklist = Settings.Global.getString(
+                resolver,
+                Settings.Global.GNSS_SATELLITE_BLACKLIST);
+        if (blacklist == null) {
+            blacklist = "";
+        }
+        if (DEBUG) {
+            Log.d(TAG, String.format("Update GNSS satellite blacklist: %s", blacklist));
+        }
+
+        List<Integer> blacklistValues;
+        try {
+            blacklistValues = parseSatelliteBlacklist(blacklist);
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "Exception thrown when parsing blacklist string.", e);
+            return;
+        }
+
+        if (blacklistValues.size() % 2 != 0) {
+            Log.e(TAG, "blacklist string has odd number of values."
+                    + "Aborting updateSatelliteBlacklist");
+            return;
+        }
+
+        int length = blacklistValues.size() / 2;
+        int[] constellations = new int[length];
+        int[] svids = new int[length];
+        for (int i = 0; i < length; i++) {
+            constellations[i] = blacklistValues.get(i * 2);
+            svids[i] = blacklistValues.get(i * 2 + 1);
+        }
+        mCallback.onUpdateSatelliteBlacklist(constellations, svids);
+    }
+
+    @VisibleForTesting
+    static List<Integer> parseSatelliteBlacklist(String blacklist) throws NumberFormatException {
+        String[] strings = blacklist.split(BLACKLIST_DELIMITER);
+        List<Integer> parsed = new ArrayList<>(strings.length);
+        for (String string : strings) {
+            string = string.trim();
+            if (!"".equals(string)) {
+                int value = Integer.parseInt(string);
+                if (value < 0) {
+                    throw new NumberFormatException("Negative value is invalid.");
+                }
+                parsed.add(value);
+            }
+        }
+        return parsed;
+    }
+}
diff --git a/com/android/server/location/GnssSatelliteBlacklistHelperTest.java b/com/android/server/location/GnssSatelliteBlacklistHelperTest.java
new file mode 100644
index 0000000..d6f5446
--- /dev/null
+++ b/com/android/server/location/GnssSatelliteBlacklistHelperTest.java
@@ -0,0 +1,130 @@
+package com.android.server.location;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Unit tests for {@link GnssSatelliteBlacklistHelper}.
+ */
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+        manifest = Config.NONE,
+        shadows = {
+        },
+        sdk = 27
+)
+@SystemLoaderPackages({"com.android.server.location"})
+@Presubmit
+public class GnssSatelliteBlacklistHelperTest {
+
+    private Context mContext;
+    private ContentResolver mContentResolver;
+    @Mock
+    private GnssSatelliteBlacklistHelper.GnssSatelliteBlacklistCallback mCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mContentResolver = mContext.getContentResolver();
+        new GnssSatelliteBlacklistHelper(mContext, Looper.myLooper(), mCallback);
+    }
+
+    @Test
+    public void blacklistOf2Satellites_callbackIsCalled() {
+        String blacklist = "3,0,5,24";
+        updateBlacklistAndVerifyCallbackIsCalled(blacklist);
+    }
+
+    @Test
+    public void blacklistWithSpaces_callbackIsCalled() {
+        String blacklist = "3, 11";
+        updateBlacklistAndVerifyCallbackIsCalled(blacklist);
+    }
+
+    @Test
+    public void emptyBlacklist_callbackIsCalled() {
+        String blacklist = "";
+        updateBlacklistAndVerifyCallbackIsCalled(blacklist);
+    }
+
+    @Test
+    public void blacklistWithOddNumberOfValues_callbackIsNotCalled() {
+        String blacklist = "3,0,5";
+        updateBlacklistAndNotifyContentObserver(blacklist);
+        verify(mCallback, never()).onUpdateSatelliteBlacklist(any(int[].class), any(int[].class));
+    }
+
+    @Test
+    public void blacklistWithNegativeValue_callbackIsNotCalled() {
+        String blacklist = "3,-11";
+        updateBlacklistAndNotifyContentObserver(blacklist);
+        verify(mCallback, never()).onUpdateSatelliteBlacklist(any(int[].class), any(int[].class));
+    }
+
+    @Test
+    public void blacklistWithNonDigitCharacter_callbackIsNotCalled() {
+        String blacklist = "3,1a,5,11";
+        updateBlacklistAndNotifyContentObserver(blacklist);
+        verify(mCallback, never()).onUpdateSatelliteBlacklist(any(int[].class), any(int[].class));
+    }
+
+    private void updateBlacklistAndNotifyContentObserver(String blacklist) {
+        Settings.Global.putString(mContentResolver,
+                Settings.Global.GNSS_SATELLITE_BLACKLIST, blacklist);
+        notifyContentObserverFor(Settings.Global.GNSS_SATELLITE_BLACKLIST);
+    }
+
+    private void updateBlacklistAndVerifyCallbackIsCalled(String blacklist) {
+        updateBlacklistAndNotifyContentObserver(blacklist);
+
+        ArgumentCaptor<int[]> constellationsCaptor = ArgumentCaptor.forClass(int[].class);
+        ArgumentCaptor<int[]> svIdsCaptor = ArgumentCaptor.forClass(int[].class);
+        verify(mCallback).onUpdateSatelliteBlacklist(constellationsCaptor.capture(),
+                svIdsCaptor.capture());
+
+        int[] constellations = constellationsCaptor.getValue();
+        int[] svIds = svIdsCaptor.getValue();
+        List<Integer> values = GnssSatelliteBlacklistHelper.parseSatelliteBlacklist(blacklist);
+        assertThat(values.size()).isEqualTo(constellations.length * 2);
+        assertThat(svIds.length).isEqualTo(constellations.length);
+        for (int i = 0; i < constellations.length; i++) {
+            assertThat(constellations[i]).isEqualTo(values.get(i * 2));
+            assertThat(svIds[i]).isEqualTo(values.get(i * 2 + 1));
+        }
+    }
+
+    private static void notifyContentObserverFor(String globalSetting) {
+        Collection<ContentObserver> contentObservers =
+                Shadows.shadowOf(RuntimeEnvironment.application.getContentResolver())
+                        .getContentObservers(Settings.Global.getUriFor(globalSetting));
+        assertThat(contentObservers).isNotEmpty();
+        contentObservers.iterator().next().onChange(false /* selfChange */);
+    }
+}
diff --git a/com/android/server/location/NtpTimeHelper.java b/com/android/server/location/NtpTimeHelper.java
new file mode 100644
index 0000000..296b500
--- /dev/null
+++ b/com/android/server/location/NtpTimeHelper.java
@@ -0,0 +1,191 @@
+package com.android.server.location;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.NtpTrustedTime;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Date;
+
+/**
+ * Handles inject NTP time to GNSS.
+ *
+ * <p>The client is responsible to call {@link #onNetworkAvailable()} when network is available
+ * for retrieving NTP Time.
+ */
+class NtpTimeHelper {
+
+    private static final String TAG = "NtpTimeHelper";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // states for injecting ntp
+    private static final int STATE_PENDING_NETWORK = 0;
+    private static final int STATE_RETRIEVING_AND_INJECTING = 1;
+    private static final int STATE_IDLE = 2;
+
+    // how often to request NTP time, in milliseconds
+    // current setting 24 hours
+    @VisibleForTesting
+    static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
+
+    // how long to wait if we have a network error in NTP
+    // the initial value of the exponential backoff
+    // current setting - 5 minutes
+    @VisibleForTesting
+    static final long RETRY_INTERVAL = 5 * 60 * 1000;
+    // how long to wait if we have a network error in NTP
+    // the max value of the exponential backoff
+    // current setting - 4 hours
+    private static final long MAX_RETRY_INTERVAL = 4 * 60 * 60 * 1000;
+
+    private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
+    private static final String WAKELOCK_KEY = "NtpTimeHelper";
+
+    private final ExponentialBackOff mNtpBackOff = new ExponentialBackOff(RETRY_INTERVAL,
+            MAX_RETRY_INTERVAL);
+
+    private final ConnectivityManager mConnMgr;
+    private final NtpTrustedTime mNtpTime;
+    private final WakeLock mWakeLock;
+    private final Handler mHandler;
+
+    @GuardedBy("this")
+    private final InjectNtpTimeCallback mCallback;
+
+    // flags to trigger NTP when network becomes available
+    // initialized to STATE_PENDING_NETWORK so we do NTP when the network comes up after booting
+    @GuardedBy("this")
+    private int mInjectNtpTimeState = STATE_PENDING_NETWORK;
+
+    // set to true if the GPS engine requested on-demand NTP time requests
+    @GuardedBy("this")
+    private boolean mOnDemandTimeInjection;
+
+    interface InjectNtpTimeCallback {
+        void injectTime(long time, long timeReference, int uncertainty);
+    }
+
+    @VisibleForTesting
+    NtpTimeHelper(Context context, Looper looper, InjectNtpTimeCallback callback,
+            NtpTrustedTime ntpTime) {
+        mConnMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        mCallback = callback;
+        mNtpTime = ntpTime;
+        mHandler = new Handler(looper);
+        PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
+    }
+
+    NtpTimeHelper(Context context, Looper looper, InjectNtpTimeCallback callback) {
+        this(context, looper, callback, NtpTrustedTime.getInstance(context));
+    }
+
+    synchronized void enablePeriodicTimeInjection() {
+        mOnDemandTimeInjection = true;
+    }
+
+    synchronized void onNetworkAvailable() {
+        if (mInjectNtpTimeState == STATE_PENDING_NETWORK) {
+            retrieveAndInjectNtpTime();
+        }
+    }
+
+    /**
+     * @return {@code true} if there is a network available for outgoing connections,
+     * {@code false} otherwise.
+     */
+    private boolean isNetworkConnected() {
+        NetworkInfo activeNetworkInfo = mConnMgr.getActiveNetworkInfo();
+        return activeNetworkInfo != null && activeNetworkInfo.isConnected();
+    }
+
+    synchronized void retrieveAndInjectNtpTime() {
+        if (mInjectNtpTimeState == STATE_RETRIEVING_AND_INJECTING) {
+            // already downloading data
+            return;
+        }
+        if (!isNetworkConnected()) {
+            // try again when network is up
+            mInjectNtpTimeState = STATE_PENDING_NETWORK;
+            return;
+        }
+        mInjectNtpTimeState = STATE_RETRIEVING_AND_INJECTING;
+
+        // hold wake lock while task runs
+        mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
+        new Thread(this::blockingGetNtpTimeAndInject).start();
+    }
+
+    /** {@link NtpTrustedTime#forceRefresh} is a blocking network operation. */
+    private void blockingGetNtpTimeAndInject() {
+        long delay;
+
+        // force refresh NTP cache when outdated
+        boolean refreshSuccess = true;
+        if (mNtpTime.getCacheAge() >= NTP_INTERVAL) {
+            // Blocking network operation.
+            refreshSuccess = mNtpTime.forceRefresh();
+        }
+
+        synchronized (this) {
+            mInjectNtpTimeState = STATE_IDLE;
+
+            // only update when NTP time is fresh
+            // If refreshSuccess is false, cacheAge does not drop down.
+            if (mNtpTime.getCacheAge() < NTP_INTERVAL) {
+                long time = mNtpTime.getCachedNtpTime();
+                long timeReference = mNtpTime.getCachedNtpTimeReference();
+                long certainty = mNtpTime.getCacheCertainty();
+
+                if (DEBUG) {
+                    long now = System.currentTimeMillis();
+                    Log.d(TAG, "NTP server returned: "
+                            + time + " (" + new Date(time)
+                            + ") reference: " + timeReference
+                            + " certainty: " + certainty
+                            + " system time offset: " + (time - now));
+                }
+
+                // Ok to cast to int, as can't rollover in practice
+                mHandler.post(() -> mCallback.injectTime(time, timeReference, (int) certainty));
+
+                delay = NTP_INTERVAL;
+                mNtpBackOff.reset();
+            } else {
+                Log.e(TAG, "requestTime failed");
+                delay = mNtpBackOff.nextBackoffMillis();
+            }
+
+            if (DEBUG) {
+                Log.d(TAG, String.format(
+                        "onDemandTimeInjection=%s, refreshSuccess=%s, delay=%s",
+                        mOnDemandTimeInjection,
+                        refreshSuccess,
+                        delay));
+            }
+            // TODO(b/73893222): reconcile Capabilities bit 'on demand' name vs. de facto periodic
+            // injection.
+            if (mOnDemandTimeInjection || !refreshSuccess) {
+                /* Schedule next NTP injection.
+                 * Since this is delayed, the wake lock is released right away, and will be held
+                 * again when the delayed task runs.
+                 */
+                mHandler.postDelayed(this::retrieveAndInjectNtpTime, delay);
+            }
+        }
+        try {
+            // release wake lock held by task
+            mWakeLock.release();
+        } catch (Exception e) {
+            // This happens when the WakeLock is already released.
+        }
+    }
+}
diff --git a/com/android/server/location/NtpTimeHelperTest.java b/com/android/server/location/NtpTimeHelperTest.java
new file mode 100644
index 0000000..a68b579
--- /dev/null
+++ b/com/android/server/location/NtpTimeHelperTest.java
@@ -0,0 +1,100 @@
+package com.android.server.location;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.util.NtpTrustedTime;
+
+import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowSystemClock;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link NtpTimeHelper}.
+ */
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+        manifest = Config.NONE,
+        sdk = 27
+)
+@SystemLoaderPackages({"com.android.server.location"})
+@Presubmit
+public class NtpTimeHelperTest {
+
+    private static final long MOCK_NTP_TIME = 1519930775453L;
+    @Mock
+    private NtpTrustedTime mMockNtpTrustedTime;
+    private NtpTimeHelper mNtpTimeHelper;
+    private CountDownLatch mCountDownLatch;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mCountDownLatch = new CountDownLatch(1);
+        InjectNtpTimeCallback callback =
+                (time, timeReference, uncertainty) -> {
+                    assertThat(time).isEqualTo(MOCK_NTP_TIME);
+                    mCountDownLatch.countDown();
+                };
+        mNtpTimeHelper = new NtpTimeHelper(RuntimeEnvironment.application,
+                Looper.myLooper(),
+                callback, mMockNtpTrustedTime);
+    }
+
+    @Test
+    public void handleInjectNtpTime_cachedAgeLow_injectTime() throws InterruptedException {
+        doReturn(NtpTimeHelper.NTP_INTERVAL - 1).when(mMockNtpTrustedTime).getCacheAge();
+        doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime();
+
+        mNtpTimeHelper.retrieveAndInjectNtpTime();
+
+        waitForTasksToBePostedOnHandlerAndRunThem();
+        assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+    }
+
+    @Test
+    public void handleInjectNtpTime_injectTimeFailed_injectTimeDelayed()
+            throws InterruptedException {
+        doReturn(NtpTimeHelper.NTP_INTERVAL + 1).when(mMockNtpTrustedTime).getCacheAge();
+        doReturn(false).when(mMockNtpTrustedTime).forceRefresh();
+
+        mNtpTimeHelper.retrieveAndInjectNtpTime();
+        waitForTasksToBePostedOnHandlerAndRunThem();
+        assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isFalse();
+
+        doReturn(true).when(mMockNtpTrustedTime).forceRefresh();
+        doReturn(1L).when(mMockNtpTrustedTime).getCacheAge();
+        doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime();
+        ShadowSystemClock.sleep(NtpTimeHelper.RETRY_INTERVAL);
+
+        waitForTasksToBePostedOnHandlerAndRunThem();
+        assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+    }
+
+    /**
+     * Since a thread is created in {@link NtpTimeHelper#retrieveAndInjectNtpTime} and the task to
+     * be verified is posted in the thread, we have to wait for the task to be posted and then it
+     * can be run.
+     */
+    private void waitForTasksToBePostedOnHandlerAndRunThem() throws InterruptedException {
+        mCountDownLatch.await(1, TimeUnit.SECONDS);
+        ShadowLooper.runUiThreadTasks();
+    }
+}
+
diff --git a/com/android/server/locksettings/SyntheticPasswordManager.java b/com/android/server/locksettings/SyntheticPasswordManager.java
index 88b2a36..0700ab3 100644
--- a/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -534,17 +534,33 @@
 
     private void destroyWeaverSlot(long handle, int userId) {
         int slot = loadWeaverSlot(handle, userId);
+        destroyState(WEAVER_SLOT_NAME, handle, userId);
         if (slot != INVALID_WEAVER_SLOT) {
-            try {
-                weaverEnroll(slot, null, null);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to destroy slot", e);
+            Set<Integer> usedSlots = getUsedWeaverSlots();
+            if (!usedSlots.contains(slot)) {
+                Log.i(TAG, "Destroy weaver slot " + slot + " for user " + userId);
+                try {
+                    weaverEnroll(slot, null, null);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Failed to destroy slot", e);
+                }
+            } else {
+                Log.w(TAG, "Skip destroying reused weaver slot " + slot + " for user " + userId);
             }
         }
-        destroyState(WEAVER_SLOT_NAME, handle, userId);
     }
 
-    private int getNextAvailableWeaverSlot() {
+    /**
+     * Return the set of weaver slots that are currently in use by all users on the device.
+     * <p>
+     * <em>Note:</em> Users who are in the process of being deleted are not tracked here
+     * (due to them being marked as partial in UserManager so not visible from
+     * {@link UserManager#getUsers}). As a result their weaver slots will not be considered
+     * taken and can be reused by new users. Care should be taken when cleaning up the
+     * deleted user in {@link #removeUser}, to prevent a reused slot from being erased
+     * unintentionally.
+     */
+    private Set<Integer> getUsedWeaverSlots() {
         Map<Integer, List<Long>> slotHandles = mStorage.listSyntheticPasswordHandlesForAllUsers(
                 WEAVER_SLOT_NAME);
         HashSet<Integer> slots = new HashSet<>();
@@ -554,8 +570,13 @@
                 slots.add(slot);
             }
         }
+        return slots;
+    }
+
+    private int getNextAvailableWeaverSlot() {
+        Set<Integer> usedSlots = getUsedWeaverSlots();
         for (int i = 0; i < mWeaverConfig.slots; i++) {
-            if (!slots.contains(i)) {
+            if (!usedSlots.contains(i)) {
                 return i;
             }
         }
@@ -592,6 +613,7 @@
         if (isWeaverAvailable()) {
             // Weaver based user password
             int weaverSlot = getNextAvailableWeaverSlot();
+            Log.i(TAG, "Weaver enroll password to slot " + weaverSlot + " for user " + userId);
             byte[] weaverSecret = weaverEnroll(weaverSlot, passwordTokenToWeaverKey(pwdToken), null);
             if (weaverSecret == null) {
                 Log.e(TAG, "Fail to enroll user password under weaver " + userId);
@@ -749,6 +771,7 @@
         if (isWeaverAvailable()) {
             int slot = getNextAvailableWeaverSlot();
             try {
+                Log.i(TAG, "Weaver enroll token to slot " + slot + " for user " + userId);
                 weaverEnroll(slot, null, tokenData.weaverSecret);
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to enroll weaver secret when activating token", e);
diff --git a/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
index 4c4176a..567eaaa 100644
--- a/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
+++ b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
@@ -20,6 +20,7 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
+import android.security.Scrypt;
 import android.security.keystore.recovery.KeyChainProtectionParams;
 import android.security.keystore.recovery.KeyChainSnapshot;
 import android.security.keystore.recovery.KeyDerivationParams;
@@ -69,6 +70,17 @@
     private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";
     private static final int TRUSTED_HARDWARE_MAX_ATTEMPTS = 10;
 
+    // TODO: Reduce the minimal length once all other components are updated
+    private static final int MIN_CREDENTIAL_LEN_TO_USE_SCRYPT = 24;
+    @VisibleForTesting
+    static final int SCRYPT_PARAM_N = 4096;
+    @VisibleForTesting
+    static final int SCRYPT_PARAM_R = 8;
+    @VisibleForTesting
+    static final int SCRYPT_PARAM_P = 1;
+    @VisibleForTesting
+    static final int SCRYPT_PARAM_OUTLEN_BYTES = 32;
+
     private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
     private final int mUserId;
     private final int mCredentialType;
@@ -78,6 +90,7 @@
     private final RecoverySnapshotStorage mRecoverySnapshotStorage;
     private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
     private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
+    private final Scrypt mScrypt;
 
     public static KeySyncTask newInstance(
             Context context,
@@ -98,7 +111,8 @@
                 credential,
                 credentialUpdated,
                 PlatformKeyManager.getInstance(context, recoverableKeyStoreDb),
-                new TestOnlyInsecureCertificateHelper());
+                new TestOnlyInsecureCertificateHelper(),
+                new Scrypt());
     }
 
     /**
@@ -110,7 +124,7 @@
      * @param credential The credential, encoded as a {@link String}.
      * @param credentialUpdated signals weather credentials were updated.
      * @param platformKeyManager platform key manager
-     * @param TestOnlyInsecureCertificateHelper utility class used for end-to-end tests
+     * @param testOnlyInsecureCertificateHelper utility class used for end-to-end tests
      */
     @VisibleForTesting
     KeySyncTask(
@@ -122,7 +136,8 @@
             String credential,
             boolean credentialUpdated,
             PlatformKeyManager platformKeyManager,
-            TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
+            TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
+            Scrypt scrypt) {
         mSnapshotListenersStorage = recoverySnapshotListenersStorage;
         mRecoverableKeyStoreDb = recoverableKeyStoreDb;
         mUserId = userId;
@@ -131,7 +146,8 @@
         mCredentialUpdated = credentialUpdated;
         mPlatformKeyManager = platformKeyManager;
         mRecoverySnapshotStorage = snapshotStorage;
-        mTestOnlyInsecureCertificateHelper = TestOnlyInsecureCertificateHelper;
+        mTestOnlyInsecureCertificateHelper = testOnlyInsecureCertificateHelper;
+        mScrypt = scrypt;
     }
 
     @Override
@@ -230,8 +246,14 @@
             }
         }
 
+        boolean useScryptToHashCredential = shouldUseScryptToHashCredential(rootCertAlias);
         byte[] salt = generateSalt();
-        byte[] localLskfHash = hashCredentials(salt, mCredential);
+        byte[] localLskfHash;
+        if (useScryptToHashCredential) {
+            localLskfHash = hashCredentialsByScrypt(salt, mCredential);
+        } else {
+            localLskfHash = hashCredentialsBySaltedSha256(salt, mCredential);
+        }
 
         Map<String, SecretKey> rawKeys;
         try {
@@ -303,10 +325,17 @@
             Log.e(TAG,"Could not encrypt with recovery key", e);
             return;
         }
+        KeyDerivationParams keyDerivationParams;
+        if (useScryptToHashCredential) {
+            keyDerivationParams = KeyDerivationParams.createScryptParams(
+                    salt, /*memoryDifficulty=*/ SCRYPT_PARAM_N);
+        } else {
+            keyDerivationParams = KeyDerivationParams.createSha256Params(salt);
+        }
         KeyChainProtectionParams metadata = new KeyChainProtectionParams.Builder()
                 .setUserSecretType(TYPE_LOCKSCREEN)
                 .setLockScreenUiFormat(getUiFormat(mCredentialType, mCredential))
-                .setKeyDerivationParams(KeyDerivationParams.createSha256Params(salt))
+                .setKeyDerivationParams(keyDerivationParams)
                 .setSecret(new byte[0])
                 .build();
 
@@ -443,7 +472,7 @@
      * @return The SHA-256 hash.
      */
     @VisibleForTesting
-    static byte[] hashCredentials(byte[] salt, String credentials) {
+    static byte[] hashCredentialsBySaltedSha256(byte[] salt, String credentials) {
         byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
         ByteBuffer byteBuffer = ByteBuffer.allocate(
                 salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
@@ -462,6 +491,12 @@
         }
     }
 
+    private byte[] hashCredentialsByScrypt(byte[] salt, String credentials) {
+        return mScrypt.scrypt(
+                credentials.getBytes(StandardCharsets.UTF_8), salt,
+                SCRYPT_PARAM_N, SCRYPT_PARAM_R, SCRYPT_PARAM_P, SCRYPT_PARAM_OUTLEN_BYTES);
+    }
+
     private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
         KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
         keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
@@ -479,4 +514,11 @@
         }
         return keyEntries;
     }
+
+    private boolean shouldUseScryptToHashCredential(String rootCertAlias) {
+        return mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
+                && mCredential.length() >= MIN_CREDENTIAL_LEN_TO_USE_SCRYPT
+                // TODO: Remove the test cert check once all other components are updated
+                && mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias);
+    }
 }
diff --git a/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
index e5ff5b8..52394d2 100644
--- a/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
+++ b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
@@ -375,6 +375,8 @@
             throws NoSuchAlgorithmException, KeyStoreException {
         String encryptAlias = getEncryptAlias(userId, generationId);
         String decryptAlias = getDecryptAlias(userId, generationId);
+        // SecretKey implementation doesn't provide reliable way to destroy the secret
+        // so it may live in memory for some time.
         SecretKey secretKey = generateAesKey();
 
         // Store decryption key first since it is more likely to fail.
@@ -398,8 +400,6 @@
                     .build());
 
         setGenerationId(userId, generationId);
-
-        // TODO: Use a reliable way to destroy the temporary secretKey in memory.
     }
 
     /**
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
index 7ebe8bf..396862d 100644
--- a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
@@ -29,7 +29,6 @@
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 
-// TODO: Rename RecoverableKeyGenerator to RecoverableKeyManager as it can import a key too now
 /**
  * Generates/imports keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
  *
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index c09d725..c484251 100644
--- a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -18,6 +18,7 @@
 
 import static android.security.keystore.recovery.RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT;
 import static android.security.keystore.recovery.RecoveryController.ERROR_DECRYPTION_FAILED;
+import static android.security.keystore.recovery.RecoveryController.ERROR_DOWNGRADE_CERTIFICATE;
 import static android.security.keystore.recovery.RecoveryController.ERROR_INSECURE_USER;
 import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_KEY_FORMAT;
 import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_CERTIFICATE;
@@ -46,12 +47,12 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.Preconditions;
-import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils;
-import com.android.server.locksettings.recoverablekeystore.certificate.SigXml;
-import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
 import com.android.server.locksettings.recoverablekeystore.certificate.CertParsingException;
+import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils;
 import com.android.server.locksettings.recoverablekeystore.certificate.CertValidationException;
 import com.android.server.locksettings.recoverablekeystore.certificate.CertXml;
+import com.android.server.locksettings.recoverablekeystore.certificate.SigXml;
+import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
@@ -212,6 +213,8 @@
                 Log.i(TAG, "The cert file serial number is the same, so skip updating.");
             } else {
                 Log.e(TAG, "The cert file serial number is older than the one in database.");
+                throw new ServiceSpecificException(ERROR_DOWNGRADE_CERTIFICATE,
+                        "The cert file serial number is older than the one in database.");
             }
             return;
         }
@@ -295,20 +298,6 @@
         initRecoveryService(rootCertificateAlias, recoveryServiceCertFile);
     }
 
-    private PublicKey parseEcPublicKey(@NonNull byte[] bytes) throws ServiceSpecificException {
-        try {
-            KeyFactory kf = KeyFactory.getInstance("EC");
-            X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(bytes);
-            return kf.generatePublic(pkSpec);
-        } catch (NoSuchAlgorithmException e) {
-            Log.wtf(TAG, "EC algorithm not available. AOSP must support this.", e);
-            throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
-        } catch (InvalidKeySpecException e) {
-            throw new ServiceSpecificException(
-                    ERROR_BAD_CERTIFICATE_FORMAT, "Not a valid X509 certificate.");
-        }
-    }
-
     /**
      * Gets all data necessary to recover application keys on new device.
      *
@@ -747,8 +736,6 @@
         int uid = Binder.getCallingUid();
         int userId = UserHandle.getCallingUserId();
 
-        // TODO: Refactor RecoverableKeyGenerator to wrap the PlatformKey logic
-
         PlatformEncryptionKey encryptionKey;
         try {
             encryptionKey = mPlatformKeyManager.getEncryptKey(userId);
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
index 53c972f..7c4360e 100644
--- a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
@@ -291,7 +291,7 @@
     }
 
     /**
-     * Sets the {@code generationId} of the platform key for the account owned by {@code userId}.
+     * Sets the {@code generationId} of the platform key for user {@code userId}.
      *
      * @return The primary key ID of the relation.
      */
@@ -630,7 +630,6 @@
      * @hide
      */
     public long setActiveRootOfTrust(int userId, int uid, @Nullable String rootAlias) {
-        // TODO: Call getDefaultCertificateAliasIfEmpty() here too?
         SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
         ContentValues values = new ContentValues();
         values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_ACTIVE_ROOT_OF_TRUST, rootAlias);
diff --git a/com/android/server/media/MediaSessionService.java b/com/android/server/media/MediaSessionService.java
index 6413ba9..a3c6c80 100644
--- a/com/android/server/media/MediaSessionService.java
+++ b/com/android/server/media/MediaSessionService.java
@@ -1515,6 +1515,24 @@
             final int uid = Binder.getCallingUid();
             final long token = Binder.clearCallingIdentity();
             try {
+                int controllerUserId = UserHandle.getUserId(controllerUid);
+                int controllerUidFromPackageName;
+                try {
+                    controllerUidFromPackageName = getContext().getPackageManager()
+                            .getPackageUidAsUser(controllerPackageName, controllerUserId);
+                } catch (NameNotFoundException e) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Package " + controllerPackageName + " doesn't exist");
+                    }
+                    return false;
+                }
+                if (controllerUidFromPackageName != controllerUid) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Package name " + controllerPackageName
+                                + " doesn't match with the uid " + controllerUid);
+                    }
+                    return false;
+                }
                 return hasMediaControlPermission(UserHandle.getUserId(uid), controllerPackageName,
                         controllerPid, controllerUid);
             } finally {
diff --git a/com/android/server/net/NetworkPolicyManagerService.java b/com/android/server/net/NetworkPolicyManagerService.java
index 29d2e55..a85960a 100644
--- a/com/android/server/net/NetworkPolicyManagerService.java
+++ b/com/android/server/net/NetworkPolicyManagerService.java
@@ -39,6 +39,7 @@
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkPolicy.LIMIT_DISABLED;
 import static android.net.NetworkPolicy.SNOOZE_NEVER;
@@ -70,9 +71,18 @@
 import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
 import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.provider.Settings.Global.NETPOLICY_OVERRIDE_ENABLED;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_ENABLED;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_FRAC_JOBS;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_FRAC_MULTIPATH;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_LIMITED;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_UNLIMITED;
 import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
 import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED;
 import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
+import static android.telephony.CarrierConfigManager.KEY_DATA_LIMIT_NOTIFICATION_BOOL;
+import static android.telephony.CarrierConfigManager.KEY_DATA_RAPID_NOTIFICATION_BOOL;
+import static android.telephony.CarrierConfigManager.KEY_DATA_WARNING_NOTIFICATION_BOOL;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 import static com.android.internal.util.ArrayUtils.appendInt;
@@ -115,6 +125,7 @@
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -188,6 +199,7 @@
 import android.util.DataUnit;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Range;
 import android.util.RecurrenceRule;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -208,6 +220,7 @@
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.internal.util.StatLogger;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
@@ -351,6 +364,11 @@
      */
     private static final long WAIT_FOR_ADMIN_DATA_TIMEOUT_MS = 10_000;
 
+    private static final long QUOTA_UNLIMITED_DEFAULT = DataUnit.MEBIBYTES.toBytes(20);
+    private static final float QUOTA_LIMITED_DEFAULT = 0.1f;
+    private static final float QUOTA_FRAC_JOBS_DEFAULT = 0.5f;
+    private static final float QUOTA_FRAC_MULTIPATH_DEFAULT = 0.5f;
+
     private static final int MSG_RULES_CHANGED = 1;
     private static final int MSG_METERED_IFACES_CHANGED = 2;
     private static final int MSG_LIMIT_REACHED = 5;
@@ -362,6 +380,7 @@
     private static final int MSG_RESET_FIREWALL_RULES_BY_UID = 15;
     private static final int MSG_SUBSCRIPTION_OVERRIDE = 16;
     private static final int MSG_METERED_RESTRICTED_PACKAGES_CHANGED = 17;
+    private static final int MSG_SET_NETWORK_TEMPLATE_ENABLED = 18;
 
     private static final int UID_MSG_STATE_CHANGED = 100;
     private static final int UID_MSG_GONE = 101;
@@ -490,6 +509,10 @@
     @GuardedBy("mNetworkPoliciesSecondLock")
     private final SparseBooleanArray mNetworkMetered = new SparseBooleanArray();
 
+    /** Map from network ID to last observed roaming state */
+    @GuardedBy("mNetworkPoliciesSecondLock")
+    private final SparseBooleanArray mNetworkRoaming = new SparseBooleanArray();
+
     /** Map from netId to subId as of last update */
     @GuardedBy("mNetworkPoliciesSecondLock")
     private final SparseIntArray mNetIdToSubId = new SparseIntArray();
@@ -526,6 +549,19 @@
 
     // TODO: migrate notifications to SystemUI
 
+
+    interface Stats {
+        int UPDATE_NETWORK_ENABLED = 0;
+        int IS_UID_NETWORKING_BLOCKED = 1;
+
+        int COUNT = IS_UID_NETWORKING_BLOCKED + 1;
+    }
+
+    public final StatLogger mStatLogger = new StatLogger(new String[] {
+            "updateNetworkEnabledNL()",
+            "isUidNetworkingBlocked()",
+    });
+
     public NetworkPolicyManagerService(Context context, IActivityManager activityManager,
             INetworkManagementService networkManagement) {
         this(context, activityManager, networkManagement, AppGlobals.getPackageManager(),
@@ -1005,6 +1041,16 @@
         }
     };
 
+    private static boolean updateCapabilityChange(SparseBooleanArray lastValues, boolean newValue,
+            Network network) {
+        final boolean lastValue = lastValues.get(network.netId, false);
+        final boolean changed = (lastValue != newValue) || lastValues.indexOfKey(network.netId) < 0;
+        if (changed) {
+            lastValues.put(network.netId, newValue);
+        }
+        return changed;
+    }
+
     private final NetworkCallback mNetworkCallback = new NetworkCallback() {
         @Override
         public void onCapabilitiesChanged(Network network,
@@ -1012,13 +1058,18 @@
             if (network == null || networkCapabilities == null) return;
 
             synchronized (mNetworkPoliciesSecondLock) {
-                final boolean oldMetered = mNetworkMetered.get(network.netId, false);
                 final boolean newMetered = !networkCapabilities
                         .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+                final boolean meteredChanged = updateCapabilityChange(
+                        mNetworkMetered, newMetered, network);
 
-                if ((oldMetered != newMetered) || mNetworkMetered.indexOfKey(network.netId) < 0) {
+                final boolean newRoaming = !networkCapabilities
+                        .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+                final boolean roamingChanged = updateCapabilityChange(
+                        mNetworkRoaming, newRoaming, network);
+
+                if (meteredChanged || roamingChanged) {
                     mLogger.meterednessChanged(network.netId, newMetered);
-                    mNetworkMetered.put(network.netId, newMetered);
                     updateNetworkRulesNL();
                 }
             }
@@ -1059,8 +1110,10 @@
         final long now = mClock.millis();
         for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
             final NetworkPolicy policy = mNetworkPolicy.valueAt(i);
+            final int subId = findRelevantSubId(policy.template);
+
             // ignore policies that aren't relevant to user
-            if (!isTemplateRelevant(policy.template)) continue;
+            if (subId == INVALID_SUBSCRIPTION_ID) continue;
             if (!policy.hasCycle()) continue;
 
             final Pair<ZonedDateTime, ZonedDateTime> cycle = NetworkPolicyManager
@@ -1069,28 +1122,43 @@
             final long cycleEnd = cycle.second.toInstant().toEpochMilli();
             final long totalBytes = getTotalBytes(policy.template, cycleStart, cycleEnd);
 
-            // Notify when data usage is over warning/limit
-            if (policy.isOverLimit(totalBytes)) {
-                final boolean snoozedThisCycle = policy.lastLimitSnooze >= cycleStart;
-                if (snoozedThisCycle) {
-                    enqueueNotification(policy, TYPE_LIMIT_SNOOZED, totalBytes, null);
-                } else {
-                    enqueueNotification(policy, TYPE_LIMIT, totalBytes, null);
-                    notifyOverLimitNL(policy.template);
+            // Carrier might want to manage notifications themselves
+            final PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId);
+            final boolean notifyWarning = getBooleanDefeatingNullable(config,
+                    KEY_DATA_WARNING_NOTIFICATION_BOOL, true);
+            final boolean notifyLimit = getBooleanDefeatingNullable(config,
+                    KEY_DATA_LIMIT_NOTIFICATION_BOOL, true);
+            final boolean notifyRapid = getBooleanDefeatingNullable(config,
+                    KEY_DATA_RAPID_NOTIFICATION_BOOL, true);
+
+            // Notify when data usage is over warning
+            if (notifyWarning) {
+                if (policy.isOverWarning(totalBytes) && !policy.isOverLimit(totalBytes)) {
+                    final boolean snoozedThisCycle = policy.lastWarningSnooze >= cycleStart;
+                    if (!snoozedThisCycle) {
+                        enqueueNotification(policy, TYPE_WARNING, totalBytes, null);
+                    }
                 }
+            }
 
-            } else {
-                notifyUnderLimitNL(policy.template);
-
-                final boolean snoozedThisCycle = policy.lastWarningSnooze >= cycleStart;
-                if (policy.isOverWarning(totalBytes) && !snoozedThisCycle) {
-                    enqueueNotification(policy, TYPE_WARNING, totalBytes, null);
+            // Notify when data usage is over limit
+            if (notifyLimit) {
+                if (policy.isOverLimit(totalBytes)) {
+                    final boolean snoozedThisCycle = policy.lastLimitSnooze >= cycleStart;
+                    if (snoozedThisCycle) {
+                        enqueueNotification(policy, TYPE_LIMIT_SNOOZED, totalBytes, null);
+                    } else {
+                        enqueueNotification(policy, TYPE_LIMIT, totalBytes, null);
+                        notifyOverLimitNL(policy.template);
+                    }
+                } else {
+                    notifyUnderLimitNL(policy.template);
                 }
             }
 
             // Warn if average usage over last 4 days is on track to blow pretty
             // far past the plan limits.
-            if (policy.limitBytes != LIMIT_DISABLED) {
+            if (notifyRapid && policy.limitBytes != LIMIT_DISABLED) {
                 final long recentDuration = TimeUnit.DAYS.toMillis(4);
                 final long recentStart = now - recentDuration;
                 final long recentEnd = now;
@@ -1167,27 +1235,26 @@
      * current device state, such as when
      * {@link TelephonyManager#getSubscriberId()} matches. This is regardless of
      * data connection status.
+     *
+     * @return relevant subId, or {@link #INVALID_SUBSCRIPTION_ID} when no
+     *         matching subId found.
      */
-    private boolean isTemplateRelevant(NetworkTemplate template) {
-        if (template.isMatchRuleMobile()) {
-            final TelephonyManager tele = mContext.getSystemService(TelephonyManager.class);
-            final SubscriptionManager sub = mContext.getSystemService(SubscriptionManager.class);
+    private int findRelevantSubId(NetworkTemplate template) {
+        final TelephonyManager tele = mContext.getSystemService(TelephonyManager.class);
+        final SubscriptionManager sub = mContext.getSystemService(SubscriptionManager.class);
 
-            // Mobile template is relevant when any active subscriber matches
-            final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
-            for (int subId : subIds) {
-                final String subscriberId = tele.getSubscriberId(subId);
-                final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
-                        TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true,
-                        true);
-                if (template.matches(probeIdent)) {
-                    return true;
-                }
+        // Mobile template is relevant when any active subscriber matches
+        final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
+        for (int subId : subIds) {
+            final String subscriberId = tele.getSubscriberId(subId);
+            final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
+                    TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true,
+                    true);
+            if (template.matches(probeIdent)) {
+                return subId;
             }
-            return false;
-        } else {
-            return true;
         }
+        return INVALID_SUBSCRIPTION_ID;
     }
 
     /**
@@ -1538,6 +1605,8 @@
         // TODO: reset any policy-disabled networks when any policy is removed
         // completely, which is currently rare case.
 
+        final long startTime = mStatLogger.getTime();
+
         for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
             final NetworkPolicy policy = mNetworkPolicy.valueAt(i);
             // shortcut when policy has no limit
@@ -1559,6 +1628,8 @@
 
             setNetworkTemplateEnabled(policy.template, networkEnabled);
         }
+
+        mStatLogger.logDurationStat(Stats.UPDATE_NETWORK_ENABLED, startTime);
     }
 
     /**
@@ -1566,6 +1637,13 @@
      * {@link NetworkTemplate}.
      */
     private void setNetworkTemplateEnabled(NetworkTemplate template, boolean enabled) {
+        // Don't call setNetworkTemplateEnabledInner() directly because we may have a lock
+        // held. Call it via the handler.
+        mHandler.obtainMessage(MSG_SET_NETWORK_TEMPLATE_ENABLED, enabled ? 1 : 0, 0, template)
+                .sendToTarget();
+    }
+
+    private void setNetworkTemplateEnabledInner(NetworkTemplate template, boolean enabled) {
         // TODO: reach into ConnectivityManager to proactively disable bringing
         // up this network, since we know that traffic will be blocked.
 
@@ -1731,10 +1809,18 @@
         }
         mMeteredIfaces = newMeteredIfaces;
 
+        final ContentResolver cr = mContext.getContentResolver();
+        final boolean quotaEnabled = Settings.Global.getInt(cr,
+                NETPOLICY_QUOTA_ENABLED, 1) != 0;
+        final long quotaUnlimited = Settings.Global.getLong(cr,
+                NETPOLICY_QUOTA_UNLIMITED, QUOTA_UNLIMITED_DEFAULT);
+        final float quotaLimited = Settings.Global.getFloat(cr,
+                NETPOLICY_QUOTA_LIMITED, QUOTA_LIMITED_DEFAULT);
+
         // Finally, calculate our opportunistic quotas
-        // TODO: add experiments support to disable or tweak ratios
         mSubscriptionOpportunisticQuota.clear();
         for (NetworkState state : states) {
+            if (!quotaEnabled) continue;
             if (state.network == null) continue;
             final int subId = getSubIdLocked(state.network);
             final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
@@ -1742,18 +1828,21 @@
 
             final long quotaBytes;
             final long limitBytes = plan.getDataLimitBytes();
-            if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
+            if (!state.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING)) {
+                // Clamp to 0 when roaming
+                quotaBytes = 0;
+            } else if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
                 quotaBytes = OPPORTUNISTIC_QUOTA_UNKNOWN;
             } else if (limitBytes == SubscriptionPlan.BYTES_UNLIMITED) {
                 // Unlimited data; let's use 20MiB/day (600MiB/month)
-                quotaBytes = DataUnit.MEBIBYTES.toBytes(20);
+                quotaBytes = quotaUnlimited;
             } else {
                 // Limited data; let's only use 10% of remaining budget
-                final Pair<ZonedDateTime, ZonedDateTime> cycle = plan.cycleIterator().next();
-                final long start = cycle.first.toInstant().toEpochMilli();
-                final long end = cycle.second.toInstant().toEpochMilli();
+                final Range<ZonedDateTime> cycle = plan.cycleIterator().next();
+                final long start = cycle.getLower().toInstant().toEpochMilli();
+                final long end = cycle.getUpper().toInstant().toEpochMilli();
                 final Instant now = mClock.instant();
-                final long startOfDay = ZonedDateTime.ofInstant(now, cycle.first.getZone())
+                final long startOfDay = ZonedDateTime.ofInstant(now, cycle.getLower().getZone())
                         .truncatedTo(ChronoUnit.DAYS)
                         .toInstant().toEpochMilli();
                 final long totalBytes = getTotalBytes(
@@ -1764,7 +1853,7 @@
                 final long remainingDays =
                         1 + ((end - now.toEpochMilli() - 1) / TimeUnit.DAYS.toMillis(1));
 
-                quotaBytes = Math.max(0, (remainingBytes / remainingDays) / 10);
+                quotaBytes = Math.max(0, (long) ((remainingBytes / remainingDays) * quotaLimited));
             }
 
             mSubscriptionOpportunisticQuota.put(subId, quotaBytes);
@@ -3034,17 +3123,25 @@
 
         // We can only override when carrier told us about plans
         synchronized (mNetworkPoliciesSecondLock) {
-            if (ArrayUtils.isEmpty(mSubscriptionPlans.get(subId))) {
+            final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
+            if (plan == null
+                    || plan.getDataLimitBehavior() == SubscriptionPlan.LIMIT_BEHAVIOR_UNKNOWN) {
                 throw new IllegalStateException(
-                        "Must provide SubscriptionPlan information before overriding");
+                        "Must provide valid SubscriptionPlan to enable overriding");
             }
         }
 
-        mHandler.sendMessage(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
-                overrideMask, overrideValue, subId));
-        if (timeoutMillis > 0) {
-            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
-                    overrideMask, 0, subId), timeoutMillis);
+        // Only allow overrides when feature is enabled. However, we always
+        // allow disabling of overrides for safety reasons.
+        final boolean overrideEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+                NETPOLICY_OVERRIDE_ENABLED, 1) != 0;
+        if (overrideEnabled || overrideValue == 0) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+                    overrideMask, overrideValue, subId));
+            if (timeoutMillis > 0) {
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+                        overrideMask, 0, subId), timeoutMillis);
+            }
         }
     }
 
@@ -3222,6 +3319,9 @@
                 }
                 fout.decreaseIndent();
 
+                fout.println();
+                mStatLogger.dump(fout);
+
                 mLogger.dumpLogs(fout);
             }
         }
@@ -4182,6 +4282,12 @@
                     setMeteredRestrictedPackagesInternal(packageNames, userId);
                     return true;
                 }
+                case MSG_SET_NETWORK_TEMPLATE_ENABLED: {
+                    final NetworkTemplate template = (NetworkTemplate) msg.obj;
+                    final boolean enabled = msg.arg1 != 0;
+                    setNetworkTemplateEnabledInner(template, enabled);
+                    return true;
+                }
                 default: {
                     return false;
                 }
@@ -4575,8 +4681,14 @@
 
     @Override
     public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
+        final long startTime = mStatLogger.getTime();
+
         mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
-        return isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+        final boolean ret = isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+
+        mStatLogger.logDurationStat(Stats.IS_UID_NETWORKING_BLOCKED, startTime);
+
+        return ret;
     }
 
     private boolean isUidNetworkingBlockedInternal(int uid, boolean isNetworkMetered) {
@@ -4651,11 +4763,17 @@
          */
         @Override
         public boolean isUidNetworkingBlocked(int uid, String ifname) {
+            final long startTime = mStatLogger.getTime();
+
             final boolean isNetworkMetered;
             synchronized (mNetworkPoliciesSecondLock) {
                 isNetworkMetered = mMeteredIfaces.contains(ifname);
             }
-            return isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+            final boolean ret = isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+
+            mStatLogger.logDurationStat(Stats.IS_UID_NETWORKING_BLOCKED, startTime);
+
+            return ret;
         }
 
         @Override
@@ -4681,11 +4799,24 @@
 
         @Override
         public long getSubscriptionOpportunisticQuota(Network network, int quotaType) {
+            final long quotaBytes;
             synchronized (mNetworkPoliciesSecondLock) {
-                // TODO: handle splitting quota between use-cases
-                return mSubscriptionOpportunisticQuota.get(getSubIdLocked(network),
+                quotaBytes = mSubscriptionOpportunisticQuota.get(getSubIdLocked(network),
                         OPPORTUNISTIC_QUOTA_UNKNOWN);
             }
+            if (quotaBytes == OPPORTUNISTIC_QUOTA_UNKNOWN) {
+                return OPPORTUNISTIC_QUOTA_UNKNOWN;
+            }
+
+            if (quotaType == QUOTA_TYPE_JOBS) {
+                return (long) (quotaBytes * Settings.Global.getFloat(mContext.getContentResolver(),
+                        NETPOLICY_QUOTA_FRAC_JOBS, QUOTA_FRAC_JOBS_DEFAULT));
+            } else if (quotaType == QUOTA_TYPE_MULTIPATH) {
+                return (long) (quotaBytes * Settings.Global.getFloat(mContext.getContentResolver(),
+                        NETPOLICY_QUOTA_FRAC_MULTIPATH, QUOTA_FRAC_MULTIPATH_DEFAULT));
+            } else {
+                return OPPORTUNISTIC_QUOTA_UNKNOWN;
+            }
         }
 
         @Override
@@ -4754,7 +4885,21 @@
     @GuardedBy("mNetworkPoliciesSecondLock")
     private SubscriptionPlan getPrimarySubscriptionPlanLocked(int subId) {
         final SubscriptionPlan[] plans = mSubscriptionPlans.get(subId);
-        return ArrayUtils.isEmpty(plans) ? null : plans[0];
+        if (!ArrayUtils.isEmpty(plans)) {
+            for (SubscriptionPlan plan : plans) {
+                if (plan.getCycleRule().isRecurring()) {
+                    // Recurring plans will always have an active cycle
+                    return plan;
+                } else {
+                    // Non-recurring plans need manual test for active cycle
+                    final Range<ZonedDateTime> cycle = plan.cycleIterator().next();
+                    if (cycle.contains(ZonedDateTime.now(mClock))) {
+                        return plan;
+                    }
+                }
+            }
+        }
+        return null;
     }
 
     /**
@@ -4801,6 +4946,11 @@
         return (val != null) ? val : new NetworkState[0];
     }
 
+    private static boolean getBooleanDefeatingNullable(@Nullable PersistableBundle bundle,
+            String key, boolean defaultValue) {
+        return (bundle != null) ? bundle.getBoolean(key, defaultValue) : defaultValue;
+    }
+
     private class NotificationId {
         private final String mTag;
         private final int mId;
diff --git a/com/android/server/net/NetworkStatsCollection.java b/com/android/server/net/NetworkStatsCollection.java
index 2ef754e..ab52523 100644
--- a/com/android/server/net/NetworkStatsCollection.java
+++ b/com/android/server/net/NetworkStatsCollection.java
@@ -47,7 +47,7 @@
 import android.util.AtomicFile;
 import android.util.IntArray;
 import android.util.MathUtils;
-import android.util.Pair;
+import android.util.Range;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
@@ -266,11 +266,11 @@
         long collectEnd = end;
 
         if (augmentEnd != SubscriptionPlan.TIME_UNKNOWN) {
-            final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = augmentPlan.cycleIterator();
+            final Iterator<Range<ZonedDateTime>> it = augmentPlan.cycleIterator();
             while (it.hasNext()) {
-                final Pair<ZonedDateTime, ZonedDateTime> cycle = it.next();
-                final long cycleStart = cycle.first.toInstant().toEpochMilli();
-                final long cycleEnd = cycle.second.toInstant().toEpochMilli();
+                final Range<ZonedDateTime> cycle = it.next();
+                final long cycleStart = cycle.getLower().toInstant().toEpochMilli();
+                final long cycleEnd = cycle.getUpper().toInstant().toEpochMilli();
                 if (cycleStart <= augmentEnd && augmentEnd < cycleEnd) {
                     augmentStart = cycleStart;
                     collectStart = Long.min(collectStart, augmentStart);
diff --git a/com/android/server/net/NetworkStatsObservers.java b/com/android/server/net/NetworkStatsObservers.java
index 741c206..d840873 100644
--- a/com/android/server/net/NetworkStatsObservers.java
+++ b/com/android/server/net/NetworkStatsObservers.java
@@ -16,7 +16,7 @@
 
 package com.android.server.net;
 
-import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
 
 import static com.android.internal.util.Preconditions.checkArgument;
 
@@ -52,8 +52,6 @@
     private static final String TAG = "NetworkStatsObservers";
     private static final boolean LOGV = false;
 
-    private static final long MIN_THRESHOLD_BYTES = 2 * MB_IN_BYTES;
-
     private static final int MSG_REGISTER = 1;
     private static final int MSG_UNREGISTER = 2;
     private static final int MSG_UPDATE_STATS = 3;
diff --git a/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java b/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
index 17c5868..766d8ca 100644
--- a/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
+++ b/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
@@ -91,7 +91,7 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             // Reset last report time
-            if (!WatchlistConfig.getInstance().isConfigSecure()) {
+            if (WatchlistConfig.getInstance().isConfigSecure()) {
                 pw.println("Error: Cannot force generate report under production config");
                 return -1;
             }
diff --git a/com/android/server/net/watchlist/ReportEncoder.java b/com/android/server/net/watchlist/ReportEncoder.java
index 2a8f4d5..a482e05 100644
--- a/com/android/server/net/watchlist/ReportEncoder.java
+++ b/com/android/server/net/watchlist/ReportEncoder.java
@@ -49,6 +49,7 @@
      * Apply DP on watchlist results, and generate a serialized watchlist report ready to store
      * in DropBox.
      */
+    @Nullable
     static byte[] encodeWatchlistReport(WatchlistConfig config, byte[] userSecret,
             List<String> appDigestList, WatchlistReportDbHelper.AggregatedResult aggregatedResult) {
         Map<String, Boolean> resultMap = PrivacyUtils.createDpEncodedReportMap(
diff --git a/com/android/server/net/watchlist/WatchlistConfig.java b/com/android/server/net/watchlist/WatchlistConfig.java
index d793842..8352ca6 100644
--- a/com/android/server/net/watchlist/WatchlistConfig.java
+++ b/com/android/server/net/watchlist/WatchlistConfig.java
@@ -16,6 +16,7 @@
 
 package com.android.server.net.watchlist;
 
+import android.annotation.Nullable;
 import android.os.FileUtils;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -55,9 +56,6 @@
     private static final String NETWORK_WATCHLIST_DB_FOR_TEST_PATH =
             "/data/misc/network_watchlist/network_watchlist_for_test.xml";
 
-    // Hash for null / unknown config, a 32 byte array filled with content 0x00
-    private static final byte[] UNKNOWN_CONFIG_HASH = new byte[32];
-
     private static class XmlTags {
         private static final String WATCHLIST_CONFIG = "watchlist-config";
         private static final String SHA256_DOMAIN = "sha256-domain";
@@ -228,16 +226,21 @@
         return mIsSecureConfig;
     }
 
+    @Nullable
+    /**
+     * Get watchlist config SHA-256 digest.
+     * Return null if watchlist config does not exist.
+     */
     public byte[] getWatchlistConfigHash() {
         if (!mXmlFile.exists()) {
-            return UNKNOWN_CONFIG_HASH;
+            return null;
         }
         try {
             return DigestUtils.getSha256Hash(mXmlFile);
         } catch (IOException | NoSuchAlgorithmException e) {
             Log.e(TAG, "Unable to get watchlist config hash", e);
         }
-        return UNKNOWN_CONFIG_HASH;
+        return null;
     }
 
     /**
@@ -271,8 +274,10 @@
     }
 
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.println("Watchlist config hash: " + HexDump.toHexString(getWatchlistConfigHash()));
+        final byte[] hash = getWatchlistConfigHash();
+        pw.println("Watchlist config hash: " + (hash != null ? HexDump.toHexString(hash) : null));
         pw.println("Domain CRC32 digest list:");
+        // mDomainDigests won't go from non-null to null so it's safe
         if (mDomainDigests != null) {
             mDomainDigests.crc32Digests.dump(fd, pw, args);
         }
@@ -281,6 +286,7 @@
             mDomainDigests.sha256Digests.dump(fd, pw, args);
         }
         pw.println("Ip CRC32 digest list:");
+        // mIpDigests won't go from non-null to null so it's safe
         if (mIpDigests != null) {
             mIpDigests.crc32Digests.dump(fd, pw, args);
         }
diff --git a/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/com/android/server/net/watchlist/WatchlistLoggingHandler.java
index b331b9c..864ce5d 100644
--- a/com/android/server/net/watchlist/WatchlistLoggingHandler.java
+++ b/com/android/server/net/watchlist/WatchlistLoggingHandler.java
@@ -346,6 +346,7 @@
      * @param ipAddresses Ip address that you want to search in watchlist.
      * @return Ip address that exists in watchlist, null if it does not match anything.
      */
+    @Nullable
     private String searchIpInWatchlist(String[] ipAddresses) {
         for (String ipAddress : ipAddresses) {
             if (isIpInWatchlist(ipAddress)) {
@@ -377,6 +378,7 @@
      * @param host Host that we want to search.
      * @return Domain that exists in watchlist, null if it does not match anything.
      */
+    @Nullable
     private String searchAllSubDomainsInWatchlist(String host) {
         if (host == null) {
             return null;
@@ -392,6 +394,7 @@
 
     /** Get all sub-domains in a host */
     @VisibleForTesting
+    @Nullable
     static String[] getAllSubDomains(String host) {
         if (host == null) {
             return null;
diff --git a/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index 632ab81..c69934a 100644
--- a/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -144,6 +144,7 @@
      * Aggregate all records in database before input timestamp, and return a
      * rappor encoded result.
      */
+    @Nullable
     public AggregatedResult getAggregatedRecords(long untilTimestamp) {
         final String selectStatement = WhiteListReportContract.TIMESTAMP + " < ?";
 
diff --git a/com/android/server/net/watchlist/WatchlistSettings.java b/com/android/server/net/watchlist/WatchlistSettings.java
index e20a510..c2f3ba0 100644
--- a/com/android/server/net/watchlist/WatchlistSettings.java
+++ b/com/android/server/net/watchlist/WatchlistSettings.java
@@ -86,7 +86,7 @@
         }
     }
 
-    public void reloadSettings() {
+    private void reloadSettings() {
         if (!mXmlFile.exists()) {
             // No settings config
             return;
diff --git a/com/android/server/notification/ManagedServices.java b/com/android/server/notification/ManagedServices.java
index d5a32aa..c98f6a2 100644
--- a/com/android/server/notification/ManagedServices.java
+++ b/com/android/server/notification/ManagedServices.java
@@ -110,7 +110,7 @@
     protected final Object mMutex;
     private final UserProfiles mUserProfiles;
     private final IPackageManager mPm;
-    private final UserManager mUm;
+    protected final UserManager mUm;
     private final Config mConfig;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
diff --git a/com/android/server/notification/NotificationDelegate.java b/com/android/server/notification/NotificationDelegate.java
index 36bc096..b61a27a 100644
--- a/com/android/server/notification/NotificationDelegate.java
+++ b/com/android/server/notification/NotificationDelegate.java
@@ -40,4 +40,6 @@
     void onNotificationExpansionChanged(String key, boolean userAction, boolean expanded);
     void onNotificationDirectReplied(String key);
     void onNotificationSettingsViewed(String key);
+    void onNotificationSmartRepliesAdded(String key, int replyCount);
+    void onNotificationSmartReplySent(String key, int replyIndex);
 }
diff --git a/com/android/server/notification/NotificationManagerService.java b/com/android/server/notification/NotificationManagerService.java
index f31ca0a..d59c9de 100644
--- a/com/android/server/notification/NotificationManagerService.java
+++ b/com/android/server/notification/NotificationManagerService.java
@@ -114,12 +114,14 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioManagerInternal;
 import android.media.IRingtonePlayer;
+import android.metrics.LogMaker;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
@@ -394,6 +396,8 @@
     private GroupHelper mGroupHelper;
     private boolean mIsTelevision;
 
+    private MetricsLogger mMetricsLogger;
+
     private static class Archive {
         final int mBufferSize;
         final ArrayDeque<StatusBarNotification> mBuffer;
@@ -492,8 +496,8 @@
                                     | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
             for (ComponentName cn : approvedAssistants) {
                 try {
-                    getBinderService().setNotificationAssistantAccessGrantedForUser(cn,
-                            userId, true);
+                    getBinderService().setNotificationAssistantAccessGrantedForUser(
+                            cn, userId, true);
                 } catch (RemoteException e) {
                     e.printStackTrace();
                 }
@@ -535,6 +539,8 @@
             mConditionProviders.migrateToXml();
             savePolicyFile();
         }
+
+        mAssistants.ensureAssistant();
     }
 
     private void loadPolicyFile() {
@@ -798,6 +804,18 @@
                         // Report to usage stats that notification was made visible
                         if (DBG) Slog.d(TAG, "Marking notification as visible " + nv.key);
                         reportSeen(r);
+
+                        // If the newly visible notification has smart replies
+                        // then log that the user has seen them.
+                        if (r.getNumSmartRepliesAdded() > 0
+                                && !r.hasSeenSmartReplies()) {
+                            r.setSeenSmartReplies(true);
+                            LogMaker logMaker = r.getLogMaker()
+                                    .setCategory(MetricsEvent.SMART_REPLY_VISIBLE)
+                                    .addTaggedData(MetricsEvent.NOTIFICATION_SMART_REPLY_COUNT,
+                                            r.getNumSmartRepliesAdded());
+                            mMetricsLogger.write(logMaker);
+                        }
                     }
                     r.setVisibility(true, nv.rank);
                     nv.recycle();
@@ -852,6 +870,31 @@
         }
 
         @Override
+        public void onNotificationSmartRepliesAdded(String key, int replyCount) {
+            synchronized (mNotificationLock) {
+                NotificationRecord r = mNotificationsByKey.get(key);
+                if (r != null) {
+                    r.setNumSmartRepliesAdded(replyCount);
+                }
+            }
+        }
+
+        @Override
+        public void onNotificationSmartReplySent(String key, int replyIndex) {
+            synchronized (mNotificationLock) {
+                NotificationRecord r = mNotificationsByKey.get(key);
+                if (r != null) {
+                    LogMaker logMaker = r.getLogMaker()
+                            .setCategory(MetricsEvent.SMART_REPLY_ACTION)
+                            .setSubtype(replyIndex);
+                    mMetricsLogger.write(logMaker);
+                    // Treat clicking on a smart reply as a user interaction.
+                    reportUserInteraction(r);
+                }
+            }
+        }
+
+        @Override
         public void onNotificationSettingsViewed(String key) {
             synchronized (mNotificationLock) {
                 NotificationRecord r = mNotificationsByKey.get(key);
@@ -1346,6 +1389,7 @@
             extractorNames = new String[0];
         }
         mUsageStats = usageStats;
+        mMetricsLogger = new MetricsLogger();
         mRankingHandler = new RankingHandlerWorker(mRankingThread.getLooper());
         mConditionProviders = conditionProviders;
         mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders);
@@ -1828,7 +1872,7 @@
         };
 
         int newSuppressedVisualEffects = incomingPolicy.suppressedVisualEffects;
-        if (targetSdkVersion <= Build.VERSION_CODES.O_MR1) {
+        if (targetSdkVersion < Build.VERSION_CODES.P) {
             // unset higher order bits introduced in P, maintain the user's higher order bits
             for (int i = 0; i < effectsIntroducedInP.length ; i++) {
                 newSuppressedVisualEffects &= ~effectsIntroducedInP[i];
@@ -2063,7 +2107,7 @@
 
         @Override
         public void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) {
-            checkCallerIsSystem();
+            enforceSystemOrSystemUI("setNotificationsEnabledForPackage");
 
             mRankingHelper.setEnabled(pkg, uid, enabled);
             // Now, cancel any outstanding notifications that are part of a just-disabled app
@@ -2286,6 +2330,12 @@
         }
 
         @Override
+        public int getBlockedChannelCount(String pkg, int uid) {
+            enforceSystemOrSystemUI("getBlockedChannelCount");
+            return mRankingHelper.getBlockedChannelCount(pkg, uid);
+        }
+
+        @Override
         public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroupsForPackage(
                 String pkg, int uid, boolean includeDeleted) {
             checkCallerIsSystem();
@@ -3179,7 +3229,7 @@
                         0, UserHandle.getUserId(MY_UID));
                 Policy currPolicy = mZenModeHelper.getNotificationPolicy();
 
-                if (applicationInfo.targetSdkVersion <= Build.VERSION_CODES.O_MR1) {
+                if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.P) {
                     int priorityCategories = policy.priorityCategories;
                     // ignore alarm and media values from new policy
                     priorityCategories &= ~Policy.PRIORITY_CATEGORY_ALARMS;
@@ -3947,9 +3997,14 @@
                     + ", notificationUid=" + notificationUid
                     + ", notification=" + notification;
             Log.e(TAG, noChannelStr);
-            doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
-                    "Failed to post notification on channel \"" + channelId + "\"\n" +
-                    "See log for more details");
+            boolean appNotificationsOff = mRankingHelper.getImportance(pkg, notificationUid)
+                    == NotificationManager.IMPORTANCE_NONE;
+
+            if (!appNotificationsOff) {
+                doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
+                        "Failed to post notification on channel \"" + channelId + "\"\n" +
+                        "See log for more details");
+            }
             return;
         }
 
@@ -6134,11 +6189,14 @@
             return !getServices().isEmpty();
         }
 
-        protected void upgradeXml(final int xmlVersion, final int userId) {
-            if (xmlVersion == 0) {
-                // one time approval of the OOB assistant
-                Slog.d(TAG, "Approving default notification assistant for user " + userId);
-                readDefaultAssistant(userId);
+        protected void ensureAssistant() {
+            final List<UserInfo> activeUsers = mUm.getUsers(true);
+            for (UserInfo userInfo : activeUsers) {
+                int userId = userInfo.getUserHandle().getIdentifier();
+                if (getAllowedPackages(userId).isEmpty()) {
+                    Slog.d(TAG, "Approving default notification assistant for user " + userId);
+                    readDefaultAssistant(userId);
+                }
             }
         }
     }
diff --git a/com/android/server/notification/NotificationRecord.java b/com/android/server/notification/NotificationRecord.java
index c887085..9bd3e52 100644
--- a/com/android/server/notification/NotificationRecord.java
+++ b/com/android/server/notification/NotificationRecord.java
@@ -149,6 +149,8 @@
     private final NotificationStats mStats;
     private int mUserSentiment;
     private boolean mIsInterruptive;
+    private int mNumberOfSmartRepliesAdded;
+    private boolean mHasSeenSmartReplies;
 
     @VisibleForTesting
     public NotificationRecord(Context context, StatusBarNotification sbn,
@@ -962,6 +964,22 @@
         mStats.setViewedSettings();
     }
 
+    public void setNumSmartRepliesAdded(int noReplies) {
+        mNumberOfSmartRepliesAdded = noReplies;
+    }
+
+    public int getNumSmartRepliesAdded() {
+        return mNumberOfSmartRepliesAdded;
+    }
+
+    public boolean hasSeenSmartReplies() {
+        return mHasSeenSmartReplies;
+    }
+
+    public void setSeenSmartReplies(boolean hasSeenSmartReplies) {
+        mHasSeenSmartReplies = hasSeenSmartReplies;
+    }
+
     public Set<Uri> getNotificationUris() {
         Notification notification = getNotification();
         Set<Uri> uris = new ArraySet<>();
diff --git a/com/android/server/notification/RankingHelper.java b/com/android/server/notification/RankingHelper.java
index 98d5c9a..43d393a 100644
--- a/com/android/server/notification/RankingHelper.java
+++ b/com/android/server/notification/RankingHelper.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.notification;
 
+import static android.app.NotificationManager.IMPORTANCE_NONE;
+
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
@@ -619,7 +621,7 @@
             updateConfig();
             return;
         }
-        if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE
+        if (channel.getImportance() < IMPORTANCE_NONE
                 || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
             throw new IllegalArgumentException("Invalid importance level");
         }
@@ -959,6 +961,23 @@
         return deletedCount;
     }
 
+    public int getBlockedChannelCount(String pkg, int uid) {
+        Preconditions.checkNotNull(pkg);
+        int blockedCount = 0;
+        Record r = getRecord(pkg, uid);
+        if (r == null) {
+            return blockedCount;
+        }
+        int N = r.channels.size();
+        for (int i = 0; i < N; i++) {
+            final NotificationChannel nc = r.channels.valueAt(i);
+            if (!nc.isDeleted() && IMPORTANCE_NONE == nc.getImportance()) {
+                blockedCount++;
+            }
+        }
+        return blockedCount;
+    }
+
     /**
      * Sets importance.
      */
@@ -969,12 +988,12 @@
     }
 
     public void setEnabled(String packageName, int uid, boolean enabled) {
-        boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE;
+        boolean wasEnabled = getImportance(packageName, uid) != IMPORTANCE_NONE;
         if (wasEnabled == enabled) {
             return;
         }
         setImportance(packageName, uid,
-                enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE);
+                enabled ? DEFAULT_IMPORTANCE : IMPORTANCE_NONE);
     }
 
     @VisibleForTesting
@@ -1199,7 +1218,7 @@
             ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
             for (int i = 0; i < N; i++) {
                 final Record r = mRecords.valueAt(i);
-                if (r.importance == NotificationManager.IMPORTANCE_NONE) {
+                if (r.importance == IMPORTANCE_NONE) {
                     packageBans.put(r.uid, r.pkg);
                 }
             }
diff --git a/com/android/server/notification/ValidateNotificationPeople.java b/com/android/server/notification/ValidateNotificationPeople.java
index 6cf8f86..639cc70 100644
--- a/com/android/server/notification/ValidateNotificationPeople.java
+++ b/com/android/server/notification/ValidateNotificationPeople.java
@@ -18,6 +18,7 @@
 
 import android.annotation.Nullable;
 import android.app.Notification;
+import android.app.Person;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.ContentObserver;
@@ -332,8 +333,8 @@
                 return array;
             }
 
-            if (arrayList.get(0) instanceof Notification.Person) {
-                ArrayList<Notification.Person> list = (ArrayList<Notification.Person>) arrayList;
+            if (arrayList.get(0) instanceof Person) {
+                ArrayList<Person> list = (ArrayList<Person>) arrayList;
                 final int N = list.size();
                 String[] array = new String[N];
                 for (int i = 0; i < N; i++) {
diff --git a/com/android/server/notification/ZenModeHelper.java b/com/android/server/notification/ZenModeHelper.java
index 586abc1..156f702 100644
--- a/com/android/server/notification/ZenModeHelper.java
+++ b/com/android/server/notification/ZenModeHelper.java
@@ -32,6 +32,7 @@
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.database.ContentObserver;
+import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioManagerInternal;
@@ -1198,8 +1199,6 @@
 
     @VisibleForTesting
     protected Notification createZenUpgradeNotification() {
-        Intent intent = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         final Bundle extras = new Bundle();
         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
                 mContext.getResources().getString(R.string.global_action_settings));
@@ -1210,15 +1209,20 @@
             title = R.string.zen_upgrade_notification_visd_title;
             content = R.string.zen_upgrade_notification_visd_content;
         }
+        Intent onboardingIntent = new Intent(Settings.ZEN_MODE_ONBOARDING);
+        onboardingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
         return new Notification.Builder(mContext, SystemNotificationChannels.DO_NOT_DISTURB)
+                .setAutoCancel(true)
                 .setSmallIcon(R.drawable.ic_settings_24dp)
+                .setLargeIcon(Icon.createWithResource(mContext, R.drawable.ic_zen_24dp))
                 .setContentTitle(mContext.getResources().getString(title))
                 .setContentText(mContext.getResources().getString(content))
+                .setContentIntent(PendingIntent.getActivity(mContext, 0, onboardingIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT))
                 .setAutoCancel(true)
                 .setLocalOnly(true)
                 .addExtras(extras)
                 .setStyle(new Notification.BigTextStyle())
-                .setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0, null))
                 .build();
     }
 
diff --git a/com/android/server/om/OverlayManagerSettings.java b/com/android/server/om/OverlayManagerSettings.java
index 863045c..e176351 100644
--- a/com/android/server/om/OverlayManagerSettings.java
+++ b/com/android/server/om/OverlayManagerSettings.java
@@ -179,15 +179,19 @@
 
     List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName,
             final int userId) {
+        // Static RROs targeting "android" are loaded from AssetManager, and so they should be
+        // ignored in OverlayManagerService.
         return selectWhereTarget(targetPackageName, userId)
-                .filter((i) -> !i.isStatic())
+                .filter((i) -> !(i.isStatic() && "android".equals(i.getTargetPackageName())))
                 .map(SettingsItem::getOverlayInfo)
                 .collect(Collectors.toList());
     }
 
     ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
+        // Static RROs targeting "android" are loaded from AssetManager, and so they should be
+        // ignored in OverlayManagerService.
         return selectWhereUser(userId)
-                .filter((i) -> !i.isStatic())
+                .filter((i) -> !(i.isStatic() && "android".equals(i.getTargetPackageName())))
                 .map(SettingsItem::getOverlayInfo)
                 .collect(Collectors.groupingBy(info -> info.targetPackageName, ArrayMap::new,
                         Collectors.toList()));
diff --git a/com/android/server/pm/Installer.java b/com/android/server/pm/Installer.java
index 9d3f48b..45f1a2b 100644
--- a/com/android/server/pm/Installer.java
+++ b/com/android/server/pm/Installer.java
@@ -67,6 +67,8 @@
     public static final int DEXOPT_ENABLE_HIDDEN_API_CHECKS = 1 << 10;
     /** Indicates that dexopt should convert to CompactDex. */
     public static final int DEXOPT_GENERATE_COMPACT_DEX = 1 << 11;
+    /** Indicates that dexopt should generate an app image */
+    public static final int DEXOPT_GENERATE_APP_IMAGE = 1 << 12;
 
     // NOTE: keep in sync with installd
     public static final int FLAG_CLEAR_CACHE_ONLY = 1 << 8;
diff --git a/com/android/server/pm/InstantAppResolver.java b/com/android/server/pm/InstantAppResolver.java
index bc9fa4b..dbf0940 100644
--- a/com/android/server/pm/InstantAppResolver.java
+++ b/com/android/server/pm/InstantAppResolver.java
@@ -256,8 +256,6 @@
         int flags = origIntent.getFlags();
         final Intent intent = new Intent();
         intent.setFlags(flags
-                | Intent.FLAG_ACTIVITY_NEW_TASK
-                | Intent.FLAG_ACTIVITY_CLEAR_TASK
                 | Intent.FLAG_ACTIVITY_NO_HISTORY
                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
         if (token != null) {
diff --git a/com/android/server/pm/LauncherAppsService.java b/com/android/server/pm/LauncherAppsService.java
index 8e78703..595de9e 100644
--- a/com/android/server/pm/LauncherAppsService.java
+++ b/com/android/server/pm/LauncherAppsService.java
@@ -39,6 +39,7 @@
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
+import android.content.pm.UserInfo;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Binder;
@@ -49,6 +50,7 @@
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.os.UserManagerInternal;
 import android.provider.Settings;
 import android.util.Log;
@@ -101,6 +103,7 @@
         private static final boolean DEBUG = false;
         private static final String TAG = "LauncherAppsService";
         private final Context mContext;
+        private final UserManager mUm;
         private final UserManagerInternal mUserManagerInternal;
         private final ActivityManagerInternal mActivityManagerInternal;
         private final ShortcutServiceInternal mShortcutServiceInternal;
@@ -113,6 +116,7 @@
 
         public LauncherAppsImpl(Context context) {
             mContext = context;
+            mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
             mUserManagerInternal = Preconditions.checkNotNull(
                     LocalServices.getService(UserManagerInternal.class));
             mActivityManagerInternal = Preconditions.checkNotNull(
@@ -233,6 +237,22 @@
          * group.
          */
         private boolean canAccessProfile(int targetUserId, String message) {
+            final int callingUserId = injectCallingUserId();
+
+            if (targetUserId == callingUserId) return true;
+
+            long ident = injectClearCallingIdentity();
+            try {
+                final UserInfo callingUserInfo = mUm.getUserInfo(callingUserId);
+                if (callingUserInfo != null && callingUserInfo.isManagedProfile()) {
+                    Slog.w(TAG, message + " for another profile "
+                            + targetUserId + " from " + callingUserId + " not allowed");
+                    return false;
+                }
+            } finally {
+                injectRestoreCallingIdentity(ident);
+            }
+
             return mUserManagerInternal.isProfileAccessible(injectCallingUserId(), targetUserId,
                     message, true);
         }
diff --git a/com/android/server/pm/OtaDexoptService.java b/com/android/server/pm/OtaDexoptService.java
index 5a7893a..320affb 100644
--- a/com/android/server/pm/OtaDexoptService.java
+++ b/com/android/server/pm/OtaDexoptService.java
@@ -267,7 +267,7 @@
                 final StringBuilder builder = new StringBuilder();
 
                 // The current version.
-                builder.append("8 ");
+                builder.append("9 ");
 
                 builder.append("dexopt");
 
diff --git a/com/android/server/pm/PackageDexOptimizer.java b/com/android/server/pm/PackageDexOptimizer.java
index 892fa12..ebab1a7 100644
--- a/com/android/server/pm/PackageDexOptimizer.java
+++ b/com/android/server/pm/PackageDexOptimizer.java
@@ -60,6 +60,7 @@
 import static com.android.server.pm.Installer.DEXOPT_IDLE_BACKGROUND_JOB;
 import static com.android.server.pm.Installer.DEXOPT_ENABLE_HIDDEN_API_CHECKS;
 import static com.android.server.pm.Installer.DEXOPT_GENERATE_COMPACT_DEX;
+import static com.android.server.pm.Installer.DEXOPT_GENERATE_APP_IMAGE;
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
 
@@ -521,6 +522,10 @@
         return getDexFlags(pkg.applicationInfo, compilerFilter, options);
     }
 
+    private boolean isAppImageEnabled() {
+        return SystemProperties.get("dalvik.vm.appimageformat", "").length() > 0;
+    }
+
     private int getDexFlags(ApplicationInfo info, String compilerFilter, DexoptOptions options) {
         int flags = info.flags;
         boolean debuggable = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
@@ -547,6 +552,14 @@
             case PackageManagerService.REASON_INSTALL:
                  generateCompactDex = false;
         }
+        // Use app images only if it is enabled and we are compiling
+        // profile-guided (so the app image doesn't conservatively contain all classes).
+        // If the app didn't request for the splits to be loaded in isolation or if it does not
+        // declare inter-split dependencies, then all the splits will be loaded in the base
+        // apk class loader (in the order of their definition, otherwise disable app images
+        // because they are unsupported for multiple class loaders. b/7269679
+        boolean generateAppImage = isProfileGuidedFilter && (info.splitDependencies == null ||
+                !info.requestsIsolatedSplitLoading()) && isAppImageEnabled();
         int dexFlags =
                 (isPublic ? DEXOPT_PUBLIC : 0)
                 | (debuggable ? DEXOPT_DEBUGGABLE : 0)
@@ -554,6 +567,7 @@
                 | (options.isBootComplete() ? DEXOPT_BOOTCOMPLETE : 0)
                 | (options.isDexoptIdleBackgroundJob() ? DEXOPT_IDLE_BACKGROUND_JOB : 0)
                 | (generateCompactDex ? DEXOPT_GENERATE_COMPACT_DEX : 0)
+                | (generateAppImage ? DEXOPT_GENERATE_APP_IMAGE : 0)
                 | hiddenApiFlag;
         return adjustDexoptFlags(dexFlags);
     }
diff --git a/com/android/server/pm/PackageInstallerSession.java b/com/android/server/pm/PackageInstallerSession.java
index ee32618..f7a0215 100644
--- a/com/android/server/pm/PackageInstallerSession.java
+++ b/com/android/server/pm/PackageInstallerSession.java
@@ -122,8 +122,9 @@
     private static final boolean LOGD = true;
     private static final String REMOVE_SPLIT_MARKER_EXTENSION = ".removed";
 
-    private static final int MSG_COMMIT = 0;
-    private static final int MSG_ON_PACKAGE_INSTALLED = 1;
+    private static final int MSG_EARLY_BIND = 0;
+    private static final int MSG_COMMIT = 1;
+    private static final int MSG_ON_PACKAGE_INSTALLED = 2;
 
     /** XML constants used for persisting a session */
     static final String TAG_SESSION = "session";
@@ -280,6 +281,9 @@
         @Override
         public boolean handleMessage(Message msg) {
             switch (msg.what) {
+                case MSG_EARLY_BIND:
+                    earlyBindToDefContainer();
+                    break;
                 case MSG_COMMIT:
                     synchronized (mLock) {
                         try {
@@ -315,6 +319,10 @@
         }
     };
 
+    private void earlyBindToDefContainer() {
+        mPm.earlyBindToDefContainer();
+    }
+
     /**
      * @return {@code true} iff the installing is app an device owner or affiliated profile owner.
      */
@@ -410,6 +418,10 @@
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
+        // attempt to bind to the DefContainer as early as possible
+        if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_EARLY_BIND));
+        }
     }
 
     public SessionInfo generateInfo() {
diff --git a/com/android/server/pm/PackageManagerService.java b/com/android/server/pm/PackageManagerService.java
index 2e530af..a047604 100644
--- a/com/android/server/pm/PackageManagerService.java
+++ b/com/android/server/pm/PackageManagerService.java
@@ -17,12 +17,12 @@
 package com.android.server.pm;
 
 import static android.Manifest.permission.DELETE_PACKAGES;
-import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
-import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
 import static android.Manifest.permission.INSTALL_PACKAGES;
+import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
 import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS;
 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
 import static android.Manifest.permission.REQUEST_DELETE_PACKAGES;
+import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
 import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
 import static android.content.pm.PackageManager.CERT_INPUT_RAW_X509;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
@@ -167,8 +167,8 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageList;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
 import android.content.pm.PackageManager.LegacyPackageDeleteObserver;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.PackageManagerInternal.PackageListObserver;
 import android.content.pm.PackageParser;
 import android.content.pm.PackageParser.ActivityIntentInfo;
@@ -274,6 +274,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IMediaContainerService;
 import com.android.internal.app.ResolverActivity;
+import com.android.internal.app.SuspendedAppActivity;
 import com.android.internal.content.NativeLibraryHelper;
 import com.android.internal.content.PackageHelper;
 import com.android.internal.logging.MetricsLogger;
@@ -310,10 +311,10 @@
 import com.android.server.pm.dex.PackageDexUsage;
 import com.android.server.pm.permission.BasePermission;
 import com.android.server.pm.permission.DefaultPermissionGrantPolicy;
-import com.android.server.pm.permission.PermissionManagerService;
-import com.android.server.pm.permission.PermissionManagerInternal;
 import com.android.server.pm.permission.DefaultPermissionGrantPolicy.DefaultPermissionGrantedCallback;
+import com.android.server.pm.permission.PermissionManagerInternal;
 import com.android.server.pm.permission.PermissionManagerInternal.PermissionCallback;
+import com.android.server.pm.permission.PermissionManagerService;
 import com.android.server.pm.permission.PermissionsState;
 import com.android.server.pm.permission.PermissionsState.PermissionState;
 import com.android.server.security.VerityUtils;
@@ -1326,6 +1327,7 @@
     static final int INTENT_FILTER_VERIFIED = 18;
     static final int WRITE_PACKAGE_LIST = 19;
     static final int INSTANT_APP_RESOLUTION_PHASE_TWO = 20;
+    static final int DEF_CONTAINER_BIND = 21;
 
     static final int WRITE_SETTINGS_DELAY = 10*1000;  // 10 seconds
 
@@ -1417,8 +1419,7 @@
             new ArrayList<HandlerParams>();
 
         private boolean connectToService() {
-            if (DEBUG_SD_INSTALL) Log.i(TAG, "Trying to bind to" +
-                    " DefaultContainerService");
+            if (DEBUG_INSTALL) Log.i(TAG, "Trying to bind to DefaultContainerService");
             Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
             Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
             if (mContext.bindServiceAsUser(service, mDefContainerConn,
@@ -1453,6 +1454,17 @@
 
         void doHandleMessage(Message msg) {
             switch (msg.what) {
+                case DEF_CONTAINER_BIND:
+                    if (!mBound) {
+                        Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "earlyBindingMCS",
+                                System.identityHashCode(mHandler));
+                        if (!connectToService()) {
+                            Slog.e(TAG, "Failed to bind to media container service");
+                        }
+                        Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "earlyBindingMCS",
+                                System.identityHashCode(mHandler));
+                    }
+                    break;
                 case INIT_COPY: {
                     HandlerParams params = (HandlerParams) msg.obj;
                     int idx = mPendingInstalls.size();
@@ -1511,7 +1523,6 @@
                                     Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER,
                                             params.traceMethod, params.traceCookie);
                                 }
-                                return;
                             }
                             mPendingInstalls.clear();
                         } else {
@@ -3935,7 +3946,7 @@
             ai.uid = UserHandle.getUid(userId, ps.appId);
             ai.primaryCpuAbi = ps.primaryCpuAbiString;
             ai.secondaryCpuAbi = ps.secondaryCpuAbiString;
-            ai.versionCode = ps.versionCode;
+            ai.setVersionCode(ps.versionCode);
             ai.flags = ps.pkgFlags;
             ai.privateFlags = ps.pkgPrivateFlags;
             pi.applicationInfo = PackageParser.generateApplicationInfo(ai, flags, state, userId);
@@ -5932,8 +5943,8 @@
     @Override
     public ResolveInfo resolveIntent(Intent intent, String resolvedType,
             int flags, int userId) {
-        return resolveIntentInternal(
-                intent, resolvedType, flags, userId, false /*resolveForStart*/);
+        return resolveIntentInternal(intent, resolvedType, flags, userId, false,
+                Binder.getCallingUid());
     }
 
     /**
@@ -5942,19 +5953,19 @@
      * since we need to allow the system to start any installed application.
      */
     private ResolveInfo resolveIntentInternal(Intent intent, String resolvedType,
-            int flags, int userId, boolean resolveForStart) {
+            int flags, int userId, boolean resolveForStart, int filterCallingUid) {
         try {
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "resolveIntent");
 
             if (!sUserManager.exists(userId)) return null;
             final int callingUid = Binder.getCallingUid();
-            flags = updateFlagsForResolve(flags, userId, intent, callingUid, resolveForStart);
+            flags = updateFlagsForResolve(flags, userId, intent, filterCallingUid, resolveForStart);
             mPermissionManager.enforceCrossUserPermission(callingUid, userId,
                     false /*requireFullPermission*/, false /*checkShell*/, "resolve intent");
 
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "queryIntentActivities");
             final List<ResolveInfo> query = queryIntentActivitiesInternal(intent, resolvedType,
-                    flags, callingUid, userId, resolveForStart, true /*allowDynamicSplits*/);
+                    flags, filterCallingUid, userId, resolveForStart, true /*allowDynamicSplits*/);
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
 
             final ResolveInfo bestChoice =
@@ -6762,7 +6773,7 @@
                 // the instant application, we'll do the right thing.
                 final ApplicationInfo ai = localInstantApp.activityInfo.applicationInfo;
                 auxiliaryResponse = new AuxiliaryResolveInfo(null /* failureActivity */,
-                                        ai.packageName, ai.versionCode, null /* splitName */);
+                                        ai.packageName, ai.longVersionCode, null /* splitName */);
             }
         }
         if (intent.isWebIntent() && auxiliaryResponse == null) {
@@ -6946,7 +6957,7 @@
                 installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
                         installFailureActivity,
                         info.activityInfo.packageName,
-                        info.activityInfo.applicationInfo.versionCode,
+                        info.activityInfo.applicationInfo.longVersionCode,
                         info.activityInfo.splitName);
                 // add a non-generic filter
                 installerInfo.filter = new IntentFilter();
@@ -7692,7 +7703,7 @@
                     installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
                             null /* installFailureActivity */,
                             info.serviceInfo.packageName,
-                            info.serviceInfo.applicationInfo.versionCode,
+                            info.serviceInfo.applicationInfo.longVersionCode,
                             info.serviceInfo.splitName);
                     // add a non-generic filter
                     installerInfo.filter = new IntentFilter();
@@ -7810,7 +7821,7 @@
                     installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
                             null /*failureActivity*/,
                             info.providerInfo.packageName,
-                            info.providerInfo.applicationInfo.versionCode,
+                            info.providerInfo.applicationInfo.longVersionCode,
                             info.providerInfo.splitName);
                     // add a non-generic filter
                     installerInfo.filter = new IntentFilter();
@@ -8185,35 +8196,22 @@
     private ProviderInfo resolveContentProviderInternal(String name, int flags, int userId) {
         if (!sUserManager.exists(userId)) return null;
         flags = updateFlagsForComponent(flags, userId, name);
-        final String instantAppPkgName = getInstantAppPackageName(Binder.getCallingUid());
-        // reader
+        final int callingUid = Binder.getCallingUid();
         synchronized (mPackages) {
             final PackageParser.Provider provider = mProvidersByAuthority.get(name);
             PackageSetting ps = provider != null
                     ? mSettings.mPackages.get(provider.owner.packageName)
                     : null;
             if (ps != null) {
-                final boolean isInstantApp = ps.getInstantApp(userId);
-                // normal application; filter out instant application provider
-                if (instantAppPkgName == null && isInstantApp) {
-                    return null;
-                }
-                // instant application; filter out other instant applications
-                if (instantAppPkgName != null
-                        && isInstantApp
-                        && !provider.owner.packageName.equals(instantAppPkgName)) {
-                    return null;
-                }
-                // instant application; filter out non-exposed provider
-                if (instantAppPkgName != null
-                        && !isInstantApp
-                        && (provider.info.flags & ProviderInfo.FLAG_VISIBLE_TO_INSTANT_APP) == 0) {
-                    return null;
-                }
                 // provider not enabled
                 if (!mSettings.isEnabledAndMatchLPr(provider.info, flags, userId)) {
                     return null;
                 }
+                final ComponentName component =
+                        new ComponentName(provider.info.packageName, provider.info.name);
+                if (filterAppAccessLPr(ps, callingUid, component, TYPE_PROVIDER, userId)) {
+                    return null;
+                }
                 return PackageParser.generateProviderInfo(
                         provider, flags, ps.readUserState(userId), userId);
             }
@@ -8702,7 +8700,7 @@
                             disabledPkgSetting /* pkgSetting */, null /* disabledPkgSetting */,
                             null /* originalPkgSetting */, null, parseFlags, scanFlags,
                             (pkg == mPlatformPackage), user);
-                    applyPolicy(pkg, parseFlags, scanFlags);
+                    applyPolicy(pkg, parseFlags, scanFlags, mPlatformPackage);
                     scanPackageOnlyLI(request, mFactoryTest, -1L);
                 }
             }
@@ -9709,7 +9707,7 @@
                     if (expectedCertDigests.length > 1) {
 
                         // For apps targeting O MR1 we require explicit enumeration of all certs.
-                        final String[] libCertDigests = (targetSdk > Build.VERSION_CODES.O)
+                        final String[] libCertDigests = (targetSdk >= Build.VERSION_CODES.O_MR1)
                                 ? PackageUtils.computeSignaturesSha256Digests(
                                 libPkg.mSigningDetails.signatures)
                                 : PackageUtils.computeSignaturesSha256Digests(
@@ -10021,7 +10019,7 @@
 
         scanFlags = adjustScanFlags(scanFlags, pkgSetting, disabledPkgSetting, user, pkg);
         synchronized (mPackages) {
-            applyPolicy(pkg, parseFlags, scanFlags);
+            applyPolicy(pkg, parseFlags, scanFlags, mPlatformPackage);
             assertPackageIsValid(pkg, parseFlags, scanFlags);
 
             SharedUserSetting sharedUserSetting = null;
@@ -10184,20 +10182,10 @@
                 // The signature has changed, but this package is in the system
                 // image...  let's recover!
                 pkgSetting.signatures.mSigningDetails = pkg.mSigningDetails;
-                // However...  if this package is part of a shared user, but it
-                // doesn't match the signature of the shared user, let's fail.
-                // What this means is that you can't change the signatures
-                // associated with an overall shared user, which doesn't seem all
-                // that unreasonable.
+                // If the system app is part of a shared user we allow that shared user to change
+                // signatures as well in part as part of an OTA.
                 if (signatureCheckPs.sharedUser != null) {
-                    if (compareSignatures(
-                            signatureCheckPs.sharedUser.signatures.mSigningDetails.signatures,
-                            pkg.mSigningDetails.signatures) != PackageManager.SIGNATURE_MATCH) {
-                        throw new PackageManagerException(
-                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
-                                "Signature mismatch for shared user: "
-                                        + pkgSetting.sharedUser);
-                    }
+                    signatureCheckPs.sharedUser.signatures.mSigningDetails = pkg.mSigningDetails;
                 }
                 // File a report about this.
                 String msg = "System package " + pkg.packageName
@@ -10701,7 +10689,7 @@
      * ideally be static, but, it requires locks to read system state.
      */
     private static void applyPolicy(PackageParser.Package pkg, final @ParseFlags int parseFlags,
-            final @ScanFlags int scanFlags) {
+            final @ScanFlags int scanFlags, PackageParser.Package platformPkg) {
         if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
             pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
             if (pkg.applicationInfo.isDirectBootAware()) {
@@ -10787,6 +10775,15 @@
             pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRODUCT;
         }
 
+        // Check if the package is signed with the same key as the platform package.
+        if (PLATFORM_PACKAGE_NAME.equals(pkg.packageName) ||
+                (platformPkg != null && compareSignatures(
+                        platformPkg.mSigningDetails.signatures,
+                        pkg.mSigningDetails.signatures) == PackageManager.SIGNATURE_MATCH)) {
+            pkg.applicationInfo.privateFlags |=
+                ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY;
+        }
+
         if (!isSystemApp(pkg)) {
             // Only system apps can use these features.
             pkg.mOriginalPackages = null;
@@ -13634,6 +13631,14 @@
         return installReason;
     }
 
+    /**
+     * Attempts to bind to the default container service explicitly instead of doing so lazily on
+     * install commit.
+     */
+    void earlyBindToDefContainer() {
+        mHandler.sendMessage(mHandler.obtainMessage(DEF_CONTAINER_BIND));
+    }
+
     void installStage(String packageName, File stagedDir,
             IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams,
             String installerPackageName, int installerUid, UserHandle user,
@@ -13991,8 +13996,8 @@
 
     @Override
     public String[] setPackagesSuspendedAsUser(String[] packageNames, boolean suspended,
-            PersistableBundle appExtras, PersistableBundle launcherExtras, String callingPackage,
-            int userId) {
+            PersistableBundle appExtras, PersistableBundle launcherExtras, String dialogMessage,
+            String callingPackage, int userId) {
         try {
             mContext.enforceCallingOrSelfPermission(android.Manifest.permission.SUSPEND_APPS, null);
         } catch (SecurityException e) {
@@ -14006,7 +14011,7 @@
                 "setPackagesSuspended for user " + userId);
         if (callingUid != Process.ROOT_UID &&
                 !UserHandle.isSameApp(getPackageUid(callingPackage, 0, userId), callingUid)) {
-            throw new IllegalArgumentException("callingPackage " + callingPackage + " does not"
+            throw new IllegalArgumentException("CallingPackage " + callingPackage + " does not"
                     + " belong to calling app id " + UserHandle.getAppId(callingUid));
         }
 
@@ -14030,20 +14035,18 @@
                     final PackageSetting pkgSetting = mSettings.mPackages.get(packageName);
                     if (pkgSetting == null
                             || filterAppAccessLPr(pkgSetting, callingUid, userId)) {
-                        Slog.w(TAG, "Could not find package setting for package \"" + packageName
-                                + "\". Skipping suspending/un-suspending.");
+                        Slog.w(TAG, "Could not find package setting for package: " + packageName
+                                + ". Skipping suspending/un-suspending.");
                         unactionedPackages.add(packageName);
                         continue;
                     }
-                    if (pkgSetting.getSuspended(userId) != suspended) {
-                        if (!canSuspendPackageForUserLocked(packageName, userId)) {
-                            unactionedPackages.add(packageName);
-                            continue;
-                        }
-                        pkgSetting.setSuspended(suspended, callingPackage, appExtras,
-                                launcherExtras, userId);
-                        changedPackagesList.add(packageName);
+                    if (!canSuspendPackageForUserLocked(packageName, userId)) {
+                        unactionedPackages.add(packageName);
+                        continue;
                     }
+                    pkgSetting.setSuspended(suspended, callingPackage, dialogMessage, appExtras,
+                            launcherExtras, userId);
+                    changedPackagesList.add(packageName);
                 }
             }
         } finally {
@@ -14058,7 +14061,6 @@
                 scheduleWritePackageRestrictionsLocked(userId);
             }
         }
-
         return unactionedPackages.toArray(new String[unactionedPackages.size()]);
     }
 
@@ -14066,7 +14068,8 @@
     public PersistableBundle getSuspendedPackageAppExtras(String packageName, int userId) {
         final int callingUid = Binder.getCallingUid();
         if (getPackageUid(packageName, 0, userId) != callingUid) {
-            mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS, null);
+            throw new SecurityException("Calling package " + packageName
+                    + " does not belong to calling uid " + callingUid);
         }
         synchronized (mPackages) {
             final PackageSetting ps = mSettings.mPackages.get(packageName);
@@ -14081,25 +14084,6 @@
         }
     }
 
-    @Override
-    public void setSuspendedPackageAppExtras(String packageName, PersistableBundle appExtras,
-            int userId) {
-        final int callingUid = Binder.getCallingUid();
-        mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS, null);
-        synchronized (mPackages) {
-            final PackageSetting ps = mSettings.mPackages.get(packageName);
-            if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {
-                throw new IllegalArgumentException("Unknown target package: " + packageName);
-            }
-            final PackageUserState packageUserState = ps.readUserState(userId);
-            if (packageUserState.suspended) {
-                packageUserState.suspendedAppExtras = appExtras;
-                sendMyPackageSuspendedOrUnsuspended(new String[] {packageName}, true, appExtras,
-                        userId);
-            }
-        }
-    }
-
     private void sendMyPackageSuspendedOrUnsuspended(String[] affectedPackages, boolean suspended,
             PersistableBundle appExtras, int userId) {
         final String action;
@@ -14142,9 +14126,6 @@
         mPermissionManager.enforceCrossUserPermission(callingUid, userId,
                 true /* requireFullPermission */, false /* checkShell */,
                 "isPackageSuspendedForUser for user " + userId);
-        if (getPackageUid(packageName, 0, userId) != callingUid) {
-            mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS, null);
-        }
         synchronized (mPackages) {
             final PackageSetting ps = mSettings.mPackages.get(packageName);
             if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {
@@ -14154,18 +14135,26 @@
         }
     }
 
-    void onSuspendingPackageRemoved(String packageName, int userId) {
-        final int[] userIds = (userId == UserHandle.USER_ALL) ? sUserManager.getUserIds()
-                : new int[] {userId};
-        synchronized (mPackages) {
-            for (PackageSetting ps : mSettings.mPackages.values()) {
-                for (int user : userIds) {
-                    final PackageUserState pus = ps.readUserState(user);
+    void onSuspendingPackageRemoved(String packageName, int removedForUser) {
+        final int[] userIds = (removedForUser == UserHandle.USER_ALL) ? sUserManager.getUserIds()
+                : new int[] {removedForUser};
+        for (int userId : userIds) {
+            List<String> affectedPackages = new ArrayList<>();
+            synchronized (mPackages) {
+                for (PackageSetting ps : mSettings.mPackages.values()) {
+                    final PackageUserState pus = ps.readUserState(userId);
                     if (pus.suspended && packageName.equals(pus.suspendingPackage)) {
-                        ps.setSuspended(false, null, null, null, user);
+                        ps.setSuspended(false, null, null, null, null, userId);
+                        affectedPackages.add(ps.name);
                     }
                 }
             }
+            if (!affectedPackages.isEmpty()) {
+                final String[] packageArray = affectedPackages.toArray(
+                        new String[affectedPackages.size()]);
+                sendMyPackageSuspendedOrUnsuspended(packageArray, false, null, userId);
+                sendPackagesSuspendedForUser(packageArray, userId, false, null);
+            }
         }
     }
 
@@ -18896,6 +18885,7 @@
                     false /*hidden*/,
                     false /*suspended*/,
                     null, /*suspendingPackage*/
+                    null, /*dialogMessage*/
                     null, /*suspendedAppExtras*/
                     null, /*suspendedLauncherExtras*/
                     false /*instantApp*/,
@@ -23780,6 +23770,30 @@
         }
 
         @Override
+        public boolean isPackageSuspended(String packageName, int userId) {
+            synchronized (mPackages) {
+                final PackageSetting ps = mSettings.mPackages.get(packageName);
+                return (ps != null) ? ps.getSuspended(userId) : false;
+            }
+        }
+
+        @Override
+        public String getSuspendingPackage(String suspendedPackage, int userId) {
+            synchronized (mPackages) {
+                final PackageSetting ps = mSettings.mPackages.get(suspendedPackage);
+                return (ps != null) ? ps.readUserState(userId).suspendingPackage : null;
+            }
+        }
+
+        @Override
+        public String getSuspendedDialogMessage(String suspendedPackage, int userId) {
+            synchronized (mPackages) {
+                final PackageSetting ps = mSettings.mPackages.get(suspendedPackage);
+                return (ps != null) ? ps.readUserState(userId).dialogMessage : null;
+            }
+        }
+
+        @Override
         public int getPackageUid(String packageName, int flags, int userId) {
             return PackageManagerService.this
                     .getPackageUid(packageName, flags, userId);
@@ -24001,9 +24015,9 @@
 
         @Override
         public ResolveInfo resolveIntent(Intent intent, String resolvedType,
-                int flags, int userId, boolean resolveForStart) {
+                int flags, int userId, boolean resolveForStart, int filterCallingUid) {
             return resolveIntentInternal(
-                    intent, resolvedType, flags, userId, resolveForStart);
+                    intent, resolvedType, flags, userId, resolveForStart, filterCallingUid);
         }
 
         @Override
diff --git a/com/android/server/pm/PackageManagerShellCommand.java b/com/android/server/pm/PackageManagerShellCommand.java
index 28e32a5..a92fbb6 100644
--- a/com/android/server/pm/PackageManagerShellCommand.java
+++ b/com/android/server/pm/PackageManagerShellCommand.java
@@ -1505,6 +1505,7 @@
     private int runSuspend(boolean suspendedState) {
         final PrintWriter pw = getOutPrintWriter();
         int userId = UserHandle.USER_SYSTEM;
+        String dialogMessage = null;
         final PersistableBundle appExtras = new PersistableBundle();
         final PersistableBundle launcherExtras = new PersistableBundle();
         String opt;
@@ -1513,6 +1514,9 @@
                 case "--user":
                     userId = UserHandle.parseUserArg(getNextArgRequired());
                     break;
+                case "--dialogMessage":
+                    dialogMessage = getNextArgRequired();
+                    break;
                 case "--ael":
                 case "--aes":
                 case "--aed":
@@ -1553,7 +1557,7 @@
                 (Binder.getCallingUid() == Process.ROOT_UID) ? "root" : "com.android.shell";
         try {
             mInterface.setPackagesSuspendedAsUser(new String[]{packageName}, suspendedState,
-                    appExtras, launcherExtras, callingPackage, userId);
+                    appExtras, launcherExtras, dialogMessage, callingPackage, userId);
             pw.println("Package " + packageName + " new suspended state: "
                     + mInterface.isPackageSuspendedForUser(packageName, userId));
             return 0;
diff --git a/com/android/server/pm/PackageSettingBase.java b/com/android/server/pm/PackageSettingBase.java
index 008a81c..138594c 100644
--- a/com/android/server/pm/PackageSettingBase.java
+++ b/com/android/server/pm/PackageSettingBase.java
@@ -20,8 +20,6 @@
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
 
-import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
-
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IntentFilterVerificationInfo;
 import android.content.pm.PackageManager;
@@ -398,11 +396,12 @@
         return readUserState(userId).suspended;
     }
 
-    void setSuspended(boolean suspended, String suspendingPackage, PersistableBundle appExtras,
-            PersistableBundle launcherExtras, int userId) {
+    void setSuspended(boolean suspended, String suspendingPackage, String dialogMessage,
+            PersistableBundle appExtras, PersistableBundle launcherExtras, int userId) {
         final PackageUserState existingUserState = modifyUserState(userId);
         existingUserState.suspended = suspended;
         existingUserState.suspendingPackage = suspended ? suspendingPackage : null;
+        existingUserState.dialogMessage = suspended ? dialogMessage : null;
         existingUserState.suspendedAppExtras = suspended ? appExtras : null;
         existingUserState.suspendedLauncherExtras = suspended ? launcherExtras : null;
     }
@@ -425,8 +424,8 @@
 
     void setUserState(int userId, long ceDataInode, int enabled, boolean installed, boolean stopped,
             boolean notLaunched, boolean hidden, boolean suspended, String suspendingPackage,
-            PersistableBundle suspendedAppExtras, PersistableBundle suspendedLauncherExtras,
-            boolean instantApp,
+            String dialogMessage, PersistableBundle suspendedAppExtras,
+            PersistableBundle suspendedLauncherExtras, boolean instantApp,
             boolean virtualPreload, String lastDisableAppCaller,
             ArraySet<String> enabledComponents, ArraySet<String> disabledComponents,
             int domainVerifState, int linkGeneration, int installReason,
@@ -440,6 +439,7 @@
         state.hidden = hidden;
         state.suspended = suspended;
         state.suspendingPackage = suspendingPackage;
+        state.dialogMessage = dialogMessage;
         state.suspendedAppExtras = suspendedAppExtras;
         state.suspendedLauncherExtras = suspendedLauncherExtras;
         state.lastDisableAppCaller = lastDisableAppCaller;
diff --git a/com/android/server/pm/Settings.java b/com/android/server/pm/Settings.java
index d0e8544..898ecf3 100644
--- a/com/android/server/pm/Settings.java
+++ b/com/android/server/pm/Settings.java
@@ -222,6 +222,7 @@
     private static final String ATTR_HIDDEN = "hidden";
     private static final String ATTR_SUSPENDED = "suspended";
     private static final String ATTR_SUSPENDING_PACKAGE = "suspending-package";
+    private static final String ATTR_SUSPEND_DIALOG_MESSAGE = "suspend_dialog_message";
     // Legacy, uninstall blocks are stored separately.
     @Deprecated
     private static final String ATTR_BLOCK_UNINSTALL = "blockUninstall";
@@ -734,6 +735,7 @@
                                 false /*hidden*/,
                                 false /*suspended*/,
                                 null, /*suspendingPackage*/
+                                null, /*dialogMessage*/
                                 null, /*suspendedAppExtras*/
                                 null, /*suspendedLauncherExtras*/
                                 instantApp,
@@ -1628,6 +1630,7 @@
                                 false /*hidden*/,
                                 false /*suspended*/,
                                 null, /*suspendingPackage*/
+                                null, /*dialogMessage*/
                                 null, /*suspendedAppExtras*/
                                 null, /*suspendedLauncherExtras*/
                                 false /*instantApp*/,
@@ -1704,6 +1707,8 @@
                             false);
                     String suspendingPackage = parser.getAttributeValue(null,
                             ATTR_SUSPENDING_PACKAGE);
+                    final String dialogMessage = parser.getAttributeValue(null,
+                            ATTR_SUSPEND_DIALOG_MESSAGE);
                     if (suspended && suspendingPackage == null) {
                         suspendingPackage = PLATFORM_PACKAGE_NAME;
                     }
@@ -1767,7 +1772,7 @@
                         setBlockUninstallLPw(userId, name, true);
                     }
                     ps.setUserState(userId, ceDataInode, enabled, installed, stopped, notLaunched,
-                            hidden, suspended, suspendingPackage, suspendedAppExtras,
+                            hidden, suspended, suspendingPackage, dialogMessage, suspendedAppExtras,
                             suspendedLauncherExtras, instantApp, virtualPreload, enabledCaller,
                             enabledComponents, disabledComponents, verifState, linkGeneration,
                             installReason, harmfulAppWarning);
@@ -2077,7 +2082,14 @@
                 }
                 if (ustate.suspended) {
                     serializer.attribute(null, ATTR_SUSPENDED, "true");
-                    serializer.attribute(null, ATTR_SUSPENDING_PACKAGE, ustate.suspendingPackage);
+                    if (ustate.suspendingPackage != null) {
+                        serializer.attribute(null, ATTR_SUSPENDING_PACKAGE,
+                                ustate.suspendingPackage);
+                    }
+                    if (ustate.dialogMessage != null) {
+                        serializer.attribute(null, ATTR_SUSPEND_DIALOG_MESSAGE,
+                                ustate.dialogMessage);
+                    }
                     if (ustate.suspendedAppExtras != null) {
                         serializer.startTag(null, TAG_SUSPENDED_APP_EXTRAS);
                         try {
@@ -4750,8 +4762,11 @@
             pw.print(" suspended=");
             pw.print(ps.getSuspended(user.id));
             if (ps.getSuspended(user.id)) {
+                final PackageUserState pus = ps.readUserState(user.id);
                 pw.print(" suspendingPackage=");
-                pw.print(ps.readUserState(user.id).suspendingPackage);
+                pw.print(pus.suspendingPackage);
+                pw.print(" dialogMessage=");
+                pw.print(pus.dialogMessage);
             }
             pw.print(" stopped=");
             pw.print(ps.getStopped(user.id));
diff --git a/com/android/server/pm/ShortcutPackageInfo.java b/com/android/server/pm/ShortcutPackageInfo.java
index eeaa333..8c7871f 100644
--- a/com/android/server/pm/ShortcutPackageInfo.java
+++ b/com/android/server/pm/ShortcutPackageInfo.java
@@ -21,6 +21,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -164,12 +165,13 @@
             ShortcutService s, String packageName, @UserIdInt int packageUserId) {
         final PackageInfo pi = s.getPackageInfoWithSignatures(packageName, packageUserId);
         // retrieve the newest sigs
-        Signature[][] signingHistory = pi.signingCertificateHistory;
-        if (signingHistory == null || signingHistory.length == 0) {
+        SigningInfo signingInfo = pi.signingInfo;
+        if (signingInfo == null) {
             Slog.e(TAG, "Can't get signatures: package=" + packageName);
             return null;
         }
-        Signature[] signatures = signingHistory[signingHistory.length - 1];
+        // TODO (b/73988180) use entire signing history in case of rollbacks
+        Signature[] signatures = signingInfo.getApkContentsSigners();
         final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.getLongVersionCode(),
                 pi.lastUpdateTime, BackupUtils.hashSignatureArray(signatures), /* shadow=*/ false);
 
@@ -192,13 +194,14 @@
             return;
         }
         // retrieve the newest sigs
-        Signature[][] signingHistory = pi.signingCertificateHistory;
-        if (signingHistory == null || signingHistory.length == 0) {
+        SigningInfo signingInfo = pi.signingInfo;
+        if (signingInfo == null) {
             Slog.w(TAG, "Not refreshing signature for " + pkg.getPackageName()
-                    + " since it appears to have no signature history.");
+                    + " since it appears to have no signing info.");
             return;
         }
-        Signature[] signatures = signingHistory[signingHistory.length - 1];
+        // TODO (b/73988180) use entire signing history in case of rollbacks
+        Signature[] signatures = signingInfo.getApkContentsSigners();
         mSigHashes = BackupUtils.hashSignatureArray(signatures);
     }
 
diff --git a/com/android/server/pm/ShortcutService.java b/com/android/server/pm/ShortcutService.java
index 15b4617..599e5a5 100644
--- a/com/android/server/pm/ShortcutService.java
+++ b/com/android/server/pm/ShortcutService.java
@@ -48,7 +48,6 @@
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
-import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.graphics.Bitmap;
@@ -100,7 +99,7 @@
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.Preconditions;
 import com.android.server.LocalServices;
-import com.android.server.StatLogger;
+import com.android.internal.util.StatLogger;
 import com.android.server.SystemService;
 import com.android.server.pm.ShortcutUser.PackageWithUser;
 
diff --git a/com/android/server/pm/permission/BasePermission.java b/com/android/server/pm/permission/BasePermission.java
index bcf4b07..1d002ef 100644
--- a/com/android/server/pm/permission/BasePermission.java
+++ b/com/android/server/pm/permission/BasePermission.java
@@ -411,17 +411,23 @@
     }
 
     public @NonNull PermissionInfo generatePermissionInfo(int adjustedProtectionLevel, int flags) {
-        final boolean protectionLevelChanged = protectionLevel != adjustedProtectionLevel;
-        // if we return different protection level, don't use the cached info
-        if (perm != null && !protectionLevelChanged) {
-            return PackageParser.generatePermissionInfo(perm, flags);
+        PermissionInfo permissionInfo;
+        if (perm != null) {
+            final boolean protectionLevelChanged = protectionLevel != adjustedProtectionLevel;
+            permissionInfo = PackageParser.generatePermissionInfo(perm, flags);
+            if (protectionLevelChanged && permissionInfo == perm.info) {
+                // if we return different protection level, don't use the cached info
+                permissionInfo = new PermissionInfo(permissionInfo);
+                permissionInfo.protectionLevel = adjustedProtectionLevel;
+            }
+            return permissionInfo;
         }
-        final PermissionInfo pi = new PermissionInfo();
-        pi.name = name;
-        pi.packageName = sourcePackageName;
-        pi.nonLocalizedLabel = name;
-        pi.protectionLevel = protectionLevelChanged ? adjustedProtectionLevel : protectionLevel;
-        return pi;
+        permissionInfo = new PermissionInfo();
+        permissionInfo.name = name;
+        permissionInfo.packageName = sourcePackageName;
+        permissionInfo.nonLocalizedLabel = name;
+        permissionInfo.protectionLevel = protectionLevel;
+        return permissionInfo;
     }
 
     public static boolean readLPw(@NonNull Map<String, BasePermission> out,
diff --git a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 518d464..4055a47 100644
--- a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -1052,7 +1052,8 @@
     private PackageParser.Package getDefaultSystemHandlerActivityPackage(
             Intent intent, int userId) {
         ResolveInfo handler = mServiceInternal.resolveIntent(intent,
-                intent.resolveType(mContext.getContentResolver()), DEFAULT_FLAGS, userId, false);
+                intent.resolveType(mContext.getContentResolver()), DEFAULT_FLAGS, userId, false,
+                Binder.getCallingUid());
         if (handler == null || handler.activityInfo == null) {
             return null;
         }
@@ -1093,7 +1094,7 @@
 
             ResolveInfo homeActivity = mServiceInternal.resolveIntent(homeIntent,
                     homeIntent.resolveType(mContext.getContentResolver()), DEFAULT_FLAGS,
-                    userId, false);
+                    userId, false, Binder.getCallingUid());
             if (homeActivity != null) {
                 continue;
             }
diff --git a/com/android/server/policy/PhoneWindowManager.java b/com/android/server/policy/PhoneWindowManager.java
index 9a25dcc..46a636c 100644
--- a/com/android/server/policy/PhoneWindowManager.java
+++ b/com/android/server/policy/PhoneWindowManager.java
@@ -87,6 +87,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_BOOT_PROGRESS;
 import static android.view.WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -618,6 +619,7 @@
     boolean mTranslucentDecorEnabled = true;
     boolean mUseTvRouting;
     int mVeryLongPressTimeout;
+    boolean mAllowStartActivityForLongPressOnPowerDuringSetup;
 
     private boolean mHandleVolumeKeysInWM;
 
@@ -1622,7 +1624,11 @@
                     : mKeyguardDelegate.isShowing();
             if (!keyguardActive) {
                 Intent intent = new Intent(Intent.ACTION_VOICE_ASSIST);
-                startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+                if (mAllowStartActivityForLongPressOnPowerDuringSetup) {
+                    mContext.startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+                } else {
+                    startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+                }
             }
             break;
         }
@@ -2134,6 +2140,8 @@
                 com.android.internal.R.integer.config_shortPressOnSleepBehavior);
         mVeryLongPressTimeout = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_veryLongPressTimeout);
+        mAllowStartActivityForLongPressOnPowerDuringSetup = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_allowStartActivityForLongPressOnPowerInSetup);
 
         mUseTvRouting = AudioSystem.getPlatformType(mContext) == AudioSystem.PLATFORM_TELEVISION;
 
@@ -5404,6 +5412,12 @@
         final boolean attachedInParent = attached != null && !layoutInScreen;
         final boolean requestedHideNavigation =
                 (requestedSysUiFl & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0;
+
+        // TYPE_BASE_APPLICATION windows are never considered floating here because they don't get
+        // cropped / shifted to the displayFrame in WindowState.
+        final boolean floatingInScreenWindow = !attrs.isFullscreen() && layoutInScreen
+                && type != TYPE_BASE_APPLICATION;
+
         // Ensure that windows with a DEFAULT or NEVER display cutout mode are laid out in
         // the cutout safe zone.
         if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
@@ -5438,7 +5452,10 @@
             }
             // Windows that are attached to a parent and laid out in said parent already avoid
             // the cutout according to that parent and don't need to be further constrained.
-            if (!attachedInParent) {
+            // Floating IN_SCREEN windows get what they ask for and lay out in the full screen.
+            // They will later be cropped or shifted using the displayFrame in WindowState,
+            // which prevents overlap with the DisplayCutout.
+            if (!attachedInParent && !floatingInScreenWindow) {
                 mTmpRect.set(pf);
                 pf.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
                 parentFrameWasClippedByDisplayCutout |= !mTmpRect.equals(pf);
@@ -8013,29 +8030,35 @@
     private VibrationEffect getVibrationEffect(int effectId) {
         long[] pattern;
         switch (effectId) {
-            case HapticFeedbackConstants.LONG_PRESS:
-                pattern = mLongPressVibePattern;
-                break;
             case HapticFeedbackConstants.CLOCK_TICK:
+            case HapticFeedbackConstants.CONTEXT_CLICK:
                 return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
+            case HapticFeedbackConstants.KEYBOARD_RELEASE:
+            case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
+            case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
+            case HapticFeedbackConstants.ENTRY_BUMP:
+            case HapticFeedbackConstants.DRAG_CROSSING:
+            case HapticFeedbackConstants.GESTURE_END:
+                return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
+            case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS
+            case HapticFeedbackConstants.VIRTUAL_KEY:
+            case HapticFeedbackConstants.EDGE_RELEASE:
+            case HapticFeedbackConstants.CONFIRM:
+            case HapticFeedbackConstants.GESTURE_START:
+                return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+            case HapticFeedbackConstants.LONG_PRESS:
+            case HapticFeedbackConstants.EDGE_SQUEEZE:
+                return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+            case HapticFeedbackConstants.REJECT:
+                return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
+
             case HapticFeedbackConstants.CALENDAR_DATE:
                 pattern = mCalendarDateVibePattern;
                 break;
             case HapticFeedbackConstants.SAFE_MODE_ENABLED:
                 pattern = mSafeModeEnabledVibePattern;
                 break;
-            case HapticFeedbackConstants.CONTEXT_CLICK:
-                return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
-            case HapticFeedbackConstants.VIRTUAL_KEY:
-                return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-            case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
-                return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
-            case HapticFeedbackConstants.KEYBOARD_PRESS:  // == HapticFeedbackConstants.KEYBOARD_TAP
-                return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-            case HapticFeedbackConstants.KEYBOARD_RELEASE:
-                return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
-            case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
-                return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
+
             default:
                 return null;
         }
@@ -8655,6 +8678,9 @@
                 pw.print("mShortPressOnWindowBehavior=");
                 pw.println(shortPressOnWindowBehaviorToString(mShortPressOnWindowBehavior));
         pw.print(prefix);
+                pw.print("mAllowStartActivityForLongPressOnPowerDuringSetup=");
+                pw.println(mAllowStartActivityForLongPressOnPowerDuringSetup);
+        pw.print(prefix);
                 pw.print("mHasSoftInput="); pw.print(mHasSoftInput);
                 pw.print(" mDismissImeOnBackKeyPressed="); pw.println(mDismissImeOnBackKeyPressed);
         pw.print(prefix);
diff --git a/com/android/server/slice/DirtyTracker.java b/com/android/server/slice/DirtyTracker.java
new file mode 100644
index 0000000..4288edc
--- /dev/null
+++ b/com/android/server/slice/DirtyTracker.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.server.slice;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * A parent object that cares when a Persistable changes and will schedule a serialization
+ * in response to the onPersistableDirty callback.
+ */
+public interface DirtyTracker {
+    void onPersistableDirty(Persistable obj);
+
+    /**
+     * An object that can be written to XML.
+     */
+    interface Persistable {
+        String getFileName();
+        void writeTo(XmlSerializer out) throws IOException;
+    }
+}
diff --git a/com/android/server/slice/SliceClientPermissions.java b/com/android/server/slice/SliceClientPermissions.java
new file mode 100644
index 0000000..e461e0d
--- /dev/null
+++ b/com/android/server/slice/SliceClientPermissions.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2018 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.server.slice;
+
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.server.slice.DirtyTracker.Persistable;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class SliceClientPermissions implements DirtyTracker, Persistable {
+
+    private static final String TAG = "SliceClientPermissions";
+
+    static final String TAG_CLIENT = "client";
+    private static final String TAG_AUTHORITY = "authority";
+    private static final String TAG_PATH = "path";
+    private static final String NAMESPACE = null;
+
+    private static final String ATTR_PKG = "pkg";
+    private static final String ATTR_AUTHORITY = "authority";
+    private static final String ATTR_FULL_ACCESS = "fullAccess";
+
+    private final PkgUser mPkg;
+    // Keyed off (authority, userId) rather than the standard (pkg, userId)
+    private final ArrayMap<PkgUser, SliceAuthority> mAuths = new ArrayMap<>();
+    private final DirtyTracker mTracker;
+    private boolean mHasFullAccess;
+
+    public SliceClientPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) {
+        mPkg = pkg;
+        mTracker = tracker;
+    }
+
+    public PkgUser getPkg() {
+        return mPkg;
+    }
+
+    public synchronized Collection<SliceAuthority> getAuthorities() {
+        return new ArrayList<>(mAuths.values());
+    }
+
+    public synchronized SliceAuthority getOrCreateAuthority(PkgUser authority, PkgUser provider) {
+        SliceAuthority ret = mAuths.get(authority);
+        if (ret == null) {
+            ret = new SliceAuthority(authority.getPkg(), provider, this);
+            mAuths.put(authority, ret);
+            onPersistableDirty(ret);
+        }
+        return ret;
+    }
+
+    public synchronized SliceAuthority getAuthority(PkgUser authority) {
+        return mAuths.get(authority);
+    }
+
+    public boolean hasFullAccess() {
+        return mHasFullAccess;
+    }
+
+    public void setHasFullAccess(boolean hasFullAccess) {
+        if (mHasFullAccess == hasFullAccess) return;
+        mHasFullAccess = hasFullAccess;
+        mTracker.onPersistableDirty(this);
+    }
+
+    public void removeAuthority(String authority, int userId) {
+        if (mAuths.remove(new PkgUser(authority, userId)) != null) {
+            mTracker.onPersistableDirty(this);
+        }
+    }
+
+    public synchronized boolean hasPermission(Uri uri, int userId) {
+        if (!Objects.equals(ContentResolver.SCHEME_CONTENT, uri.getScheme())) return false;
+        SliceAuthority authority = getAuthority(new PkgUser(uri.getAuthority(), userId));
+        return authority != null && authority.hasPermission(uri.getPathSegments());
+    }
+
+    public void grantUri(Uri uri, PkgUser providerPkg) {
+        SliceAuthority authority = getOrCreateAuthority(
+                new PkgUser(uri.getAuthority(), providerPkg.getUserId()),
+                providerPkg);
+        authority.addPath(uri.getPathSegments());
+    }
+
+    public void revokeUri(Uri uri, PkgUser providerPkg) {
+        SliceAuthority authority = getOrCreateAuthority(
+                new PkgUser(uri.getAuthority(), providerPkg.getUserId()),
+                providerPkg);
+        authority.removePath(uri.getPathSegments());
+    }
+
+    public void clear() {
+        if (!mHasFullAccess && mAuths.isEmpty()) return;
+        mHasFullAccess = false;
+        mAuths.clear();
+        onPersistableDirty(this);
+    }
+
+    @Override
+    public void onPersistableDirty(Persistable obj) {
+        mTracker.onPersistableDirty(this);
+    }
+
+    @Override
+    public String getFileName() {
+        return getFileName(mPkg);
+    }
+
+    public synchronized void writeTo(XmlSerializer out) throws IOException {
+        out.startTag(NAMESPACE, TAG_CLIENT);
+        out.attribute(NAMESPACE, ATTR_PKG, mPkg.toString());
+        out.attribute(NAMESPACE, ATTR_FULL_ACCESS, mHasFullAccess ? "1" : "0");
+
+        final int N = mAuths.size();
+        for (int i = 0; i < N; i++) {
+            out.startTag(NAMESPACE, TAG_AUTHORITY);
+            out.attribute(NAMESPACE, ATTR_AUTHORITY, mAuths.valueAt(i).mAuthority);
+            out.attribute(NAMESPACE, ATTR_PKG, mAuths.valueAt(i).mPkg.toString());
+
+            mAuths.valueAt(i).writeTo(out);
+
+            out.endTag(NAMESPACE, TAG_AUTHORITY);
+        }
+
+        out.endTag(NAMESPACE, TAG_CLIENT);
+    }
+
+    public static SliceClientPermissions createFrom(XmlPullParser parser, DirtyTracker tracker)
+            throws XmlPullParserException, IOException {
+        // Get to the beginning of the provider.
+        while (parser.getEventType() != XmlPullParser.START_TAG
+                || !TAG_CLIENT.equals(parser.getName())) {
+            parser.next();
+        }
+        int depth = parser.getDepth();
+        PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+        SliceClientPermissions provider = new SliceClientPermissions(pkgUser, tracker);
+        String fullAccess = parser.getAttributeValue(NAMESPACE, ATTR_FULL_ACCESS);
+        if (fullAccess == null) {
+            fullAccess = "0";
+        }
+        provider.mHasFullAccess = Integer.parseInt(fullAccess) != 0;
+        parser.next();
+
+        while (parser.getDepth() > depth) {
+            if (parser.getEventType() == XmlPullParser.START_TAG
+                    && TAG_AUTHORITY.equals(parser.getName())) {
+                try {
+                    PkgUser pkg = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+                    SliceAuthority authority = new SliceAuthority(
+                            parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), pkg, provider);
+                    authority.readFrom(parser);
+                    provider.mAuths.put(new PkgUser(authority.getAuthority(), pkg.getUserId()),
+                            authority);
+                } catch (IllegalArgumentException e) {
+                    Slog.e(TAG, "Couldn't read PkgUser", e);
+                }
+            }
+
+            parser.next();
+        }
+        return provider;
+    }
+
+    public static String getFileName(PkgUser pkg) {
+        return String.format("client_%s", pkg.toString());
+    }
+
+    public static class SliceAuthority implements Persistable {
+        public static final String DELIMITER = "/";
+        private final String mAuthority;
+        private final DirtyTracker mTracker;
+        private final PkgUser mPkg;
+        private final ArraySet<String[]> mPaths = new ArraySet<>();
+
+        public SliceAuthority(String authority, PkgUser pkg, DirtyTracker tracker) {
+            mAuthority = authority;
+            mPkg = pkg;
+            mTracker = tracker;
+        }
+
+        public String getAuthority() {
+            return mAuthority;
+        }
+
+        public PkgUser getPkg() {
+            return mPkg;
+        }
+
+        void addPath(List<String> path) {
+            String[] pathSegs = path.toArray(new String[path.size()]);
+            for (int i = mPaths.size() - 1; i >= 0; i--) {
+                String[] existing = mPaths.valueAt(i);
+                if (isPathPrefixMatch(existing, pathSegs)) {
+                    // Nothing to add here.
+                    return;
+                }
+                if (isPathPrefixMatch(pathSegs, existing)) {
+                    mPaths.removeAt(i);
+                }
+            }
+            mPaths.add(pathSegs);
+            mTracker.onPersistableDirty(this);
+        }
+
+        void removePath(List<String> path) {
+            boolean changed = false;
+            String[] pathSegs = path.toArray(new String[path.size()]);
+            for (int i = mPaths.size() - 1; i >= 0; i--) {
+                String[] existing = mPaths.valueAt(i);
+                if (isPathPrefixMatch(pathSegs, existing)) {
+                    changed = true;
+                    mPaths.removeAt(i);
+                }
+            }
+            if (changed) {
+                mTracker.onPersistableDirty(this);
+            }
+        }
+
+        public synchronized Collection<String[]> getPaths() {
+            return new ArraySet<>(mPaths);
+        }
+
+        public boolean hasPermission(List<String> path) {
+            for (String[] p : mPaths) {
+                if (isPathPrefixMatch(p, path.toArray(new String[path.size()]))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private boolean isPathPrefixMatch(String[] prefix, String[] path) {
+            final int prefixSize = prefix.length;
+            if (path.length < prefixSize) return false;
+
+            for (int i = 0; i < prefixSize; i++) {
+                if (!Objects.equals(path[i], prefix[i])) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public String getFileName() {
+            return null;
+        }
+
+        public synchronized void writeTo(XmlSerializer out) throws IOException {
+            final int N = mPaths.size();
+            for (int i = 0; i < N; i++) {
+                out.startTag(NAMESPACE, TAG_PATH);
+                out.text(encodeSegments(mPaths.valueAt(i)));
+                out.endTag(NAMESPACE, TAG_PATH);
+            }
+        }
+
+        public synchronized void readFrom(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            parser.next();
+            int depth = parser.getDepth();
+            while (parser.getDepth() >= depth) {
+                if (parser.getEventType() == XmlPullParser.START_TAG
+                        && TAG_PATH.equals(parser.getName())) {
+                    mPaths.add(decodeSegments(parser.nextText()));
+                }
+                parser.next();
+            }
+        }
+
+        private String encodeSegments(String[] s) {
+            String[] out = new String[s.length];
+            for (int i = 0; i < s.length; i++) {
+                out[i] = Uri.encode(s[i]);
+            }
+            return TextUtils.join(DELIMITER, out);
+        }
+
+        private String[] decodeSegments(String s) {
+            String[] sets = s.split(DELIMITER, -1);
+            for (int i = 0; i < sets.length; i++) {
+                sets[i] = Uri.decode(sets[i]);
+            }
+            return sets;
+        }
+
+        /**
+         * Only for testing, no deep equality of these are done normally.
+         */
+        @Override
+        public boolean equals(Object obj) {
+            if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+            SliceAuthority other = (SliceAuthority) obj;
+            if (mPaths.size() != other.mPaths.size()) return false;
+            ArrayList<String[]> p1 = new ArrayList<>(mPaths);
+            ArrayList<String[]> p2 = new ArrayList<>(other.mPaths);
+            p1.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
+            p2.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
+            for (int i = 0; i < p1.size(); i++) {
+                String[] a1 = p1.get(i);
+                String[] a2 = p2.get(i);
+                if (a1.length != a2.length) return false;
+                for (int j = 0; j < a1.length; j++) {
+                    if (!Objects.equals(a1[j], a2[j])) return false;
+                }
+            }
+            return Objects.equals(mAuthority, other.mAuthority)
+                    && Objects.equals(mPkg, other.mPkg);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("(%s, %s: %s)", mAuthority, mPkg.toString(), pathToString(mPaths));
+        }
+
+        private String pathToString(ArraySet<String[]> paths) {
+            return TextUtils.join(", ", paths.stream().map(s -> TextUtils.join("/", s))
+                    .collect(Collectors.toList()));
+        }
+    }
+}
diff --git a/com/android/server/slice/SliceManagerService.java b/com/android/server/slice/SliceManagerService.java
index fd0b6f1..b7b9612 100644
--- a/com/android/server/slice/SliceManagerService.java
+++ b/com/android/server/slice/SliceManagerService.java
@@ -31,14 +31,15 @@
 import android.app.ContentProviderHolder;
 import android.app.IActivityManager;
 import android.app.slice.ISliceManager;
-import android.app.slice.SliceManager;
 import android.app.slice.SliceSpec;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentProvider;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
@@ -51,7 +52,6 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.Xml.Encoding;
@@ -72,7 +72,6 @@
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -91,13 +90,9 @@
 
     @GuardedBy("mLock")
     private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>();
-    @GuardedBy("mLock")
-    private final ArraySet<SliceGrant> mUserGrants = new ArraySet<>();
     private final Handler mHandler;
-    @GuardedBy("mSliceAccessFile")
-    private final AtomicFile mSliceAccessFile;
-    @GuardedBy("mAccessList")
-    private final SliceFullAccessList mAccessList;
+
+    private final SlicePermissionManager mPermissions;
     private final UsageStatsManagerInternal mAppUsageStats;
 
     public SliceManagerService(Context context) {
@@ -113,24 +108,9 @@
         mAssistUtils = new AssistUtils(context);
         mHandler = new Handler(looper);
 
-        final File systemDir = new File(Environment.getDataDirectory(), "system");
-        mSliceAccessFile = new AtomicFile(new File(systemDir, "slice_access.xml"));
-        mAccessList = new SliceFullAccessList(mContext);
         mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
 
-        synchronized (mSliceAccessFile) {
-            if (!mSliceAccessFile.exists()) return;
-            try {
-                InputStream input = mSliceAccessFile.openRead();
-                XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
-                parser.setInput(input, Encoding.UTF_8.name());
-                synchronized (mAccessList) {
-                    mAccessList.readXml(parser);
-                }
-            } catch (IOException | XmlPullParserException e) {
-                Slog.d(TAG, "Can't read slice access file", e);
-            }
-        }
+        mPermissions = new SlicePermissionManager(mContext, looper);
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
@@ -211,26 +191,58 @@
     }
 
     @Override
+    public void grantSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
+        verifyCaller(pkg);
+        int user = Binder.getCallingUserHandle().getIdentifier();
+        enforceOwner(pkg, uri, user);
+        mPermissions.grantSliceAccess(toPkg, user, pkg, user, uri);
+    }
+
+    @Override
+    public void revokeSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
+        verifyCaller(pkg);
+        int user = Binder.getCallingUserHandle().getIdentifier();
+        enforceOwner(pkg, uri, user);
+        mPermissions.revokeSliceAccess(toPkg, user, pkg, user, uri);
+    }
+
+    @Override
     public int checkSlicePermission(Uri uri, String pkg, int pid, int uid,
-            String[] autoGrantPermissions) throws RemoteException {
+            String[] autoGrantPermissions) {
+        int userId = UserHandle.getUserId(uid);
+        if (pkg == null) {
+            for (String p : mContext.getPackageManager().getPackagesForUid(uid)) {
+                if (checkSlicePermission(uri, p, pid, uid, autoGrantPermissions)
+                        == PERMISSION_GRANTED) {
+                    return PERMISSION_GRANTED;
+                }
+            }
+            return PERMISSION_DENIED;
+        }
+        if (hasFullSliceAccess(pkg, userId)) {
+            return PackageManager.PERMISSION_GRANTED;
+        }
+        if (mPermissions.hasPermission(pkg, userId, uri)) {
+            return PackageManager.PERMISSION_GRANTED;
+        }
+        if (autoGrantPermissions != null) {
+            // Need to own the Uri to call in with permissions to grant.
+            enforceOwner(pkg, uri, userId);
+            for (String perm : autoGrantPermissions) {
+                if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) {
+                    int providerUser = ContentProvider.getUserIdFromUri(uri, userId);
+                    String providerPkg = getProviderPkg(uri, providerUser);
+                    mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, uri);
+                    return PackageManager.PERMISSION_GRANTED;
+                }
+            }
+        }
+        // Fallback to allowing uri permissions through.
         if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                 == PERMISSION_GRANTED) {
-            return SliceManager.PERMISSION_GRANTED;
+            return PackageManager.PERMISSION_GRANTED;
         }
-        if (hasFullSliceAccess(pkg, UserHandle.getUserId(uid))) {
-            return SliceManager.PERMISSION_GRANTED;
-        }
-        for (String perm : autoGrantPermissions) {
-            if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) {
-                return SliceManager.PERMISSION_USER_GRANTED;
-            }
-        }
-        synchronized (mLock) {
-            if (mUserGrants.contains(new SliceGrant(uri, pkg, UserHandle.getUserId(uid)))) {
-                return SliceManager.PERMISSION_USER_GRANTED;
-            }
-        }
-        return SliceManager.PERMISSION_DENIED;
+        return PackageManager.PERMISSION_DENIED;
     }
 
     @Override
@@ -238,16 +250,17 @@
         verifyCaller(callingPkg);
         getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS,
                 "Slice granting requires MANAGE_SLICE_PERMISSIONS");
+        int userId = Binder.getCallingUserHandle().getIdentifier();
         if (allSlices) {
-            synchronized (mAccessList) {
-                mAccessList.grantFullAccess(pkg, Binder.getCallingUserHandle().getIdentifier());
-            }
-            mHandler.post(mSaveAccessList);
+            mPermissions.grantFullAccess(pkg, userId);
         } else {
-            synchronized (mLock) {
-                mUserGrants.add(new SliceGrant(uri, pkg,
-                        Binder.getCallingUserHandle().getIdentifier()));
-            }
+            // When granting, grant to all slices in the provider.
+            Uri grantUri = uri.buildUpon()
+                    .path("")
+                    .build();
+            int providerUser = ContentProvider.getUserIdFromUri(grantUri, userId);
+            String providerPkg = getProviderPkg(grantUri, providerUser);
+            mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, grantUri);
         }
         long ident = Binder.clearCallingIdentity();
         try {
@@ -268,19 +281,17 @@
             Slog.w(TAG, "getBackupPayload: cannot backup policy for user " + user);
             return null;
         }
-        synchronized(mSliceAccessFile) {
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            try {
-                XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
-                out.setOutput(baos, Encoding.UTF_8.name());
-                synchronized (mAccessList) {
-                    mAccessList.writeXml(out, user);
-                }
-                out.flush();
-                return baos.toByteArray();
-            } catch (IOException | XmlPullParserException e) {
-                Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
-            }
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+            out.setOutput(baos, Encoding.UTF_8.name());
+
+            mPermissions.writeBackup(out);
+
+            out.flush();
+            return baos.toByteArray();
+        } catch (IOException | XmlPullParserException e) {
+            Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
         }
         return null;
     }
@@ -299,27 +310,21 @@
             Slog.w(TAG, "applyRestore: cannot restore policy for user " + user);
             return;
         }
-        synchronized(mSliceAccessFile) {
-            final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
-            try {
-                XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
-                parser.setInput(bais, Encoding.UTF_8.name());
-                synchronized (mAccessList) {
-                    mAccessList.readXml(parser);
-                }
-                mHandler.post(mSaveAccessList);
-            } catch (NumberFormatException | XmlPullParserException | IOException e) {
-                Slog.w(TAG, "applyRestore: error reading payload", e);
-            }
+        final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
+        try {
+            XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+            parser.setInput(bais, Encoding.UTF_8.name());
+            mPermissions.readRestore(parser);
+        } catch (NumberFormatException | XmlPullParserException | IOException e) {
+            Slog.w(TAG, "applyRestore: error reading payload", e);
         }
     }
 
     ///  ----- internal code -----
-    private void removeFullAccess(String pkg, int userId) {
-        synchronized (mAccessList) {
-            mAccessList.removeGrant(pkg, userId);
+    private void enforceOwner(String pkg, Uri uri, int user) {
+        if (!Objects.equals(getProviderPkg(uri, user), pkg) || pkg == null) {
+            throw new SecurityException("Caller must own " + uri);
         }
-        mHandler.post(mSaveAccessList);
     }
 
     protected void removePinnedSlice(Uri uri) {
@@ -368,19 +373,7 @@
     }
 
     protected int checkAccess(String pkg, Uri uri, int uid, int pid) {
-        int user = UserHandle.getUserId(uid);
-        // Check for default launcher/assistant.
-        if (!hasFullSliceAccess(pkg, user)) {
-            // Also allow things with uri access.
-            if (getContext().checkUriPermission(uri, pid, uid,
-                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != PERMISSION_GRANTED) {
-                // Last fallback (if the calling app owns the authority, then it can have access).
-                if (!Objects.equals(getProviderPkg(uri, user), pkg)) {
-                    return PERMISSION_DENIED;
-                }
-            }
-        }
-        return PERMISSION_GRANTED;
+        return checkSlicePermission(uri, pkg, uid, pid, null);
     }
 
     private String getProviderPkg(Uri uri, int user) {
@@ -425,15 +418,11 @@
     private void enforceAccess(String pkg, Uri uri) throws RemoteException {
         if (checkAccess(pkg, uri, Binder.getCallingUid(), Binder.getCallingPid())
                 != PERMISSION_GRANTED) {
-            throw new SecurityException("Access to slice " + uri + " is required");
-        }
-        enforceCrossUser(pkg, uri);
-    }
-
-    private void enforceFullAccess(String pkg, String name, Uri uri) {
-        int user = Binder.getCallingUserHandle().getIdentifier();
-        if (!hasFullSliceAccess(pkg, user)) {
-            throw new SecurityException(String.format("Call %s requires full slice access", name));
+            int userId = ContentProvider.getUserIdFromUri(uri,
+                    Binder.getCallingUserHandle().getIdentifier());
+            if (!Objects.equals(pkg, getProviderPkg(uri, userId))) {
+                throw new SecurityException("Access to slice " + uri + " is required");
+            }
         }
         enforceCrossUser(pkg, uri);
     }
@@ -513,9 +502,7 @@
     }
 
     private boolean isGrantedFullAccess(String pkg, int userId) {
-        synchronized (mAccessList) {
-            return mAccessList.hasFullAccess(pkg, userId);
-        }
+        return mPermissions.hasFullAccess(pkg, userId);
     }
 
     private static ServiceThread createHandler() {
@@ -525,34 +512,6 @@
         return handlerThread;
     }
 
-    private final Runnable mSaveAccessList = new Runnable() {
-        @Override
-        public void run() {
-            synchronized (mSliceAccessFile) {
-                final FileOutputStream stream;
-                try {
-                    stream = mSliceAccessFile.startWrite();
-                } catch (IOException e) {
-                    Slog.w(TAG, "Failed to save access file", e);
-                    return;
-                }
-
-                try {
-                    XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
-                    out.setOutput(stream, Encoding.UTF_8.name());
-                    synchronized (mAccessList) {
-                        mAccessList.writeXml(out, UserHandle.USER_ALL);
-                    }
-                    out.flush();
-                    mSliceAccessFile.finishWrite(stream);
-                } catch (IOException | XmlPullParserException e) {
-                    Slog.w(TAG, "Failed to save access file, restoring backup", e);
-                    mSliceAccessFile.failWrite(stream);
-                }
-            }
-        }
-    };
-
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -572,11 +531,11 @@
                     final boolean replacing =
                             intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
                     if (!replacing) {
-                        removeFullAccess(pkg, userId);
+                        mPermissions.removePkg(pkg, userId);
                     }
                     break;
                 case Intent.ACTION_PACKAGE_DATA_CLEARED:
-                    removeFullAccess(pkg, userId);
+                    mPermissions.removePkg(pkg, userId);
                     break;
             }
         }
diff --git a/com/android/server/slice/SlicePermissionManager.java b/com/android/server/slice/SlicePermissionManager.java
new file mode 100644
index 0000000..d25ec89
--- /dev/null
+++ b/com/android/server/slice/SlicePermissionManager.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2018 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.server.slice;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml.Encoding;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.XmlUtils;
+import com.android.server.slice.SliceProviderPermissions.SliceAuthority;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+public class SlicePermissionManager implements DirtyTracker {
+
+    private static final String TAG = "SlicePermissionManager";
+
+    /**
+     * The amount of time we'll cache a SliceProviderPermissions or SliceClientPermissions
+     * in case they are used again.
+     */
+    private static final long PERMISSION_CACHE_PERIOD = 5 * DateUtils.MINUTE_IN_MILLIS;
+
+    /**
+     * The amount of time we delay flushing out permission changes to disk because they usually
+     * come in short bursts.
+     */
+    private static final long WRITE_GRACE_PERIOD = 500;
+
+    private static final String SLICE_DIR = "slice";
+
+    // If/when this bumps again we'll need to write it out in the disk somewhere.
+    // Currently we don't have a central file for this in version 2 and there is no
+    // reason to add one until we actually have incompatible version bumps.
+    // This does however block us from reading backups from P-DP1 which may contain
+    // a very different XML format for perms.
+    static final int DB_VERSION = 2;
+
+    private static final String TAG_LIST = "slice-access-list";
+    private final String ATT_VERSION = "version";
+
+    private final File mSliceDir;
+    private final Context mContext;
+    private final Handler mHandler;
+    private final ArrayMap<PkgUser, SliceProviderPermissions> mCachedProviders = new ArrayMap<>();
+    private final ArrayMap<PkgUser, SliceClientPermissions> mCachedClients = new ArrayMap<>();
+    private final ArraySet<Persistable> mDirty = new ArraySet<>();
+
+    @VisibleForTesting
+    SlicePermissionManager(Context context, Looper looper, File sliceDir) {
+        mContext = context;
+        mHandler = new H(looper);
+        mSliceDir = sliceDir;
+    }
+
+    public SlicePermissionManager(Context context, Looper looper) {
+        this(context, looper, new File(Environment.getDataDirectory(), "system/" + SLICE_DIR));
+    }
+
+    public void grantFullAccess(String pkg, int userId) {
+        PkgUser pkgUser = new PkgUser(pkg, userId);
+        SliceClientPermissions client = getClient(pkgUser);
+        client.setHasFullAccess(true);
+    }
+
+    public void grantSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
+            Uri uri) {
+        PkgUser pkgUser = new PkgUser(pkg, userId);
+        PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
+
+        SliceClientPermissions client = getClient(pkgUser);
+        client.grantUri(uri, providerPkgUser);
+
+        SliceProviderPermissions provider = getProvider(providerPkgUser);
+        provider.getOrCreateAuthority(ContentProvider.getUriWithoutUserId(uri).getAuthority())
+                .addPkg(pkgUser);
+    }
+
+    public void revokeSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
+            Uri uri) {
+        PkgUser pkgUser = new PkgUser(pkg, userId);
+        PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
+
+        SliceClientPermissions client = getClient(pkgUser);
+        client.revokeUri(uri, providerPkgUser);
+    }
+
+    public void removePkg(String pkg, int userId) {
+        PkgUser pkgUser = new PkgUser(pkg, userId);
+        SliceProviderPermissions provider = getProvider(pkgUser);
+
+        for (SliceAuthority authority : provider.getAuthorities()) {
+            for (PkgUser p : authority.getPkgs()) {
+                getClient(p).removeAuthority(authority.getAuthority(), userId);
+            }
+        }
+        SliceClientPermissions client = getClient(pkgUser);
+        client.clear();
+        mHandler.obtainMessage(H.MSG_REMOVE, pkgUser);
+    }
+
+    public boolean hasFullAccess(String pkg, int userId) {
+        PkgUser pkgUser = new PkgUser(pkg, userId);
+        return getClient(pkgUser).hasFullAccess();
+    }
+
+    public boolean hasPermission(String pkg, int userId, Uri uri) {
+        PkgUser pkgUser = new PkgUser(pkg, userId);
+        SliceClientPermissions client = getClient(pkgUser);
+        int providerUserId = ContentProvider.getUserIdFromUri(uri, userId);
+        return client.hasFullAccess()
+                || client.hasPermission(ContentProvider.getUriWithoutUserId(uri), providerUserId);
+    }
+
+    @Override
+    public void onPersistableDirty(Persistable obj) {
+        mHandler.removeMessages(H.MSG_PERSIST);
+        mHandler.obtainMessage(H.MSG_ADD_DIRTY, obj).sendToTarget();
+        mHandler.sendEmptyMessageDelayed(H.MSG_PERSIST, WRITE_GRACE_PERIOD);
+    }
+
+    public void writeBackup(XmlSerializer out) throws IOException, XmlPullParserException {
+        synchronized (this) {
+            out.startTag(null, TAG_LIST);
+            out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION));
+
+            // Don't do anything with changes from the backup, because there shouldn't be any.
+            DirtyTracker tracker = obj -> { };
+            if (mHandler.hasMessages(H.MSG_PERSIST)) {
+                mHandler.removeMessages(H.MSG_PERSIST);
+                handlePersist();
+            }
+            for (String file : new File(mSliceDir.getAbsolutePath()).list()) {
+                if (file.isEmpty()) continue;
+                try (ParserHolder parser = getParser(file)) {
+                    Persistable p;
+                    while (parser.parser.getEventType() != XmlPullParser.START_TAG) {
+                        parser.parser.next();
+                    }
+                    if (SliceClientPermissions.TAG_CLIENT.equals(parser.parser.getName())) {
+                        p = SliceClientPermissions.createFrom(parser.parser, tracker);
+                    } else {
+                        p = SliceProviderPermissions.createFrom(parser.parser, tracker);
+                    }
+                    p.writeTo(out);
+                }
+            }
+
+            out.endTag(null, TAG_LIST);
+        }
+    }
+
+    public void readRestore(XmlPullParser parser) throws IOException, XmlPullParserException {
+        synchronized (this) {
+            while ((parser.getEventType() != XmlPullParser.START_TAG
+                    || !TAG_LIST.equals(parser.getName()))
+                    && parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+                parser.next();
+            }
+            int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0);
+            if (xmlVersion < DB_VERSION) {
+                // No conversion support right now.
+                return;
+            }
+            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+                if (parser.getEventType() == XmlPullParser.START_TAG) {
+                    if (SliceClientPermissions.TAG_CLIENT.equals(parser.getName())) {
+                        SliceClientPermissions client = SliceClientPermissions.createFrom(parser,
+                                this);
+                        synchronized (mCachedClients) {
+                            mCachedClients.put(client.getPkg(), client);
+                        }
+                        onPersistableDirty(client);
+                        mHandler.sendMessageDelayed(
+                                mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, client.getPkg()),
+                                PERMISSION_CACHE_PERIOD);
+                    } else if (SliceProviderPermissions.TAG_PROVIDER.equals(parser.getName())) {
+                        SliceProviderPermissions provider = SliceProviderPermissions.createFrom(
+                                parser, this);
+                        synchronized (mCachedProviders) {
+                            mCachedProviders.put(provider.getPkg(), provider);
+                        }
+                        onPersistableDirty(provider);
+                        mHandler.sendMessageDelayed(
+                                mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, provider.getPkg()),
+                                PERMISSION_CACHE_PERIOD);
+                    } else {
+                        parser.next();
+                    }
+                } else {
+                    parser.next();
+                }
+            }
+        }
+    }
+
+    private SliceClientPermissions getClient(PkgUser pkgUser) {
+        SliceClientPermissions client;
+        synchronized (mCachedClients) {
+            client = mCachedClients.get(pkgUser);
+        }
+        if (client == null) {
+            try (ParserHolder parser = getParser(SliceClientPermissions.getFileName(pkgUser))) {
+                client = SliceClientPermissions.createFrom(parser.parser, this);
+                synchronized (mCachedClients) {
+                    mCachedClients.put(pkgUser, client);
+                }
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, pkgUser),
+                        PERMISSION_CACHE_PERIOD);
+                return client;
+            } catch (FileNotFoundException e) {
+                // No client exists yet.
+            } catch (IOException e) {
+                Log.e(TAG, "Can't read client", e);
+            } catch (XmlPullParserException e) {
+                Log.e(TAG, "Can't read client", e);
+            }
+            // Can't read or no permissions exist, create a clean object.
+            client = new SliceClientPermissions(pkgUser, this);
+        }
+        return client;
+    }
+
+    private SliceProviderPermissions getProvider(PkgUser pkgUser) {
+        SliceProviderPermissions provider;
+        synchronized (mCachedProviders) {
+            provider = mCachedProviders.get(pkgUser);
+        }
+        if (provider == null) {
+            try (ParserHolder parser = getParser(SliceProviderPermissions.getFileName(pkgUser))) {
+                provider = SliceProviderPermissions.createFrom(parser.parser, this);
+                synchronized (mCachedProviders) {
+                    mCachedProviders.put(pkgUser, provider);
+                }
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, pkgUser),
+                        PERMISSION_CACHE_PERIOD);
+                return provider;
+            } catch (FileNotFoundException e) {
+                // No provider exists yet.
+            } catch (IOException e) {
+                Log.e(TAG, "Can't read provider", e);
+            } catch (XmlPullParserException e) {
+                Log.e(TAG, "Can't read provider", e);
+            }
+            // Can't read or no permissions exist, create a clean object.
+            provider = new SliceProviderPermissions(pkgUser, this);
+        }
+        return provider;
+    }
+
+    private ParserHolder getParser(String fileName)
+            throws FileNotFoundException, XmlPullParserException {
+        AtomicFile file = getFile(fileName);
+        ParserHolder holder = new ParserHolder();
+        holder.input = file.openRead();
+        holder.parser = XmlPullParserFactory.newInstance().newPullParser();
+        holder.parser.setInput(holder.input, Encoding.UTF_8.name());
+        return holder;
+    }
+
+    private AtomicFile getFile(String fileName) {
+        if (!mSliceDir.exists()) {
+            mSliceDir.mkdir();
+        }
+        return new AtomicFile(new File(mSliceDir, fileName));
+    }
+
+    private void handlePersist() {
+        synchronized (this) {
+            for (Persistable persistable : mDirty) {
+                AtomicFile file = getFile(persistable.getFileName());
+                final FileOutputStream stream;
+                try {
+                    stream = file.startWrite();
+                } catch (IOException e) {
+                    Slog.w(TAG, "Failed to save access file", e);
+                    return;
+                }
+
+                try {
+                    XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+                    out.setOutput(stream, Encoding.UTF_8.name());
+
+                    persistable.writeTo(out);
+
+                    out.flush();
+                    file.finishWrite(stream);
+                } catch (IOException | XmlPullParserException e) {
+                    Slog.w(TAG, "Failed to save access file, restoring backup", e);
+                    file.failWrite(stream);
+                }
+            }
+            mDirty.clear();
+        }
+    }
+
+    private void handleRemove(PkgUser pkgUser) {
+        getFile(SliceClientPermissions.getFileName(pkgUser)).delete();
+        getFile(SliceProviderPermissions.getFileName(pkgUser)).delete();
+        mDirty.remove(mCachedClients.remove(pkgUser));
+        mDirty.remove(mCachedProviders.remove(pkgUser));
+    }
+
+    private final class H extends Handler {
+        private static final int MSG_ADD_DIRTY = 1;
+        private static final int MSG_PERSIST = 2;
+        private static final int MSG_REMOVE = 3;
+        private static final int MSG_CLEAR_CLIENT = 4;
+        private static final int MSG_CLEAR_PROVIDER = 5;
+
+        public H(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_ADD_DIRTY:
+                    mDirty.add((Persistable) msg.obj);
+                    break;
+                case MSG_PERSIST:
+                    handlePersist();
+                    break;
+                case MSG_REMOVE:
+                    handleRemove((PkgUser) msg.obj);
+                    break;
+                case MSG_CLEAR_CLIENT:
+                    synchronized (mCachedClients) {
+                        mCachedClients.remove(msg.obj);
+                    }
+                    break;
+                case MSG_CLEAR_PROVIDER:
+                    synchronized (mCachedProviders) {
+                        mCachedProviders.remove(msg.obj);
+                    }
+                    break;
+            }
+        }
+    }
+
+    public static class PkgUser {
+        private static final String SEPARATOR = "@";
+        private static final String FORMAT = "%s" + SEPARATOR + "%d";
+        private final String mPkg;
+        private final int mUserId;
+
+        public PkgUser(String pkg, int userId) {
+            mPkg = pkg;
+            mUserId = userId;
+        }
+
+        public PkgUser(String pkgUserStr) throws IllegalArgumentException {
+            try {
+                String[] vals = pkgUserStr.split(SEPARATOR, 2);
+                mPkg = vals[0];
+                mUserId = Integer.parseInt(vals[1]);
+            } catch (Exception e) {
+                throw new IllegalArgumentException(e);
+            }
+        }
+
+        public String getPkg() {
+            return mPkg;
+        }
+
+        public int getUserId() {
+            return mUserId;
+        }
+
+        @Override
+        public int hashCode() {
+            return mPkg.hashCode() + mUserId;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+            PkgUser other = (PkgUser) obj;
+            return Objects.equals(other.mPkg, mPkg) && other.mUserId == mUserId;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(FORMAT, mPkg, mUserId);
+        }
+    }
+
+    private class ParserHolder implements AutoCloseable {
+
+        private InputStream input;
+        private XmlPullParser parser;
+
+        @Override
+        public void close() throws IOException {
+            input.close();
+        }
+    }
+}
diff --git a/com/android/server/slice/SliceProviderPermissions.java b/com/android/server/slice/SliceProviderPermissions.java
new file mode 100644
index 0000000..6e602d5
--- /dev/null
+++ b/com/android/server/slice/SliceProviderPermissions.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2018 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.server.slice;
+
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.server.slice.DirtyTracker.Persistable;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+
+public class SliceProviderPermissions implements DirtyTracker, Persistable {
+
+    private static final String TAG = "SliceProviderPermissions";
+
+    static final String TAG_PROVIDER = "provider";
+    private static final String TAG_AUTHORITY = "authority";
+    private static final String TAG_PKG = "pkg";
+    private static final String NAMESPACE = null;
+
+    private static final String ATTR_PKG = "pkg";
+    private static final String ATTR_AUTHORITY = "authority";
+
+    private final PkgUser mPkg;
+    private final ArrayMap<String, SliceAuthority> mAuths = new ArrayMap<>();
+    private final DirtyTracker mTracker;
+
+    public SliceProviderPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) {
+        mPkg = pkg;
+        mTracker = tracker;
+    }
+
+    public PkgUser getPkg() {
+        return mPkg;
+    }
+
+    public synchronized Collection<SliceAuthority> getAuthorities() {
+        return new ArrayList<>(mAuths.values());
+    }
+
+    public synchronized SliceAuthority getOrCreateAuthority(String authority) {
+        SliceAuthority ret = mAuths.get(authority);
+        if (ret == null) {
+            ret = new SliceAuthority(authority, this);
+            mAuths.put(authority, ret);
+            onPersistableDirty(ret);
+        }
+        return ret;
+    }
+
+    @Override
+    public void onPersistableDirty(Persistable obj) {
+        mTracker.onPersistableDirty(this);
+    }
+
+    @Override
+    public String getFileName() {
+        return getFileName(mPkg);
+    }
+
+    public synchronized void writeTo(XmlSerializer out) throws IOException {
+        out.startTag(NAMESPACE, TAG_PROVIDER);
+        out.attribute(NAMESPACE, ATTR_PKG, mPkg.toString());
+
+        final int N = mAuths.size();
+        for (int i = 0; i < N; i++) {
+            out.startTag(NAMESPACE, TAG_AUTHORITY);
+            out.attribute(NAMESPACE, ATTR_AUTHORITY, mAuths.valueAt(i).mAuthority);
+
+            mAuths.valueAt(i).writeTo(out);
+
+            out.endTag(NAMESPACE, TAG_AUTHORITY);
+        }
+
+        out.endTag(NAMESPACE, TAG_PROVIDER);
+    }
+
+    public static SliceProviderPermissions createFrom(XmlPullParser parser, DirtyTracker tracker)
+            throws XmlPullParserException, IOException {
+        // Get to the beginning of the provider.
+        while (parser.getEventType() != XmlPullParser.START_TAG
+                || !TAG_PROVIDER.equals(parser.getName())) {
+            parser.next();
+        }
+        int depth = parser.getDepth();
+        PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+        SliceProviderPermissions provider = new SliceProviderPermissions(pkgUser, tracker);
+        parser.next();
+
+        while (parser.getDepth() > depth) {
+            if (parser.getEventType() == XmlPullParser.START_TAG
+                    && TAG_AUTHORITY.equals(parser.getName())) {
+                try {
+                    SliceAuthority authority = new SliceAuthority(
+                            parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), provider);
+                    authority.readFrom(parser);
+                    provider.mAuths.put(authority.getAuthority(), authority);
+                } catch (IllegalArgumentException e) {
+                    Slog.e(TAG, "Couldn't read PkgUser", e);
+                }
+            }
+
+            parser.next();
+        }
+        return provider;
+    }
+
+    public static String getFileName(PkgUser pkg) {
+        return String.format("provider_%s", pkg.toString());
+    }
+
+    public static class SliceAuthority implements Persistable {
+        private final String mAuthority;
+        private final DirtyTracker mTracker;
+        private final ArraySet<PkgUser> mPkgs = new ArraySet<>();
+
+        public SliceAuthority(String authority, DirtyTracker tracker) {
+            mAuthority = authority;
+            mTracker = tracker;
+        }
+
+        public String getAuthority() {
+            return mAuthority;
+        }
+
+        public synchronized void addPkg(PkgUser pkg) {
+            if (mPkgs.add(pkg)) {
+                mTracker.onPersistableDirty(this);
+            }
+        }
+
+        public synchronized void removePkg(PkgUser pkg) {
+            if (mPkgs.remove(pkg)) {
+                mTracker.onPersistableDirty(this);
+            }
+        }
+
+        public synchronized Collection<PkgUser> getPkgs() {
+            return new ArraySet<>(mPkgs);
+        }
+
+        @Override
+        public String getFileName() {
+            return null;
+        }
+
+        public synchronized void writeTo(XmlSerializer out) throws IOException {
+            final int N = mPkgs.size();
+            for (int i = 0; i < N; i++) {
+                out.startTag(NAMESPACE, TAG_PKG);
+                out.text(mPkgs.valueAt(i).toString());
+                out.endTag(NAMESPACE, TAG_PKG);
+            }
+        }
+
+        public synchronized void readFrom(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            parser.next();
+            int depth = parser.getDepth();
+            while (parser.getDepth() >= depth) {
+                if (parser.getEventType() == XmlPullParser.START_TAG
+                        && TAG_PKG.equals(parser.getName())) {
+                    mPkgs.add(new PkgUser(parser.nextText()));
+                }
+                parser.next();
+            }
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+            SliceAuthority other = (SliceAuthority) obj;
+            return Objects.equals(mAuthority, other.mAuthority)
+                    && Objects.equals(mPkgs, other.mPkgs);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("(%s: %s)", mAuthority, mPkgs.toString());
+        }
+    }
+}
diff --git a/com/android/server/soundtrigger/SoundTriggerService.java b/com/android/server/soundtrigger/SoundTriggerService.java
index 1160943..cd524a5 100644
--- a/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/com/android/server/soundtrigger/SoundTriggerService.java
@@ -926,25 +926,24 @@
                             Slog.w(TAG, mPuuid + ": Dropped operation as too many operations were "
                                     + "run in last 24 hours");
                         }
-                        return;
-                    }
+                    } else {
+                        mNumOps.addOp(currentTime);
 
-                    mNumOps.addOp(currentTime);
+                        // Find a free opID
+                        int opId = mNumTotalOpsPerformed;
+                        do {
+                            mNumTotalOpsPerformed++;
+                        } while (mRunningOpIds.contains(opId));
 
-                    // Find a free opID
-                    int opId = mNumTotalOpsPerformed;
-                    do {
-                        mNumTotalOpsPerformed++;
-                    } while (mRunningOpIds.contains(opId));
+                        // Run OP
+                        try {
+                            if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
 
-                    // Run OP
-                    try {
-                        if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
-
-                        op.run(opId, mService);
-                        mRunningOpIds.add(opId);
-                    } catch (Exception e) {
-                        Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
+                            op.run(opId, mService);
+                            mRunningOpIds.add(opId);
+                        } catch (Exception e) {
+                            Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
+                        }
                     }
 
                     // Unbind from service if no operations are left (i.e. if the operation failed)
diff --git a/com/android/server/stats/StatsCompanionService.java b/com/android/server/stats/StatsCompanionService.java
index fae0b24..5f2ac4f 100644
--- a/com/android/server/stats/StatsCompanionService.java
+++ b/com/android/server/stats/StatsCompanionService.java
@@ -273,7 +273,8 @@
 
         // Add in all the apps for every user/profile.
         for (UserInfo profile : users) {
-            List<PackageInfo> pi = pm.getInstalledPackagesAsUser(0, profile.id);
+            List<PackageInfo> pi =
+                pm.getInstalledPackagesAsUser(PackageManager.MATCH_DISABLED_COMPONENTS, profile.id);
             for (int j = 0; j < pi.size(); j++) {
                 if (pi.get(j).applicationInfo != null) {
                     uids.add(pi.get(j).applicationInfo.uid);
@@ -379,7 +380,7 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             if (DEBUG)
-                Slog.d(TAG, "Time to poll something.");
+                Slog.d(TAG, "Time to trigger periodic alarm.");
             synchronized (sStatsdLock) {
                 if (sStatsd == null) {
                     Slog.w(TAG, "Could not access statsd to inform it of periodic alarm firing.");
@@ -455,13 +456,13 @@
     public void setAlarmForSubscriberTriggering(long timestampMs) {
         enforceCallingPermission();
         if (DEBUG)
-            Slog.d(TAG, "Setting periodic alarm at " + timestampMs);
+            Slog.d(TAG, "Setting periodic alarm in about " +
+                    (timestampMs - SystemClock.elapsedRealtime()));
         final long callingToken = Binder.clearCallingIdentity();
         try {
             // using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will
             // only fire when it awakens.
-            // This alarm is inexact, leaving its exactness completely up to the OS optimizations.
-            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, timestampMs, mPeriodicAlarmIntent);
+            mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, timestampMs, mPeriodicAlarmIntent);
         } finally {
             Binder.restoreCallingIdentity(callingToken);
         }
diff --git a/com/android/server/statusbar/StatusBarManagerService.java b/com/android/server/statusbar/StatusBarManagerService.java
index 8af1101..36fa868 100644
--- a/com/android/server/statusbar/StatusBarManagerService.java
+++ b/com/android/server/statusbar/StatusBarManagerService.java
@@ -23,7 +23,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Rect;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.IBiometricPromptReceiver;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -547,7 +547,7 @@
     }
 
     @Override
-    public void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) {
+    public void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) {
         if (mBar != null) {
             try {
                 mBar.showFingerprintDialog(bundle, receiver);
@@ -1096,6 +1096,30 @@
     }
 
     @Override
+    public void onNotificationSmartRepliesAdded(String key, int replyCount)
+            throws RemoteException {
+        enforceStatusBarService();
+        long identity = Binder.clearCallingIdentity();
+        try {
+            mNotificationDelegate.onNotificationSmartRepliesAdded(key, replyCount);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public void onNotificationSmartReplySent(String key, int replyIndex)
+            throws RemoteException {
+        enforceStatusBarService();
+        long identity = Binder.clearCallingIdentity();
+        try {
+            mNotificationDelegate.onNotificationSmartReplySent(key, replyIndex);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
     public void onNotificationSettingsViewed(String key) throws RemoteException {
         enforceStatusBarService();
         long identity = Binder.clearCallingIdentity();
diff --git a/com/android/server/usb/UsbDebuggingManager.java b/com/android/server/usb/UsbDebuggingManager.java
index 74d8e12..3b08505 100644
--- a/com/android/server/usb/UsbDebuggingManager.java
+++ b/com/android/server/usb/UsbDebuggingManager.java
@@ -461,7 +461,7 @@
         long token = dump.start(idName, id);
 
         dump.write("connected_to_adb", UsbDebuggingManagerProto.CONNECTED_TO_ADB, mThread != null);
-        writeStringIfNotNull(dump, "last_key_received", UsbDebuggingManagerProto.LAST_KEY_RECEVIED,
+        writeStringIfNotNull(dump, "last_key_received", UsbDebuggingManagerProto.LAST_KEY_RECEIVED,
                 mFingerprints);
 
         try {
diff --git a/com/android/server/wifi/ScanRequestProxy.java b/com/android/server/wifi/ScanRequestProxy.java
index d4c9b3e..b9e48ea 100644
--- a/com/android/server/wifi/ScanRequestProxy.java
+++ b/com/android/server/wifi/ScanRequestProxy.java
@@ -34,6 +34,8 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 
 import javax.annotation.concurrent.NotThreadSafe;
@@ -51,15 +53,21 @@
  * c) Will send out the {@link WifiManager#SCAN_RESULTS_AVAILABLE_ACTION} broadcast when new
  * scan results are available.
  * d) Throttle scan requests from non-setting apps:
- *    d.1) For foreground apps, throttle to a max of 1 scan per app every 30 seconds.
- *    d.2) For background apps, throttle to a max of 1 scan from any app every 30 minutes.
+ *  a) Each foreground app can request a max of
+ *   {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS} scan every
+ *   {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS}.
+ *  b) Background apps combined can request 1 scan every
+ *   {@link #SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
  * Note: This class is not thread-safe. It needs to be invoked from WifiStateMachine thread only.
  */
 @NotThreadSafe
 public class ScanRequestProxy {
     private static final String TAG = "WifiScanRequestProxy";
+
     @VisibleForTesting
-    public static final int SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS = 30 * 1000;
+    public static final int SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS = 120 * 1000;
+    @VisibleForTesting
+    public static final int SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS = 4;
     @VisibleForTesting
     public static final int SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS = 30 * 60 * 1000;
 
@@ -81,8 +89,8 @@
     private boolean mIsScanProcessingComplete = true;
     // Timestamps for the last scan requested by any background app.
     private long mLastScanTimestampForBgApps = 0;
-    // Timestamps for the last scan requested by each foreground app.
-    private final ArrayMap<String, Long> mLastScanTimestampsForFgApps = new ArrayMap();
+    // Timestamps for the list of last few scan requests by each foreground app.
+    private final ArrayMap<String, LinkedList<Long>> mLastScanTimestampsForFgApps = new ArrayMap();
     // Scan results cached from the last full single scan request.
     private final List<ScanResult> mLastScanResults = new ArrayList<>();
     // Common scan listener for scan requests.
@@ -223,19 +231,47 @@
         }
     }
 
+    private void trimPastScanRequestTimesForForegroundApp(
+            List<Long> scanRequestTimestamps, long currentTimeMillis) {
+        Iterator<Long> timestampsIter = scanRequestTimestamps.iterator();
+        while (timestampsIter.hasNext()) {
+            Long scanRequestTimeMillis = timestampsIter.next();
+            if ((currentTimeMillis - scanRequestTimeMillis)
+                    > SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS) {
+                timestampsIter.remove();
+            } else {
+                // This list is sorted by timestamps, so we can skip any more checks
+                break;
+            }
+        }
+    }
+
+    private LinkedList<Long> getOrCreateScanRequestTimestampsForForegroundApp(String packageName) {
+        LinkedList<Long> scanRequestTimestamps = mLastScanTimestampsForFgApps.get(packageName);
+        if (scanRequestTimestamps == null) {
+            scanRequestTimestamps = new LinkedList<>();
+            mLastScanTimestampsForFgApps.put(packageName, scanRequestTimestamps);
+        }
+        return scanRequestTimestamps;
+    }
+
     /**
      * Checks if the scan request from the app (specified by packageName) needs
      * to be throttled.
+     * The throttle limit allows a max of {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS}
+     * in {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS} window.
      */
     private boolean shouldScanRequestBeThrottledForForegroundApp(String packageName) {
-        long lastScanMs = mLastScanTimestampsForFgApps.getOrDefault(packageName, 0L);
-        long elapsedRealtime = mClock.getElapsedSinceBootMillis();
-        if (lastScanMs != 0
-                && (elapsedRealtime - lastScanMs) < SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS) {
+        LinkedList<Long> scanRequestTimestamps =
+                getOrCreateScanRequestTimestampsForForegroundApp(packageName);
+        long currentTimeMillis = mClock.getElapsedSinceBootMillis();
+        // First evict old entries from the list.
+        trimPastScanRequestTimesForForegroundApp(scanRequestTimestamps, currentTimeMillis);
+        if (scanRequestTimestamps.size() >= SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS) {
             return true;
         }
         // Proceed with the scan request and record the time.
-        mLastScanTimestampsForFgApps.put(packageName, elapsedRealtime);
+        scanRequestTimestamps.addLast(currentTimeMillis);
         return false;
     }
 
@@ -275,11 +311,6 @@
     /**
      * Checks if the scan request from the app (specified by callingUid & packageName) needs
      * to be throttled.
-     *
-     * a) Each foreground app can request 1 scan every
-     * {@link #SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS}.
-     * b) Background apps combined can request 1 scan every
-     * {@link #SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
      */
     private boolean shouldScanRequestBeThrottledForApp(int callingUid, String packageName) {
         boolean isThrottled;
diff --git a/com/android/server/wifi/ScoredNetworkEvaluator.java b/com/android/server/wifi/ScoredNetworkEvaluator.java
index 223423e..9bb764e 100644
--- a/com/android/server/wifi/ScoredNetworkEvaluator.java
+++ b/com/android/server/wifi/ScoredNetworkEvaluator.java
@@ -114,13 +114,12 @@
         String packageName = mNetworkScoreManager.getActiveScorerPackage();
         if (networkScorerAppData == null || packageName == null) return false;
         int uid = networkScorerAppData.packageUid;
-        boolean allow;
         try {
-            allow = mWifiPermissionsUtil.canAccessScanResults(packageName, uid);
+            mWifiPermissionsUtil.enforceCanAccessScanResults(packageName, uid);
+            return true;
         } catch (SecurityException e) {
-            allow = false;
+            return false;
         }
-        return allow;
     }
 
     @Override
diff --git a/com/android/server/wifi/ScoringParams.java b/com/android/server/wifi/ScoringParams.java
index 0cad225..5b9951f 100644
--- a/com/android/server/wifi/ScoringParams.java
+++ b/com/android/server/wifi/ScoringParams.java
@@ -64,6 +64,12 @@
         public static final int MAX_HORIZON = 60;
         public int horizon = 15;
 
+        /** Number 0-10 influencing requests for network unreachability detection */
+        public static final String KEY_NUD = "nud";
+        public static final int MIN_NUD = 0;
+        public static final int MAX_NUD = 10;
+        public int nud = 8;
+
         Values() {
         }
 
@@ -78,6 +84,7 @@
                 pps[i] = source.pps[i];
             }
             horizon = source.horizon;
+            nud = source.nud;
         }
 
         public void validate() throws IllegalArgumentException {
@@ -85,6 +92,7 @@
             validateRssiArray(rssi5);
             validateOrderedNonNegativeArray(pps);
             validateRange(horizon, MIN_HORIZON, MAX_HORIZON);
+            validateRange(nud, MIN_NUD, MAX_NUD);
         }
 
         private void validateRssiArray(int[] rssi) throws IllegalArgumentException {
@@ -122,6 +130,7 @@
             updateIntArray(rssi5, parser, KEY_RSSI5);
             updateIntArray(pps, parser, KEY_PPS);
             horizon = updateInt(parser, KEY_HORIZON, horizon);
+            nud = updateInt(parser, KEY_NUD, nud);
         }
 
         private int updateInt(KeyValueListParser parser, String key, int defaultValue)
@@ -153,11 +162,12 @@
             appendInts(sb, rssi2);
             appendKey(sb, KEY_RSSI5);
             appendInts(sb, rssi5);
-            //TODO(b/74613347) - leave these out, pending unit test updates
-            // appendKey(sb, KEY_PPS);
-            // appendInts(sb, pps);
+            appendKey(sb, KEY_PPS);
+            appendInts(sb, pps);
             appendKey(sb, KEY_HORIZON);
             sb.append(horizon);
+            appendKey(sb, KEY_NUD);
+            sb.append(nud);
             return sb.toString();
         }
 
@@ -327,6 +337,21 @@
         return mVal.pps[2];
     }
 
+    /**
+     * Returns a number between 0 and 10 inclusive that indicates
+     * how aggressive to be about asking for IP configuration checks
+     * (also known as Network Unreachabilty Detection, or NUD).
+     *
+     * 0 - no nud checks requested by scorer (framework still checks after roam)
+     * 1 - check when score becomes very low
+     *     ...
+     * 10 - check when score first breaches threshold, and again as it gets worse
+     *
+     */
+    public int getNudKnob() {
+        return mVal.nud;
+    }
+
     private static final int MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5000;
 
     private int[] getRssiArray(int frequency) {
diff --git a/com/android/server/wifi/SelfRecovery.java b/com/android/server/wifi/SelfRecovery.java
index d3985f5..c9e95a7 100644
--- a/com/android/server/wifi/SelfRecovery.java
+++ b/com/android/server/wifi/SelfRecovery.java
@@ -38,11 +38,13 @@
      */
     public static final int REASON_LAST_RESORT_WATCHDOG = 0;
     public static final int REASON_WIFINATIVE_FAILURE = 1;
+    public static final int REASON_STA_IFACE_DOWN = 2;
     public static final long MAX_RESTARTS_IN_TIME_WINDOW = 2; // 2 restarts per hour
     public static final long MAX_RESTARTS_TIME_WINDOW_MILLIS = 60 * 60 * 1000; // 1 hour
     protected static final String[] REASON_STRINGS = {
-            "Last Resort Watchdog", // REASON_LAST_RESORT_WATCHDOG
-            "WifiNative Failure"    // REASON_WIFINATIVE_FAILURE
+            "Last Resort Watchdog",  // REASON_LAST_RESORT_WATCHDOG
+            "WifiNative Failure",    // REASON_WIFINATIVE_FAILURE
+            "Sta Interface Down"     // REASON_STA_IFACE_DOWN
     };
 
     private final WifiController mWifiController;
@@ -59,28 +61,39 @@
      * Trigger recovery.
      *
      * This method does the following:
-     * 1. Raises a wtf.
-     * 2. Sends {@link WifiController#CMD_RESTART_WIFI} to {@link WifiController} to initiate the
-     * stack restart.
+     * 1. Checks reason code used to trigger recovery
+     * 2. Checks for sta iface down triggers and disables wifi by sending {@link
+     * WifiController#CMD_RECOVERY_DISABLE_WIFI} to {@link WifiController} to disable wifi.
+     * 3. Throttles restart calls for underlying native failures
+     * 4. Sends {@link WifiController#CMD_RECOVERY_RESTART_WIFI} to {@link WifiController} to
+     * initiate the stack restart.
      * @param reason One of the above |REASON_*| codes.
      */
     public void trigger(int reason) {
-        if (!(reason == REASON_LAST_RESORT_WATCHDOG || reason == REASON_WIFINATIVE_FAILURE)) {
+        if (!(reason == REASON_LAST_RESORT_WATCHDOG || reason == REASON_WIFINATIVE_FAILURE
+                  || reason == REASON_STA_IFACE_DOWN)) {
             Log.e(TAG, "Invalid trigger reason. Ignoring...");
             return;
         }
+        if (reason == REASON_STA_IFACE_DOWN) {
+            Log.e(TAG, "STA interface down, disable wifi");
+            mWifiController.sendMessage(WifiController.CMD_RECOVERY_DISABLE_WIFI);
+            return;
+        }
+
         Log.e(TAG, "Triggering recovery for reason: " + REASON_STRINGS[reason]);
         if (reason == REASON_WIFINATIVE_FAILURE) {
             trimPastRestartTimes();
             // Ensure there haven't been too many restarts within MAX_RESTARTS_TIME_WINDOW
             if (mPastRestartTimes.size() >= MAX_RESTARTS_IN_TIME_WINDOW) {
                 Log.e(TAG, "Already restarted wifi (" + MAX_RESTARTS_IN_TIME_WINDOW + ") times in"
-                        + " last (" + MAX_RESTARTS_TIME_WINDOW_MILLIS + "ms ). Ignoring...");
+                        + " last (" + MAX_RESTARTS_TIME_WINDOW_MILLIS + "ms ). Disabling wifi");
+                mWifiController.sendMessage(WifiController.CMD_RECOVERY_DISABLE_WIFI);
                 return;
             }
             mPastRestartTimes.add(mClock.getElapsedSinceBootMillis());
         }
-        mWifiController.sendMessage(WifiController.CMD_RESTART_WIFI, reason);
+        mWifiController.sendMessage(WifiController.CMD_RECOVERY_RESTART_WIFI, reason);
     }
 
     /**
diff --git a/com/android/server/wifi/VelocityBasedConnectedScore.java b/com/android/server/wifi/VelocityBasedConnectedScore.java
index cb47978..e7d61f7 100644
--- a/com/android/server/wifi/VelocityBasedConnectedScore.java
+++ b/com/android/server/wifi/VelocityBasedConnectedScore.java
@@ -62,6 +62,7 @@
     @Override
     public void reset() {
         mLastMillis = 0;
+        mThresholdAdjustment = 0;
     }
 
     /**
@@ -153,8 +154,9 @@
         if (txSuccessPps < mMinimumPpsForMeasuringSuccess) return;
         if (rxSuccessPps < mMinimumPpsForMeasuringSuccess) return;
         double txBadPps = wifiInfo.txBadRate;
-        double probabilityOfSuccessfulTx = txSuccessPps / (txSuccessPps + txBadPps);
-        if (probabilityOfSuccessfulTx >= 0.2) {
+        double txRetriesPps = wifiInfo.txRetriesRate;
+        double probabilityOfSuccessfulTx = txSuccessPps / (txSuccessPps + txBadPps + txRetriesPps);
+        if (probabilityOfSuccessfulTx > 0.2) {
             // May want this amount to vary with how close to threshold we are
             mThresholdAdjustment -= 0.5;
         }
diff --git a/com/android/server/wifi/WakeupConfigStoreData.java b/com/android/server/wifi/WakeupConfigStoreData.java
index b936a4c..d985677 100644
--- a/com/android/server/wifi/WakeupConfigStoreData.java
+++ b/com/android/server/wifi/WakeupConfigStoreData.java
@@ -39,12 +39,14 @@
     private static final String XML_TAG_FEATURE_STATE_SECTION = "FeatureState";
     private static final String XML_TAG_IS_ACTIVE = "IsActive";
     private static final String XML_TAG_IS_ONBOARDED = "IsOnboarded";
+    private static final String XML_TAG_NOTIFICATIONS_SHOWN = "NotificationsShown";
     private static final String XML_TAG_NETWORK_SECTION = "Network";
     private static final String XML_TAG_SSID = "SSID";
     private static final String XML_TAG_SECURITY = "Security";
 
     private final DataSource<Boolean> mIsActiveDataSource;
     private final DataSource<Boolean> mIsOnboardedDataSource;
+    private final DataSource<Integer> mNotificationsDataSource;
     private final DataSource<Set<ScanResultMatchInfo>> mNetworkDataSource;
     private boolean mHasBeenRead = false;
 
@@ -76,9 +78,11 @@
     public WakeupConfigStoreData(
             DataSource<Boolean> isActiveDataSource,
             DataSource<Boolean> isOnboardedDataSource,
+            DataSource<Integer> notificationsDataSource,
             DataSource<Set<ScanResultMatchInfo>> networkDataSource) {
         mIsActiveDataSource = isActiveDataSource;
         mIsOnboardedDataSource = isOnboardedDataSource;
+        mNotificationsDataSource = notificationsDataSource;
         mNetworkDataSource = networkDataSource;
     }
 
@@ -116,6 +120,8 @@
 
         XmlUtil.writeNextValue(out, XML_TAG_IS_ACTIVE, mIsActiveDataSource.getData());
         XmlUtil.writeNextValue(out, XML_TAG_IS_ONBOARDED, mIsOnboardedDataSource.getData());
+        XmlUtil.writeNextValue(out, XML_TAG_NOTIFICATIONS_SHOWN,
+                mNotificationsDataSource.getData());
 
         XmlUtil.writeNextSectionEnd(out, XML_TAG_FEATURE_STATE_SECTION);
     }
@@ -185,6 +191,7 @@
             throws IOException, XmlPullParserException {
         boolean isActive = false;
         boolean isOnboarded = false;
+        int notificationsShown = 0;
 
         while (!XmlUtil.isNextSectionEnd(in, outerTagDepth)) {
             String[] valueName = new String[1];
@@ -199,6 +206,9 @@
                 case XML_TAG_IS_ONBOARDED:
                     isOnboarded = (Boolean) value;
                     break;
+                case XML_TAG_NOTIFICATIONS_SHOWN:
+                    notificationsShown = (Integer) value;
+                    break;
                 default:
                     throw new XmlPullParserException("Unknown value found: " + valueName[0]);
             }
@@ -206,6 +216,7 @@
 
         mIsActiveDataSource.setData(isActive);
         mIsOnboardedDataSource.setData(isOnboarded);
+        mNotificationsDataSource.setData(notificationsShown);
     }
 
     /**
@@ -248,6 +259,7 @@
             mNetworkDataSource.setData(Collections.emptySet());
             mIsActiveDataSource.setData(false);
             mIsOnboardedDataSource.setData(false);
+            mNotificationsDataSource.setData(0);
         }
     }
 
diff --git a/com/android/server/wifi/WakeupController.java b/com/android/server/wifi/WakeupController.java
index 9743390..2af8ec9 100644
--- a/com/android/server/wifi/WakeupController.java
+++ b/com/android/server/wifi/WakeupController.java
@@ -26,7 +26,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.provider.Settings;
-import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -38,6 +37,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 
 /**
@@ -127,24 +127,30 @@
         mContentObserver = new ContentObserver(mHandler) {
             @Override
             public void onChange(boolean selfChange) {
-                mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting(
-                                mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1;
-                Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled"));
+                readWifiWakeupEnabledFromSettings();
+                mWakeupOnboarding.setOnboarded();
             }
         };
         mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(
                 Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver);
-        mContentObserver.onChange(false /* selfChange */);
+        readWifiWakeupEnabledFromSettings();
 
         // registering the store data here has the effect of reading the persisted value of the
         // data sources after system boot finishes
         mWakeupConfigStoreData = new WakeupConfigStoreData(
                 new IsActiveDataSource(),
-                mWakeupOnboarding.getDataSource(),
+                mWakeupOnboarding.getIsOnboadedDataSource(),
+                mWakeupOnboarding.getNotificationsDataSource(),
                 mWakeupLock.getDataSource());
         wifiConfigStore.registerStoreData(mWakeupConfigStoreData);
     }
 
+    private void readWifiWakeupEnabledFromSettings() {
+        mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting(
+                mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1;
+        Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled"));
+    }
+
     private void setActive(boolean isActive) {
         if (mIsActive != isActive) {
             Log.d(TAG, "Setting active to " + isActive);
@@ -164,9 +170,9 @@
         Log.d(TAG, "start()");
         mWifiInjector.getWifiScanner().registerScanListener(mScanListener);
 
-        // If already active, we don't want to re-initialize the lock, so return early.
+        // If already active, we don't want to restart the session, so return early.
         if (mIsActive) {
-            // TODO record metric for calls to start() when already active
+            mWifiWakeMetrics.recordIgnoredStart();
             return;
         }
         setActive(true);
@@ -175,14 +181,14 @@
         if (isEnabled()) {
             mWakeupOnboarding.maybeShowNotification();
 
-            Set<ScanResultMatchInfo> mostRecentSavedScanResults = getMostRecentSavedScanResults();
-
+            Set<ScanResultMatchInfo> savedNetworksFromLatestScan = getSavedNetworksFromLatestScan();
             if (mVerboseLoggingEnabled) {
-                Log.d(TAG, "Saved networks in most recent scan:" + mostRecentSavedScanResults);
+                Log.d(TAG, "Saved networks in most recent scan:" + savedNetworksFromLatestScan);
             }
 
-            mWifiWakeMetrics.recordStartEvent(mostRecentSavedScanResults.size());
-            mWakeupLock.initialize(mostRecentSavedScanResults);
+            mWifiWakeMetrics.recordStartEvent(savedNetworksFromLatestScan.size());
+            mWakeupLock.setLock(savedNetworksFromLatestScan);
+            // TODO(b/77291248): request low latency scan here
         }
     }
 
@@ -212,18 +218,30 @@
         mWakeupLock.enableVerboseLogging(mVerboseLoggingEnabled);
     }
 
-    /** Returns a list of saved networks from the last full scan. */
-    private Set<ScanResultMatchInfo> getMostRecentSavedScanResults() {
-        Set<ScanResultMatchInfo> goodSavedNetworks = getGoodSavedNetworks();
+    /** Returns a filtered list of saved networks from the last full scan. */
+    private Set<ScanResultMatchInfo> getSavedNetworksFromLatestScan() {
+        Set<ScanResult> filteredScanResults =
+                filterScanResults(mWifiInjector.getWifiScanner().getSingleScanResults());
+        Set<ScanResultMatchInfo> goodMatchInfos  = toMatchInfos(filteredScanResults);
+        goodMatchInfos.retainAll(getGoodSavedNetworks());
 
-        List<ScanResult> scanResults = mWifiInjector.getWifiScanner().getSingleScanResults();
-        Set<ScanResultMatchInfo> lastSeenNetworks = new HashSet<>(scanResults.size());
-        for (ScanResult scanResult : scanResults) {
-            lastSeenNetworks.add(ScanResultMatchInfo.fromScanResult(scanResult));
+        return goodMatchInfos;
+    }
+
+    /** Returns a set of ScanResults with all DFS channels removed. */
+    private Set<ScanResult> filterScanResults(Collection<ScanResult> scanResults) {
+        int[] dfsChannels = mWifiInjector.getWifiNative()
+                .getChannelsForBand(WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY);
+        if (dfsChannels == null) {
+            dfsChannels = new int[0];
         }
 
-        lastSeenNetworks.retainAll(goodSavedNetworks);
-        return lastSeenNetworks;
+        final Set<Integer> dfsChannelSet = Arrays.stream(dfsChannels).boxed()
+                .collect(Collectors.toSet());
+
+        return scanResults.stream()
+                .filter(scanResult -> !dfsChannelSet.contains(scanResult.frequency))
+                .collect(Collectors.toSet());
     }
 
     /** Returns a filtered list of saved networks from WifiConfigManager. */
@@ -245,16 +263,18 @@
     }
 
     //TODO(b/69271702) implement WAN filtering
-    private boolean isWideAreaNetwork(WifiConfiguration wifiConfiguration) {
+    private static boolean isWideAreaNetwork(WifiConfiguration config) {
         return false;
     }
 
     /**
      * Handles incoming scan results.
      *
-     * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is
-     * empty, it evaluates scan results for a match with saved networks. If a match exists, it
-     * enables wifi.
+     * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is not
+     * yet fully initialized, it adds the current scanResults to the lock and returns. If WakeupLock
+     * is initialized but not empty, the controller updates the lock with the current scan. If it is
+     * both initialized and empty, it evaluates scan results for a match with saved networks. If a
+     * match exists, it enables wifi.
      *
      * <p>The feature must be enabled and the store data must be loaded in order for the controller
      * to handle scan results.
@@ -269,41 +289,22 @@
 
         // only count scan as handled if isEnabled
         mNumScansHandled++;
-
         if (mVerboseLoggingEnabled) {
-            Log.d(TAG, "Incoming scan. Total scans handled: " + mNumScansHandled);
-            Log.d(TAG, "ScanResults: " + scanResults);
+            Log.d(TAG, "Incoming scan #" + mNumScansHandled);
         }
 
-        // need to show notification here in case user enables Wifi Wake when Wifi is off
+        // need to show notification here in case user turns phone on while wifi is off
         mWakeupOnboarding.maybeShowNotification();
-        if (!mWakeupOnboarding.isOnboarded()) {
+
+        Set<ScanResult> filteredScanResults = filterScanResults(scanResults);
+
+        mWakeupLock.update(toMatchInfos(filteredScanResults));
+        if (!mWakeupLock.isUnlocked()) {
             return;
         }
 
-        // only update the wakeup lock if it's not already empty
-        if (!mWakeupLock.isEmpty()) {
-            if (mVerboseLoggingEnabled) {
-                Log.d(TAG, "WakeupLock not empty. Updating.");
-            }
-
-            Set<ScanResultMatchInfo> networks = new ArraySet<>();
-            for (ScanResult scanResult : scanResults) {
-                networks.add(ScanResultMatchInfo.fromScanResult(scanResult));
-            }
-            mWakeupLock.update(networks);
-
-            // if wakeup lock is still not empty, return
-            if (!mWakeupLock.isEmpty()) {
-                return;
-            }
-
-            Log.d(TAG, "WakeupLock emptied");
-            mWifiWakeMetrics.recordUnlockEvent(mNumScansHandled);
-        }
-
         ScanResult network =
-                mWakeupEvaluator.findViableNetwork(scanResults, getGoodSavedNetworks());
+                mWakeupEvaluator.findViableNetwork(filteredScanResults, getGoodSavedNetworks());
 
         if (network != null) {
             Log.d(TAG, "Enabling wifi for network: " + network.SSID);
@@ -312,6 +313,15 @@
     }
 
     /**
+     * Converts ScanResults to ScanResultMatchInfos.
+     */
+    private static Set<ScanResultMatchInfo> toMatchInfos(Collection<ScanResult> scanResults) {
+        return scanResults.stream()
+                .map(ScanResultMatchInfo::fromScanResult)
+                .collect(Collectors.toSet());
+    }
+
+    /**
      * Enables wifi.
      *
      * <p>This method ignores all checks and assumes that {@link WifiStateMachine} is currently
diff --git a/com/android/server/wifi/WakeupLock.java b/com/android/server/wifi/WakeupLock.java
index 9e617a4..c6a8f5a 100644
--- a/com/android/server/wifi/WakeupLock.java
+++ b/com/android/server/wifi/WakeupLock.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wifi;
 
+import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 
@@ -29,7 +30,7 @@
 import java.util.Set;
 
 /**
- * A lock to determine whether Auto Wifi can re-enable Wifi.
+ * A lock to determine whether Wifi Wake can re-enable Wifi.
  *
  * <p>Wakeuplock manages a list of networks to determine whether the device's location has changed.
  */
@@ -39,44 +40,147 @@
 
     @VisibleForTesting
     static final int CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT = 3;
-
+    @VisibleForTesting
+    static final long MAX_LOCK_TIME_MILLIS = 10 * DateUtils.MINUTE_IN_MILLIS;
 
     private final WifiConfigManager mWifiConfigManager;
     private final Map<ScanResultMatchInfo, Integer> mLockedNetworks = new ArrayMap<>();
-    private boolean mVerboseLoggingEnabled;
+    private final WifiWakeMetrics mWifiWakeMetrics;
+    private final Clock mClock;
 
-    public WakeupLock(WifiConfigManager wifiConfigManager) {
+    private boolean mVerboseLoggingEnabled;
+    private long mLockTimestamp;
+    private boolean mIsInitialized;
+    private int mNumScans;
+
+    public WakeupLock(WifiConfigManager wifiConfigManager, WifiWakeMetrics wifiWakeMetrics,
+                      Clock clock) {
         mWifiConfigManager = wifiConfigManager;
+        mWifiWakeMetrics = wifiWakeMetrics;
+        mClock = clock;
     }
 
     /**
-     * Initializes the WakeupLock with the given {@link ScanResultMatchInfo} list.
+     * Sets the WakeupLock with the given {@link ScanResultMatchInfo} list.
      *
-     * <p>This saves the wakeup lock to the store.
+     * <p>This saves the wakeup lock to the store and begins the initialization process.
      *
      * @param scanResultList list of ScanResultMatchInfos to start the lock with
      */
-    public void initialize(Collection<ScanResultMatchInfo> scanResultList) {
+    public void setLock(Collection<ScanResultMatchInfo> scanResultList) {
+        mLockTimestamp = mClock.getElapsedSinceBootMillis();
+        mIsInitialized = false;
+        mNumScans = 0;
+
         mLockedNetworks.clear();
         for (ScanResultMatchInfo scanResultMatchInfo : scanResultList) {
             mLockedNetworks.put(scanResultMatchInfo, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
         }
 
-        Log.d(TAG, "Lock initialized. Number of networks: " + mLockedNetworks.size());
+        Log.d(TAG, "Lock set. Number of networks: " + mLockedNetworks.size());
 
         mWifiConfigManager.saveToStore(false /* forceWrite */);
     }
 
     /**
-     * Updates the lock with the given {@link ScanResultMatchInfo} list.
+     * Maybe sets the WakeupLock as initialized based on total scans handled.
+     *
+     * @param numScans total number of elapsed scans in the current WifiWake session
+     */
+    private void maybeSetInitializedByScans(int numScans) {
+        if (mIsInitialized) {
+            return;
+        }
+        boolean shouldBeInitialized = numScans >= CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT;
+        if (shouldBeInitialized) {
+            mIsInitialized = true;
+
+            Log.d(TAG, "Lock initialized by handled scans. Scans: " + numScans);
+            if (mVerboseLoggingEnabled) {
+                Log.d(TAG, "State of lock: " + mLockedNetworks);
+            }
+
+            // log initialize event
+            mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size());
+        }
+    }
+
+    /**
+     * Maybe sets the WakeupLock as initialized based on elapsed time.
+     *
+     * @param timestampMillis current timestamp
+     */
+    private void maybeSetInitializedByTimeout(long timestampMillis) {
+        if (mIsInitialized) {
+            return;
+        }
+        long elapsedTime = timestampMillis - mLockTimestamp;
+        boolean shouldBeInitialized = elapsedTime > MAX_LOCK_TIME_MILLIS;
+
+        if (shouldBeInitialized) {
+            mIsInitialized = true;
+
+            Log.d(TAG, "Lock initialized by timeout. Elapsed time: " + elapsedTime);
+            if (mNumScans == 0) {
+                Log.w(TAG, "Lock initialized with 0 handled scans!");
+            }
+            if (mVerboseLoggingEnabled) {
+                Log.d(TAG, "State of lock: " + mLockedNetworks);
+            }
+
+            // log initialize event
+            mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size());
+        }
+    }
+
+    /** Returns whether the lock has been fully initialized. */
+    public boolean isInitialized() {
+        return mIsInitialized;
+    }
+
+    /**
+     * Adds the given networks to the lock.
+     *
+     * <p>This is called during the initialization step.
+     *
+     * @param networkList The list of networks to be added
+     */
+    private void addToLock(Collection<ScanResultMatchInfo> networkList) {
+        if (mVerboseLoggingEnabled) {
+            Log.d(TAG, "Initializing lock with networks: " + networkList);
+        }
+
+        boolean hasChanged = false;
+
+        for (ScanResultMatchInfo network : networkList) {
+            if (!mLockedNetworks.containsKey(network)) {
+                mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
+                hasChanged = true;
+            }
+        }
+
+        if (hasChanged) {
+            mWifiConfigManager.saveToStore(false /* forceWrite */);
+        }
+
+        // Set initialized if the lock has handled enough scans, and log the event
+        maybeSetInitializedByScans(mNumScans);
+    }
+
+    /**
+     * Removes networks from the lock if not present in the given {@link ScanResultMatchInfo} list.
      *
      * <p>If a network in the lock is not present in the list, reduce the number of scans
      * required to evict by one. Remove any entries in the list with 0 scans required to evict. If
      * any entries in the lock are removed, the store is updated.
      *
-     * @param scanResultList list of present ScanResultMatchInfos to update the lock with
+     * @param networkList list of present ScanResultMatchInfos to update the lock with
      */
-    public void update(Collection<ScanResultMatchInfo> scanResultList) {
+    private void removeFromLock(Collection<ScanResultMatchInfo> networkList) {
+        if (mVerboseLoggingEnabled) {
+            Log.d(TAG, "Filtering lock with networks: " + networkList);
+        }
+
         boolean hasChanged = false;
         Iterator<Map.Entry<ScanResultMatchInfo, Integer>> it =
                 mLockedNetworks.entrySet().iterator();
@@ -84,7 +188,7 @@
             Map.Entry<ScanResultMatchInfo, Integer> entry = it.next();
 
             // if present in scan list, reset to max
-            if (scanResultList.contains(entry.getKey())) {
+            if (networkList.contains(entry.getKey())) {
                 if (mVerboseLoggingEnabled) {
                     Log.d(TAG, "Found network in lock: " + entry.getKey().networkSsid);
                 }
@@ -104,13 +208,48 @@
         if (hasChanged) {
             mWifiConfigManager.saveToStore(false /* forceWrite */);
         }
+
+        if (isUnlocked()) {
+            Log.d(TAG, "Lock emptied. Recording unlock event.");
+            mWifiWakeMetrics.recordUnlockEvent(mNumScans);
+        }
     }
 
     /**
-     * Returns whether the internal network set is empty.
+     * Updates the lock with the given {@link ScanResultMatchInfo} list.
+     *
+     * <p>Based on the current initialization state of the lock, either adds or removes networks
+     * from the lock.
+     *
+     * <p>The lock is initialized after {@link #CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT}
+     * scans have been handled, or after {@link #MAX_LOCK_TIME_MILLIS} milliseconds have elapsed
+     * since {@link #setLock(Collection)}.
+     *
+     * @param networkList list of present ScanResultMatchInfos to update the lock with
      */
-    public boolean isEmpty() {
-        return mLockedNetworks.isEmpty();
+    public void update(Collection<ScanResultMatchInfo> networkList) {
+        // update is no-op if already unlocked
+        if (isUnlocked()) {
+            return;
+        }
+        // Before checking handling the scan, we check to see whether we've exceeded the maximum
+        // time allowed for initialization. If so, we set initialized and treat this scan as a
+        // "removeFromLock()" instead of an "addToLock()".
+        maybeSetInitializedByTimeout(mClock.getElapsedSinceBootMillis());
+
+        mNumScans++;
+
+        // add or remove networks based on initialized status
+        if (mIsInitialized) {
+            removeFromLock(networkList);
+        } else {
+            addToLock(networkList);
+        }
+    }
+
+    /** Returns whether the WakeupLock is unlocked */
+    public boolean isUnlocked() {
+        return mIsInitialized && mLockedNetworks.isEmpty();
     }
 
     /** Returns the data source for the WakeupLock config store data. */
@@ -121,6 +260,8 @@
     /** Dumps wakeup lock contents. */
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("WakeupLock: ");
+        pw.println("mNumScans: " + mNumScans);
+        pw.println("mIsInitialized: " + mIsInitialized);
         pw.println("Locked networks: " + mLockedNetworks.size());
         for (Map.Entry<ScanResultMatchInfo, Integer> entry : mLockedNetworks.entrySet()) {
             pw.println(entry.getKey() + ", scans to evict: " + entry.getValue());
@@ -146,7 +287,8 @@
             for (ScanResultMatchInfo network : data) {
                 mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
             }
-
+            // lock is considered initialized if loaded from store
+            mIsInitialized = true;
         }
     }
 }
diff --git a/com/android/server/wifi/WakeupNotificationFactory.java b/com/android/server/wifi/WakeupNotificationFactory.java
index 42ae467..23f31a7 100644
--- a/com/android/server/wifi/WakeupNotificationFactory.java
+++ b/com/android/server/wifi/WakeupNotificationFactory.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 
 import com.android.internal.R;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 
 
@@ -37,6 +38,9 @@
     public static final String ACTION_TURN_OFF_WIFI_WAKE =
             "com.android.server.wifi.wakeup.TURN_OFF_WIFI_WAKE";
 
+    /** Notification channel ID for onboarding messages. */
+    public static final int ONBOARD_ID = SystemMessage.NOTE_WIFI_WAKE_ONBOARD;
+
     private final Context mContext;
     private final FrameworkFacade mFrameworkFacade;
 
diff --git a/com/android/server/wifi/WakeupOnboarding.java b/com/android/server/wifi/WakeupOnboarding.java
index d4caa0f..b6bcbc3 100644
--- a/com/android/server/wifi/WakeupOnboarding.java
+++ b/com/android/server/wifi/WakeupOnboarding.java
@@ -27,22 +27,31 @@
 import android.content.IntentFilter;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.SystemClock;
 import android.provider.Settings;
+import android.text.format.DateUtils;
 import android.util.Log;
 
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.annotations.VisibleForTesting;
 
 /**
  * Manages the WiFi Wake onboarding notification.
  *
  * <p>If a user disables wifi with Wifi Wake enabled, this notification is shown to explain that
- * wifi may turn back on automatically. Wifi will not automatically turn back on until after the
- * user interacts with the onboarding notification in some way (e.g. dismiss, tap).
+ * wifi may turn back on automatically. It will be displayed up to 3 times, or until the
+ * user either interacts with the onboarding notification in some way (e.g. dismiss, tap) or
+ * manually enables/disables the feature in WifiSettings.
  */
 public class WakeupOnboarding {
 
     private static final String TAG = "WakeupOnboarding";
 
+    @VisibleForTesting
+    static final int NOTIFICATIONS_UNTIL_ONBOARDED = 3;
+    @VisibleForTesting
+    static final long REQUIRED_NOTIFICATION_DELAY = DateUtils.DAY_IN_MILLIS;
+    private static final long NOT_SHOWN_TIMESTAMP = -1;
+
     private final Context mContext;
     private final WakeupNotificationFactory mWakeupNotificationFactory;
     private NotificationManager mNotificationManager;
@@ -52,6 +61,8 @@
     private final FrameworkFacade mFrameworkFacade;
 
     private boolean mIsOnboarded;
+    private int mTotalNotificationsShown;
+    private long mLastShownTimestamp = NOT_SHOWN_TIMESTAMP;
     private boolean mIsNotificationShowing;
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -104,17 +115,46 @@
 
     /** Shows the onboarding notification if applicable. */
     public void maybeShowNotification() {
-        if (isOnboarded() || mIsNotificationShowing) {
+        maybeShowNotification(SystemClock.elapsedRealtime());
+    }
+
+    @VisibleForTesting
+    void maybeShowNotification(long timestamp) {
+        if (!shouldShowNotification(timestamp)) {
             return;
         }
-
         Log.d(TAG, "Showing onboarding notification.");
 
+        incrementTotalNotificationsShown();
+        mIsNotificationShowing = true;
+        mLastShownTimestamp = timestamp;
+
         mContext.registerReceiver(mBroadcastReceiver, mIntentFilter,
                 null /* broadcastPermission */, mHandler);
-        getNotificationManager().notify(SystemMessage.NOTE_WIFI_WAKE_ONBOARD,
+        getNotificationManager().notify(WakeupNotificationFactory.ONBOARD_ID,
                 mWakeupNotificationFactory.createOnboardingNotification());
-        mIsNotificationShowing = true;
+    }
+
+    /**
+     * Increment the total number of shown notifications and onboard the user if reached the
+     * required amount.
+     */
+    private void incrementTotalNotificationsShown() {
+        mTotalNotificationsShown++;
+        if (mTotalNotificationsShown >= NOTIFICATIONS_UNTIL_ONBOARDED) {
+            setOnboarded();
+        } else {
+            mWifiConfigManager.saveToStore(false /* forceWrite */);
+        }
+    }
+
+    private boolean shouldShowNotification(long timestamp) {
+        if (isOnboarded() || mIsNotificationShowing) {
+            return false;
+        }
+
+        return mLastShownTimestamp == NOT_SHOWN_TIMESTAMP
+                || (timestamp - mLastShownTimestamp) > REQUIRED_NOTIFICATION_DELAY;
     }
 
     /** Handles onboarding cleanup on stop. */
@@ -132,11 +172,15 @@
         }
 
         mContext.unregisterReceiver(mBroadcastReceiver);
-        getNotificationManager().cancel(SystemMessage.NOTE_WIFI_WAKE_ONBOARD);
+        getNotificationManager().cancel(WakeupNotificationFactory.ONBOARD_ID);
         mIsNotificationShowing = false;
     }
 
-    private void setOnboarded() {
+    /** Sets the user as onboarded and persists to store. */
+    public void setOnboarded() {
+        if (mIsOnboarded) {
+            return;
+        }
         Log.d(TAG, "Setting user as onboarded.");
         mIsOnboarded = true;
         mWifiConfigManager.saveToStore(false /* forceWrite */);
@@ -150,12 +194,17 @@
         return mNotificationManager;
     }
 
-    /** Returns the {@link WakeupConfigStoreData.DataSource} for the {@link WifiConfigStore}. */
-    public WakeupConfigStoreData.DataSource<Boolean> getDataSource() {
-        return new OnboardingDataSource();
+    /** Returns the {@link WakeupConfigStoreData.DataSource} for the onboarded status. */
+    public WakeupConfigStoreData.DataSource<Boolean> getIsOnboadedDataSource() {
+        return new IsOnboardedDataSource();
     }
 
-    private class OnboardingDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
+    /** Returns the {@link WakeupConfigStoreData.DataSource} for the notification status. */
+    public WakeupConfigStoreData.DataSource<Integer> getNotificationsDataSource() {
+        return new NotificationsDataSource();
+    }
+
+    private class IsOnboardedDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
 
         @Override
         public Boolean getData() {
@@ -167,4 +216,17 @@
             mIsOnboarded = data;
         }
     }
+
+    private class NotificationsDataSource implements WakeupConfigStoreData.DataSource<Integer> {
+
+        @Override
+        public Integer getData() {
+            return mTotalNotificationsShown;
+        }
+
+        @Override
+        public void setData(Integer data) {
+            mTotalNotificationsShown = data;
+        }
+    }
 }
diff --git a/com/android/server/wifi/WifiConfigStoreLegacy.java b/com/android/server/wifi/WifiConfigStoreLegacy.java
index 184ee2f..ef6d82f 100644
--- a/com/android/server/wifi/WifiConfigStoreLegacy.java
+++ b/com/android/server/wifi/WifiConfigStoreLegacy.java
@@ -81,16 +81,28 @@
      */
     private final WifiNetworkHistory mWifiNetworkHistory;
     private final WifiNative mWifiNative;
-    private final IpConfigStore mIpconfigStore;
+    private final IpConfigStoreWrapper mIpconfigStoreWrapper;
 
     private final LegacyPasspointConfigParser mPasspointConfigParser;
 
+    /**
+     * Used to help mocking the static methods of IpconfigStore.
+     */
+    public static class IpConfigStoreWrapper {
+        /**
+         * Read IP configurations from Ip config store.
+         */
+        public SparseArray<IpConfiguration> readIpAndProxyConfigurations(String filePath) {
+            return IpConfigStore.readIpAndProxyConfigurations(filePath);
+        }
+    }
+
     WifiConfigStoreLegacy(WifiNetworkHistory wifiNetworkHistory,
-            WifiNative wifiNative, IpConfigStore ipConfigStore,
+            WifiNative wifiNative, IpConfigStoreWrapper ipConfigStore,
             LegacyPasspointConfigParser passpointConfigParser) {
         mWifiNetworkHistory = wifiNetworkHistory;
         mWifiNative = wifiNative;
-        mIpconfigStore = ipConfigStore;
+        mIpconfigStoreWrapper = ipConfigStore;
         mPasspointConfigParser = passpointConfigParser;
     }
 
@@ -105,7 +117,7 @@
     private static WifiConfiguration lookupWifiConfigurationUsingConfigKeyHash(
             Map<String, WifiConfiguration> configurationMap, int hashCode) {
         for (Map.Entry<String, WifiConfiguration> entry : configurationMap.entrySet()) {
-            if (entry.getKey().hashCode() == hashCode) {
+            if (entry.getKey() != null && entry.getKey().hashCode() == hashCode) {
                 return entry.getValue();
             }
         }
@@ -122,7 +134,8 @@
         // This is a map of the hash code of the network's configKey to the corresponding
         // IpConfiguration.
         SparseArray<IpConfiguration> ipConfigurations =
-                mIpconfigStore.readIpAndProxyConfigurations(IP_CONFIG_FILE.getAbsolutePath());
+                mIpconfigStoreWrapper.readIpAndProxyConfigurations(
+                        IP_CONFIG_FILE.getAbsolutePath());
         if (ipConfigurations == null || ipConfigurations.size() == 0) {
             Log.w(TAG, "No ip configurations found in ipconfig store");
             return;
diff --git a/com/android/server/wifi/WifiController.java b/com/android/server/wifi/WifiController.java
index 5cc0306..76b44c8 100644
--- a/com/android/server/wifi/WifiController.java
+++ b/com/android/server/wifi/WifiController.java
@@ -76,21 +76,23 @@
 
     private static final int BASE = Protocol.BASE_WIFI_CONTROLLER;
 
-    static final int CMD_EMERGENCY_MODE_CHANGED        = BASE + 1;
-    static final int CMD_SCAN_ALWAYS_MODE_CHANGED      = BASE + 7;
-    static final int CMD_WIFI_TOGGLED                  = BASE + 8;
-    static final int CMD_AIRPLANE_TOGGLED              = BASE + 9;
-    static final int CMD_SET_AP                        = BASE + 10;
-    static final int CMD_DEFERRED_TOGGLE               = BASE + 11;
-    static final int CMD_USER_PRESENT                  = BASE + 12;
-    static final int CMD_AP_START_FAILURE              = BASE + 13;
-    static final int CMD_EMERGENCY_CALL_STATE_CHANGED  = BASE + 14;
-    static final int CMD_AP_STOPPED                    = BASE + 15;
-    static final int CMD_STA_START_FAILURE             = BASE + 16;
+    static final int CMD_EMERGENCY_MODE_CHANGED                 = BASE + 1;
+    static final int CMD_SCAN_ALWAYS_MODE_CHANGED               = BASE + 7;
+    static final int CMD_WIFI_TOGGLED                           = BASE + 8;
+    static final int CMD_AIRPLANE_TOGGLED                       = BASE + 9;
+    static final int CMD_SET_AP                                 = BASE + 10;
+    static final int CMD_DEFERRED_TOGGLE                        = BASE + 11;
+    static final int CMD_USER_PRESENT                           = BASE + 12;
+    static final int CMD_AP_START_FAILURE                       = BASE + 13;
+    static final int CMD_EMERGENCY_CALL_STATE_CHANGED           = BASE + 14;
+    static final int CMD_AP_STOPPED                             = BASE + 15;
+    static final int CMD_STA_START_FAILURE                      = BASE + 16;
     // Command used to trigger a wifi stack restart when in active mode
-    static final int CMD_RESTART_WIFI                  = BASE + 17;
+    static final int CMD_RECOVERY_RESTART_WIFI                  = BASE + 17;
     // Internal command used to complete wifi stack restart
-    private static final int CMD_RESTART_WIFI_CONTINUE = BASE + 18;
+    private static final int CMD_RECOVERY_RESTART_WIFI_CONTINUE = BASE + 18;
+    // Command to disable wifi when SelfRecovery is throttled or otherwise not doing full recovery
+    static final int CMD_RECOVERY_DISABLE_WIFI                  = BASE + 19;
 
     private DefaultState mDefaultState = new DefaultState();
     private StaEnabledState mStaEnabledState = new StaEnabledState();
@@ -215,8 +217,9 @@
                 case CMD_AP_START_FAILURE:
                 case CMD_AP_STOPPED:
                 case CMD_STA_START_FAILURE:
-                case CMD_RESTART_WIFI:
-                case CMD_RESTART_WIFI_CONTINUE:
+                case CMD_RECOVERY_RESTART_WIFI:
+                case CMD_RECOVERY_RESTART_WIFI_CONTINUE:
+                case CMD_RECOVERY_DISABLE_WIFI:
                     break;
                 case CMD_USER_PRESENT:
                     mFirstUserSignOnSeen = true;
@@ -297,7 +300,7 @@
                     log("DEFERRED_TOGGLE handled");
                     sendMessage((Message)(msg.obj));
                     break;
-                case CMD_RESTART_WIFI_CONTINUE:
+                case CMD_RECOVERY_RESTART_WIFI_CONTINUE:
                     transitionTo(mDeviceActiveState);
                     break;
                 default:
@@ -649,23 +652,27 @@
                 }
                 mFirstUserSignOnSeen = true;
                 return HANDLED;
-            } else if (msg.what == CMD_RESTART_WIFI) {
-                final String bugTitle = "Wi-Fi BugReport";
+            } else if (msg.what == CMD_RECOVERY_RESTART_WIFI) {
+                final String bugTitle;
                 final String bugDetail;
-                if (msg.obj != null && msg.arg1 < SelfRecovery.REASON_STRINGS.length
-                        && msg.arg1 >= 0) {
+                if (msg.arg1 < SelfRecovery.REASON_STRINGS.length && msg.arg1 >= 0) {
                     bugDetail = SelfRecovery.REASON_STRINGS[msg.arg1];
+                    bugTitle = "Wi-Fi BugReport: " + bugDetail;
                 } else {
                     bugDetail = "";
+                    bugTitle = "Wi-Fi BugReport";
                 }
                 if (msg.arg1 != SelfRecovery.REASON_LAST_RESORT_WATCHDOG) {
                     (new Handler(mWifiStateMachineLooper)).post(() -> {
                         mWifiStateMachine.takeBugReport(bugTitle, bugDetail);
                     });
                 }
-                deferMessage(obtainMessage(CMD_RESTART_WIFI_CONTINUE));
+                deferMessage(obtainMessage(CMD_RECOVERY_RESTART_WIFI_CONTINUE));
                 transitionTo(mApStaDisabledState);
                 return HANDLED;
+            } else if (msg.what == CMD_RECOVERY_DISABLE_WIFI) {
+                loge("Recovery has been throttled, disable wifi");
+                transitionTo(mApStaDisabledState);
             }
             return NOT_HANDLED;
         }
diff --git a/com/android/server/wifi/WifiInjector.java b/com/android/server/wifi/WifiInjector.java
index 04bb3f4..a1ecca2 100644
--- a/com/android/server/wifi/WifiInjector.java
+++ b/com/android/server/wifi/WifiInjector.java
@@ -189,7 +189,7 @@
                 ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
         mWifiNative = new WifiNative(
                 mWifiVendorHal, mSupplicantStaIfaceHal, mHostapdHal, mWificondControl,
-                mNwManagementService, mPropertyService, mWifiMetrics);
+                mWifiMonitor, mNwManagementService, mPropertyService, mWifiMetrics);
         mWifiP2pMonitor = new WifiP2pMonitor(this);
         mSupplicantP2pIfaceHal = new SupplicantP2pIfaceHal(mWifiP2pMonitor);
         mWifiP2pNative = new WifiP2pNative(mSupplicantP2pIfaceHal, mHalDeviceManager);
@@ -214,7 +214,7 @@
         mWifiNetworkHistory = new WifiNetworkHistory(mContext, writer);
         mIpConfigStore = new IpConfigStore(writer);
         mWifiConfigStoreLegacy = new WifiConfigStoreLegacy(
-                mWifiNetworkHistory, mWifiNative, mIpConfigStore,
+                mWifiNetworkHistory, mWifiNative, new WifiConfigStoreLegacy.IpConfigStoreWrapper(),
                 new LegacyPasspointConfigParser());
         // Config Manager
         mWifiConfigManager = new WifiConfigManager(mContext, mClock,
@@ -273,7 +273,8 @@
                 mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade,
                 wakeupNotificationFactory);
         mWakeupController = new WakeupController(mContext,
-                mWifiStateMachineHandlerThread.getLooper(), new WakeupLock(mWifiConfigManager),
+                mWifiStateMachineHandlerThread.getLooper(),
+                new WakeupLock(mWifiConfigManager, mWifiMetrics.getWakeupMetrics(), mClock),
                 WakeupEvaluator.fromContext(mContext), wakeupOnboarding, mWifiConfigManager,
                 mWifiConfigStore, mWifiMetrics.getWakeupMetrics(), this, mFrameworkFacade);
         mLockManager = new WifiLockManager(mContext, BatteryStatsService.getService());
diff --git a/com/android/server/wifi/WifiMetrics.java b/com/android/server/wifi/WifiMetrics.java
index 669eff6..6660597 100644
--- a/com/android/server/wifi/WifiMetrics.java
+++ b/com/android/server/wifi/WifiMetrics.java
@@ -287,6 +287,8 @@
         public static final int FAILURE_ROAM_TIMEOUT = 9;
         // DHCP failure
         public static final int FAILURE_DHCP = 10;
+        // ASSOCIATION_TIMED_OUT
+        public static final int FAILURE_ASSOCIATION_TIMED_OUT = 11;
 
         RouterFingerPrint mRouterFingerPrint;
         private long mRealStartTime;
@@ -375,6 +377,10 @@
                         break;
                     case FAILURE_DHCP:
                         sb.append("DHCP");
+                        break;
+                    case FAILURE_ASSOCIATION_TIMED_OUT:
+                        sb.append("ASSOCIATION_TIMED_OUT");
+                        break;
                     default:
                         sb.append("UNKNOWN");
                         break;
diff --git a/com/android/server/wifi/WifiNative.java b/com/android/server/wifi/WifiNative.java
index aebb236..474a897 100644
--- a/com/android/server/wifi/WifiNative.java
+++ b/com/android/server/wifi/WifiNative.java
@@ -74,21 +74,22 @@
     private final HostapdHal mHostapdHal;
     private final WifiVendorHal mWifiVendorHal;
     private final WificondControl mWificondControl;
+    private final WifiMonitor mWifiMonitor;
     private final INetworkManagementService mNwManagementService;
     private final PropertyService mPropertyService;
     private final WifiMetrics mWifiMetrics;
     private boolean mVerboseLoggingEnabled = false;
 
-    // TODO(b/69426063): Remove interfaceName from constructor once WifiStateMachine switches over
-    // to the new interface management methods.
     public WifiNative(WifiVendorHal vendorHal,
                       SupplicantStaIfaceHal staIfaceHal, HostapdHal hostapdHal,
-                      WificondControl condControl, INetworkManagementService nwService,
+                      WificondControl condControl, WifiMonitor wifiMonitor,
+                      INetworkManagementService nwService,
                       PropertyService propertyService, WifiMetrics wifiMetrics) {
         mWifiVendorHal = vendorHal;
         mSupplicantStaIfaceHal = staIfaceHal;
         mHostapdHal = hostapdHal;
         mWificondControl = condControl;
+        mWifiMonitor = wifiMonitor;
         mNwManagementService = nwService;
         mPropertyService = propertyService;
         mWifiMetrics = wifiMetrics;
@@ -391,6 +392,7 @@
     /** Helper method invoked to teardown client iface and perform necessary cleanup */
     private void onClientInterfaceDestroyed(@NonNull Iface iface) {
         synchronized (mLock) {
+            mWifiMonitor.stopMonitoring(iface.name);
             if (!unregisterNetworkObserver(iface.networkObserver)) {
                 Log.e(TAG, "Failed to unregister network observer on " + iface);
             }
@@ -840,6 +842,7 @@
                 teardownInterface(iface.name);
                 return null;
             }
+            mWifiMonitor.startMonitoring(iface.name);
             // Just to avoid any race conditions with interface state change callbacks,
             // update the interface state before we exit.
             onInterfaceStateChanged(iface, isInterfaceUp(iface.name));
diff --git a/com/android/server/wifi/WifiScoreReport.java b/com/android/server/wifi/WifiScoreReport.java
index 32529f8..c6a7105 100644
--- a/com/android/server/wifi/WifiScoreReport.java
+++ b/com/android/server/wifi/WifiScoreReport.java
@@ -39,9 +39,8 @@
     private boolean mVerboseLoggingEnabled = false;
     private static final long FIRST_REASONABLE_WALL_CLOCK = 1490000000000L; // mid-December 2016
 
-    // Cache of the last score report.
-    private String mReport;
-    private boolean mReportValid = false;
+    // Cache of the last score
+    private int mScore = NetworkAgent.WIFI_BASE_SCORE;
 
     private final ScoringParams mScoringParams;
     private final Clock mClock;
@@ -58,39 +57,18 @@
     }
 
     /**
-     * Method returning the String representation of the last score report.
-     *
-     *  @return String score report
-     */
-    public String getLastReport() {
-        return mReport;
-    }
-
-    /**
      * Reset the last calculated score.
      */
     public void reset() {
-        mReport = "";
-        if (mReportValid) {
-            mSessionNumber++;
-            mReportValid = false;
-        }
+        mSessionNumber++;
+        mScore = NetworkAgent.WIFI_BASE_SCORE;
+        mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE;
         mAggressiveConnectedScore.reset();
         mVelocityBasedConnectedScore.reset();
         if (mVerboseLoggingEnabled) Log.d(TAG, "reset");
     }
 
     /**
-     * Checks if the last report data is valid or not. This will be cleared when {@link #reset()} is
-     * invoked.
-     *
-     * @return true if valid, false otherwise.
-     */
-    public boolean isLastReportValid() {
-        return mReportValid;
-    }
-
-    /**
      * Enable/Disable verbose logging in score report generation.
      */
     public void enableVerboseLogging(boolean enable) {
@@ -162,7 +140,7 @@
         //report score
         if (score != wifiInfo.score) {
             if (mVerboseLoggingEnabled) {
-                Log.d(TAG, " report new wifi score " + score);
+                Log.d(TAG, "report new wifi score " + score);
             }
             wifiInfo.score = score;
             if (networkAgent != null) {
@@ -170,9 +148,67 @@
             }
         }
 
-        mReport = String.format(Locale.US, " score=%d", score);
-        mReportValid = true;
         wifiMetrics.incrementWifiScoreCount(score);
+        mScore = score;
+    }
+
+    private static final double TIME_CONSTANT_MILLIS = 30.0e+3;
+    private static final long NUD_THROTTLE_MILLIS = 5000;
+    private long mLastKnownNudCheckTimeMillis = 0;
+    private int mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE;
+    private int mNudYes = 0;    // Counts when we voted for a NUD
+    private int mNudCount = 0;  // Counts when we were told a NUD was sent
+
+    /**
+     * Recommends that a layer 3 check be done
+     *
+     * The caller can use this to (help) decide that an IP reachability check
+     * is desirable. The check is not done here; that is the caller's responsibility.
+     *
+     * @return true to indicate that an IP reachability check is recommended
+     */
+    public boolean shouldCheckIpLayer() {
+        int nud = mScoringParams.getNudKnob();
+        if (nud == 0) {
+            return false;
+        }
+        long millis = mClock.getWallClockMillis();
+        long deltaMillis = millis - mLastKnownNudCheckTimeMillis;
+        // Don't ever ask back-to-back - allow at least 5 seconds
+        // for the previous one to finish.
+        if (deltaMillis < NUD_THROTTLE_MILLIS) {
+            return false;
+        }
+        // nud is between 1 and 10 at this point
+        double deltaLevel = 11 - nud;
+        // nextNudBreach is the bar the score needs to cross before we ask for NUD
+        double nextNudBreach = ConnectedScore.WIFI_TRANSITION_SCORE;
+        // If we were below threshold the last time we checked, then compute a new bar
+        // that starts down from there and decays exponentially back up to the steady-state
+        // bar. If 5 time constants have passed, we are 99% of the way there, so skip the math.
+        if (mLastKnownNudCheckScore < ConnectedScore.WIFI_TRANSITION_SCORE
+                && deltaMillis < 5.0 * TIME_CONSTANT_MILLIS) {
+            double a = Math.exp(-deltaMillis / TIME_CONSTANT_MILLIS);
+            nextNudBreach = a * (mLastKnownNudCheckScore - deltaLevel) + (1.0 - a) * nextNudBreach;
+        }
+        if (mScore >= nextNudBreach) {
+            return false;
+        }
+        mNudYes++;
+        return true;
+    }
+
+    /**
+     * Should be called when a reachability check has been issued
+     *
+     * When the caller has requested an IP reachability check, calling this will
+     * help to rate-limit requests via shouldCheckIpLayer()
+     */
+    public void noteIpCheck() {
+        long millis = mClock.getWallClockMillis();
+        mLastKnownNudCheckTimeMillis = millis;
+        mLastKnownNudCheckScore = mScore;
+        mNudCount++;
     }
 
     /**
@@ -201,10 +237,11 @@
         try {
             String timestamp = new SimpleDateFormat("MM-dd HH:mm:ss.SSS").format(new Date(now));
             s = String.format(Locale.US, // Use US to avoid comma/decimal confusion
-                    "%s,%d,%d,%.1f,%.1f,%.1f,%d,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%d",
+                    "%s,%d,%d,%.1f,%.1f,%.1f,%d,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d",
                     timestamp, mSessionNumber, netId,
                     rssi, filteredRssi, rssiThreshold, freq, linkSpeed,
                     txSuccessRate, txRetriesRate, txBadRate, rxSuccessRate,
+                    mNudYes, mNudCount,
                     s1, s2, score);
         } catch (Exception e) {
             Log.e(TAG, "format problem", e);
@@ -235,7 +272,7 @@
             history = new LinkedList<>(mLinkMetricsHistory);
         }
         pw.println("time,session,netid,rssi,filtered_rssi,rssi_threshold,"
-                + "freq,linkspeed,tx_good,tx_retry,tx_bad,rx_pps,s1,s2,score");
+                + "freq,linkspeed,tx_good,tx_retry,tx_bad,rx_pps,nudrq,nuds,s1,s2,score");
         for (String line : history) {
             pw.println(line);
         }
diff --git a/com/android/server/wifi/WifiServiceImpl.java b/com/android/server/wifi/WifiServiceImpl.java
index 3496b2c..7d6c109 100644
--- a/com/android/server/wifi/WifiServiceImpl.java
+++ b/com/android/server/wifi/WifiServiceImpl.java
@@ -54,10 +54,8 @@
 import android.database.ContentObserver;
 import android.net.DhcpInfo;
 import android.net.DhcpResults;
-import android.net.IpConfiguration;
 import android.net.Network;
 import android.net.NetworkUtils;
-import android.net.StaticIpConfiguration;
 import android.net.Uri;
 import android.net.ip.IpClient;
 import android.net.wifi.ISoftApCallback;
@@ -999,7 +997,7 @@
                 .c(Binder.getCallingUid()).c(mode).flush();
 
         // null wifiConfig is a meaningful input for CMD_SET_AP
-        if (wifiConfig == null || isValid(wifiConfig)) {
+        if (wifiConfig == null || WifiApConfigStore.validateApWifiConfiguration(wifiConfig)) {
             SoftApModeConfiguration softApConfig = new SoftApModeConfiguration(mode, wifiConfig);
             mWifiController.sendMessage(CMD_SET_AP, 1, 0, softApConfig);
             return true;
@@ -1520,12 +1518,13 @@
     /**
      * see {@link WifiManager#setWifiApConfiguration(WifiConfiguration)}
      * @param wifiConfig WifiConfiguration details for soft access point
-     * @throws SecurityException if the caller does not have permission to write the sotap config
+     * @return boolean indicating success or failure of the operation
+     * @throws SecurityException if the caller does not have permission to write the softap config
      */
     @Override
-    public void setWifiApConfiguration(WifiConfiguration wifiConfig, String packageName) {
+    public boolean setWifiApConfiguration(WifiConfiguration wifiConfig, String packageName) {
         if (enforceChangePermission(packageName) != MODE_ALLOWED) {
-            return;
+            return false;
         }
         int uid = Binder.getCallingUid();
         // only allow Settings UI to write the stored SoftApConfig
@@ -1536,13 +1535,15 @@
         }
         mLog.info("setWifiApConfiguration uid=%").c(uid).flush();
         if (wifiConfig == null)
-            return;
-        if (isValid(wifiConfig)) {
+            return false;
+        if (WifiApConfigStore.validateApWifiConfiguration(wifiConfig)) {
             mWifiStateMachineHandler.post(() -> {
                 mWifiApConfigStore.setApConfiguration(wifiConfig);
             });
+            return true;
         } else {
             Slog.e(TAG, "Invalid WifiConfiguration");
+            return false;
         }
     }
 
@@ -1942,13 +1943,13 @@
                         == PackageManager.PERMISSION_GRANTED) {
                     hideDefaultMacAddress = false;
                 }
-                if (mWifiPermissionsUtil.canAccessScanResults(callingPackage, uid)) {
-                    hideBssidAndSsid = false;
-                }
+                mWifiPermissionsUtil.enforceCanAccessScanResults(callingPackage, uid);
+                hideBssidAndSsid = false;
             } catch (RemoteException e) {
                 Log.e(TAG, "Error checking receiver permission", e);
             } catch (SecurityException e) {
-                Log.e(TAG, "Security exception checking receiver permission", e);
+                Log.e(TAG, "Security exception checking receiver permission"
+                        + ", hiding ssid and bssid", e);
             }
             if (hideDefaultMacAddress) {
                 result.setMacAddress(WifiInfo.DEFAULT_MAC_ADDRESS);
@@ -1974,9 +1975,7 @@
         int uid = Binder.getCallingUid();
         long ident = Binder.clearCallingIdentity();
         try {
-            if (!mWifiPermissionsUtil.canAccessScanResults(callingPackage, uid)) {
-                return new ArrayList<ScanResult>();
-            }
+            mWifiPermissionsUtil.enforceCanAccessScanResults(callingPackage, uid);
             final List<ScanResult> scanResults = new ArrayList<>();
             boolean success = mWifiInjector.getWifiStateMachineHandler().runWithScissors(() -> {
                 scanResults.addAll(mScanRequestProxy.getScanResults());
@@ -1985,6 +1984,8 @@
                 Log.e(TAG, "Failed to post runnable to fetch scan results");
             }
             return scanResults;
+        } catch (SecurityException e) {
+            return new ArrayList<ScanResult>();
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -2599,39 +2600,6 @@
         return false;
     }
 
-    public static boolean isValid(WifiConfiguration config) {
-        String validity = checkValidity(config);
-        return validity == null || logAndReturnFalse(validity);
-    }
-
-    public static String checkValidity(WifiConfiguration config) {
-        if (config.allowedKeyManagement == null)
-            return "allowed kmgmt";
-
-        if (config.allowedKeyManagement.cardinality() > 1) {
-            if (config.allowedKeyManagement.cardinality() != 2) {
-                return "cardinality != 2";
-            }
-            if (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP)) {
-                return "not WPA_EAP";
-            }
-            if ((!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.IEEE8021X))
-                    && (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK))) {
-                return "not PSK or 8021X";
-            }
-        }
-        if (config.getIpAssignment() == IpConfiguration.IpAssignment.STATIC) {
-            StaticIpConfiguration staticIpConf = config.getStaticIpConfiguration();
-            if (staticIpConf == null) {
-                return "null StaticIpConfiguration";
-            }
-            if (staticIpConf.ipAddress == null) {
-                return "null static ip Address";
-            }
-        }
-        return null;
-    }
-
     @Override
     public Network getCurrentNetwork() {
         enforceAccessPermission();
diff --git a/com/android/server/wifi/WifiStateMachine.java b/com/android/server/wifi/WifiStateMachine.java
index eb3848d..1005037 100644
--- a/com/android/server/wifi/WifiStateMachine.java
+++ b/com/android/server/wifi/WifiStateMachine.java
@@ -229,15 +229,23 @@
     private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() {
         @Override
         public void onDestroyed(String ifaceName) {
-            sendMessage(CMD_INTERFACE_DESTROYED);
+            if (mInterfaceName != null && mInterfaceName.equals(ifaceName)) {
+                sendMessage(CMD_INTERFACE_DESTROYED);
+            }
         }
 
         @Override
         public void onUp(String ifaceName) {
+            if (mInterfaceName != null && mInterfaceName.equals(ifaceName)) {
+                sendMessage(CMD_INTERFACE_STATUS_CHANGED, 1);
+            }
         }
 
         @Override
         public void onDown(String ifaceName) {
+            if (mInterfaceName != null && mInterfaceName.equals(ifaceName)) {
+                sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0);
+            }
         }
     };
     private boolean mIpReachabilityDisconnectEnabled = true;
@@ -326,8 +334,6 @@
      */
     public static final String SUPPLICANT_BSSID_ANY = "any";
 
-    private int mSupplicantRestartCount = 0;
-
     /**
      * The link properties of the wifi interface.
      * Do not modify this directly; use updateLinkProperties instead.
@@ -459,12 +465,14 @@
     static final int CMD_STOP_SUPPLICANT                                = BASE + 12;
     /* STA interface destroyed */
     static final int CMD_INTERFACE_DESTROYED                            = BASE + 13;
+    /* STA interface down */
+    static final int CMD_INTERFACE_DOWN                                 = BASE + 14;
     /* Indicates Static IP succeeded */
     static final int CMD_STATIC_IP_SUCCESS                              = BASE + 15;
     /* Indicates Static IP failed */
     static final int CMD_STATIC_IP_FAILURE                              = BASE + 16;
-    /* A delayed message sent to start driver when it fail to come up */
-    static final int CMD_DRIVER_START_TIMED_OUT                         = BASE + 19;
+    /* Interface status change */
+    static final int CMD_INTERFACE_STATUS_CHANGED                       = BASE + 20;
 
     /* Start the soft access point */
     static final int CMD_START_AP                                       = BASE + 21;
@@ -769,8 +777,6 @@
     private State mDefaultState = new DefaultState();
     /* Temporary initial state */
     private State mInitialState = new InitialState();
-    /* Driver loaded, waiting for supplicant to start */
-    private State mSupplicantStartingState = new SupplicantStartingState();
     /* Driver loaded and supplicant ready */
     private State mSupplicantStartedState = new SupplicantStartedState();
     /* Scan for networks, no connection will be established */
@@ -992,7 +998,6 @@
         // CHECKSTYLE:OFF IndentationCheck
         addState(mDefaultState);
             addState(mInitialState, mDefaultState);
-                addState(mSupplicantStartingState, mInitialState);
                 addState(mSupplicantStartedState, mInitialState);
                     addState(mConnectModeState, mSupplicantStartedState);
                         addState(mL2ConnectedState, mConnectModeState);
@@ -1045,9 +1050,6 @@
                 getHandler());
         mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.RX_HS20_ANQP_ICON_EVENT,
                 getHandler());
-        mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_CONNECTION_EVENT, getHandler());
-        mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_DISCONNECTION_EVENT,
-                getHandler());
         mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT,
                 getHandler());
         mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_REQUEST_IDENTITY,
@@ -2208,9 +2210,7 @@
                 if (report != null) {
                     sb.append(" ").append(report);
                 }
-                if (mWifiScoreReport.isLastReportValid()) {
-                    sb.append(mWifiScoreReport.getLastReport());
-                }
+                sb.append(String.format(" score=%d", mWifiInfo.score));
                 break;
             case CMD_START_CONNECT:
             case WifiManager.CONNECT_NETWORK:
@@ -2890,17 +2890,6 @@
         mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
     }
 
-    private void handleSupplicantConnectionLoss(boolean killSupplicant) {
-        /* Socket connection can be lost when we do a graceful shutdown
-        * or when the driver is hung. Ensure supplicant is stopped here.
-        */
-        if (killSupplicant) {
-            mWifiMonitor.stopAllMonitoring();
-        }
-        sendSupplicantConnectionChangedBroadcast(false);
-        setWifiState(WIFI_STATE_DISABLED);
-    }
-
     void handlePreDhcpSetup() {
         if (!mBluetoothConnectionActive) {
             /*
@@ -3380,6 +3369,25 @@
                 + macRandomizationEnabled);
     }
 
+    /**
+     * Handle the error case where our underlying interface went down (if we do not have mac
+     * randomization enabled (b/72459123).
+     *
+     * This method triggers SelfRecovery with the error of REASON_STA_IFACE_DOWN.  SelfRecovery then
+     * decides if wifi should be restarted or disabled.
+     */
+    private void handleInterfaceDown() {
+        if (mEnableConnectedMacRandomization.get()) {
+            // interface will go down when mac randomization is active, skip
+            Log.d(TAG, "MacRandomization enabled, ignoring iface down");
+            return;
+        }
+
+        Log.e(TAG, "Detected an interface down, report failure to SelfRecovery");
+        // report a failure
+        mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_STA_IFACE_DOWN);
+    }
+
     /********************************************************
      * HSM states
      *******************************************************/
@@ -3488,7 +3496,6 @@
                     break;
                 case CMD_START_SUPPLICANT:
                 case CMD_STOP_SUPPLICANT:
-                case CMD_DRIVER_START_TIMED_OUT:
                 case CMD_START_AP_FAILURE:
                 case CMD_STOP_AP:
                 case CMD_AP_STOPPED:
@@ -3496,8 +3503,6 @@
                 case CMD_RECONNECT:
                 case CMD_REASSOCIATE:
                 case CMD_RELOAD_TLS_AND_RECONNECT:
-                case WifiMonitor.SUP_CONNECTION_EVENT:
-                case WifiMonitor.SUP_DISCONNECTION_EVENT:
                 case WifiMonitor.NETWORK_CONNECTION_EVENT:
                 case WifiMonitor.NETWORK_DISCONNECTION_EVENT:
                 case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
@@ -3524,6 +3529,8 @@
                 case CMD_SELECT_TX_POWER_SCENARIO:
                 case CMD_WIFINATIVE_FAILURE:
                 case CMD_INTERFACE_DESTROYED:
+                case CMD_INTERFACE_DOWN:
+                case CMD_INTERFACE_STATUS_CHANGED:
                     messageHandlingStatus = MESSAGE_HANDLING_STATUS_DISCARD;
                     break;
                 case CMD_START_AP:
@@ -3712,14 +3719,28 @@
     }
 
     class InitialState extends State {
+        private boolean mIfaceIsUp;
+
+        private void onUpChanged(boolean isUp) {
+            if (isUp == mIfaceIsUp) {
+                return;  // no change
+            }
+            mIfaceIsUp = isUp;
+            if (isUp) {
+                Log.d(TAG, "Client mode interface is up");
+                // for now, do nothing - client mode has never waited for iface up
+            } else {
+                // A driver/firmware hang can now put the interface in a down state.
+                // We detect the interface going down and recover from it
+                handleInterfaceDown();
+            }
+        }
+
         private void cleanup() {
             // tell scanning service that scans are not available - about to kill the interface and
             // supplicant
             sendWifiScanAvailable(false);
 
-            // Tearing down the client interfaces below is going to stop our supplicant.
-            mWifiMonitor.stopAllMonitoring();
-
             mWifiNative.registerStatusListener(mWifiNativeStatusListener);
             // TODO: This teardown should ideally be handled in STOP_SUPPLICANT to be consistent
             // with other mode managers. But, client mode is not yet controlled by
@@ -3727,11 +3748,12 @@
             // TODO: Remove this big hammer. We cannot support concurrent interfaces with this!
             mWifiNative.teardownAllInterfaces();
             mInterfaceName = null;
+            mIfaceIsUp = false;
         }
 
         @Override
         public void enter() {
-            mWifiMonitor.stopAllMonitoring();
+            mIfaceIsUp = false;
             mWifiStateTracker.updateState(WifiStateTracker.INVALID);
             cleanup();
             sendMessage(CMD_START_SUPPLICANT);
@@ -3751,15 +3773,17 @@
                         transitionTo(mDefaultState);
                         break;
                     }
+                    // now that we have the interface, initialize our up/down status
+                    onUpChanged(mWifiNative.isInterfaceUp(mInterfaceName));
+
                     mIpClient = mFacade.makeIpClient(
                             mContext, mInterfaceName, new IpClientCallback());
                     mIpClient.setMulticastFilter(true);
                     if (mVerboseLoggingEnabled) log("Supplicant start successful");
                     registerForWifiMonitorEvents();
-                    mWifiMonitor.startMonitoring(mInterfaceName);
                     mWifiInjector.getWifiLastResortWatchdog().clearAllFailureCounts();
                     setSupplicantLogLevel();
-                    transitionTo(mSupplicantStartingState);
+                    transitionTo(mSupplicantStartedState);
                     break;
                 case CMD_SET_OPERATIONAL_MODE:
                     if (message.arg1 == CONNECT_MODE) {
@@ -3773,46 +3797,20 @@
                             WifiDiagnostics.REPORT_REASON_WIFINATIVE_FAILURE);
                     mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_WIFINATIVE_FAILURE);
                     break;
-                default:
-                    return NOT_HANDLED;
-            }
-            return HANDLED;
-        }
-    }
-
-    class SupplicantStartingState extends State {
-
-        @Override
-        public boolean processMessage(Message message) {
-            logStateAndMessage(message, this);
-
-            switch(message.what) {
-                case WifiMonitor.SUP_CONNECTION_EVENT:
-                    if (mVerboseLoggingEnabled) log("Supplicant connection established");
-
-                    mSupplicantRestartCount = 0;
-                    /* Reset the supplicant state to indicate the supplicant
-                     * state is not known at this time */
-                    mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
-                    /* Initialize data structures */
-                    mLastBssid = null;
-                    mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
-                    mLastSignalLevel = -1;
-                    mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));
-                    // Attempt to migrate data out of legacy store.
-                    if (!mWifiConfigManager.migrateFromLegacyStore()) {
-                        Log.e(TAG, "Failed to migrate from legacy config store");
+                case CMD_INTERFACE_STATUS_CHANGED:
+                    boolean isUp = message.arg1 == 1;
+                    // For now, this message can be triggered due to link state and/or interface
+                    // status changes (b/77218676).  First check if we really see an iface down by
+                    // consulting our view of supplicant state.
+                    if (!isUp && SupplicantState.isDriverActive(mWifiInfo.getSupplicantState())) {
+                        // the driver is active, so this could just be part of normal operation, do
+                        // not disable wifi in these cases (ex, a network was removed) or worry
+                        // about the link status
+                        break;
                     }
-                    sendSupplicantConnectionChangedBroadcast(true);
-                    transitionTo(mSupplicantStartedState);
+
+                    onUpChanged(isUp);
                     break;
-                case WifiMonitor.SUP_DISCONNECTION_EVENT:
-                    // since control is split between WSM and WSMP - do not worry about supplicant
-                    // dying if we haven't seen it up yet
-                    break;
-                case CMD_START_SUPPLICANT:
-                case CMD_STOP_SUPPLICANT:
-                case CMD_STOP_AP:
                 default:
                     return NOT_HANDLED;
             }
@@ -3827,6 +3825,19 @@
                 logd("SupplicantStartedState enter");
             }
 
+            // reset state related to supplicant starting
+            mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
+            // Initialize data structures
+            mLastBssid = null;
+            mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
+            mLastSignalLevel = -1;
+            mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));
+            // Attempt to migrate data out of legacy store.
+            if (!mWifiConfigManager.migrateFromLegacyStore()) {
+                Log.e(TAG, "Failed to migrate from legacy config store");
+            }
+            sendSupplicantConnectionChangedBroadcast(true);
+
             mWifiNative.setExternalSim(mInterfaceName, true);
 
             setRandomMacOui();
@@ -3897,19 +3908,6 @@
             logStateAndMessage(message, this);
 
             switch(message.what) {
-                case WifiMonitor.SUP_DISCONNECTION_EVENT:  /* Supplicant connection lost */
-                    // first check if we are expecting a mode switch
-                    if (mModeChange) {
-                        logd("expecting a mode change, do not restart supplicant");
-                        return HANDLED;
-                    }
-                    loge("Connection lost, restart supplicant");
-                    handleSupplicantConnectionLoss(true);
-                    handleNetworkDisconnect();
-                    mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
-                    sendMessage(CMD_START_SUPPLICANT);
-                    transitionTo(mInitialState);
-                    break;
                 case CMD_TARGET_BSSID:
                     // Trying to associate to this BSSID
                     if (message.obj != null) {
@@ -4082,12 +4080,6 @@
             case WifiManager.FORGET_NETWORK:
                 s = "FORGET_NETWORK";
                 break;
-            case WifiMonitor.SUP_CONNECTION_EVENT:
-                s = "SUP_CONNECTION_EVENT";
-                break;
-            case WifiMonitor.SUP_DISCONNECTION_EVENT:
-                s = "SUP_DISCONNECTION_EVENT";
-                break;
             case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
                 s = "SUPPLICANT_STATE_CHANGE_EVENT";
                 break;
@@ -4295,7 +4287,9 @@
                     mSupplicantStateTracker.sendMessage(WifiMonitor.ASSOCIATION_REJECTION_EVENT);
                     // If rejection occurred while Metrics is tracking a ConnnectionEvent, end it.
                     reportConnectionAttemptEnd(
-                            WifiMetrics.ConnectionEvent.FAILURE_ASSOCIATION_REJECTION,
+                            timedOut
+                                    ? WifiMetrics.ConnectionEvent.FAILURE_ASSOCIATION_TIMED_OUT
+                                    : WifiMetrics.ConnectionEvent.FAILURE_ASSOCIATION_REJECTION,
                             WifiMetricsProto.ConnectionEvent.HLF_NONE);
                     mWifiInjector.getWifiLastResortWatchdog()
                             .noteConnectionFailureAndTriggerIfNeeded(
@@ -4336,20 +4330,6 @@
                     break;
                 case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
                     SupplicantState state = handleSupplicantStateChange(message);
-                    // A driver/firmware hang can now put the interface in a down state.
-                    // We detect the interface going down and recover from it
-                    if (!SupplicantState.isDriverActive(state) && !mModeChange
-                            && !mEnableConnectedMacRandomization.get()) {
-                        if (mNetworkInfo.getState() != NetworkInfo.State.DISCONNECTED) {
-                            handleNetworkDisconnect();
-                        }
-                        log("Detected an interface down, restart driver");
-                        // Rely on the fact that this will force us into killing supplicant and then
-                        // restart supplicant from a clean state.
-                        sendMessage(CMD_START_SUPPLICANT);
-                        transitionTo(mInitialState);
-                        break;
-                    }
 
                     // Supplicant can fail to report a NETWORK_DISCONNECTION_EVENT
                     // when authentication times out after a successful connection,
@@ -4370,7 +4350,20 @@
                     // interest (e.g. routers); harmless if none are configured.
                     if (state == SupplicantState.COMPLETED) {
                         mIpClient.confirmConfiguration();
+                        mWifiScoreReport.noteIpCheck();
                     }
+
+                    if (!SupplicantState.isDriverActive(state)) {
+                        // still use supplicant to detect interface down while work to
+                        // mitigate b/77218676 is in progress
+                        // note: explicitly using this command to dedup iface down notification
+                        // paths (onUpChanged filters out duplicate updates)
+                        sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0);
+                        if (mVerboseLoggingEnabled) {
+                            Log.d(TAG, "detected interface down via supplicant");
+                        }
+                    }
+
                     break;
                 case WifiP2pServiceImpl.DISCONNECT_WIFI_REQUEST:
                     if (message.arg1 == 1) {
@@ -5195,6 +5188,10 @@
                             // Send the update score to network agent.
                             mWifiScoreReport.calculateAndReportScore(
                                     mWifiInfo, mNetworkAgent, mWifiMetrics);
+                            if (mWifiScoreReport.shouldCheckIpLayer()) {
+                                mIpClient.confirmConfiguration();
+                                mWifiScoreReport.noteIpCheck();
+                            }
                         }
                         sendMessageDelayed(obtainMessage(CMD_RSSI_POLL, mRssiPollToken, 0),
                                 mPollRssiIntervalMsecs);
diff --git a/com/android/server/wifi/WifiWakeMetrics.java b/com/android/server/wifi/WifiWakeMetrics.java
index fba7236..5b70006 100644
--- a/com/android/server/wifi/WifiWakeMetrics.java
+++ b/com/android/server/wifi/WifiWakeMetrics.java
@@ -42,6 +42,8 @@
 
     private boolean mIsInSession = false;
     private int mTotalSessions = 0;
+    private int mTotalWakeups = 0;
+    private int mIgnoredStarts = 0;
 
     private final Object mLock = new Object();
 
@@ -50,7 +52,7 @@
      *
      * <p>Starts the session.
      *
-     * @param numNetworks The total number of networks stored in the WakeupLock at initialization.
+     * @param numNetworks The total number of networks stored in the WakeupLock at start.
      */
     public void recordStartEvent(int numNetworks) {
         synchronized (mLock) {
@@ -60,10 +62,29 @@
     }
 
     /**
+     * Records the initialize event of the current Wifi Wake session.
+     *
+     * <p>Note: The start event must be recorded before this event, otherwise this call will be
+     * ignored.
+     *
+     * @param numScans The total number of elapsed scans since start.
+     * @param numNetworks The total number of networks in the lock.
+     */
+    public void recordInitializeEvent(int numScans, int numNetworks) {
+        synchronized (mLock) {
+            if (!mIsInSession) {
+                return;
+            }
+            mCurrentSession.recordInitializeEvent(numScans, numNetworks,
+                    SystemClock.elapsedRealtime());
+        }
+    }
+
+    /**
      * Records the unlock event of the current Wifi Wake session.
      *
      * <p>The unlock event occurs when the WakeupLock has all of its networks removed. This event
-     * will not be recorded if the start event recorded 0 locked networks.
+     * will not be recorded if the initialize event recorded 0 locked networks.
      *
      * <p>Note: The start event must be recorded before this event, otherwise this call will be
      * ignored.
@@ -116,6 +137,11 @@
             }
             mCurrentSession.recordResetEvent(numScans, SystemClock.elapsedRealtime());
 
+            // tally successful wakeups here since this is the actual point when wifi is turned on
+            if (mCurrentSession.hasWakeupTriggered()) {
+                mTotalWakeups++;
+            }
+
             mTotalSessions++;
             if (mSessions.size() < MAX_RECORDED_SESSIONS) {
                 mSessions.add(mCurrentSession);
@@ -125,12 +151,21 @@
     }
 
     /**
+     * Records instance of the start event being ignored due to the controller already being active.
+     */
+    public void recordIgnoredStart() {
+        mIgnoredStarts++;
+    }
+
+    /**
      * Returns the consolidated WifiWakeStats proto for WifiMetrics.
      */
     public WifiWakeStats buildProto() {
         WifiWakeStats proto = new WifiWakeStats();
 
         proto.numSessions = mTotalSessions;
+        proto.numWakeups = mTotalWakeups;
+        proto.numIgnoredStarts = mIgnoredStarts;
         proto.sessions = new WifiWakeStats.Session[mSessions.size()];
 
         for (int i = 0; i < mSessions.size(); i++) {
@@ -148,6 +183,8 @@
         synchronized (mLock) {
             pw.println("-------WifiWake metrics-------");
             pw.println("mTotalSessions: " + mTotalSessions);
+            pw.println("mTotalWakeups: " + mTotalWakeups);
+            pw.println("mIgnoredStarts: " + mIgnoredStarts);
             pw.println("mIsInSession: " + mIsInSession);
             pw.println("Stored Sessions: " + mSessions.size());
             for (Session session : mSessions) {
@@ -167,21 +204,29 @@
      * <p>Keeps the current WifiWake session.
      */
     public void clear() {
-        mSessions.clear();
-        mTotalSessions = 0;
+        synchronized (mLock) {
+            mSessions.clear();
+            mTotalSessions = 0;
+            mTotalWakeups = 0;
+            mIgnoredStarts = 0;
+        }
     }
 
     /** A single WifiWake session. */
     public static class Session {
 
         private final long mStartTimestamp;
-        private final int mNumNetworks;
+        private final int mStartNetworks;
+        private int mInitializeNetworks = 0;
 
         @VisibleForTesting
         @Nullable
         Event mUnlockEvent;
         @VisibleForTesting
         @Nullable
+        Event mInitEvent;
+        @VisibleForTesting
+        @Nullable
         Event mWakeupEvent;
         @VisibleForTesting
         @Nullable
@@ -189,11 +234,27 @@
 
         /** Creates a new WifiWake session. */
         public Session(int numNetworks, long timestamp) {
-            mNumNetworks = numNetworks;
+            mStartNetworks = numNetworks;
             mStartTimestamp = timestamp;
         }
 
         /**
+         * Records an initialize event.
+         *
+         * <p>Ignores subsequent calls.
+         *
+         * @param numScans Total number of scans at the time of this event.
+         * @param numNetworks Total number of networks in the lock.
+         * @param timestamp The timestamp of the event.
+         */
+        public void recordInitializeEvent(int numScans, int numNetworks, long timestamp) {
+            if (mInitEvent == null) {
+                mInitializeNetworks = numNetworks;
+                mInitEvent = new Event(numScans, timestamp - mStartTimestamp);
+            }
+        }
+
+        /**
          * Records an unlock event.
          *
          * <p>Ignores subsequent calls.
@@ -222,6 +283,13 @@
         }
 
         /**
+         * Returns whether the current session has had its wakeup event triggered.
+         */
+        public boolean hasWakeupTriggered() {
+            return mWakeupEvent != null;
+        }
+
+        /**
          * Records a reset event.
          *
          * <p>Ignores subsequent calls.
@@ -239,8 +307,12 @@
         public WifiWakeStats.Session buildProto() {
             WifiWakeStats.Session sessionProto = new WifiWakeStats.Session();
             sessionProto.startTimeMillis = mStartTimestamp;
-            sessionProto.lockedNetworksAtStart = mNumNetworks;
+            sessionProto.lockedNetworksAtStart = mStartNetworks;
 
+            if (mInitEvent != null) {
+                sessionProto.lockedNetworksAtInitialize = mInitializeNetworks;
+                sessionProto.initializeEvent = mInitEvent.buildProto();
+            }
             if (mUnlockEvent != null) {
                 sessionProto.unlockEvent = mUnlockEvent.buildProto();
             }
@@ -258,7 +330,9 @@
         public void dump(PrintWriter pw) {
             pw.println("WifiWakeMetrics.Session:");
             pw.println("mStartTimestamp: " + mStartTimestamp);
-            pw.println("mNumNetworks: " + mNumNetworks);
+            pw.println("mStartNetworks: " + mStartNetworks);
+            pw.println("mInitializeNetworks: " + mInitializeNetworks);
+            pw.println("mInitEvent: " + (mInitEvent == null ? "{}" : mInitEvent.toString()));
             pw.println("mUnlockEvent: " + (mUnlockEvent == null ? "{}" : mUnlockEvent.toString()));
             pw.println("mWakeupEvent: " + (mWakeupEvent == null ? "{}" : mWakeupEvent.toString()));
             pw.println("mResetEvent: " + (mResetEvent == null ? "{}" : mResetEvent.toString()));
diff --git a/com/android/server/wifi/p2p/WifiP2pServiceImpl.java b/com/android/server/wifi/p2p/WifiP2pServiceImpl.java
index b525555..fdad657 100644
--- a/com/android/server/wifi/p2p/WifiP2pServiceImpl.java
+++ b/com/android/server/wifi/p2p/WifiP2pServiceImpl.java
@@ -3459,7 +3459,6 @@
          */
         private WifiP2pDeviceList getPeers(Bundle pkg, int uid) {
             String pkgName = pkg.getString(WifiP2pManager.CALLING_PACKAGE);
-            boolean scanPermission = false;
             WifiPermissionsUtil wifiPermissionsUtil;
             // getPeers() is guaranteed to be invoked after Wifi Service is up
             // This ensures getInstance() will return a non-null object now
@@ -3468,13 +3467,10 @@
             }
             wifiPermissionsUtil = mWifiInjector.getWifiPermissionsUtil();
             try {
-                scanPermission = wifiPermissionsUtil.canAccessScanResults(pkgName, uid);
-            } catch (SecurityException e) {
-                Log.e(TAG, "Security Exception, cannot access peer list");
-            }
-            if (scanPermission) {
+                wifiPermissionsUtil.enforceCanAccessScanResults(pkgName, uid);
                 return new WifiP2pDeviceList(mPeers);
-            } else {
+            } catch (SecurityException e) {
+                Log.v(TAG, "Security Exception, cannot access peer list");
                 return new WifiP2pDeviceList();
             }
         }
diff --git a/com/android/server/wifi/util/WifiPermissionsUtil.java b/com/android/server/wifi/util/WifiPermissionsUtil.java
index 0f333d4..3d83864 100644
--- a/com/android/server/wifi/util/WifiPermissionsUtil.java
+++ b/com/android/server/wifi/util/WifiPermissionsUtil.java
@@ -166,12 +166,12 @@
     }
 
     /**
-     * API to determine if the caller has permissions to get scan results.
+     * API to determine if the caller has permissions to get scan results. Throws SecurityException
+     * if the caller has no permission.
      * @param pkgName package name of the application requesting access
      * @param uid The uid of the package
-     * @return boolean true or false if permissions is granted
      */
-    public boolean canAccessScanResults(String pkgName, int uid) throws SecurityException {
+    public void enforceCanAccessScanResults(String pkgName, int uid) throws SecurityException {
         mAppOps.checkPackage(uid, pkgName);
         // Check if the calling Uid has CAN_READ_PEER_MAC_ADDRESS permission.
         boolean canCallingUidAccessLocation = checkCallerHasPeersMacAddressPermission(uid);
@@ -192,22 +192,18 @@
         if (!canCallingUidAccessLocation && !canAppPackageUseLocation) {
             // also check if it is a connectivity app
             if (!appTypeConnectivity) {
-                mLog.tC("Denied: no location permission");
-                return false;
+                throw new SecurityException("UID " + uid + " has no location permission");
             }
         }
         // Check if Wifi Scan request is an operation allowed for this App.
         if (!isScanAllowedbyApps(pkgName, uid)) {
-            mLog.tC("Denied: app wifi scan not allowed");
-            return false;
+            throw new SecurityException("UID " + uid + " has no wifi scan permission");
         }
         // If the User or profile is current, permission is granted
         // Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission.
         if (!isCurrentProfile(uid) && !checkInteractAcrossUsersFull(uid)) {
-            mLog.tC("Denied: Profile not permitted");
-            return false;
+            throw new SecurityException("UID " + uid + " profile not permitted");
         }
-        return true;
     }
 
     /**
diff --git a/com/android/server/wm/AccessibilityController.java b/com/android/server/wm/AccessibilityController.java
index 641a1ba..608d0aa 100644
--- a/com/android/server/wm/AccessibilityController.java
+++ b/com/android/server/wm/AccessibilityController.java
@@ -1102,35 +1102,37 @@
                         }
                     }
 
-                    // Account for the space this window takes if the window
-                    // is not an accessibility overlay which does not change
-                    // the reported windows.
                     if (windowState.mAttrs.type !=
                             WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY) {
+
+                        // Account for the space this window takes if the window
+                        // is not an accessibility overlay which does not change
+                        // the reported windows.
                         unaccountedSpace.op(boundsInScreen, unaccountedSpace,
                                 Region.Op.REVERSE_DIFFERENCE);
-                    }
 
-                    // If a window is modal it prevents other windows from being touched
-                    if ((flags & (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)) == 0) {
-                        // Account for all space in the task, whether the windows in it are
-                        // touchable or not. The modal window blocks all touches from the task's
-                        // area.
-                        unaccountedSpace.op(windowState.getDisplayFrameLw(), unaccountedSpace,
-                                Region.Op.REVERSE_DIFFERENCE);
+                        // If a window is modal it prevents other windows from being touched
+                        if ((flags & (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)) == 0) {
+                            // Account for all space in the task, whether the windows in it are
+                            // touchable or not. The modal window blocks all touches from the task's
+                            // area.
+                            unaccountedSpace.op(windowState.getDisplayFrameLw(), unaccountedSpace,
+                                    Region.Op.REVERSE_DIFFERENCE);
 
-                        if (task != null) {
-                            // If the window is associated with a particular task, we can skip the
-                            // rest of the windows for that task.
-                            skipRemainingWindowsForTasks.add(task.mTaskId);
-                            continue;
-                        } else {
-                            // If the window is not associated with a particular task, then it is
-                            // globally modal. In this case we can skip all remaining windows.
-                            break;
+                            if (task != null) {
+                                // If the window is associated with a particular task, we can skip the
+                                // rest of the windows for that task.
+                                skipRemainingWindowsForTasks.add(task.mTaskId);
+                                continue;
+                            } else {
+                                // If the window is not associated with a particular task, then it is
+                                // globally modal. In this case we can skip all remaining windows.
+                                break;
+                            }
                         }
                     }
+
                     // We figured out what is touchable for the entire screen - done.
                     if (unaccountedSpace.isEmpty()) {
                         break;
diff --git a/com/android/server/wm/AnimatingAppWindowTokenRegistry.java b/com/android/server/wm/AnimatingAppWindowTokenRegistry.java
index 416469b..9c00d1a 100644
--- a/com/android/server/wm/AnimatingAppWindowTokenRegistry.java
+++ b/com/android/server/wm/AnimatingAppWindowTokenRegistry.java
@@ -37,6 +37,8 @@
 
     private ArrayList<Runnable> mTmpRunnableList = new ArrayList<>();
 
+    private boolean mEndingDeferredFinish;
+
     /**
      * Notifies that an {@link AppWindowToken} has started animating.
      */
@@ -50,6 +52,11 @@
     void notifyFinished(AppWindowToken token) {
         mAnimatingTokens.remove(token);
         mFinishedTokens.remove(token);
+
+        // If we were the last token, make sure the end all deferred finishes.
+        if (mAnimatingTokens.isEmpty()) {
+            endDeferringFinished();
+        }
     }
 
     /**
@@ -78,16 +85,28 @@
     }
 
     private void endDeferringFinished() {
-        // Copy it into a separate temp list to avoid modifying the collection while iterating as
-        // calling the callback may call back into notifyFinished.
-        for (int i = mFinishedTokens.size() - 1; i >= 0; i--) {
-            mTmpRunnableList.add(mFinishedTokens.valueAt(i));
+
+        // Don't start recursing. Running the finished listener invokes notifyFinished, which may
+        // invoked us again.
+        if (mEndingDeferredFinish) {
+            return;
         }
-        mFinishedTokens.clear();
-        for (int i = mTmpRunnableList.size() - 1; i >= 0; i--) {
-            mTmpRunnableList.get(i).run();
+        try {
+            mEndingDeferredFinish = true;
+
+            // Copy it into a separate temp list to avoid modifying the collection while iterating
+            // as calling the callback may call back into notifyFinished.
+            for (int i = mFinishedTokens.size() - 1; i >= 0; i--) {
+                mTmpRunnableList.add(mFinishedTokens.valueAt(i));
+            }
+            mFinishedTokens.clear();
+            for (int i = mTmpRunnableList.size() - 1; i >= 0; i--) {
+                mTmpRunnableList.get(i).run();
+            }
+            mTmpRunnableList.clear();
+        } finally {
+            mEndingDeferredFinish = false;
         }
-        mTmpRunnableList.clear();
     }
 
     void dump(PrintWriter pw, String header, String prefix) {
diff --git a/com/android/server/wm/AppWindowContainerController.java b/com/android/server/wm/AppWindowContainerController.java
index 1575694..165a409 100644
--- a/com/android/server/wm/AppWindowContainerController.java
+++ b/com/android/server/wm/AppWindowContainerController.java
@@ -113,63 +113,73 @@
         mListener.onWindowsGone();
     };
 
-    private final Runnable mAddStartingWindow = () -> {
-        final StartingData startingData;
-        final AppWindowToken container;
+    private final Runnable mAddStartingWindow = new Runnable() {
 
-        synchronized (mWindowMap) {
-            if (mContainer == null) {
-                if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "mContainer was null while trying to"
-                        + " add starting window");
+        @Override
+        public void run() {
+            final StartingData startingData;
+            final AppWindowToken container;
+
+            synchronized (mWindowMap) {
+                if (mContainer == null) {
+                    if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "mContainer was null while trying to"
+                            + " add starting window");
+                    return;
+                }
+
+                // There can only be one adding request, silly caller!
+                mService.mAnimationHandler.removeCallbacks(this);
+
+                startingData = mContainer.startingData;
+                container = mContainer;
+            }
+
+            if (startingData == null) {
+                // Animation has been canceled... do nothing.
+                if (DEBUG_STARTING_WINDOW)
+                    Slog.v(TAG_WM, "startingData was nulled out before handling"
+                            + " mAddStartingWindow: " + mContainer);
                 return;
             }
-            startingData = mContainer.startingData;
-            container = mContainer;
-        }
 
-        if (startingData == null) {
-            // Animation has been canceled... do nothing.
-            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "startingData was nulled out before handling"
-                    + " mAddStartingWindow: " + mContainer);
-            return;
-        }
+            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Add starting "
+                    + AppWindowContainerController.this + ": startingData="
+                    + container.startingData);
 
-        if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Add starting "
-                + this + ": startingData=" + container.startingData);
-
-        StartingSurface surface = null;
-        try {
-            surface = startingData.createStartingSurface(container);
-        } catch (Exception e) {
-            Slog.w(TAG_WM, "Exception when adding starting window", e);
-        }
-        if (surface != null) {
-            boolean abort = false;
-            synchronized(mWindowMap) {
-                // If the window was successfully added, then
-                // we need to remove it.
-                if (container.removed || container.startingData == null) {
-                    if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
-                            "Aborted starting " + container
-                                    + ": removed=" + container.removed
-                                    + " startingData=" + container.startingData);
-                    container.startingWindow = null;
-                    container.startingData = null;
-                    abort = true;
-                } else {
-                    container.startingSurface = surface;
+            StartingSurface surface = null;
+            try {
+                surface = startingData.createStartingSurface(container);
+            } catch (Exception e) {
+                Slog.w(TAG_WM, "Exception when adding starting window", e);
+            }
+            if (surface != null) {
+                boolean abort = false;
+                synchronized (mWindowMap) {
+                    // If the window was successfully added, then
+                    // we need to remove it.
+                    if (container.removed || container.startingData == null) {
+                        if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
+                                "Aborted starting " + container
+                                        + ": removed=" + container.removed
+                                        + " startingData=" + container.startingData);
+                        container.startingWindow = null;
+                        container.startingData = null;
+                        abort = true;
+                    } else {
+                        container.startingSurface = surface;
+                    }
+                    if (DEBUG_STARTING_WINDOW && !abort) Slog.v(TAG_WM,
+                            "Added starting " + mContainer
+                                    + ": startingWindow="
+                                    + container.startingWindow + " startingView="
+                                    + container.startingSurface);
                 }
-                if (DEBUG_STARTING_WINDOW && !abort) Slog.v(TAG_WM,
-                        "Added starting " + mContainer
-                                + ": startingWindow="
-                                + container.startingWindow + " startingView="
-                                + container.startingSurface);
+                if (abort) {
+                    surface.remove();
+                }
+            } else if (DEBUG_STARTING_WINDOW) {
+                Slog.v(TAG_WM, "Surface returned was null: " + mContainer);
             }
-            if (abort) {
-                surface.remove();
-            }
-        } else if (DEBUG_STARTING_WINDOW) {
-            Slog.v(TAG_WM, "Surface returned was null: " + mContainer);
         }
     };
 
@@ -558,8 +568,10 @@
         // Note: we really want to do sendMessageAtFrontOfQueue() because we
         // want to process the message ASAP, before any other queued
         // messages.
-        if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Enqueueing ADD_STARTING");
-        mService.mAnimationHandler.postAtFrontOfQueue(mAddStartingWindow);
+        if (!mService.mAnimationHandler.hasCallbacks(mAddStartingWindow)) {
+            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Enqueueing ADD_STARTING");
+            mService.mAnimationHandler.postAtFrontOfQueue(mAddStartingWindow);
+        }
     }
 
     private boolean createSnapshot(TaskSnapshot snapshot) {
diff --git a/com/android/server/wm/AppWindowToken.java b/com/android/server/wm/AppWindowToken.java
index f19c554..5676f58 100644
--- a/com/android/server/wm/AppWindowToken.java
+++ b/com/android/server/wm/AppWindowToken.java
@@ -1312,7 +1312,8 @@
         if (prevWinMode != WINDOWING_MODE_UNDEFINED && winMode == WINDOWING_MODE_PINNED) {
             // Entering PiP from fullscreen, reset the snap fraction
             mDisplayContent.mPinnedStackControllerLocked.resetReentrySnapFraction(this);
-        } else if (prevWinMode == WINDOWING_MODE_PINNED && winMode != WINDOWING_MODE_UNDEFINED) {
+        } else if (prevWinMode == WINDOWING_MODE_PINNED && winMode != WINDOWING_MODE_UNDEFINED
+                && !isHidden()) {
             // Leaving PiP to fullscreen, save the snap fraction based on the pre-animation bounds
             // for the next re-entry into PiP (assuming the activity is not hidden or destroyed)
             final TaskStack pinnedStack = mDisplayContent.getPinnedStack();
@@ -1714,7 +1715,8 @@
                     adapter = new LocalAnimationAdapter(
                             new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
                                     mService.mAppTransition.canSkipFirstFrame(),
-                                    mService.mAppTransition.getAppStackClipMode()),
+                                    mService.mAppTransition.getAppStackClipMode(),
+                                    true /* isAppAnimation */),
                             mService.mSurfaceAnimationRunner);
                     if (a.getZAdjustment() == Animation.ZORDER_TOP) {
                         mNeedsZBoost = true;
@@ -1887,7 +1889,7 @@
                 "AppWindowToken");
 
         clearThumbnail();
-        setClientHidden(hiddenRequested);
+        setClientHidden(isHidden() && hiddenRequested);
 
         if (mService.mInputMethodTarget != null && mService.mInputMethodTarget.mAppToken == this) {
             getDisplayContent().computeImeTarget(true /* updateImeTarget */);
diff --git a/com/android/server/wm/BoundsAnimationController.java b/com/android/server/wm/BoundsAnimationController.java
index ba67ff6..112d93c 100644
--- a/com/android/server/wm/BoundsAnimationController.java
+++ b/com/android/server/wm/BoundsAnimationController.java
@@ -161,7 +161,10 @@
 
         // Timeout callback to ensure we continue the animation if waiting for resuming or app
         // windows drawn fails
-        private final Runnable mResumeRunnable = () -> resume();
+        private final Runnable mResumeRunnable = () -> {
+            if (DEBUG) Slog.d(TAG, "pause: timed out waiting for windows drawn");
+            resume();
+        };
 
         BoundsAnimator(BoundsAnimationTarget target, Rect from, Rect to,
                 @SchedulePipModeChangedState int schedulePipModeChangedState,
@@ -213,7 +216,7 @@
 
                 // When starting an animation from fullscreen, pause here and wait for the
                 // windows-drawn signal before we start the rest of the transition down into PiP.
-                if (mMoveFromFullscreen) {
+                if (mMoveFromFullscreen && mTarget.shouldDeferStartOnMoveToFullscreen()) {
                     pause();
                 }
             } else if (mPrevSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END &&
diff --git a/com/android/server/wm/BoundsAnimationTarget.java b/com/android/server/wm/BoundsAnimationTarget.java
index 647a2d6..68be4e8 100644
--- a/com/android/server/wm/BoundsAnimationTarget.java
+++ b/com/android/server/wm/BoundsAnimationTarget.java
@@ -34,6 +34,12 @@
     void onAnimationStart(boolean schedulePipModeChangedCallback, boolean forceUpdate);
 
     /**
+     * @return Whether the animation should be paused waiting for the windows to draw before
+     *         entering PiP.
+     */
+    boolean shouldDeferStartOnMoveToFullscreen();
+
+    /**
      * Sets the size of the target (without any intermediate steps, like scheduling animation)
      * but freezes the bounds of any tasks in the target at taskBounds, to allow for more
      * flexibility during resizing. Only works for the pinned stack at the moment.  This will
diff --git a/com/android/server/wm/DisplayContent.java b/com/android/server/wm/DisplayContent.java
index c4f2bd4..79eb2c9 100644
--- a/com/android/server/wm/DisplayContent.java
+++ b/com/android/server/wm/DisplayContent.java
@@ -3568,7 +3568,8 @@
                     if (s.inSplitScreenWindowingMode() && mSplitScreenDividerAnchor != null) {
                         t.setLayer(mSplitScreenDividerAnchor, layer++);
                     }
-                    if (s.isAppAnimating() && state != ALWAYS_ON_TOP_STATE) {
+                    if ((s.isTaskAnimating() || s.isAppAnimating())
+                            && state != ALWAYS_ON_TOP_STATE) {
                         // Ensure the animation layer ends up above the
                         // highest animating stack and no higher.
                         layerForAnimationLayer = layer++;
@@ -3727,8 +3728,12 @@
 
             mLastWindowForcedOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
 
-            if (policy.isKeyguardShowingAndNotOccluded()
-                    || mService.mAppTransition.getAppTransition() == TRANSIT_KEYGUARD_UNOCCLUDE) {
+            // Only allow force setting the orientation when all unknown visibilities have been
+            // resolved, as otherwise we just may be starting another occluding activity.
+            final boolean isUnoccluding =
+                    mService.mAppTransition.getAppTransition() == TRANSIT_KEYGUARD_UNOCCLUDE
+                            && mService.mUnknownAppVisibilityController.allResolved();
+            if (policy.isKeyguardShowingAndNotOccluded() || isUnoccluding) {
                 return mLastKeyguardForcedOrientation;
             }
 
diff --git a/com/android/server/wm/DockedStackDividerController.java b/com/android/server/wm/DockedStackDividerController.java
index 5e2bb10..2cd2ef1 100644
--- a/com/android/server/wm/DockedStackDividerController.java
+++ b/com/android/server/wm/DockedStackDividerController.java
@@ -666,13 +666,16 @@
         }
         final TaskStack topSecondaryStack = mDisplayContent.getTopStackInWindowingMode(
                 WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        final RecentsAnimationController recentsAnim = mService.getRecentsAnimationController();
+        final boolean minimizedForRecentsAnimation = recentsAnim != null &&
+                recentsAnim.isSplitScreenMinimized();
         boolean homeVisible = homeTask.getTopVisibleAppToken() != null;
         if (homeVisible && topSecondaryStack != null) {
             // Home should only be considered visible if it is greater or equal to the top secondary
             // stack in terms of z-order.
             homeVisible = homeStack.compareTo(topSecondaryStack) >= 0;
         }
-        setMinimizedDockedStack(homeVisible, animate);
+        setMinimizedDockedStack(homeVisible || minimizedForRecentsAnimation, animate);
     }
 
     private boolean isWithinDisplay(Task task) {
diff --git a/com/android/server/wm/LocalAnimationAdapter.java b/com/android/server/wm/LocalAnimationAdapter.java
index 529aacc..d89d6f0 100644
--- a/com/android/server/wm/LocalAnimationAdapter.java
+++ b/com/android/server/wm/LocalAnimationAdapter.java
@@ -146,6 +146,13 @@
             return false;
         }
 
+        /**
+         * @return {@code true} if we need to wake-up SurfaceFlinger earlier during this animation.
+         *
+         * @see Transaction#setEarlyWakeup
+         */
+        default boolean needsEarlyWakeup() { return false; }
+
         void dump(PrintWriter pw, String prefix);
 
         default void writeToProto(ProtoOutputStream proto, long fieldId) {
diff --git a/com/android/server/wm/RecentsAnimationController.java b/com/android/server/wm/RecentsAnimationController.java
index 7274aee..1ee642a 100644
--- a/com/android/server/wm/RecentsAnimationController.java
+++ b/com/android/server/wm/RecentsAnimationController.java
@@ -17,18 +17,18 @@
 package com.android.server.wm;
 
 import static android.app.ActivityManagerInternal.APP_TRANSITION_RECENTS_ANIM;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-import static com.android.server.wm.WindowManagerService.H.NOTIFY_APP_TRANSITION_STARTING;
-import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
 import static com.android.server.wm.AnimationAdapterProto.REMOTE;
+import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_RECENTS_ANIMATIONS;
+import static com.android.server.wm.WindowManagerService.H.NOTIFY_APP_TRANSITION_STARTING;
 
+import android.annotation.IntDef;
 import android.app.ActivityManager.TaskSnapshot;
 import android.app.WindowConfiguration;
 import android.graphics.Point;
@@ -38,23 +38,22 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.ArraySet;
-import android.util.Log;
-import android.util.Slog;import android.util.proto.ProtoOutputStream;
+import android.util.Slog;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
+import android.util.proto.ProtoOutputStream;
 import android.view.IRecentsAnimationController;
 import android.view.IRecentsAnimationRunner;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
-
-import com.google.android.collect.Sets;
-
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
 import com.android.server.wm.utils.InsetUtils;
-
+import com.google.android.collect.Sets;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+
 /**
  * Controls a single instance of the remote driven recents animation. In particular, this allows
  * the calling SystemUI to animate the visible task windows as a part of the transition. The remote
@@ -63,19 +62,31 @@
  * app if it requires the animation to be canceled at any time (ie. due to timeout, etc.)
  */
 public class RecentsAnimationController implements DeathRecipient {
-    private static final String TAG = TAG_WITH_CLASS_NAME ? "RecentsAnimationController" : TAG_WM;
-    private static final boolean DEBUG = false;
+    private static final String TAG = RecentsAnimationController.class.getSimpleName();
     private static final long FAILSAFE_DELAY = 1000;
 
+    public static final int REORDER_KEEP_IN_PLACE = 0;
+    public static final int REORDER_MOVE_TO_TOP = 1;
+    public static final int REORDER_MOVE_TO_ORIGINAL_POSITION = 2;
+
+    @IntDef(prefix = { "REORDER_MODE_" }, value = {
+            REORDER_KEEP_IN_PLACE,
+            REORDER_MOVE_TO_TOP,
+            REORDER_MOVE_TO_ORIGINAL_POSITION
+    })
+    public @interface ReorderMode {}
+
     private final WindowManagerService mService;
     private final IRecentsAnimationRunner mRunner;
     private final RecentsAnimationCallbacks mCallbacks;
     private final ArrayList<TaskAnimationAdapter> mPendingAnimations = new ArrayList<>();
     private final int mDisplayId;
-    private final Runnable mFailsafeRunnable = this::cancelAnimation;
+    private final Runnable mFailsafeRunnable = () -> {
+        cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "failSafeRunnable");
+    };
 
     // The recents component app token that is shown behind the visibile tasks
-    private AppWindowToken mHomeAppToken;
+    private AppWindowToken mTargetAppToken;
     private Rect mMinimizedHomeBounds = new Rect();
 
     // We start the RecentsAnimationController in a pending-start state since we need to wait for
@@ -84,16 +95,22 @@
     private boolean mPendingStart = true;
 
     // Set when the animation has been canceled
-    private boolean mCanceled = false;
+    private boolean mCanceled;
 
     // Whether or not the input consumer is enabled. The input consumer must be both registered and
     // enabled for it to start intercepting touch events.
     private boolean mInputConsumerEnabled;
 
-    private Rect mTmpRect = new Rect();
+    // Whether or not the recents animation should cause the primary split-screen stack to be
+    // minimized
+    private boolean mSplitScreenMinimized;
+
+    private final Rect mTmpRect = new Rect();
+
+    private boolean mLinkedToDeathOfRunner;
 
     public interface RecentsAnimationCallbacks {
-        void onAnimationFinished(boolean moveHomeToTop);
+        void onAnimationFinished(@ReorderMode int reorderMode);
     }
 
     private final IRecentsAnimationController mController =
@@ -101,8 +118,9 @@
 
         @Override
         public TaskSnapshot screenshotTask(int taskId) {
-            if (DEBUG) Log.d(TAG, "screenshotTask(" + taskId + "): mCanceled=" + mCanceled);
-            long token = Binder.clearCallingIdentity();
+            if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "screenshotTask(" + taskId + "):"
+                    + " mCanceled=" + mCanceled);
+            final long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mService.getWindowManagerLock()) {
                     if (mCanceled) {
@@ -130,8 +148,9 @@
 
         @Override
         public void finish(boolean moveHomeToTop) {
-            if (DEBUG) Log.d(TAG, "finish(" + moveHomeToTop + "): mCanceled=" + mCanceled);
-            long token = Binder.clearCallingIdentity();
+            if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "finish(" + moveHomeToTop + "):"
+                    + " mCanceled=" + mCanceled);
+            final long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mService.getWindowManagerLock()) {
                     if (mCanceled) {
@@ -141,7 +160,9 @@
 
                 // Note, the callback will handle its own synchronization, do not lock on WM lock
                 // prior to calling the callback
-                mCallbacks.onAnimationFinished(moveHomeToTop);
+                mCallbacks.onAnimationFinished(moveHomeToTop
+                        ? REORDER_MOVE_TO_TOP
+                        : REORDER_MOVE_TO_ORIGINAL_POSITION);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -150,7 +171,7 @@
         @Override
         public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars)
                 throws RemoteException {
-            long token = Binder.clearCallingIdentity();
+            final long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mService.getWindowManagerLock()) {
                     for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
@@ -165,9 +186,9 @@
 
         @Override
         public void setInputConsumerEnabled(boolean enabled) {
-            if (DEBUG) Log.d(TAG, "setInputConsumerEnabled(" + enabled + "): mCanceled="
-                    + mCanceled);
-            long token = Binder.clearCallingIdentity();
+            if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "setInputConsumerEnabled(" + enabled + "):"
+                    + " mCanceled=" + mCanceled);
+            final long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mService.getWindowManagerLock()) {
                     if (mCanceled) {
@@ -182,6 +203,23 @@
                 Binder.restoreCallingIdentity(token);
             }
         }
+
+        @Override
+        public void setSplitScreenMinimized(boolean minimized) {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mService.getWindowManagerLock()) {
+                    if (mCanceled) {
+                        return;
+                    }
+
+                    mSplitScreenMinimized = minimized;
+                    mService.checkSplitScreenMinimizedChanged(true /* animate */);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
     };
 
     /**
@@ -203,7 +241,7 @@
      * because it may call cancelAnimation() which needs to properly clean up the controller
      * in the window manager.
      */
-    public void initialize(SparseBooleanArray recentTaskIds) {
+    public void initialize(int targetActivityType, SparseBooleanArray recentTaskIds) {
         // Make leashes for each of the visible tasks and add it to the recents animation to be
         // started
         final DisplayContent dc = mService.mRoot.getDisplayContent(mDisplayId);
@@ -214,7 +252,7 @@
             final WindowConfiguration config = task.getWindowConfiguration();
             if (config.tasksAreFloating()
                     || config.getWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
-                    || config.getActivityType() == ACTIVITY_TYPE_HOME) {
+                    || config.getActivityType() == targetActivityType) {
                 continue;
             }
             addAnimation(task, !recentTaskIds.get(task.mTaskId));
@@ -222,23 +260,24 @@
 
         // Skip the animation if there is nothing to animate
         if (mPendingAnimations.isEmpty()) {
-            cancelAnimation();
+            cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-noVisibleTasks");
             return;
         }
 
         try {
-            mRunner.asBinder().linkToDeath(this, 0);
+            linkToDeathOfRunner();
         } catch (RemoteException e) {
-            cancelAnimation();
+            cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-failedToLinkToDeath");
             return;
         }
 
-        // Adjust the wallpaper visibility for the showing home activity
-        final AppWindowToken recentsComponentAppToken =
-                dc.getHomeStack().getTopChild().getTopFullscreenAppToken();
+        // Adjust the wallpaper visibility for the showing target activity
+        final AppWindowToken recentsComponentAppToken = dc.getStack(WINDOWING_MODE_UNDEFINED,
+                targetActivityType).getTopChild().getTopFullscreenAppToken();
         if (recentsComponentAppToken != null) {
-            if (DEBUG) Log.d(TAG, "setHomeApp(" + recentsComponentAppToken.getName() + ")");
-            mHomeAppToken = recentsComponentAppToken;
+            if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "setHomeApp("
+                    + recentsComponentAppToken.getName() + ")");
+            mTargetAppToken = recentsComponentAppToken;
             if (recentsComponentAppToken.windowsCanBeWallpaperTarget()) {
                 dc.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
                 dc.setLayoutNeeded();
@@ -251,8 +290,10 @@
         mService.mWindowPlacerLocked.performSurfacePlacement();
     }
 
-    private void addAnimation(Task task, boolean isRecentTaskInvisible) {
-        if (DEBUG) Log.d(TAG, "addAnimation(" + task.getName() + ")");
+    @VisibleForTesting
+    AnimationAdapter addAnimation(Task task, boolean isRecentTaskInvisible) {
+        if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "addAnimation(" + task.getName() + ")");
+        // TODO: Refactor this to use the task's animator
         final SurfaceAnimator anim = new SurfaceAnimator(task, null /* animationFinishedCallback */,
                 mService);
         final TaskAnimationAdapter taskAdapter = new TaskAnimationAdapter(task,
@@ -260,10 +301,20 @@
         anim.startAnimation(task.getPendingTransaction(), taskAdapter, false /* hidden */);
         task.commitPendingTransaction();
         mPendingAnimations.add(taskAdapter);
+        return taskAdapter;
+    }
+
+    @VisibleForTesting
+    void removeAnimation(TaskAnimationAdapter taskAdapter) {
+        if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "removeAnimation("
+                + taskAdapter.mTask.mTaskId + ")");
+        taskAdapter.mTask.setCanAffectSystemUiFlags(true);
+        taskAdapter.mCapturedFinishCallback.onAnimationFinished(taskAdapter);
+        mPendingAnimations.remove(taskAdapter);
     }
 
     void startAnimation() {
-        if (DEBUG) Log.d(TAG, "startAnimation(): mPendingStart=" + mPendingStart
+        if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "startAnimation(): mPendingStart=" + mPendingStart
                 + " mCanceled=" + mCanceled);
         if (!mPendingStart || mCanceled) {
             // Skip starting if we've already started or canceled the animation
@@ -272,24 +323,42 @@
         try {
             final ArrayList<RemoteAnimationTarget> appAnimations = new ArrayList<>();
             for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
-                final RemoteAnimationTarget target =
-                        mPendingAnimations.get(i).createRemoteAnimationApp();
+                final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i);
+                final RemoteAnimationTarget target = taskAdapter.createRemoteAnimationApp();
                 if (target != null) {
                     appAnimations.add(target);
+                } else {
+                    removeAnimation(taskAdapter);
                 }
             }
+
+            // Skip the animation if there is nothing to animate
+            if (appAnimations.isEmpty()) {
+                cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "startAnimation-noAppWindows");
+                return;
+            }
+
             final RemoteAnimationTarget[] appTargets = appAnimations.toArray(
                     new RemoteAnimationTarget[appAnimations.size()]);
             mPendingStart = false;
 
-            final Rect minimizedHomeBounds =
-                    mHomeAppToken != null && mHomeAppToken.inSplitScreenSecondaryWindowingMode()
-                            ? mMinimizedHomeBounds : null;
-            final Rect contentInsets =
-                    mHomeAppToken != null && mHomeAppToken.findMainWindow() != null
-                            ? mHomeAppToken.findMainWindow().mContentInsets : null;
+            final Rect minimizedHomeBounds = mTargetAppToken != null
+                    && mTargetAppToken.inSplitScreenSecondaryWindowingMode()
+                            ? mMinimizedHomeBounds
+                            : null;
+            final Rect contentInsets = mTargetAppToken != null
+                    && mTargetAppToken.findMainWindow() != null
+                            ? mTargetAppToken.findMainWindow().mContentInsets
+                            : null;
             mRunner.onAnimationStart_New(mController, appTargets, contentInsets,
                     minimizedHomeBounds);
+            if (DEBUG_RECENTS_ANIMATIONS) {
+                Slog.d(TAG, "startAnimation(): Notify animation start:");
+                for (int i = 0; i < mPendingAnimations.size(); i++) {
+                    final Task task = mPendingAnimations.get(i).mTask;
+                    Slog.d(TAG, "\t" + task.mTaskId);
+                }
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to start recents animation", e);
         }
@@ -299,8 +368,8 @@
                 reasons).sendToTarget();
     }
 
-    void cancelAnimation() {
-        if (DEBUG) Log.d(TAG, "cancelAnimation()");
+    void cancelAnimation(@ReorderMode int reorderMode, String reason) {
+        if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "cancelAnimation()");
         synchronized (mService.getWindowManagerLock()) {
             if (mCanceled) {
                 // We've already canceled the animation
@@ -314,26 +383,26 @@
                 Slog.e(TAG, "Failed to cancel recents animation", e);
             }
         }
+
         // Clean up and return to the previous app
         // Don't hold the WM lock here as it calls back to AM/RecentsAnimation
-        mCallbacks.onAnimationFinished(false /* moveHomeToTop */);
+        mCallbacks.onAnimationFinished(reorderMode);
     }
 
-    void cleanupAnimation(boolean moveHomeToTop) {
-        if (DEBUG) Log.d(TAG, "cleanupAnimation(): mPendingAnimations="
-                + mPendingAnimations.size());
+    void cleanupAnimation(@ReorderMode int reorderMode) {
+        if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG,
+                "cleanupAnimation(): Notify animation finished mPendingAnimations="
+                        + mPendingAnimations.size() + " reorderMode=" + reorderMode);
         for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
-            final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
-            adapter.mTask.setCanAffectSystemUiFlags(true);
-            if (moveHomeToTop) {
-                adapter.mTask.dontAnimateDimExit();
+            final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i);
+            if (reorderMode == REORDER_MOVE_TO_TOP || reorderMode == REORDER_KEEP_IN_PLACE) {
+                taskAdapter.mTask.dontAnimateDimExit();
             }
-            adapter.mCapturedFinishCallback.onAnimationFinished(adapter);
+            removeAnimation(taskAdapter);
         }
-        mPendingAnimations.clear();
 
-        mRunner.asBinder().unlinkToDeath(this, 0);
-
+        unlinkToDeathOfRunner();
+        // Clear associated input consumers
         mService.mInputMonitor.updateInputWindowsLw(true /*force*/);
         mService.destroyInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
     }
@@ -342,14 +411,28 @@
         mService.mH.postDelayed(mFailsafeRunnable, FAILSAFE_DELAY);
     }
 
+    private void linkToDeathOfRunner() throws RemoteException {
+        if (!mLinkedToDeathOfRunner) {
+            mRunner.asBinder().linkToDeath(this, 0);
+            mLinkedToDeathOfRunner = true;
+        }
+    }
+
+    private void unlinkToDeathOfRunner() {
+        if (mLinkedToDeathOfRunner) {
+            mRunner.asBinder().unlinkToDeath(this, 0);
+            mLinkedToDeathOfRunner = false;
+        }
+    }
+
     @Override
     public void binderDied() {
-        cancelAnimation();
+        cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "binderDied");
     }
 
     void checkAnimationReady(WallpaperController wallpaperController) {
         if (mPendingStart) {
-            final boolean wallpaperReady = !isHomeAppOverWallpaper()
+            final boolean wallpaperReady = !isTargetOverWallpaper()
                     || (wallpaperController.getWallpaperTarget() != null
                             && wallpaperController.wallpaperTransitionReady());
             if (wallpaperReady) {
@@ -358,9 +441,13 @@
         }
     }
 
+    boolean isSplitScreenMinimized() {
+        return mSplitScreenMinimized;
+    }
+
     boolean isWallpaperVisible(WindowState w) {
-        return w != null && w.mAppToken != null && mHomeAppToken == w.mAppToken
-                && isHomeAppOverWallpaper();
+        return w != null && w.mAppToken != null && mTargetAppToken == w.mAppToken
+                && isTargetOverWallpaper();
     }
 
     boolean hasInputConsumerForApp(AppWindowToken appToken) {
@@ -369,12 +456,12 @@
 
     boolean updateInputConsumerForApp(InputConsumerImpl recentsAnimationInputConsumer,
             boolean hasFocus) {
-        // Update the input consumer touchable region to match the home app main window
-        final WindowState homeAppMainWindow = mHomeAppToken != null
-                ? mHomeAppToken.findMainWindow()
+        // Update the input consumer touchable region to match the target app main window
+        final WindowState targetAppMainWindow = mTargetAppToken != null
+                ? mTargetAppToken.findMainWindow()
                 : null;
-        if (homeAppMainWindow != null) {
-            homeAppMainWindow.getBounds(mTmpRect);
+        if (targetAppMainWindow != null) {
+            targetAppMainWindow.getBounds(mTmpRect);
             recentsAnimationInputConsumer.mWindowHandle.hasFocus = hasFocus;
             recentsAnimationInputConsumer.mWindowHandle.touchableRegion.set(mTmpRect);
             return true;
@@ -382,11 +469,20 @@
         return false;
     }
 
-    private boolean isHomeAppOverWallpaper() {
-        if (mHomeAppToken == null) {
+    private boolean isTargetOverWallpaper() {
+        if (mTargetAppToken == null) {
             return false;
         }
-        return mHomeAppToken.windowsCanBeWallpaperTarget();
+        return mTargetAppToken.windowsCanBeWallpaperTarget();
+    }
+
+    boolean isAnimatingTask(Task task) {
+        for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+            if (task == mPendingAnimations.get(i).mTask) {
+                return true;
+            }
+        }
+        return false;
     }
 
     private boolean isAnimatingApp(AppWindowToken appToken) {
@@ -402,25 +498,26 @@
         return false;
     }
 
-    private class TaskAnimationAdapter implements AnimationAdapter {
+    @VisibleForTesting
+    class TaskAnimationAdapter implements AnimationAdapter {
 
         private final Task mTask;
         private SurfaceControl mCapturedLeash;
         private OnAnimationFinishedCallback mCapturedFinishCallback;
         private final boolean mIsRecentTaskInvisible;
         private RemoteAnimationTarget mTarget;
+        private final Point mPosition = new Point();
+        private final Rect mBounds = new Rect();
 
         TaskAnimationAdapter(Task task, boolean isRecentTaskInvisible) {
             mTask = task;
             mIsRecentTaskInvisible = isRecentTaskInvisible;
+            final WindowContainer container = mTask.getParent();
+            container.getRelativePosition(mPosition);
+            container.getBounds(mBounds);
         }
 
         RemoteAnimationTarget createRemoteAnimationApp() {
-            final Point position = new Point();
-            final Rect bounds = new Rect();
-            final WindowContainer container = mTask.getParent();
-            container.getRelativePosition(position);
-            container.getBounds(bounds);
             final WindowState mainWindow = mTask.getTopVisibleAppMainWindow();
             if (mainWindow == null) {
                 return null;
@@ -429,7 +526,7 @@
             InsetUtils.addInsets(insets, mainWindow.mAppToken.getLetterboxInsets());
             mTarget = new RemoteAnimationTarget(mTask.mTaskId, MODE_CLOSING, mCapturedLeash,
                     !mTask.fillsParent(), mainWindow.mWinAnimator.mLastClipRect,
-                    insets, mTask.getPrefixOrderIndex(), position, bounds,
+                    insets, mTask.getPrefixOrderIndex(), mPosition, mBounds,
                     mTask.getWindowConfiguration(), mIsRecentTaskInvisible);
             return mTarget;
         }
@@ -452,13 +549,14 @@
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
                 OnAnimationFinishedCallback finishCallback) {
+            t.setPosition(animationLeash, mPosition.x, mPosition.y);
             mCapturedLeash = animationLeash;
             mCapturedFinishCallback = finishCallback;
         }
 
         @Override
         public void onAnimationCancelled(SurfaceControl animationLeash) {
-            cancelAnimation();
+            cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "taskAnimationAdapterCanceled");
         }
 
         @Override
@@ -480,6 +578,10 @@
             } else {
                 pw.print(prefix); pw.println("Target: null");
             }
+            pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible);
+            pw.println("mPosition=" + mPosition);
+            pw.println("mBounds=" + mBounds);
+            pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible);
         }
 
         @Override
@@ -496,6 +598,10 @@
         final String innerPrefix = prefix + "  ";
         pw.print(prefix); pw.println(RecentsAnimationController.class.getSimpleName() + ":");
         pw.print(innerPrefix); pw.println("mPendingStart=" + mPendingStart);
-        pw.print(innerPrefix); pw.println("mHomeAppToken=" + mHomeAppToken);
+        pw.print(innerPrefix); pw.println("mCanceled=" + mCanceled);
+        pw.print(innerPrefix); pw.println("mInputConsumerEnabled=" + mInputConsumerEnabled);
+        pw.print(innerPrefix); pw.println("mSplitScreenMinimized=" + mSplitScreenMinimized);
+        pw.print(innerPrefix); pw.println("mTargetAppToken=" + mTargetAppToken);
+        pw.print(innerPrefix); pw.println("isTargetOverWallpaper=" + isTargetOverWallpaper());
     }
 }
diff --git a/com/android/server/wm/RemoteAnimationController.java b/com/android/server/wm/RemoteAnimationController.java
index 3be7b23..1b06b2f 100644
--- a/com/android/server/wm/RemoteAnimationController.java
+++ b/com/android/server/wm/RemoteAnimationController.java
@@ -19,6 +19,7 @@
 import static com.android.server.wm.AnimationAdapterProto.REMOTE;
 import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_REMOTE_ANIMATIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
@@ -49,7 +50,9 @@
  * Helper class to run app animations in a remote process.
  */
 class RemoteAnimationController implements DeathRecipient {
-    private static final String TAG = TAG_WITH_CLASS_NAME ? "RemoteAnimationController" : TAG_WM;
+    private static final String TAG = TAG_WITH_CLASS_NAME
+            || (DEBUG_REMOTE_ANIMATIONS && !DEBUG_APP_TRANSITIONS)
+                    ? "RemoteAnimationController" : TAG_WM;
     private static final long TIMEOUT_MS = 2000;
 
     private final WindowManagerService mService;
@@ -57,10 +60,11 @@
     private final ArrayList<RemoteAnimationAdapterWrapper> mPendingAnimations = new ArrayList<>();
     private final Rect mTmpRect = new Rect();
     private final Handler mHandler;
-    private final Runnable mTimeoutRunnable = this::cancelAnimation;
+    private final Runnable mTimeoutRunnable = () -> cancelAnimation("timeoutRunnable");
 
     private FinishedCallback mFinishedCallback;
     private boolean mCanceled;
+    private boolean mLinkedToDeathOfRunner;
 
     RemoteAnimationController(WindowManagerService service,
             RemoteAnimationAdapter remoteAnimationAdapter, Handler handler) {
@@ -79,6 +83,7 @@
      */
     AnimationAdapter createAnimationAdapter(AppWindowToken appWindowToken, Point position,
             Rect stackBounds) {
+        if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "createAnimationAdapter(): token=" + appWindowToken);
         final RemoteAnimationAdapterWrapper adapter = new RemoteAnimationAdapterWrapper(
                 appWindowToken, position, stackBounds);
         mPendingAnimations.add(adapter);
@@ -89,7 +94,10 @@
      * Called when the transition is ready to be started, and all leashes have been set up.
      */
     void goodToGo() {
+        if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo()");
         if (mPendingAnimations.isEmpty() || mCanceled) {
+            if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo(): Animation finished before good to go, canceled="
+                    + mCanceled + " mPendingAnimations=" + mPendingAnimations.size());
             onAnimationFinished();
             return;
         }
@@ -101,25 +109,32 @@
 
         final RemoteAnimationTarget[] animations = createAnimations();
         if (animations.length == 0) {
+            if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo(): No apps to animate");
             onAnimationFinished();
             return;
         }
         mService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
             try {
-                mRemoteAnimationAdapter.getRunner().asBinder().linkToDeath(this, 0);
+                linkToDeathOfRunner();
                 mRemoteAnimationAdapter.getRunner().onAnimationStart(animations, mFinishedCallback);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to start remote animation", e);
                 onAnimationFinished();
             }
+            if (DEBUG_REMOTE_ANIMATIONS) {
+                Slog.d(TAG, "startAnimation(): Notify animation start:");
+                for (int i = 0; i < mPendingAnimations.size(); i++) {
+                    Slog.d(TAG, "\t" + mPendingAnimations.get(i).mAppWindowToken);
+                }
+            } else if (DEBUG_APP_TRANSITIONS) {
+                writeStartDebugStatement();
+            }
         });
         sendRunningRemoteAnimation(true);
-        if (DEBUG_APP_TRANSITIONS) {
-            writeStartDebugStatement();
-        }
     }
 
-    private void cancelAnimation() {
+    private void cancelAnimation(String reason) {
+        if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "cancelAnimation(): reason=" + reason);
         synchronized (mService.getWindowManagerLock()) {
             if (mCanceled) {
                 return;
@@ -142,14 +157,16 @@
     }
 
     private RemoteAnimationTarget[] createAnimations() {
+        if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "createAnimations()");
         final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
         for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
             final RemoteAnimationAdapterWrapper wrapper = mPendingAnimations.get(i);
-            final RemoteAnimationTarget target =
-                    mPendingAnimations.get(i).createRemoteAppAnimation();
+            final RemoteAnimationTarget target = wrapper.createRemoteAppAnimation();
             if (target != null) {
+                if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\tAdd token=" + wrapper.mAppWindowToken);
                 targets.add(target);
             } else {
+                if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\tRemove token=" + wrapper.mAppWindowToken);
 
                 // We can't really start an animation but we still need to make sure to finish the
                 // pending animation that was started by SurfaceAnimator
@@ -163,22 +180,29 @@
     }
 
     private void onAnimationFinished() {
+        if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "onAnimationFinished(): mPendingAnimations="
+                + mPendingAnimations.size());
         mHandler.removeCallbacks(mTimeoutRunnable);
-        mRemoteAnimationAdapter.getRunner().asBinder().unlinkToDeath(this, 0);
         synchronized (mService.mWindowMap) {
+            unlinkToDeathOfRunner();
             releaseFinishedCallback();
             mService.openSurfaceTransaction();
             try {
+                if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "onAnimationFinished(): Notify animation finished:");
                 for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
                     final RemoteAnimationAdapterWrapper adapter = mPendingAnimations.get(i);
                     adapter.mCapturedFinishCallback.onAnimationFinished(adapter);
+                    if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\t" + adapter.mAppWindowToken);
                 }
+            } catch (Exception e) {
+                Slog.e(TAG, "Failed to finish remote animation", e);
+                throw e;
             } finally {
                 mService.closeSurfaceTransaction("RemoteAnimationController#finished");
             }
         }
         sendRunningRemoteAnimation(false);
-        if (DEBUG_APP_TRANSITIONS) Slog.i(TAG, "Finishing remote animation");
+        if (DEBUG_REMOTE_ANIMATIONS) Slog.i(TAG, "Finishing remote animation");
     }
 
     private void invokeAnimationCancelled() {
@@ -204,9 +228,23 @@
         mService.sendSetRunningRemoteAnimation(pid, running);
     }
 
+    private void linkToDeathOfRunner() throws RemoteException {
+        if (!mLinkedToDeathOfRunner) {
+            mRemoteAnimationAdapter.getRunner().asBinder().linkToDeath(this, 0);
+            mLinkedToDeathOfRunner = true;
+        }
+    }
+
+    private void unlinkToDeathOfRunner() {
+        if (mLinkedToDeathOfRunner) {
+            mRemoteAnimationAdapter.getRunner().asBinder().unlinkToDeath(this, 0);
+            mLinkedToDeathOfRunner = false;
+        }
+    }
+
     @Override
     public void binderDied() {
-        cancelAnimation();
+        cancelAnimation("binderDied");
     }
 
     private static final class FinishedCallback extends IRemoteAnimationFinishedCallback.Stub {
@@ -219,6 +257,7 @@
 
         @Override
         public void onAnimationFinished() throws RemoteException {
+            if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "app-onAnimationFinished(): mOuter=" + mOuter);
             final long token = Binder.clearCallingIdentity();
             try {
                 if (mOuter != null) {
@@ -238,6 +277,7 @@
          * to prevent memory leak.
          */
         void release() {
+            if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "app-release(): mOuter=" + mOuter);
             mOuter = null;
         }
     };
@@ -301,6 +341,7 @@
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
                 OnAnimationFinishedCallback finishCallback) {
+            if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "startAnimation");
 
             // Restore z-layering, position and stack crop until client has a chance to modify it.
             t.setLayer(animationLeash, mAppWindowToken.getPrefixOrderIndex());
diff --git a/com/android/server/wm/RootWindowContainer.java b/com/android/server/wm/RootWindowContainer.java
index 52d8177..fd965fb 100644
--- a/com/android/server/wm/RootWindowContainer.java
+++ b/com/android/server/wm/RootWindowContainer.java
@@ -417,6 +417,14 @@
         }, true /* traverseTopToBottom */);
     }
 
+    void updateHiddenWhileSuspendedState(final ArraySet<String> packages, final boolean suspended) {
+        forAllWindows((w) -> {
+            if (packages.contains(w.getOwningPackage())) {
+                w.setHiddenWhileSuspended(suspended);
+            }
+        }, false);
+    }
+
     void updateAppOpsState() {
         forAllWindows((w) -> {
             w.updateAppOpsState();
diff --git a/com/android/server/wm/Session.java b/com/android/server/wm/Session.java
index 662d51d..4003d5a 100644
--- a/com/android/server/wm/Session.java
+++ b/com/android/server/wm/Session.java
@@ -194,7 +194,7 @@
             InputChannel outInputChannel) {
         return addToDisplay(window, seq, attrs, viewVisibility, Display.DEFAULT_DISPLAY,
                 new Rect() /* outFrame */, outContentInsets, outStableInsets, null /* outOutsets */,
-                null /* cutout */, outInputChannel);
+                new DisplayCutout.ParcelableWrapper()  /* cutout */, outInputChannel);
     }
 
     @Override
@@ -218,7 +218,7 @@
             int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets) {
         return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                 new Rect() /* outFrame */, outContentInsets, outStableInsets, null /* outOutsets */,
-                null /* cutout */, null /* outInputChannel */);
+                new DisplayCutout.ParcelableWrapper() /* cutout */, null /* outInputChannel */);
     }
 
     @Override
@@ -233,16 +233,16 @@
 
     @Override
     public int relayout(IWindow window, int seq, WindowManager.LayoutParams attrs,
-            int requestedWidth, int requestedHeight, int viewFlags,
-            int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
-            Rect outVisibleInsets, Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
-            DisplayCutout.ParcelableWrapper cutout,
-            MergedConfiguration mergedConfiguration, Surface outSurface) {
+            int requestedWidth, int requestedHeight, int viewFlags, int flags, long frameNumber,
+            Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets,
+            Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
+            DisplayCutout.ParcelableWrapper cutout, MergedConfiguration mergedConfiguration,
+            Surface outSurface) {
         if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from "
                 + Binder.getCallingPid());
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mRelayoutTag);
         int res = mService.relayoutWindow(this, window, seq, attrs,
-                requestedWidth, requestedHeight, viewFlags, flags,
+                requestedWidth, requestedHeight, viewFlags, flags, frameNumber,
                 outFrame, outOverscanInsets, outContentInsets, outVisibleInsets,
                 outStableInsets, outsets, outBackdropFrame, cutout,
                 mergedConfiguration, outSurface);
diff --git a/com/android/server/wm/SurfaceAnimationRunner.java b/com/android/server/wm/SurfaceAnimationRunner.java
index 98fcb0b..7211533 100644
--- a/com/android/server/wm/SurfaceAnimationRunner.java
+++ b/com/android/server/wm/SurfaceAnimationRunner.java
@@ -220,6 +220,9 @@
     }
 
     private void applyTransformation(RunningAnimation a, Transaction t, long currentPlayTime) {
+        if (a.mAnimSpec.needsEarlyWakeup()) {
+            t.setEarlyWakeup();
+        }
         a.mAnimSpec.apply(t, a.mLeash, currentPlayTime);
     }
 
diff --git a/com/android/server/wm/Task.java b/com/android/server/wm/Task.java
index e8d3210..95223d8 100644
--- a/com/android/server/wm/Task.java
+++ b/com/android/server/wm/Task.java
@@ -44,6 +44,7 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.Surface;
 
+import android.view.SurfaceControl;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
@@ -559,6 +560,23 @@
                 && !mStack.isAnimatingBoundsToFullscreen() && !mPreserveNonFloatingState;
     }
 
+    @Override
+    public SurfaceControl getAnimationLeashParent() {
+        // Reparent to the animation layer so that we aren't clipped by the non-minimized
+        // stack bounds, currently we only animate the task for the recents animation
+        return getAppAnimationLayer(false /* boosted */);
+    }
+
+    boolean isTaskAnimating() {
+        final RecentsAnimationController recentsAnim = mService.getRecentsAnimationController();
+        if (recentsAnim != null) {
+            if (recentsAnim.isAnimatingTask(this)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     WindowState getTopVisibleAppMainWindow() {
         final AppWindowToken token = getTopVisibleAppToken();
         return token != null ? token.findMainWindow() : null;
diff --git a/com/android/server/wm/TaskSnapshotSurface.java b/com/android/server/wm/TaskSnapshotSurface.java
index a9e53a1..5721bd8 100644
--- a/com/android/server/wm/TaskSnapshotSurface.java
+++ b/com/android/server/wm/TaskSnapshotSurface.java
@@ -173,6 +173,8 @@
             windowFlags = topFullscreenWindow.getAttrs().flags;
             windowPrivateFlags = topFullscreenWindow.getAttrs().privateFlags;
 
+            layoutParams.packageName = mainWindow.getAttrs().packageName;
+            layoutParams.windowAnimations = mainWindow.getAttrs().windowAnimations;
             layoutParams.dimAmount = mainWindow.getAttrs().dimAmount;
             layoutParams.type = TYPE_APPLICATION_STARTING;
             layoutParams.format = snapshot.getSnapshot().getFormat();
@@ -213,8 +215,8 @@
                 currentOrientation);
         window.setOuter(snapshotSurface);
         try {
-            session.relayout(window, window.mSeq, layoutParams, -1, -1, View.VISIBLE, 0, tmpFrame,
-                    tmpRect, tmpContentInsets, tmpRect, tmpStableInsets, tmpRect, tmpRect,
+            session.relayout(window, window.mSeq, layoutParams, -1, -1, View.VISIBLE, 0, -1,
+                    tmpFrame, tmpRect, tmpContentInsets, tmpRect, tmpStableInsets, tmpRect, tmpRect,
                     tmpCutout, tmpMergedConfiguration, surface);
         } catch (RemoteException e) {
             // Local call.
diff --git a/com/android/server/wm/TaskStack.java b/com/android/server/wm/TaskStack.java
index 62754ad..ae9e802 100644
--- a/com/android/server/wm/TaskStack.java
+++ b/com/android/server/wm/TaskStack.java
@@ -33,12 +33,11 @@
 import static android.view.WindowManager.DOCKED_RIGHT;
 import static android.view.WindowManager.DOCKED_TOP;
 import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.StackProto.ADJUSTED_BOUNDS;
 import static com.android.server.wm.StackProto.ADJUSTED_FOR_IME;
 import static com.android.server.wm.StackProto.ADJUST_DIVIDER_AMOUNT;
 import static com.android.server.wm.StackProto.ADJUST_IME_AMOUNT;
+import static com.android.server.wm.StackProto.ANIMATING_BOUNDS;
 import static com.android.server.wm.StackProto.ANIMATION_BACKGROUND_SURFACE_IS_DIMMING;
 import static com.android.server.wm.StackProto.BOUNDS;
 import static com.android.server.wm.StackProto.DEFER_REMOVAL;
@@ -47,6 +46,8 @@
 import static com.android.server.wm.StackProto.MINIMIZE_AMOUNT;
 import static com.android.server.wm.StackProto.TASKS;
 import static com.android.server.wm.StackProto.WINDOW_CONTAINER;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.annotation.CallSuper;
 import android.content.res.Configuration;
@@ -62,12 +63,10 @@
 import android.view.DisplayInfo;
 import android.view.Surface;
 import android.view.SurfaceControl;
-
 import com.android.internal.policy.DividerSnapAlgorithm;
 import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
 import com.android.internal.policy.DockedDividerUtils;
 import com.android.server.EventLogTags;
-
 import java.io.PrintWriter;
 
 public class TaskStack extends WindowContainer<Task> implements
@@ -829,6 +828,14 @@
             }
         }
 
+        if (inSplitScreenSecondaryWindowingMode()) {
+            // When the stack is resized due to entering split screen secondary, offset the
+            // windows to compensate for the new stack position.
+            forAllWindows(w -> {
+                w.mWinAnimator.setOffsetPositionForStackResize(true);
+            }, true);
+        }
+
         updateDisplayInfo(bounds);
         updateSurfaceBounds();
     }
@@ -1333,6 +1340,20 @@
         return mMinimizeAmount != 0f;
     }
 
+    /**
+     * @return {@code true} if we have a {@link Task} that is animating (currently only used for the
+     *         recents animation); {@code false} otherwise.
+     */
+    boolean isTaskAnimating() {
+        for (int j = mChildren.size() - 1; j >= 0; j--) {
+            final Task task = mChildren.get(j);
+            if (task.isTaskAnimating()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @CallSuper
     @Override
     public void writeToProto(ProtoOutputStream proto, long fieldId, boolean trim) {
@@ -1351,6 +1372,7 @@
         proto.write(ADJUST_IME_AMOUNT, mAdjustImeAmount);
         proto.write(ADJUST_DIVIDER_AMOUNT, mAdjustDividerAmount);
         mAdjustedBounds.writeToProto(proto, ADJUSTED_BOUNDS);
+        proto.write(ANIMATING_BOUNDS, mBoundsAnimating);
         proto.end(token);
     }
 
@@ -1687,6 +1709,25 @@
         }
     }
 
+    @Override
+    public boolean shouldDeferStartOnMoveToFullscreen() {
+        // Workaround for the recents animation -- normally we need to wait for the new activity to
+        // show before starting the PiP animation, but because we start and show the home activity
+        // early for the recents animation prior to the PiP animation starting, there is no
+        // subsequent all-drawn signal. In this case, we can skip the pause when the home stack is
+        // already visible and drawn.
+        final TaskStack homeStack = mDisplayContent.getHomeStack();
+        if (homeStack == null) {
+            return true;
+        }
+        final Task homeTask = homeStack.getTopChild();
+        final AppWindowToken homeApp = homeTask.getTopVisibleAppToken();
+        if (!homeTask.isVisible() || homeApp == null) {
+            return true;
+        }
+        return !homeApp.allDrawn;
+    }
+
     /**
      * @return True if we are currently animating the pinned stack from fullscreen to non-fullscreen
      *         bounds and we have a deferred PiP mode changed callback set with the animation.
diff --git a/com/android/server/wm/WallpaperController.java b/com/android/server/wm/WallpaperController.java
index c509980..c63da77 100644
--- a/com/android/server/wm/WallpaperController.java
+++ b/com/android/server/wm/WallpaperController.java
@@ -26,6 +26,7 @@
 
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
@@ -618,7 +619,8 @@
 
             // If there was a recents animation in progress, cancel that animation
             if (mService.getRecentsAnimationController() != null) {
-                mService.getRecentsAnimationController().cancelAnimation();
+                mService.getRecentsAnimationController().cancelAnimation(
+                        REORDER_MOVE_TO_ORIGINAL_POSITION, "wallpaperDrawPendingTimeout");
             }
             return true;
         }
diff --git a/com/android/server/wm/WindowAnimationSpec.java b/com/android/server/wm/WindowAnimationSpec.java
index 7b7cb30..548e23a 100644
--- a/com/android/server/wm/WindowAnimationSpec.java
+++ b/com/android/server/wm/WindowAnimationSpec.java
@@ -47,21 +47,24 @@
     private final Point mPosition = new Point();
     private final ThreadLocal<TmpValues> mThreadLocalTmps = ThreadLocal.withInitial(TmpValues::new);
     private final boolean mCanSkipFirstFrame;
+    private final boolean mIsAppAnimation;
     private final Rect mStackBounds = new Rect();
     private int mStackClipMode;
     private final Rect mTmpRect = new Rect();
 
     public WindowAnimationSpec(Animation animation, Point position, boolean canSkipFirstFrame)  {
-        this(animation, position, null /* stackBounds */, canSkipFirstFrame, STACK_CLIP_NONE);
+        this(animation, position, null /* stackBounds */, canSkipFirstFrame, STACK_CLIP_NONE,
+                false /* isAppAnimation */);
     }
 
     public WindowAnimationSpec(Animation animation, Point position, Rect stackBounds,
-            boolean canSkipFirstFrame, int stackClipMode) {
+            boolean canSkipFirstFrame, int stackClipMode, boolean isAppAnimation) {
         mAnimation = animation;
         if (position != null) {
             mPosition.set(position.x, position.y);
         }
         mCanSkipFirstFrame = canSkipFirstFrame;
+        mIsAppAnimation = isAppAnimation;
         mStackClipMode = stackClipMode;
         if (stackBounds != null) {
             mStackBounds.set(stackBounds);
@@ -135,6 +138,11 @@
     }
 
     @Override
+    public boolean needsEarlyWakeup() {
+        return mIsAppAnimation;
+    }
+
+    @Override
     public void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.println(mAnimation);
     }
diff --git a/com/android/server/wm/WindowManagerDebugConfig.java b/com/android/server/wm/WindowManagerDebugConfig.java
index 9d9805a..990eb97 100644
--- a/com/android/server/wm/WindowManagerDebugConfig.java
+++ b/com/android/server/wm/WindowManagerDebugConfig.java
@@ -74,6 +74,9 @@
     static final boolean SHOW_STACK_CRAWLS = false;
     static final boolean DEBUG_WINDOW_CROP = false;
     static final boolean DEBUG_UNKNOWN_APP_VISIBILITY = false;
+    // TODO (b/73188263): Reset debugging flags
+    static final boolean DEBUG_RECENTS_ANIMATIONS = true;
+    static final boolean DEBUG_REMOTE_ANIMATIONS = DEBUG_APP_TRANSITIONS || true;
 
     static final String TAG_KEEP_SCREEN_ON = "DebugKeepScreenOn";
     static final boolean DEBUG_KEEP_SCREEN_ON = false;
diff --git a/com/android/server/wm/WindowManagerService.java b/com/android/server/wm/WindowManagerService.java
index f1cd46b..407312a 100644
--- a/com/android/server/wm/WindowManagerService.java
+++ b/com/android/server/wm/WindowManagerService.java
@@ -128,6 +128,7 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
@@ -264,6 +265,7 @@
 import java.net.Socket;
 import java.text.DateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 /** {@hide} */
@@ -426,6 +428,7 @@
     final ActivityManagerInternal mAmInternal;
 
     final AppOpsManager mAppOps;
+    final PackageManagerInternal mPmInternal;
 
     final DisplaySettings mDisplaySettings;
 
@@ -1006,6 +1009,22 @@
         mAppOps.startWatchingMode(OP_SYSTEM_ALERT_WINDOW, null, opListener);
         mAppOps.startWatchingMode(AppOpsManager.OP_TOAST_WINDOW, null, opListener);
 
+        mPmInternal = LocalServices.getService(PackageManagerInternal.class);
+        final IntentFilter suspendPackagesFilter = new IntentFilter();
+        suspendPackagesFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+        suspendPackagesFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+        context.registerReceiverAsUser(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final String[] affectedPackages =
+                        intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+                final boolean suspended =
+                        Intent.ACTION_PACKAGES_SUSPENDED.equals(intent.getAction());
+                updateHiddenWhileSuspendedState(new ArraySet<>(Arrays.asList(affectedPackages)),
+                        suspended);
+            }
+        }, UserHandle.ALL, suspendPackagesFilter, null, null);
+
         // Get persisted window scale setting
         mWindowAnimationScaleSetting = Settings.Global.getFloat(context.getContentResolver(),
                 Settings.Global.WINDOW_ANIMATION_SCALE, mWindowAnimationScaleSetting);
@@ -1224,6 +1243,10 @@
                     Slog.w(TAG_WM, "Attempted to add window with exiting application token "
                           + token + ".  Aborting.");
                     return WindowManagerGlobal.ADD_APP_EXITING;
+                } else if (type == TYPE_APPLICATION_STARTING && atoken.startingWindow != null) {
+                    Slog.w(TAG_WM, "Attempted to add starting window to token with already existing"
+                            + " starting window");
+                    return WindowManagerGlobal.ADD_DUPLICATE_ADD;
                 }
             } else if (rootType == TYPE_INPUT_METHOD) {
                 if (token.windowType != TYPE_INPUT_METHOD) {
@@ -1360,6 +1383,10 @@
 
             win.initAppOpsState();
 
+            final boolean suspended = mPmInternal.isPackageSuspended(win.getOwningPackage(),
+                    UserHandle.getUserId(win.getOwningUid()));
+            win.setHiddenWhileSuspended(suspended);
+
             final boolean hideSystemAlertWindows = !mHidingNonSystemOverlayWindows.isEmpty();
             win.setForceHideNonSystemOverlayWindowIfNeeded(hideSystemAlertWindows);
 
@@ -1619,6 +1646,8 @@
         if (DEBUG_ADD_REMOVE) Slog.v(TAG_WM, "postWindowRemoveCleanupLocked: " + win);
         mWindowMap.remove(win.mClient.asBinder());
 
+        markForSeamlessRotation(win, false);
+
         win.resetAppOpsState();
 
         if (mCurrentFocus == null) {
@@ -1683,6 +1712,12 @@
         dc.computeImeTarget(true /* updateImeTarget */);
     }
 
+    private void updateHiddenWhileSuspendedState(ArraySet<String> packages, boolean suspended) {
+        synchronized (mWindowMap) {
+            mRoot.updateHiddenWhileSuspendedState(packages, suspended);
+        }
+    }
+
     private void updateAppOpsState() {
         synchronized(mWindowMap) {
             mRoot.updateAppOpsState();
@@ -1800,10 +1835,9 @@
         }
     }
 
-    public int relayoutWindow(Session session, IWindow client, int seq,
-            LayoutParams attrs, int requestedWidth,
-            int requestedHeight, int viewVisibility, int flags,
-            Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
+    public int relayoutWindow(Session session, IWindow client, int seq, LayoutParams attrs,
+            int requestedWidth, int requestedHeight, int viewVisibility, int flags,
+            long frameNumber, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
             Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame,
             DisplayCutout.ParcelableWrapper outCutout, MergedConfiguration mergedConfiguration,
             Surface outSurface) {
@@ -1830,6 +1864,7 @@
                 win.setRequestedSize(requestedWidth, requestedHeight);
             }
 
+            win.setFrameNumber(frameNumber);
             int attrChanges = 0;
             int flagChanges = 0;
             if (attrs != null) {
@@ -1899,7 +1934,15 @@
                 winAnimator.setOpaqueLocked(false);
             }
 
-            boolean imMayMove = (flagChanges & (FLAG_ALT_FOCUSABLE_IM | FLAG_NOT_FOCUSABLE)) != 0;
+            final int oldVisibility = win.mViewVisibility;
+
+            // If the window is becoming visible, visibleOrAdding may change which may in turn
+            // change the IME target.
+            final boolean becameVisible =
+                    (oldVisibility == View.INVISIBLE || oldVisibility == View.GONE)
+                            && viewVisibility == View.VISIBLE;
+            boolean imMayMove = (flagChanges & (FLAG_ALT_FOCUSABLE_IM | FLAG_NOT_FOCUSABLE)) != 0
+                    || becameVisible;
             final boolean isDefaultDisplay = win.isDefaultDisplay();
             boolean focusMayChange = isDefaultDisplay && (win.mViewVisibility != viewVisibility
                     || ((flagChanges & FLAG_NOT_FOCUSABLE) != 0)
@@ -1915,7 +1958,6 @@
             win.mRelayoutCalled = true;
             win.mInRelayout = true;
 
-            final int oldVisibility = win.mViewVisibility;
             win.mViewVisibility = viewVisibility;
             if (DEBUG_SCREEN_ON) {
                 RuntimeException stack = new RuntimeException();
@@ -2658,7 +2700,7 @@
         }
     }
 
-    public void initializeRecentsAnimation(
+    public void initializeRecentsAnimation(int targetActivityType,
             IRecentsAnimationRunner recentsAnimationRunner,
             RecentsAnimationController.RecentsAnimationCallbacks callbacks, int displayId,
             SparseBooleanArray recentTaskIds) {
@@ -2666,7 +2708,7 @@
             mRecentsAnimationController = new RecentsAnimationController(this,
                     recentsAnimationRunner, callbacks, displayId);
             mAppTransition.updateBooster();
-            mRecentsAnimationController.initialize(recentTaskIds);
+            mRecentsAnimationController.initialize(targetActivityType, recentTaskIds);
         }
     }
 
@@ -2687,20 +2729,26 @@
         }
     }
 
-    public void cancelRecentsAnimation() {
+    /**
+     * Cancels any running recents animation. The caller should NOT hold the WM lock while calling
+     * this method, as it can call back into AM, and locking will be done in the animation
+     * controller itself.
+     */
+    public void cancelRecentsAnimation(@RecentsAnimationController.ReorderMode int reorderMode,
+            String reason) {
         // Note: Do not hold the WM lock, this will lock appropriately in the call which also
         // calls through to AM/RecentsAnimation.onAnimationFinished()
         if (mRecentsAnimationController != null) {
             // This call will call through to cleanupAnimation() below after the animation is
             // canceled
-            mRecentsAnimationController.cancelAnimation();
+            mRecentsAnimationController.cancelAnimation(reorderMode, reason);
         }
     }
 
-    public void cleanupRecentsAnimation(boolean moveHomeToTop) {
+    public void cleanupRecentsAnimation(@RecentsAnimationController.ReorderMode int reorderMode) {
         synchronized (mWindowMap) {
             if (mRecentsAnimationController != null) {
-                mRecentsAnimationController.cleanupAnimation(moveHomeToTop);
+                mRecentsAnimationController.cleanupAnimation(reorderMode);
                 mRecentsAnimationController = null;
                 mAppTransition.updateBooster();
             }
diff --git a/com/android/server/wm/WindowState.java b/com/android/server/wm/WindowState.java
index 54c2e9b..da5bc73 100644
--- a/com/android/server/wm/WindowState.java
+++ b/com/android/server/wm/WindowState.java
@@ -20,6 +20,8 @@
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.MODE_DEFAULT;
 import static android.app.AppOpsManager.OP_NONE;
+import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
+import static android.app.AppOpsManager.OP_TOAST_WINDOW;
 import static android.os.PowerManager.DRAW_WAKE_LOCK;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -64,7 +66,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
-import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType;
@@ -178,6 +179,7 @@
 import android.util.Slog;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
+import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 import android.view.Gravity;
@@ -266,6 +268,8 @@
      * animation is done.
      */
     boolean mPolicyVisibilityAfterAnim = true;
+    // overlay window is hidden because the owning app is suspended
+    private boolean mHiddenWhileSuspended;
     private boolean mAppOpVisibility = true;
     boolean mPermanentlyHidden; // the window should never be shown again
     // This is a non-system overlay window that is currently force hidden.
@@ -632,6 +636,11 @@
     private PowerManagerWrapper mPowerManagerWrapper;
 
     /**
+     * A frame number in which changes requested in this layout will be rendered.
+     */
+    private long mFrameNumber = -1;
+
+    /**
      * Compares two window sub-layers and returns -1 if the first is lesser than the second in terms
      * of z-order and 1 otherwise.
      */
@@ -1366,6 +1375,7 @@
         // Window was not laid out for this display yet, so make sure mLayoutSeq does not match.
         if (dc != null) {
             mLayoutSeq = dc.mLayoutSeq - 1;
+            mInputWindowHandle.displayId = dc.getDisplayId();
         }
     }
 
@@ -1378,7 +1388,7 @@
     public int getDisplayId() {
         final DisplayContent displayContent = getDisplayContent();
         if (displayContent == null) {
-            return -1;
+            return Display.INVALID_DISPLAY;
         }
         return displayContent.getDisplayId();
     }
@@ -1996,6 +2006,11 @@
                     // Try starting an animation.
                     if (mWinAnimator.applyAnimationLocked(transit, false)) {
                         mAnimatingExit = true;
+
+                        // mAnimatingExit affects canAffectSystemUiFlags(). Run layout such that
+                        // any change from that is performed immediately.
+                        setDisplayLayoutNeeded();
+                        mService.requestTraversal();
                     }
                     //TODO (multidisplay): Magnification is supported only for the default display.
                     if (mService.mAccessibilityController != null && displayId == DEFAULT_DISPLAY) {
@@ -2482,6 +2497,10 @@
             // to handle their windows being removed from under them.
             return false;
         }
+        if (mHiddenWhileSuspended) {
+            // Being hidden due to owner package being suspended.
+            return false;
+        }
         if (mForceHideNonSystemOverlayWindow) {
             // This is an alert window that is currently force hidden.
             return false;
@@ -2578,6 +2597,22 @@
         }
     }
 
+    void setHiddenWhileSuspended(boolean hide) {
+        if (mOwnerCanAddInternalSystemWindow
+                || (!isSystemAlertWindowType(mAttrs.type) && mAttrs.type != TYPE_TOAST)) {
+            return;
+        }
+        if (mHiddenWhileSuspended == hide) {
+            return;
+        }
+        mHiddenWhileSuspended = hide;
+        if (hide) {
+            hideLw(true, true);
+        } else {
+            showLw(true, true);
+        }
+    }
+
     private void setAppOpVisibilityLw(boolean state) {
         if (mAppOpVisibility != state) {
             mAppOpVisibility = state;
@@ -3298,7 +3333,8 @@
             pw.println(Integer.toHexString(mSystemUiVisibility));
         }
         if (!mPolicyVisibility || !mPolicyVisibilityAfterAnim || !mAppOpVisibility
-                || isParentWindowHidden()|| mPermanentlyHidden || mForceHideNonSystemOverlayWindow) {
+                || isParentWindowHidden()|| mPermanentlyHidden || mForceHideNonSystemOverlayWindow
+                || mHiddenWhileSuspended) {
             pw.print(prefix); pw.print("mPolicyVisibility=");
                     pw.print(mPolicyVisibility);
                     pw.print(" mPolicyVisibilityAfterAnim=");
@@ -3307,6 +3343,7 @@
                     pw.print(mAppOpVisibility);
                     pw.print(" parentHidden="); pw.print(isParentWindowHidden());
                     pw.print(" mPermanentlyHidden="); pw.print(mPermanentlyHidden);
+                    pw.print(" mHiddenWhileSuspended="); pw.print(mHiddenWhileSuspended);
                     pw.print(" mForceHideNonSystemOverlayWindow="); pw.println(
                     mForceHideNonSystemOverlayWindow);
         }
@@ -4623,7 +4660,7 @@
                 mLastSurfaceInsets.set(mAttrs.surfaceInsets);
                 t.deferTransactionUntil(mSurfaceControl,
                         mWinAnimator.mSurfaceController.mSurfaceControl.getHandle(),
-                        mAttrs.frameNumber);
+                        getFrameNumber());
             }
         }
     }
@@ -4739,6 +4776,14 @@
         return mService.mInputMethodTarget == this;
     }
 
+    long getFrameNumber() {
+        return mFrameNumber;
+    }
+
+    void setFrameNumber(long frameNumber) {
+        mFrameNumber = frameNumber;
+    }
+
     private final class MoveAnimationSpec implements AnimationSpec {
 
         private final long mDuration;
diff --git a/com/android/server/wm/WindowStateAnimator.java b/com/android/server/wm/WindowStateAnimator.java
index e92d460..ab5e24a 100644
--- a/com/android/server/wm/WindowStateAnimator.java
+++ b/com/android/server/wm/WindowStateAnimator.java
@@ -67,7 +67,6 @@
 
 import com.android.server.policy.WindowManagerPolicy;
 
-import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
 /**
@@ -217,6 +216,12 @@
     int mXOffset = 0;
     int mYOffset = 0;
 
+    /**
+     * A flag to determine if the WSA needs to offset its position to compensate for the stack's
+     * position update before the WSA surface has resized.
+     */
+    private boolean mOffsetPositionForStackResize;
+
     private final Rect mTmpSize = new Rect();
 
     private final SurfaceControl.Transaction mReparentTransaction = new SurfaceControl.Transaction();
@@ -230,6 +235,8 @@
     // once per animation.
     boolean mPipAnimationStarted = false;
 
+    private final Point mTmpPos = new Point();
+
     WindowStateAnimator(final WindowState win) {
         final WindowManagerService service = win.mService;
 
@@ -498,6 +505,8 @@
             mSurfaceController = new WindowSurfaceController(mSession.mSurfaceSession,
                     attrs.getTitle().toString(), width, height, format, flags, this,
                     windowType, ownerUid);
+
+            setOffsetPositionForStackResize(false);
             mSurfaceFormat = format;
 
             w.setHasSurface(true);
@@ -859,7 +868,8 @@
         // However, this would be unsafe, as the client may be in the middle
         // of producing a frame at the old size, having just completed layout
         // to find the surface size changed underneath it.
-        if (!w.mRelayoutCalled || w.mInRelayout) {
+        final boolean relayout = !w.mRelayoutCalled || w.mInRelayout;
+        if (relayout) {
             mSurfaceResized = mSurfaceController.setSizeInTransaction(
                     mTmpSize.width(), mTmpSize.height(), recoveringMemory);
         } else {
@@ -996,7 +1006,38 @@
             mPipAnimationStarted = false;
 
             if (!w.mSeamlesslyRotated) {
-                mSurfaceController.setPositionInTransaction(mXOffset, mYOffset, recoveringMemory);
+                // Used to offset the WSA when stack position changes before a resize.
+                int xOffset = mXOffset;
+                int yOffset = mYOffset;
+                if (mOffsetPositionForStackResize) {
+                    if (relayout) {
+                        // Once a relayout is called, reset the offset back to 0 and defer
+                        // setting it until a new frame with the updated size. This ensures that
+                        // the WS position is reset (so the stack position is shown) at the same
+                        // time that the buffer size changes.
+                        setOffsetPositionForStackResize(false);
+                        mSurfaceController.deferTransactionUntil(mSurfaceController.getHandle(),
+                                mWin.getFrameNumber());
+                    } else {
+                        final TaskStack stack = mWin.getStack();
+                        mTmpPos.x = 0;
+                        mTmpPos.y = 0;
+                        if (stack != null) {
+                            stack.getRelativePosition(mTmpPos);
+                        }
+
+                        xOffset = -mTmpPos.x;
+                        yOffset = -mTmpPos.y;
+
+                        // Crop also needs to be extended so the bottom isn't cut off when the WSA
+                        // position is moved.
+                        if (clipRect != null) {
+                            clipRect.right += mTmpPos.x;
+                            clipRect.bottom += mTmpPos.y;
+                        }
+                    }
+                }
+                mSurfaceController.setPositionInTransaction(xOffset, yOffset, recoveringMemory);
             }
         }
 
@@ -1499,4 +1540,8 @@
     int getLayer() {
         return mLastLayer;
     }
+
+    void setOffsetPositionForStackResize(boolean offsetPositionForStackResize) {
+        mOffsetPositionForStackResize = offsetPositionForStackResize;
+    }
 }
diff --git a/com/android/settingslib/TwoTargetPreference.java b/com/android/settingslib/TwoTargetPreference.java
index 8b39f60..02b68d8 100644
--- a/com/android/settingslib/TwoTargetPreference.java
+++ b/com/android/settingslib/TwoTargetPreference.java
@@ -16,6 +16,7 @@
 
 package com.android.settingslib;
 
+import android.annotation.IntDef;
 import android.content.Context;
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceViewHolder;
@@ -24,10 +25,24 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 public class TwoTargetPreference extends Preference {
 
-    private boolean mUseSmallIcon;
+    @IntDef({ICON_SIZE_DEFAULT, ICON_SIZE_MEDIUM, ICON_SIZE_SMALL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface IconSize {
+    }
+
+    public static final int ICON_SIZE_DEFAULT = 0;
+    public static final int ICON_SIZE_MEDIUM = 1;
+    public static final int ICON_SIZE_SMALL = 2;
+
+    @IconSize
+    private int mIconSize;
     private int mSmallIconSize;
+    private int mMediumIconSize;
 
     public TwoTargetPreference(Context context, AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
@@ -54,22 +69,30 @@
         setLayoutResource(R.layout.preference_two_target);
         mSmallIconSize = context.getResources().getDimensionPixelSize(
                 R.dimen.two_target_pref_small_icon_size);
+        mMediumIconSize = context.getResources().getDimensionPixelSize(
+                R.dimen.two_target_pref_medium_icon_size);
         final int secondTargetResId = getSecondTargetResId();
         if (secondTargetResId != 0) {
             setWidgetLayoutResource(secondTargetResId);
         }
     }
 
-    public void setUseSmallIcon(boolean useSmallIcon) {
-        mUseSmallIcon = useSmallIcon;
+    public void setIconSize(@IconSize int iconSize) {
+        mIconSize = iconSize;
     }
 
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
-        if (mUseSmallIcon) {
-            ImageView icon = holder.itemView.findViewById(android.R.id.icon);
-            icon.setLayoutParams(new LinearLayout.LayoutParams(mSmallIconSize, mSmallIconSize));
+        final ImageView icon = holder.itemView.findViewById(android.R.id.icon);
+        switch (mIconSize) {
+            case ICON_SIZE_SMALL:
+                icon.setLayoutParams(new LinearLayout.LayoutParams(mSmallIconSize, mSmallIconSize));
+                break;
+            case ICON_SIZE_MEDIUM:
+                icon.setLayoutParams(
+                        new LinearLayout.LayoutParams(mMediumIconSize, mMediumIconSize));
+                break;
         }
         final View divider = holder.findViewById(R.id.two_target_divider);
         final View widgetFrame = holder.findViewById(android.R.id.widget_frame);
diff --git a/com/android/settingslib/bluetooth/BluetoothCallback.java b/com/android/settingslib/bluetooth/BluetoothCallback.java
index 4a6df50..bab59f1 100644
--- a/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -29,5 +29,5 @@
     void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState);
     void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state);
     void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile);
-    void onProfileAudioStateChanged(int bluetoothProfile, int state);
+    void onAudioModeChanged();
 }
diff --git a/com/android/settingslib/bluetooth/BluetoothEventManager.java b/com/android/settingslib/bluetooth/BluetoothEventManager.java
index b74b2cd..06fe4de 100644
--- a/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.telephony.TelephonyManager;
 import android.util.Log;
 
 import com.android.settingslib.R;
@@ -119,6 +120,12 @@
         addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
                    new ActiveDeviceChangedHandler());
 
+        // Headset state changed broadcasts
+        addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
+                new AudioModeChangedHandler());
+        addHandler(TelephonyManager.ACTION_PHONE_STATE_CHANGED,
+                new AudioModeChangedHandler());
+
         mContext.registerReceiver(mBroadcastReceiver, mAdapterIntentFilter, null, mReceiverHandler);
         mContext.registerReceiver(mProfileBroadcastReceiver, mProfileIntentFilter, null, mReceiverHandler);
     }
@@ -456,4 +463,25 @@
             }
         }
     }
+
+    private class AudioModeChangedHandler implements Handler {
+
+        @Override
+        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+            final String action = intent.getAction();
+            if (action == null) {
+                Log.w(TAG, "AudioModeChangedHandler() action is null");
+                return;
+            }
+            dispatchAudioModeChanged();
+        }
+    }
+
+    private void dispatchAudioModeChanged() {
+        synchronized (mCallbacks) {
+            for (BluetoothCallback callback : mCallbacks) {
+                callback.onAudioModeChanged();
+            }
+        }
+    }
 }
diff --git a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 6c068ff..dc2ecea 100644
--- a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -631,7 +631,7 @@
         }
         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
         if (hearingAidProfile != null) {
-            mIsActiveDeviceHearingAid = hearingAidProfile.isActiveDevice(mDevice);
+            mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
         }
     }
 
diff --git a/com/android/settingslib/bluetooth/HearingAidProfile.java b/com/android/settingslib/bluetooth/HearingAidProfile.java
index 920500f..6c5ecbf 100644
--- a/com/android/settingslib/bluetooth/HearingAidProfile.java
+++ b/com/android/settingslib/bluetooth/HearingAidProfile.java
@@ -136,13 +136,12 @@
 
     public boolean setActiveDevice(BluetoothDevice device) {
         if (mService == null) return false;
-        mService.setActiveDevice(device);
-        return true;
+        return mService.setActiveDevice(device);
     }
 
-    public boolean isActiveDevice(BluetoothDevice device) {
-        if (mService == null) return false;
-        return mService.isActiveDevice(device);
+    public List<BluetoothDevice> getActiveDevices() {
+        if (mService == null) return new ArrayList<>();
+        return mService.getActiveDevices();
     }
 
     public boolean isPreferred(BluetoothDevice device) {
diff --git a/com/android/settingslib/bluetooth/HidDeviceProfile.java b/com/android/settingslib/bluetooth/HidDeviceProfile.java
new file mode 100644
index 0000000..941964a
--- /dev/null
+++ b/com/android/settingslib/bluetooth/HidDeviceProfile.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 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.settingslib.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHidDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.settingslib.R;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * HidProfile handles Bluetooth HID profile.
+ */
+public class HidDeviceProfile implements LocalBluetoothProfile {
+    private static final String TAG = "HidDeviceProfile";
+    // Order of this profile in device profiles list
+    private static final int ORDINAL = 18;
+    // HID Device Profile is always preferred.
+    private static final int PREFERRED_VALUE = -1;
+    private static final boolean DEBUG = true;
+
+    private final LocalBluetoothAdapter mLocalAdapter;
+    private final CachedBluetoothDeviceManager mDeviceManager;
+    private final LocalBluetoothProfileManager mProfileManager;
+    static final String NAME = "HID DEVICE";
+
+    private BluetoothHidDevice mService;
+    private boolean mIsProfileReady;
+
+    HidDeviceProfile(Context context, LocalBluetoothAdapter adapter,
+            CachedBluetoothDeviceManager deviceManager,
+            LocalBluetoothProfileManager profileManager) {
+        mLocalAdapter = adapter;
+        mDeviceManager = deviceManager;
+        mProfileManager = profileManager;
+        adapter.getProfileProxy(context, new HidDeviceServiceListener(),
+                BluetoothProfile.HID_DEVICE);
+    }
+
+    // These callbacks run on the main thread.
+    private final class HidDeviceServiceListener
+            implements BluetoothProfile.ServiceListener {
+
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (DEBUG) {
+                Log.d(TAG,"Bluetooth service connected :-)");
+            }
+            mService = (BluetoothHidDevice) proxy;
+            // We just bound to the service, so refresh the UI for any connected HID devices.
+            List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+            for (BluetoothDevice nextDevice : deviceList) {
+                CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+                // we may add a new device here, but generally this should not happen
+                if (device == null) {
+                    Log.w(TAG, "HidProfile found new device: " + nextDevice);
+                    device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice);
+                }
+                Log.d(TAG, "Connection status changed: " + device);
+                device.onProfileStateChanged(HidDeviceProfile.this,
+                        BluetoothProfile.STATE_CONNECTED);
+                device.refresh();
+            }
+            mIsProfileReady = true;
+        }
+
+        public void onServiceDisconnected(int profile) {
+            if (DEBUG) {
+                Log.d(TAG, "Bluetooth service disconnected");
+            }
+            mIsProfileReady = false;
+        }
+    }
+
+    @Override
+    public boolean isProfileReady() {
+        return mIsProfileReady;
+    }
+
+    @Override
+    public boolean isConnectable() {
+        return true;
+    }
+
+    @Override
+    public boolean isAutoConnectable() {
+        return false;
+    }
+
+    @Override
+    public boolean connect(BluetoothDevice device) {
+        return false;
+    }
+
+    @Override
+    public boolean disconnect(BluetoothDevice device) {
+        if (mService == null) {
+            return false;
+        }
+        return mService.disconnect(device);
+    }
+
+    @Override
+    public int getConnectionStatus(BluetoothDevice device) {
+        if (mService == null) {
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+        List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+
+        return !deviceList.isEmpty() && deviceList.contains(device)
+                ? mService.getConnectionState(device)
+                : BluetoothProfile.STATE_DISCONNECTED;
+    }
+
+    @Override
+    public boolean isPreferred(BluetoothDevice device) {
+        return getConnectionStatus(device) != BluetoothProfile.STATE_DISCONNECTED;
+    }
+
+    @Override
+    public int getPreferred(BluetoothDevice device) {
+        return PREFERRED_VALUE;
+    }
+
+    @Override
+    public void setPreferred(BluetoothDevice device, boolean preferred) {
+        // if set preferred to false, then disconnect to the current device
+        if (!preferred) {
+            mService.disconnect(device);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return NAME;
+    }
+
+    @Override
+    public int getOrdinal() {
+        return ORDINAL;
+    }
+
+    @Override
+    public int getNameResource(BluetoothDevice device) {
+        return R.string.bluetooth_profile_hid;
+    }
+
+    @Override
+    public int getSummaryResourceForDevice(BluetoothDevice device) {
+        final int state = getConnectionStatus(device);
+        switch (state) {
+            case BluetoothProfile.STATE_DISCONNECTED:
+                return R.string.bluetooth_hid_profile_summary_use_for;
+            case BluetoothProfile.STATE_CONNECTED:
+                return R.string.bluetooth_hid_profile_summary_connected;
+            default:
+                return Utils.getConnectionStateSummary(state);
+        }
+    }
+
+    @Override
+    public int getDrawableResource(BluetoothClass btClass) {
+        return R.drawable.ic_bt_misc_hid;
+    }
+
+    protected void finalize() {
+        if (DEBUG) {
+            Log.d(TAG, "finalize()");
+        }
+        if (mService != null) {
+            try {
+                BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HID_DEVICE,
+                        mService);
+                mService = null;
+            } catch (Throwable t) {
+                Log.w(TAG, "Error cleaning up HID proxy", t);
+            }
+        }
+    }
+}
diff --git a/com/android/settingslib/bluetooth/HidProfile.java b/com/android/settingslib/bluetooth/HidProfile.java
index 213002f..93c4017 100644
--- a/com/android/settingslib/bluetooth/HidProfile.java
+++ b/com/android/settingslib/bluetooth/HidProfile.java
@@ -48,7 +48,7 @@
     private static final int ORDINAL = 3;
 
     // These callbacks run on the main thread.
-    private final class InputDeviceServiceListener
+    private final class HidHostServiceListener
             implements BluetoothProfile.ServiceListener {
 
         public void onServiceConnected(int profile, BluetoothProfile proxy) {
@@ -86,7 +86,7 @@
         mLocalAdapter = adapter;
         mDeviceManager = deviceManager;
         mProfileManager = profileManager;
-        adapter.getProfileProxy(context, new InputDeviceServiceListener(),
+        adapter.getProfileProxy(context, new HidHostServiceListener(),
                 BluetoothProfile.HID_HOST);
     }
 
diff --git a/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 34a099c..6413aab 100644
--- a/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -22,6 +22,7 @@
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHeadsetClient;
 import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothHidDevice;
 import android.bluetooth.BluetoothHidHost;
 import android.bluetooth.BluetoothMap;
 import android.bluetooth.BluetoothMapClient;
@@ -86,6 +87,7 @@
     private MapProfile mMapProfile;
     private MapClientProfile mMapClientProfile;
     private final HidProfile mHidProfile;
+    private HidDeviceProfile mHidDeviceProfile;
     private OppProfile mOppProfile;
     private final PanProfile mPanProfile;
     private PbapClientProfile mPbapClientProfile;
@@ -123,7 +125,7 @@
             updateLocalProfiles(uuids);
         }
 
-        // Always add HID and PAN profiles
+        // Always add HID host, HID device, and PAN profiles
         mHidProfile = new HidProfile(context, mLocalAdapter, mDeviceManager, this);
         addProfile(mHidProfile, HidProfile.NAME,
                 BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
@@ -132,6 +134,10 @@
         addPanProfile(mPanProfile, PanProfile.NAME,
                 BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
 
+        mHidDeviceProfile = new HidDeviceProfile(context, mLocalAdapter, mDeviceManager, this);
+        addProfile(mHidDeviceProfile, HidDeviceProfile.NAME,
+                BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
+
         if(DEBUG) Log.d(TAG, "Adding local MAP profile");
         if (mUseMapClient) {
             mMapClientProfile = new MapClientProfile(mContext, mLocalAdapter, mDeviceManager, this);
@@ -195,8 +201,10 @@
                 if (DEBUG) Log.d(TAG, "Adding local HEADSET profile");
                 mHeadsetProfile = new HeadsetProfile(mContext, mLocalAdapter,
                         mDeviceManager, this);
-                addProfile(mHeadsetProfile, HeadsetProfile.NAME,
-                        BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+                addHeadsetProfile(mHeadsetProfile, HeadsetProfile.NAME,
+                        BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,
+                        BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
+                        BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
             }
         } else if (mHeadsetProfile != null) {
             Log.w(TAG, "Warning: HEADSET profile was previously added but the UUID is now missing.");
@@ -208,8 +216,10 @@
                 if(DEBUG) Log.d(TAG, "Adding local HfpClient profile");
                 mHfpClientProfile =
                     new HfpClientProfile(mContext, mLocalAdapter, mDeviceManager, this);
-                addProfile(mHfpClientProfile, HfpClientProfile.NAME,
-                        BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+                addHeadsetProfile(mHfpClientProfile, HfpClientProfile.NAME,
+                        BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED,
+                        BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED,
+                        BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED);
             }
         } else if (mHfpClientProfile != null) {
             Log.w(TAG,
@@ -277,6 +287,15 @@
         // There is no local SDP record for HID and Settings app doesn't control PBAP Server.
     }
 
+    private void addHeadsetProfile(LocalBluetoothProfile profile, String profileName,
+            String stateChangedAction, String audioStateChangedAction, int audioDisconnectedState) {
+        BluetoothEventManager.Handler handler = new HeadsetStateChangeHandler(
+                profile, audioStateChangedAction, audioDisconnectedState);
+        mEventManager.addProfileHandler(stateChangedAction, handler);
+        mEventManager.addProfileHandler(audioStateChangedAction, handler);
+        mProfileNameMap.put(profileName, profile);
+    }
+
     private final Collection<ServiceListener> mServiceListeners =
             new ArrayList<ServiceListener>();
 
@@ -323,18 +342,47 @@
                 cachedDevice = mDeviceManager.addDevice(mLocalAdapter,
                         LocalBluetoothProfileManager.this, device);
             }
+            onReceiveInternal(intent, cachedDevice);
+        }
+
+        protected void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
             int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
             int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
             if (newState == BluetoothProfile.STATE_DISCONNECTED &&
                     oldState == BluetoothProfile.STATE_CONNECTING) {
                 Log.i(TAG, "Failed to connect " + mProfile + " device");
             }
-
             cachedDevice.onProfileStateChanged(mProfile, newState);
             cachedDevice.refresh();
         }
     }
 
+    /** Connectivity and audio state change handler for headset profiles. */
+    private class HeadsetStateChangeHandler extends StateChangedHandler {
+        private final String mAudioChangeAction;
+        private final int mAudioDisconnectedState;
+
+        HeadsetStateChangeHandler(LocalBluetoothProfile profile, String audioChangeAction,
+                int audioDisconnectedState) {
+            super(profile);
+            mAudioChangeAction = audioChangeAction;
+            mAudioDisconnectedState = audioDisconnectedState;
+        }
+
+        @Override
+        public void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
+            if (mAudioChangeAction.equals(intent.getAction())) {
+                int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
+                if (newState != mAudioDisconnectedState) {
+                    cachedDevice.onProfileStateChanged(mProfile, BluetoothProfile.STATE_CONNECTED);
+                }
+                cachedDevice.refresh();
+            } else {
+                super.onReceiveInternal(intent, cachedDevice);
+            }
+        }
+    }
+
     /** State change handler for NAP and PANU profiles. */
     private class PanStateChangedHandler extends StateChangedHandler {
 
@@ -505,6 +553,12 @@
             removedProfiles.remove(mHidProfile);
         }
 
+        if (mHidProfile != null && mHidDeviceProfile.getConnectionStatus(device)
+                != BluetoothProfile.STATE_DISCONNECTED) {
+            profiles.add(mHidDeviceProfile);
+            removedProfiles.remove(mHidDeviceProfile);
+        }
+
         if(isPanNapConnected)
             if(DEBUG) Log.d(TAG, "Valid PAN-NAP connection exists.");
         if ((BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.NAP) &&
diff --git a/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/com/android/settingslib/fuelgauge/BatterySaverUtils.java
index 835ff07..f7b16f8 100644
--- a/com/android/settingslib/fuelgauge/BatterySaverUtils.java
+++ b/com/android/settingslib/fuelgauge/BatterySaverUtils.java
@@ -148,15 +148,32 @@
         Secure.putInt(context.getContentResolver(), Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1);
     }
 
+    /**
+     * Don't show the automatic battery suggestion notification in the future.
+     */
     public static void suppressAutoBatterySaver(Context context) {
         Secure.putInt(context.getContentResolver(),
                 Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, 1);
     }
 
-    public static void scheduleAutoBatterySaver(Context context, int level) {
+    /**
+     * Set the automatic battery saver trigger level to {@code level}.
+     */
+    public static void setAutoBatterySaverTriggerLevel(Context context, int level) {
+        if (level > 0) {
+            suppressAutoBatterySaver(context);
+        }
+        Global.putInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, level);
+    }
+
+    /**
+     * Set the automatic battery saver trigger level to {@code level}, but only when
+     * automatic battery saver isn't enabled yet.
+     */
+    public static void ensureAutoBatterySaver(Context context, int level) {
         if (Global.getInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0)
                 == 0) {
-            Global.putInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, level);
+            setAutoBatterySaverTriggerLevel(context, level);
         }
     }
 }
diff --git a/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java b/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
index 7081678..06e2ee1 100644
--- a/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
+++ b/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.fuelgauge;
 
+import android.content.pm.PackageManager;
 import android.os.IDeviceIdleController;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -24,6 +25,8 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import com.android.internal.util.ArrayUtils;
+
 /**
  * Handles getting/changing the whitelist for the exceptions to battery saving features.
  */
@@ -68,6 +71,19 @@
         return mSysWhitelistedAppsExceptIdle.contains(pkg);
     }
 
+    public boolean isSysWhitelistedExceptIdle(String[] pkgs) {
+        if (ArrayUtils.isEmpty(pkgs)) {
+            return false;
+        }
+        for (String pkg : pkgs) {
+            if (isSysWhitelistedExceptIdle(pkg)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     public void addApp(String pkg) {
         try {
             mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
diff --git a/com/android/settingslib/users/UserManagerHelper.java b/com/android/settingslib/users/UserManagerHelper.java
index c4ca339..113256f 100644
--- a/com/android/settingslib/users/UserManagerHelper.java
+++ b/com/android/settingslib/users/UserManagerHelper.java
@@ -41,6 +41,7 @@
     private static final String TAG = "UserManagerHelper";
     private final Context mContext;
     private final UserManager mUserManager;
+    private final ActivityManager mActivityManager;
     private OnUsersUpdateListener mUpdateListener;
     private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
         @Override
@@ -52,6 +53,7 @@
     public UserManagerHelper(Context context) {
         mContext = context;
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
     }
 
     /**
@@ -72,30 +74,64 @@
     }
 
     /**
-     * Gets {@link UserInfo} for the current user.
+     * Gets UserInfo for the foreground user.
      *
-     * @return {@link UserInfo} for the current user.
+     * Concept of foreground user is relevant for the multi-user deployment. Foreground user
+     * corresponds to the currently "logged in" user.
+     *
+     * @return {@link UserInfo} for the foreground user.
      */
-    public UserInfo getCurrentUserInfo() {
-        return mUserManager.getUserInfo(UserHandle.myUserId());
+    public UserInfo getForegroundUserInfo() {
+        return mUserManager.getUserInfo(getForegroundUserId());
     }
 
     /**
-     * Gets all the other users on the system that are not the current user.
-     *
-     * @return List of {@code UserInfo} for each user that is not the current user.
+     * @return Id of the foreground user.
      */
-    public List<UserInfo> getAllUsersExcludesCurrentUser() {
-        List<UserInfo> others = getAllUsers();
+    public int getForegroundUserId() {
+        return mActivityManager.getCurrentUser();
+    }
 
-        for (Iterator<UserInfo> iterator = others.iterator(); iterator.hasNext(); ) {
-            UserInfo userInfo = iterator.next();
-            if (userInfo.id == UserHandle.myUserId()) {
-                // Remove current user from the list.
-                iterator.remove();
-            }
-        }
-        return others;
+    /**
+     * Gets UserInfo for the user running the caller process.
+     *
+     * Differentiation between foreground user and current process user is relevant for multi-user
+     * deployments.
+     *
+     * Some multi-user aware components (like SystemUI) might run as a singleton - one component
+     * for all users. Current process user is then always the same for that component, even when
+     * the foreground user changes.
+     *
+     * @return {@link UserInfo} for the user running the current process.
+     */
+    public UserInfo getCurrentProcessUserInfo() {
+        return mUserManager.getUserInfo(getCurrentProcessUserId());
+    }
+
+    /**
+     * @return Id for the user running the current process.
+     */
+    public int getCurrentProcessUserId() {
+        return UserHandle.myUserId();
+    }
+
+    /**
+     * Gets all the other users on the system that are not the user running the current process.
+     *
+     * @return List of {@code UserInfo} for each user that is not the user running the process.
+     */
+    public List<UserInfo> getAllUsersExcludesCurrentProcessUser() {
+        return getAllUsersExceptUser(getCurrentProcessUserId());
+    }
+
+    /**
+     * Gets all the existing users on the system that are not the currently running as the
+     * foreground user.
+     *
+     * @return List of {@code UserInfo} for each user that is not the foreground user.
+     */
+    public List<UserInfo> getAllUsersExcludesForegroundUser() {
+        return getAllUsersExceptUser(getForegroundUserId());
     }
 
     /**
@@ -104,12 +140,22 @@
      * @return List of {@code UserInfo} for each user that is not the system user.
      */
     public List<UserInfo> getAllUsersExcludesSystemUser() {
+        return getAllUsersExceptUser(UserHandle.USER_SYSTEM);
+    }
+
+    /**
+     * Get all the users except the one with userId passed in.
+     *
+     * @param userId of the user not to be returned.
+     * @return All users other than user with userId.
+     */
+    public List<UserInfo> getAllUsersExceptUser(int userId) {
         List<UserInfo> others = getAllUsers();
 
         for (Iterator<UserInfo> iterator = others.iterator(); iterator.hasNext(); ) {
             UserInfo userInfo = iterator.next();
-            if (userIsSystemUser(userInfo)) {
-                // Remove system user from the list.
+            if (userInfo.id == userId) {
+                // Remove user with userId from the list.
                 iterator.remove();
             }
         }
@@ -146,78 +192,115 @@
     }
 
     /**
-     * Checks whether passed in user is the user that's currently logged in.
+     * Checks whether passed in user is the foreground user.
      *
      * @param userInfo User to check.
-     * @return {@code true} if current user, {@code false} otherwise.
+     * @return {@code true} if foreground user, {@code false} otherwise.
      */
-    public boolean userIsCurrentUser(UserInfo userInfo) {
-        return getCurrentUserInfo().id == userInfo.id;
+    public boolean userIsForegroundUser(UserInfo userInfo) {
+        return getForegroundUserId() == userInfo.id;
     }
 
-    // Current user information accessors
+    /**
+     * Checks whether passed in user is the user that's running the current process.
+     *
+     * @param userInfo User to check.
+     * @return {@code true} if user running the process, {@code false} otherwise.
+     */
+    public boolean userIsRunningCurrentProcess(UserInfo userInfo) {
+        return getCurrentProcessUserId() == userInfo.id;
+    }
+
+    // Foreground user information accessors.
 
     /**
-     * Checks if the current user is a demo user.
+     * Checks if the foreground user is a guest user.
      */
-    public boolean isDemoUser() {
+    public boolean foregroundUserIsGuestUser() {
+      return getForegroundUserInfo().isGuest();
+    }
+
+    /**
+     * Return whether the foreground user has a restriction.
+     *
+     * @param restriction Restriction to check. Should be a UserManager.* restriction.
+     * @return Whether that restriction exists for the foreground user.
+     */
+    public boolean foregroundUserHasUserRestriction(String restriction) {
+        return mUserManager.hasUserRestriction(restriction, getForegroundUserInfo().getUserHandle());
+    }
+
+    /**
+     * Checks if the foreground user can add new users.
+     */
+    public boolean foregroundUserCanAddUsers() {
+        return !foregroundUserHasUserRestriction(UserManager.DISALLOW_ADD_USER);
+    }
+
+    // Current process user information accessors
+
+    /**
+     * Checks if the calling app is running in a demo user.
+     */
+    public boolean currentProcessRunningAsDemoUser() {
         return mUserManager.isDemoUser();
     }
 
     /**
-     * Checks if the current user is a guest user.
+     * Checks if the calling app is running as a guest user.
      */
-    public boolean isGuestUser() {
+    public boolean currentProcessRunningAsGuestUser() {
         return mUserManager.isGuestUser();
     }
 
     /**
-     * Checks if the current user is the system user (User 0).
+     * Checks whether this process is running under the system user.
      */
-    public boolean isSystemUser() {
+    public boolean currentProcessRunningAsSystemUser() {
         return mUserManager.isSystemUser();
     }
 
-    // Current user restriction accessors
+    // Current process user restriction accessors
 
     /**
-     * Return whether the current user has a restriction.
+     * Return whether the user running the current process has a restriction.
      *
      * @param restriction Restriction to check. Should be a UserManager.* restriction.
-     * @return Whether that restriction exists for the current user.
+     * @return Whether that restriction exists for the user running the process.
      */
-    public boolean hasUserRestriction(String restriction) {
+    public boolean currentProcessHasUserRestriction(String restriction) {
         return mUserManager.hasUserRestriction(restriction);
     }
 
     /**
-     * Checks if the current user can add new users.
+     * Checks if the user running the current process can add new users.
      */
-    public boolean canAddUsers() {
-        return !hasUserRestriction(UserManager.DISALLOW_ADD_USER);
+    public boolean currentProcessCanAddUsers() {
+        return !currentProcessHasUserRestriction(UserManager.DISALLOW_ADD_USER);
     }
 
     /**
-     * Checks if the current user can remove users.
+     * Checks if the user running the current process can remove users.
      */
-    public boolean canRemoveUsers() {
-        return !hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
+    public boolean currentProcessCanRemoveUsers() {
+        return !currentProcessHasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
     }
 
     /**
-     * Checks if the current user is allowed to switch to another user.
+     * Checks if the user running the current process is allowed to switch to another user.
      */
-    public boolean canSwitchUsers() {
-        return !hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+    public boolean currentProcessCanSwitchUsers() {
+        return !currentProcessHasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
     }
 
     /**
-     * Checks if the current user can modify accounts. Demo and Guest users cannot modify accounts
-     * even if the DISALLOW_MODIFY_ACCOUNTS restriction is not applied.
+     * Checks if the current process user can modify accounts. Demo and Guest users cannot modify
+     * accounts even if the DISALLOW_MODIFY_ACCOUNTS restriction is not applied.
      */
-    public boolean canModifyAccounts() {
-        return !hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS) && !isDemoUser()
-                && !isGuestUser();
+    public boolean currentProcessCanModifyAccounts() {
+        return !currentProcessHasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)
+                && !currentProcessRunningAsDemoUser()
+                && !currentProcessRunningAsGuestUser();
     }
 
     // User actions
@@ -242,8 +325,8 @@
 
     /**
      * Tries to remove the user that's passed in. System user cannot be removed.
-     * If the user to be removed is current user, it switches to the system user first, and then
-     * removes the user.
+     * If the user to be removed is user currently running the process,
+     * it switches to the system user first, and then removes the user.
      *
      * @param userInfo User to be removed
      * @return {@code true} if user is successfully removed, {@code false} otherwise.
@@ -254,7 +337,7 @@
             return false;
         }
 
-        if (userInfo.id == getCurrentUserInfo().id) {
+        if (userInfo.id == getCurrentProcessUserId()) {
             switchToUserId(UserHandle.USER_SYSTEM);
         }
 
@@ -267,7 +350,7 @@
      * @param userInfo User to switch to.
      */
     public void switchToUser(UserInfo userInfo) {
-        if (userInfo.id == getCurrentUserInfo().id) {
+        if (userInfo.id == getForegroundUserId()) {
             return;
         }
 
@@ -276,15 +359,6 @@
             return;
         }
 
-        if (UserManager.isGuestUserEphemeral()) {
-            // If switching from guest, we want to bring up the guest exit dialog instead of
-            // switching
-            UserInfo currUserInfo = getCurrentUserInfo();
-            if (currUserInfo != null && currUserInfo.isGuest()) {
-                return;
-            }
-        }
-
         switchToUserId(userInfo.id);
     }
 
@@ -348,6 +422,9 @@
         filter.addAction(Intent.ACTION_USER_REMOVED);
         filter.addAction(Intent.ACTION_USER_ADDED);
         filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
+        filter.addAction(Intent.ACTION_USER_STOPPED);
+        filter.addAction(Intent.ACTION_USER_UNLOCKED);
         mContext.registerReceiverAsUser(mUserChangeReceiver, UserHandle.ALL, filter, null, null);
     }
 
@@ -366,9 +443,7 @@
 
     private void switchToUserId(int id) {
         try {
-            final ActivityManager am = (ActivityManager)
-                    mContext.getSystemService(Context.ACTIVITY_SERVICE);
-            am.switchUser(id);
+            mActivityManager.switchUser(id);
         } catch (Exception e) {
             Log.e(TAG, "Couldn't switch user.", e);
         }
@@ -389,4 +464,3 @@
         void onUsersUpdate();
     }
 }
-
diff --git a/com/android/settingslib/utils/PowerUtil.java b/com/android/settingslib/utils/PowerUtil.java
index 8b3da39..de29030 100644
--- a/com/android/settingslib/utils/PowerUtil.java
+++ b/com/android/settingslib/utils/PowerUtil.java
@@ -144,7 +144,8 @@
                         FIFTEEN_MINUTES_MILLIS);
 
         // convert the time to a properly formatted string.
-        DateFormat fmt = DateFormat.getTimeInstance(DateFormat.SHORT);
+        String skeleton = android.text.format.DateFormat.getTimeFormatString(context);
+        DateFormat fmt = DateFormat.getInstanceForSkeleton(skeleton);
         Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs));
         CharSequence timeString = fmt.format(date);
 
diff --git a/com/android/settingslib/wifi/WifiStatusTracker.java b/com/android/settingslib/wifi/WifiStatusTracker.java
index 9347674..547cd9a 100644
--- a/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -41,8 +41,9 @@
     private final WifiManager mWifiManager;
     private final NetworkScoreManager mNetworkScoreManager;
     private final ConnectivityManager mConnectivityManager;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final WifiNetworkScoreCache.CacheListener mCacheListener =
-            new WifiNetworkScoreCache.CacheListener(new Handler(Looper.getMainLooper())) {
+            new WifiNetworkScoreCache.CacheListener(mHandler) {
                 @Override
                 public void networkCacheUpdated(List<ScoredNetwork> updatedNetworks) {
                     updateStatusLabel();
@@ -89,7 +90,8 @@
             mNetworkScoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI,
                     mWifiNetworkScoreCache, NetworkScoreManager.CACHE_FILTER_CURRENT_NETWORK);
             mWifiNetworkScoreCache.registerListener(mCacheListener);
-            mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
+            mConnectivityManager.registerNetworkCallback(
+                    mNetworkRequest, mNetworkCallback, mHandler);
         } else {
             mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI,
                     mWifiNetworkScoreCache);
diff --git a/com/android/settingslib/wifi/WifiTracker.java b/com/android/settingslib/wifi/WifiTracker.java
index a128b54..d8f0886 100644
--- a/com/android/settingslib/wifi/WifiTracker.java
+++ b/com/android/settingslib/wifi/WifiTracker.java
@@ -313,7 +313,8 @@
             mContext.registerReceiver(mReceiver, mFilter, null /* permission */, mWorkHandler);
             // NetworkCallback objects cannot be reused. http://b/20701525 .
             mNetworkCallback = new WifiTrackerNetworkCallback();
-            mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
+            mConnectivityManager.registerNetworkCallback(
+                    mNetworkRequest, mNetworkCallback, mWorkHandler);
             mRegistered = true;
         }
     }
@@ -788,7 +789,7 @@
                 // We don't send a NetworkInfo object along with this message, because even if we
                 // fetch one from ConnectivityManager, it might be older than the most recent
                 // NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast.
-                mWorkHandler.post(() -> updateNetworkInfo(null));
+                updateNetworkInfo(null);
             }
         }
     }
diff --git a/com/android/setupwizardlib/span/LinkSpan.java b/com/android/setupwizardlib/span/LinkSpan.java
index a5f0424..26a3d16 100644
--- a/com/android/setupwizardlib/span/LinkSpan.java
+++ b/com/android/setupwizardlib/span/LinkSpan.java
@@ -21,10 +21,13 @@
 import android.graphics.Typeface;
 import android.os.Build;
 import android.support.annotation.Nullable;
+import android.text.Selection;
+import android.text.Spannable;
 import android.text.TextPaint;
 import android.text.style.ClickableSpan;
 import android.util.Log;
 import android.view.View;
+import android.widget.TextView;
 
 /**
  * A clickable span that will listen for click events and send it back to the context. To use this
@@ -86,11 +89,19 @@
     public void onClick(View view) {
         if (dispatchClick(view)) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                // Prevent the touch event from bubbling up to the parent views.
                 view.cancelPendingInputEvents();
             }
         } else {
             Log.w(TAG, "Dropping click event. No listener attached.");
         }
+        if (view instanceof TextView) {
+            // Remove the highlight effect when the click happens by clearing the selection
+            CharSequence text = ((TextView) view).getText();
+            if (text instanceof Spannable) {
+                Selection.setSelection((Spannable) text, 0);
+            }
+        }
     }
 
     private boolean dispatchClick(View view) {
diff --git a/com/android/setupwizardlib/span/LinkSpanTest.java b/com/android/setupwizardlib/span/LinkSpanTest.java
index fe72e03..3aafa7d 100644
--- a/com/android/setupwizardlib/span/LinkSpanTest.java
+++ b/com/android/setupwizardlib/span/LinkSpanTest.java
@@ -16,11 +16,16 @@
 
 package com.android.setupwizardlib.span;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertSame;
 import static org.robolectric.RuntimeEnvironment.application;
 
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.method.LinkMovementMethod;
 import android.widget.TextView;
 
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -32,7 +37,7 @@
 public class LinkSpanTest {
 
     @Test
-    public void testOnClick() {
+    public void onClick_shouldCallListenerOnContext() {
         final TestContext context = new TestContext(application);
         final TextView textView = new TextView(context);
         final LinkSpan linkSpan = new LinkSpan("test_id");
@@ -43,7 +48,7 @@
     }
 
     @Test
-    public void testNonImplementingContext() {
+    public void onClick_contextDoesNotImplementOnClickListener_shouldBeNoOp() {
         final TextView textView = new TextView(application);
         final LinkSpan linkSpan = new LinkSpan("test_id");
 
@@ -54,7 +59,7 @@
     }
 
     @Test
-    public void testWrappedListener() {
+    public void onClick_contextWrapsOnClickListener_shouldCallWrappedListener() {
         final TestContext context = new TestContext(application);
         final Context wrapperContext = new ContextWrapper(context);
         final TextView textView = new TextView(wrapperContext);
@@ -65,6 +70,27 @@
         assertSame("Clicked LinkSpan should be passed to setup", linkSpan, context.clickedSpan);
     }
 
+    @Test
+    public void onClick_shouldClearSelection() {
+        final TestContext context = new TestContext(application);
+        final TextView textView = new TextView(context);
+        textView.setMovementMethod(LinkMovementMethod.getInstance());
+        textView.setFocusable(true);
+        textView.setFocusableInTouchMode(true);
+        final LinkSpan linkSpan = new LinkSpan("test_id");
+
+        SpannableStringBuilder text = new SpannableStringBuilder("Lorem ipsum dolor sit");
+        textView.setText(text);
+        text.setSpan(linkSpan, /* start= */ 0, /* end= */ 5, /* flags= */ 0);
+        // Simulate the touch effect set by TextView when touched.
+        Selection.setSelection(text, /* start= */ 0, /* end= */ 5);
+
+        linkSpan.onClick(textView);
+
+        assertThat(Selection.getSelectionStart(textView.getText())).isEqualTo(0);
+        assertThat(Selection.getSelectionEnd(textView.getText())).isEqualTo(0);
+    }
+
     @SuppressWarnings("deprecation")
     private static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
 
diff --git a/com/android/setupwizardlib/view/RichTextView.java b/com/android/setupwizardlib/view/RichTextView.java
index aab3238..fa68a68 100644
--- a/com/android/setupwizardlib/view/RichTextView.java
+++ b/com/android/setupwizardlib/view/RichTextView.java
@@ -20,16 +20,18 @@
 import android.text.Annotation;
 import android.text.SpannableString;
 import android.text.Spanned;
-import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.TextAppearanceSpan;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.widget.TextView;
 
 import com.android.setupwizardlib.span.LinkSpan;
 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
 import com.android.setupwizardlib.span.SpanHelper;
+import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
 
 /**
  * An extension of TextView that automatically replaces the annotation tags as specified in
@@ -112,7 +114,7 @@
             // nullifying any return values of MovementMethod.onTouchEvent.
             // To still allow propagating touch events to the parent when this view doesn't have
             // links, we only set the movement method here if the text contains links.
-            setMovementMethod(LinkMovementMethod.getInstance());
+            setMovementMethod(TouchableLinkMovementMethod.getInstance());
         } else {
             setMovementMethod(null);
         }
@@ -137,6 +139,25 @@
         return false;
     }
 
+    @Override
+    @SuppressWarnings("ClickableViewAccessibility")  // super.onTouchEvent is called
+    public boolean onTouchEvent(MotionEvent event) {
+        // Since View#onTouchEvent always return true if the view is clickable (which is the case
+        // when a TextView has a movement method), override the implementation to allow the movement
+        // method, if it implements TouchableMovementMethod, to say that the touch is not handled,
+        // allowing the event to bubble up to the parent view.
+        boolean superResult = super.onTouchEvent(event);
+        MovementMethod movementMethod = getMovementMethod();
+        if (movementMethod instanceof TouchableMovementMethod) {
+            TouchableMovementMethod touchableMovementMethod =
+                    (TouchableMovementMethod) movementMethod;
+            if (touchableMovementMethod.getLastTouchEvent() == event) {
+                return touchableMovementMethod.isLastTouchEventHandled();
+            }
+        }
+        return superResult;
+    }
+
     public void setOnLinkClickListener(OnLinkClickListener listener) {
         mOnLinkClickListener = listener;
     }
diff --git a/com/android/setupwizardlib/test/RichTextViewTest.java b/com/android/setupwizardlib/view/RichTextViewTest.java
similarity index 67%
rename from com/android/setupwizardlib/test/RichTextViewTest.java
rename to com/android/setupwizardlib/view/RichTextViewTest.java
index 5f3eb9f..f77de68 100644
--- a/com/android/setupwizardlib/test/RichTextViewTest.java
+++ b/com/android/setupwizardlib/view/RichTextViewTest.java
@@ -14,39 +14,45 @@
  * limitations under the License.
  */
 
-package com.android.setupwizardlib.test;
+package com.android.setupwizardlib.view;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.application;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 import android.text.Annotation;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.TextAppearanceSpan;
+import android.view.MotionEvent;
 
+import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 import com.android.setupwizardlib.span.LinkSpan;
 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
-import com.android.setupwizardlib.view.RichTextView;
+import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
 
 import java.util.Arrays;
 
-@RunWith(AndroidJUnit4.class)
-@SmallTest
+@RunWith(SuwLibRobolectricTestRunner.class)
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 public class RichTextViewTest {
 
     @Test
@@ -55,12 +61,14 @@
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText(ssb);
 
         final CharSequence text = textView.getText();
         assertTrue("Text should be spanned", text instanceof Spanned);
 
+        assertThat(textView.getMovementMethod()).isInstanceOf(TouchableLinkMovementMethod.class);
+
         Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class);
         assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length);
 
@@ -77,7 +85,7 @@
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText(ssb);
 
         OnLinkClickListener listener = mock(OnLinkClickListener.class);
@@ -99,7 +107,7 @@
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        TestContext context = spy(new TestContext(InstrumentationRegistry.getTargetContext()));
+        TestContext context = spy(new TestContext(application));
         RichTextView textView = new RichTextView(context);
         textView.setText(ssb);
 
@@ -111,12 +119,50 @@
     }
 
     @Test
+    public void onTouchEvent_clickOnLinks_shouldReturnTrue() {
+        Annotation link = new Annotation("link", "foobar");
+        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
+        ssb.setSpan(link, 0, 2, 0 /* flags */);
+
+        RichTextView textView = new RichTextView(application);
+        textView.setText(ssb);
+
+        TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
+        textView.setMovementMethod(mockMovementMethod);
+
+        MotionEvent motionEvent =
+                MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
+        doReturn(true).when(mockMovementMethod).isLastTouchEventHandled();
+        assertThat(textView.onTouchEvent(motionEvent)).isTrue();
+    }
+
+    @Test
+    public void onTouchEvent_clickOutsideLinks_shouldReturnFalse() {
+        Annotation link = new Annotation("link", "foobar");
+        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
+        ssb.setSpan(link, 0, 2, 0 /* flags */);
+
+        RichTextView textView = new RichTextView(application);
+        textView.setText(ssb);
+
+        TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
+        textView.setMovementMethod(mockMovementMethod);
+
+        MotionEvent motionEvent =
+                MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
+        doReturn(false).when(mockMovementMethod).isLastTouchEventHandled();
+        assertThat(textView.onTouchEvent(motionEvent)).isFalse();
+    }
+
+    @Test
     public void testTextStyle() {
         Annotation link = new Annotation("textAppearance", "foobar");
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText(ssb);
 
         final CharSequence text = textView.getText();
@@ -137,7 +183,7 @@
         SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("Linked");
         spannableStringBuilder.setSpan(testLink, 0, 3, 0);
 
-        RichTextView view = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView view = new RichTextView(application);
         view.setText(spannableStringBuilder);
 
         assertTrue("TextView should be focusable since it contains spans", view.isFocusable());
@@ -147,7 +193,7 @@
     @SuppressLint("SetTextI18n")  // It's OK. This is just a test.
     @Test
     public void testTextContainingNoLinksAreNotFocusable() {
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText("Thou shall not be focusable!");
 
         assertFalse("TextView should not be focusable since it does not contain any span",
@@ -160,16 +206,23 @@
     @SuppressLint("SetTextI18n")  // It's OK. This is just a test.
     @Test
     public void testRichTextViewFocusChangesWithTextChange() {
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText("Thou shall not be focusable!");
 
         assertFalse(textView.isFocusable());
+        assertFalse(textView.isFocusableInTouchMode());
 
         SpannableStringBuilder spannableStringBuilder =
                 new SpannableStringBuilder("I am focusable");
         spannableStringBuilder.setSpan(new Annotation("link", "focus:on_me"), 0, 1, 0);
         textView.setText(spannableStringBuilder);
         assertTrue(textView.isFocusable());
+        if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+            assertTrue(textView.isFocusableInTouchMode());
+            assertFalse(textView.getRevealOnFocusHint());
+        } else {
+            assertFalse(textView.isFocusableInTouchMode());
+        }
     }
 
     public static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
diff --git a/com/android/setupwizardlib/view/TouchableMovementMethod.java b/com/android/setupwizardlib/view/TouchableMovementMethod.java
new file mode 100644
index 0000000..10e91f4
--- /dev/null
+++ b/com/android/setupwizardlib/view/TouchableMovementMethod.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.setupwizardlib.view;
+
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+/**
+ * A movement method that tracks the last result of whether touch events are handled. This is
+ * used to patch the return value of {@link TextView#onTouchEvent} so that it consumes the touch
+ * events only when the movement method says the event is consumed.
+ */
+public interface TouchableMovementMethod {
+
+    /**
+     * @return The last touch event received in {@link MovementMethod#onTouchEvent}
+     */
+    MotionEvent getLastTouchEvent();
+
+    /**
+     * @return The return value of the last {@link MovementMethod#onTouchEvent}, or whether the
+     * last touch event should be considered handled by the text view
+     */
+    boolean isLastTouchEventHandled();
+
+    /**
+     * An extension of LinkMovementMethod that tracks whether the event is handled when it is
+     * touched.
+     */
+    class TouchableLinkMovementMethod extends LinkMovementMethod
+            implements TouchableMovementMethod {
+
+        public static TouchableLinkMovementMethod getInstance() {
+            return new TouchableLinkMovementMethod();
+        }
+
+        boolean mLastEventResult = false;
+        MotionEvent mLastEvent;
+
+        @Override
+        public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+            mLastEvent = event;
+            boolean result = super.onTouchEvent(widget, buffer, event);
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                // Unfortunately, LinkMovementMethod extends ScrollMovementMethod, and it always
+                // consume the down event. So here we use the selection instead as a hint of whether
+                // the down event landed on a link.
+                mLastEventResult = Selection.getSelectionStart(buffer) != -1;
+            } else {
+                mLastEventResult = result;
+            }
+            return result;
+        }
+
+        @Override
+        public MotionEvent getLastTouchEvent() {
+            return mLastEvent;
+        }
+
+        @Override
+        public boolean isLastTouchEventHandled() {
+            return mLastEventResult;
+        }
+    }
+}
diff --git a/com/android/systemui/BatteryMeterView.java b/com/android/systemui/BatteryMeterView.java
index 1ae06d7..0683514 100644
--- a/com/android/systemui/BatteryMeterView.java
+++ b/com/android/systemui/BatteryMeterView.java
@@ -81,6 +81,14 @@
     private float mDarkIntensity;
     private int mUser;
 
+    /**
+     * Whether we should use colors that adapt based on wallpaper/the scrim behind quick settings.
+     */
+    private boolean mUseWallpaperTextColors;
+
+    private int mNonAdaptedForegroundColor;
+    private int mNonAdaptedBackgroundColor;
+
     public BatteryMeterView(Context context) {
         this(context, null, 0);
     }
@@ -140,6 +148,29 @@
         updateShowPercent();
     }
 
+    /**
+     * Sets whether the battery meter view uses the wallpaperTextColor. If we're not using it, we'll
+     * revert back to dark-mode-based/tinted colors.
+     *
+     * @param shouldUseWallpaperTextColor whether we should use wallpaperTextColor for all
+     *                                    components
+     */
+    public void useWallpaperTextColor(boolean shouldUseWallpaperTextColor) {
+        if (shouldUseWallpaperTextColor == mUseWallpaperTextColors) {
+            return;
+        }
+
+        mUseWallpaperTextColors = shouldUseWallpaperTextColor;
+
+        if (mUseWallpaperTextColors) {
+            updateColors(
+                    Utils.getColorAttr(mContext, R.attr.wallpaperTextColor),
+                    Utils.getColorAttr(mContext, R.attr.wallpaperTextColorSecondary));
+        } else {
+            updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor);
+        }
+    }
+
     public void setColorsFromContext(Context context) {
         if (context == null) {
             return;
@@ -179,7 +210,8 @@
         getContext().getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(SHOW_BATTERY_PERCENT), false, mSettingObserver, mUser);
         updateShowPercent();
-        Dependency.get(TunerService.class).addTunable(this, StatusBarIconController.ICON_BLACKLIST);
+        Dependency.get(TunerService.class)
+                .addTunable(this, StatusBarIconController.ICON_BLACKLIST);
         Dependency.get(ConfigurationController.class).addCallback(this);
         mUserTracker.startTracking();
     }
@@ -273,19 +305,23 @@
     @Override
     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
         mDarkIntensity = darkIntensity;
+
         float intensity = DarkIconDispatcher.isInArea(area, this) ? darkIntensity : 0;
-        int foreground = getColorForDarkIntensity(intensity, mLightModeFillColor,
-                mDarkModeFillColor);
-        int background = getColorForDarkIntensity(intensity, mLightModeBackgroundColor,
-                mDarkModeBackgroundColor);
-        mDrawable.setColors(foreground, background);
-        setTextColor(foreground);
+        mNonAdaptedForegroundColor = getColorForDarkIntensity(
+                intensity, mLightModeFillColor, mDarkModeFillColor);
+        mNonAdaptedBackgroundColor = getColorForDarkIntensity(
+                intensity, mLightModeBackgroundColor,mDarkModeBackgroundColor);
+
+        if (!mUseWallpaperTextColors) {
+            updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor);
+        }
     }
 
-    public void setTextColor(int color) {
-        mTextColor = color;
+    private void updateColors(int foregroundColor, int backgroundColor) {
+        mDrawable.setColors(foregroundColor, backgroundColor);
+        mTextColor = foregroundColor;
         if (mBatteryPercentView != null) {
-            mBatteryPercentView.setTextColor(color);
+            mBatteryPercentView.setTextColor(foregroundColor);
         }
     }
 
diff --git a/com/android/systemui/ForegroundServiceController.java b/com/android/systemui/ForegroundServiceController.java
index 5a2263c..ae6ee2a 100644
--- a/com/android/systemui/ForegroundServiceController.java
+++ b/com/android/systemui/ForegroundServiceController.java
@@ -73,7 +73,7 @@
     void onAppOpChanged(int code, int uid, String packageName, boolean active);
 
     /**
-     * Gets active app ops for this user and package.
+     * Gets active app ops for this user and package
      */
     @Nullable ArraySet<Integer> getAppOps(int userId, String packageName);
 }
diff --git a/com/android/systemui/ImageWallpaper.java b/com/android/systemui/ImageWallpaper.java
index a4f8d8c..b8a57bf 100644
--- a/com/android/systemui/ImageWallpaper.java
+++ b/com/android/systemui/ImageWallpaper.java
@@ -444,13 +444,7 @@
             final Surface surface = getSurfaceHolder().getSurface();
             surface.hwuiDestroy();
 
-            mLoader = new AsyncTask<Void, Void, Bitmap>() {
-                @Override
-                protected Bitmap doInBackground(Void... params) {
-                    mWallpaperManager.forgetLoadedWallpaper();
-                    return null;
-                }
-            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+            mWallpaperManager.forgetLoadedWallpaper();
         }
 
         private void scheduleUnloadWallpaper() {
diff --git a/com/android/systemui/OverviewProxyService.java b/com/android/systemui/OverviewProxyService.java
index 8cff56d..b1020cf 100644
--- a/com/android/systemui/OverviewProxyService.java
+++ b/com/android/systemui/OverviewProxyService.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
 import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Handler;
@@ -39,6 +40,7 @@
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.GraphicBufferCompat;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.policy.CallbackController;
@@ -50,6 +52,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP;
 import static com.android.systemui.shared.system.NavigationBarCompat.InteractionType;
 
@@ -71,11 +74,13 @@
     private final DeviceProvisionedController mDeviceProvisionedController
             = Dependency.get(DeviceProvisionedController.class);
     private final List<OverviewProxyListener> mConnectionCallbacks = new ArrayList<>();
+    private final Intent mQuickStepIntent;
 
     private IOverviewProxy mOverviewProxy;
     private int mConnectionBackoffAttempts;
     private CharSequence mOnboardingText;
     private @InteractionType int mInteractionFlags;
+    private boolean mIsEnabled;
 
     private ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() {
 
@@ -130,14 +135,23 @@
                     });
                 }
             } finally {
+                Prefs.putInt(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS, mInteractionFlags);
                 Binder.restoreCallingIdentity(token);
             }
         }
     };
 
-    private final BroadcastReceiver mLauncherAddedReceiver = new BroadcastReceiver() {
+    private final BroadcastReceiver mLauncherStateChangedReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            updateEnabledState();
+
+            // When launcher service is disabled, reset interaction flags because it is inactive
+            if (!isEnabled()) {
+                mInteractionFlags = 0;
+                Prefs.remove(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS);
+            }
+
             // Reconnect immediately, instead of waiting for resume to arrive.
             startConnectionToCurrentUser();
         }
@@ -196,17 +210,21 @@
         mConnectionBackoffAttempts = 0;
         mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
                 com.android.internal.R.string.config_recentsComponentName));
+        mQuickStepIntent = new Intent(ACTION_QUICKSTEP)
+                .setPackage(mRecentsComponentName.getPackageName());
+        mInteractionFlags = Prefs.getInt(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS, 0);
 
         // Listen for the package update changes.
         if (SystemServicesProxy.getInstance(context)
                 .isSystemUser(mDeviceProvisionedController.getCurrentUser())) {
+            updateEnabledState();
             mDeviceProvisionedController.addCallback(mDeviceProvisionedCallback);
             IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
             filter.addDataScheme("package");
             filter.addDataSchemeSpecificPart(mRecentsComponentName.getPackageName(),
                     PatternMatcher.PATTERN_LITERAL);
             filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-            mContext.registerReceiver(mLauncherAddedReceiver, filter);
+            mContext.registerReceiver(mLauncherStateChangedReceiver, filter);
         }
     }
 
@@ -222,7 +240,7 @@
         disconnectFromLauncherService();
 
         // If user has not setup yet or already connected, do not try to connect
-        if (!mDeviceProvisionedController.isCurrentUserSetup()) {
+        if (!mDeviceProvisionedController.isCurrentUserSetup() || !isEnabled()) {
             return;
         }
         mHandler.removeCallbacks(mConnectionRunnable);
@@ -248,6 +266,7 @@
     public void addCallback(OverviewProxyListener listener) {
         mConnectionCallbacks.add(listener);
         listener.onConnectionChanged(mOverviewProxy != null);
+        listener.onInteractionFlagsChanged(mInteractionFlags);
     }
 
     @Override
@@ -256,7 +275,11 @@
     }
 
     public boolean shouldShowSwipeUpUI() {
-        return getProxy() != null && ((mInteractionFlags & FLAG_DISABLE_SWIPE_UP) == 0);
+        return isEnabled() && ((mInteractionFlags & FLAG_DISABLE_SWIPE_UP) == 0);
+    }
+
+    public boolean isEnabled() {
+        return mIsEnabled;
     }
 
     public IOverviewProxy getProxy() {
@@ -292,6 +315,12 @@
         }
     }
 
+    private void updateEnabledState() {
+        mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent,
+                MATCH_DIRECT_BOOT_UNAWARE,
+                ActivityManagerWrapper.getInstance().getCurrentUserId()) != null;
+    }
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println(TAG_OPS + " state:");
diff --git a/com/android/systemui/Prefs.java b/com/android/systemui/Prefs.java
index 2a27147..7f7a769 100644
--- a/com/android/systemui/Prefs.java
+++ b/com/android/systemui/Prefs.java
@@ -54,7 +54,8 @@
             Key.HAS_SEEN_RECENTS_ONBOARDING,
             Key.SEEN_RINGER_GUIDANCE_COUNT,
             Key.QS_HAS_TURNED_OFF_MOBILE_DATA,
-            Key.TOUCHED_RINGER_TOGGLE
+            Key.TOUCHED_RINGER_TOGGLE,
+            Key.QUICK_STEP_INTERACTION_FLAGS
     })
     public @interface Key {
         @Deprecated
@@ -93,6 +94,7 @@
         String QS_TILE_SPECS_REVEALED = "QsTileSpecsRevealed";
         String QS_HAS_TURNED_OFF_MOBILE_DATA = "QsHasTurnedOffMobileData";
         String TOUCHED_RINGER_TOGGLE = "TouchedRingerToggle";
+        String QUICK_STEP_INTERACTION_FLAGS = "QuickStepInteractionFlags";
     }
 
     public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
diff --git a/com/android/systemui/ScreenDecorations.java b/com/android/systemui/ScreenDecorations.java
index a0fa69e..e54b083 100644
--- a/com/android/systemui/ScreenDecorations.java
+++ b/com/android/systemui/ScreenDecorations.java
@@ -14,6 +14,10 @@
 
 package com.android.systemui;
 
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -21,12 +25,14 @@
 import static com.android.systemui.tuner.TunablePadding.FLAG_START;
 import static com.android.systemui.tuner.TunablePadding.FLAG_END;
 
+import android.annotation.Dimension;
 import android.app.Fragment;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.PixelFormat;
@@ -41,6 +47,7 @@
 import android.view.DisplayInfo;
 import android.view.Gravity;
 import android.view.LayoutInflater;
+import android.view.Surface;
 import android.view.View;
 import android.view.View.OnLayoutChangeListener;
 import android.view.ViewGroup;
@@ -359,6 +366,7 @@
             if (!mBoundingPath.isEmpty()) {
                 mPaint.setColor(Color.BLACK);
                 mPaint.setStyle(Paint.Style.FILL);
+                mPaint.setAntiAlias(true);
                 canvas.drawPath(mBoundingPath, mPaint);
             }
         }
@@ -388,7 +396,7 @@
             if (hasCutout()) {
                 mBounds.set(mInfo.displayCutout.getBounds());
                 localBounds(mBoundingRect);
-                mInfo.displayCutout.getBounds().getBoundaryPath(mBoundingPath);
+                updateBoundingPath();
                 invalidate();
                 newVisible = VISIBLE;
             } else {
@@ -400,6 +408,44 @@
             }
         }
 
+        private void updateBoundingPath() {
+            int lw = mInfo.logicalWidth;
+            int lh = mInfo.logicalHeight;
+
+            boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270;
+
+            int dw = flipped ? lh : lw;
+            int dh = flipped ? lw : lh;
+
+            mBoundingPath.set(DisplayCutout.pathFromResources(getResources(), lw, lh));
+            Matrix m = new Matrix();
+            transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
+            mBoundingPath.transform(m);
+        }
+
+        private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation,
+                @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
+            switch (rotation) {
+                case ROTATION_0:
+                    out.reset();
+                    break;
+                case ROTATION_90:
+                    out.setRotate(270);
+                    out.postTranslate(0, physicalWidth);
+                    break;
+                case ROTATION_180:
+                    out.setRotate(180);
+                    out.postTranslate(physicalWidth, physicalHeight);
+                    break;
+                case ROTATION_270:
+                    out.setRotate(90);
+                    out.postTranslate(physicalHeight, 0);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown rotation: " + rotation);
+            }
+        }
+
         private boolean hasCutout() {
             final DisplayCutout displayCutout = mInfo.displayCutout;
             if (displayCutout == null) {
diff --git a/com/android/systemui/SystemUIFactory.java b/com/android/systemui/SystemUIFactory.java
index 039e7b5..52d458c 100644
--- a/com/android/systemui/SystemUIFactory.java
+++ b/com/android/systemui/SystemUIFactory.java
@@ -25,6 +25,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.colorextraction.ColorExtractor.GradientColors;
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.Dependency.DependencyProvider;
 import com.android.systemui.classifier.FalsingManager;
@@ -41,9 +42,11 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.SmartReplyLogger;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockIcon;
 import com.android.systemui.statusbar.phone.LockscreenWallpaper;
@@ -100,12 +103,13 @@
                 dismissCallbackRegistry, FalsingManager.getInstance(context));
     }
 
-    public ScrimController createScrimController(LightBarController lightBarController,
-            ScrimView scrimBehind, ScrimView scrimInFront, LockscreenWallpaper lockscreenWallpaper,
+    public ScrimController createScrimController(ScrimView scrimBehind, ScrimView scrimInFront,
+            LockscreenWallpaper lockscreenWallpaper, Consumer<Float> scrimBehindAlphaListener,
+            Consumer<GradientColors> scrimInFrontColorListener,
             Consumer<Integer> scrimVisibleListener, DozeParameters dozeParameters,
             AlarmManager alarmManager) {
-        return new ScrimController(lightBarController, scrimBehind, scrimInFront,
-                scrimVisibleListener, dozeParameters, alarmManager);
+        return new ScrimController(scrimBehind, scrimInFront, scrimBehindAlphaListener,
+                scrimInFrontColorListener, scrimVisibleListener, dozeParameters, alarmManager);
     }
 
     public NotificationIconAreaController createNotificationIconAreaController(Context context,
@@ -142,5 +146,7 @@
         providers.put(NotificationViewHierarchyManager.class,
                 () -> new NotificationViewHierarchyManager(context));
         providers.put(NotificationEntryManager.class, () -> new NotificationEntryManager(context));
+        providers.put(KeyguardDismissUtil.class, KeyguardDismissUtil::new);
+        providers.put(SmartReplyLogger.class, () -> new SmartReplyLogger(context));
     }
 }
diff --git a/com/android/systemui/classifier/AnglesClassifier.java b/com/android/systemui/classifier/AnglesClassifier.java
index e18ac74..cdf4ba7 100644
--- a/com/android/systemui/classifier/AnglesClassifier.java
+++ b/com/android/systemui/classifier/AnglesClassifier.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.classifier;
 
+import android.os.Build;
+import android.os.SystemProperties;
+import android.util.Log;
 import android.view.MotionEvent;
 
 import java.util.ArrayList;
@@ -49,13 +52,18 @@
 public class AnglesClassifier extends StrokeClassifier {
     private HashMap<Stroke, Data> mStrokeMap = new HashMap<>();
 
+    public static final boolean VERBOSE = SystemProperties.getBoolean("debug.falsing_log.ang",
+            Build.IS_DEBUGGABLE);
+
+    private static String TAG = "ANG";
+
     public AnglesClassifier(ClassifierData classifierData) {
         mClassifierData = classifierData;
     }
 
     @Override
     public String getTag() {
-        return "ANG";
+        return TAG;
     }
 
     @Override
@@ -170,18 +178,31 @@
 
         public float getAnglesVariance() {
             float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+            if (VERBOSE) {
+                FalsingLog.i(TAG, "getAnglesVariance: (first pass) " + anglesVariance);
+                FalsingLog.i(TAG, "   - mFirstLength=" + mFirstLength);
+                FalsingLog.i(TAG, "   - mLength=" + mLength);
+            }
             if (mFirstLength < mLength / 2f) {
                 anglesVariance = Math.min(anglesVariance, mFirstAngleVariance
                         + getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount));
+                if (VERBOSE) FalsingLog.i(TAG, "getAnglesVariance: (second pass) " + anglesVariance);
             }
             return anglesVariance;
         }
 
         public float getAnglesPercentage() {
             if (mAnglesCount == 0.0f) {
+                if (VERBOSE) FalsingLog.i(TAG, "getAnglesPercentage: count==0, result=1");
                 return 1.0f;
             }
-            return (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
+            final float result = (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
+            if (VERBOSE) {
+                FalsingLog.i(TAG, "getAnglesPercentage: left=" + mLeftAngles + " right="
+                        + mRightAngles + " straight=" + mStraightAngles + " count=" + mAnglesCount
+                        + " result=" + result);
+            }
+            return result;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/com/android/systemui/classifier/AnglesVarianceEvaluator.java b/com/android/systemui/classifier/AnglesVarianceEvaluator.java
index 6883dd0..9ffe783 100644
--- a/com/android/systemui/classifier/AnglesVarianceEvaluator.java
+++ b/com/android/systemui/classifier/AnglesVarianceEvaluator.java
@@ -18,14 +18,11 @@
 
 public class AnglesVarianceEvaluator {
     public static float evaluate(float value, int type) {
-        final boolean secureUnlock = type == Classifier.BOUNCER_UNLOCK;
         float evaluation = 0.0f;
-        if (value > 0.05) evaluation++;
-        if (value > 0.10) evaluation++;
         if (value > 0.20) evaluation++;
-        if (value > 0.40 && !secureUnlock) evaluation++;
-        if (value > 0.80 && !secureUnlock) evaluation++;
-        if (value > 1.50 && !secureUnlock) evaluation++;
+        if (value > 0.40) evaluation++;
+        if (value > 0.80) evaluation++;
+        if (value > 1.50) evaluation++;
         return evaluation;
     }
 }
diff --git a/com/android/systemui/classifier/SpeedAnglesClassifier.java b/com/android/systemui/classifier/SpeedAnglesClassifier.java
index 6df72b1..66f0cf6 100644
--- a/com/android/systemui/classifier/SpeedAnglesClassifier.java
+++ b/com/android/systemui/classifier/SpeedAnglesClassifier.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.classifier;
 
+import android.os.Build;
+import android.os.SystemProperties;
 import android.view.MotionEvent;
 
 import java.util.ArrayList;
@@ -34,6 +36,10 @@
  * should be in this interval.
  */
 public class SpeedAnglesClassifier extends StrokeClassifier {
+    public static final boolean VERBOSE = SystemProperties.getBoolean("debug.falsing_log.spd_ang",
+            Build.IS_DEBUGGABLE);
+    public static final String TAG = "SPD_ANG";
+
     private HashMap<Stroke, Data> mStrokeMap = new HashMap<>();
 
     public SpeedAnglesClassifier(ClassifierData classifierData) {
@@ -42,7 +48,7 @@
 
     @Override
     public String getTag() {
-        return "SPD_ANG";
+        return TAG;
     }
 
     @Override
@@ -135,14 +141,24 @@
         }
 
         public float getAnglesVariance() {
-            return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
+            final float v = mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
+            if (VERBOSE) {
+                FalsingLog.i(TAG, "getAnglesVariance: sum^2=" + mSumSquares
+                        + " count=" + mCount + " result=" + v);
+            }
+            return v;
         }
 
         public float getAnglesPercentage() {
             if (mAnglesCount == 0.0f) {
                 return 1.0f;
             }
-            return (mAcceleratingAngles) / mAnglesCount;
+            final float v = (mAcceleratingAngles) / mAnglesCount;
+            if (VERBOSE) {
+                FalsingLog.i(TAG, "getAnglesPercentage: angles=" + mAcceleratingAngles
+                        + " count=" + mAnglesCount + " result=" + v);
+            }
+            return v;
         }
     }
 }
\ No newline at end of file
diff --git a/com/android/systemui/doze/DozeUi.java b/com/android/systemui/doze/DozeUi.java
index 778e630..c390764 100644
--- a/com/android/systemui/doze/DozeUi.java
+++ b/com/android/systemui/doze/DozeUi.java
@@ -109,7 +109,11 @@
         switch (newState) {
             case DOZE_AOD:
                 if (oldState == DOZE_AOD_PAUSED) {
+                    // Whenever turning on the display, it's necessary to push a new frame.
+                    // The display buffers will be empty and need to be filled.
                     mHost.dozeTimeTick();
+                    // The first frame may arrive when the display isn't ready yet.
+                    mHandler.postDelayed(mHost::dozeTimeTick, 100);
                 }
                 scheduleTimeTick();
                 break;
diff --git a/com/android/systemui/fingerprint/FingerprintDialogImpl.java b/com/android/systemui/fingerprint/FingerprintDialogImpl.java
index 3577c0f..a81043e 100644
--- a/com/android/systemui/fingerprint/FingerprintDialogImpl.java
+++ b/com/android/systemui/fingerprint/FingerprintDialogImpl.java
@@ -18,8 +18,8 @@
 
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.hardware.biometrics.BiometricDialog;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricPromptReceiver;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
@@ -48,7 +48,7 @@
 
     private FingerprintDialogView mDialogView;
     private WindowManager mWindowManager;
-    private IBiometricDialogReceiver mReceiver;
+    private IBiometricPromptReceiver mReceiver;
     private boolean mDialogShowing;
 
     private Handler mHandler = new Handler() {
@@ -97,7 +97,7 @@
     }
 
     @Override
-    public void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) {
+    public void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) {
         if (DEBUG) Log.d(TAG, "showFingerprintDialog");
         // Remove these messages as they are part of the previous client
         mHandler.removeMessages(MSG_FINGERPRINT_ERROR);
@@ -134,12 +134,15 @@
     }
 
     private void handleShowDialog(SomeArgs args) {
-        if (DEBUG) Log.d(TAG, "handleShowDialog");
-        if (mDialogShowing) {
+        if (DEBUG) Log.d(TAG, "handleShowDialog, isAnimatingAway: "
+                + mDialogView.isAnimatingAway());
+        if (mDialogView.isAnimatingAway()) {
+            mDialogView.forceRemove();
+        } else if (mDialogShowing) {
             Log.w(TAG, "Dialog already showing");
             return;
         }
-        mReceiver = (IBiometricDialogReceiver) args.arg2;
+        mReceiver = (IBiometricPromptReceiver) args.arg2;
         mDialogView.setBundle((Bundle)args.arg1);
         mWindowManager.addView(mDialogView, mDialogView.getLayoutParams());
         mDialogShowing = true;
@@ -168,7 +171,7 @@
     }
 
     private void handleHideDialog(boolean userCanceled) {
-        if (DEBUG) Log.d(TAG, "handleHideDialog");
+        if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled);
         if (!mDialogShowing) {
             // This can happen if there's a race and we get called from both
             // onAuthenticated and onError, etc.
@@ -177,7 +180,7 @@
         }
         if (userCanceled) {
             try {
-                mReceiver.onDialogDismissed(BiometricDialog.DISMISSED_REASON_USER_CANCEL);
+                mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
             } catch (RemoteException e) {
                 Log.e(TAG, "RemoteException when hiding dialog", e);
             }
@@ -193,7 +196,7 @@
             return;
         }
         try {
-            mReceiver.onDialogDismissed(BiometricDialog.DISMISSED_REASON_NEGATIVE);
+            mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
         } catch (RemoteException e) {
             Log.e(TAG, "Remote exception when handling negative button", e);
         }
@@ -206,7 +209,7 @@
             return;
         }
         try {
-            mReceiver.onDialogDismissed(BiometricDialog.DISMISSED_REASON_POSITIVE);
+            mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE);
         } catch (RemoteException e) {
             Log.e(TAG, "Remote exception when handling positive button", e);
         }
diff --git a/com/android/systemui/fingerprint/FingerprintDialogView.java b/com/android/systemui/fingerprint/FingerprintDialogView.java
index 95258b0..8013a9e 100644
--- a/com/android/systemui/fingerprint/FingerprintDialogView.java
+++ b/com/android/systemui/fingerprint/FingerprintDialogView.java
@@ -17,17 +17,16 @@
 package com.android.systemui.fingerprint;
 
 import android.content.Context;
-import android.content.res.Configuration;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
-import android.hardware.biometrics.BiometricDialog;
+import android.hardware.biometrics.BiometricPrompt;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -44,7 +43,6 @@
 
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
-import com.android.systemui.util.leak.RotationUtils;
 
 /**
  * This class loads the view for the system-provided dialog. The view consists of:
@@ -67,7 +65,7 @@
     private final Interpolator mLinearOutSlowIn;
     private final WindowManager mWindowManager;
     private final float mAnimationTranslationOffset;
-    private final int mErrorTextColor;
+    private final int mErrorColor;
     private final int mTextColor;
     private final int mFingerprintColor;
 
@@ -77,9 +75,29 @@
     private Bundle mBundle;
     private final LinearLayout mDialog;
     private int mLastState;
+    private boolean mAnimatingAway;
+    private boolean mWasForceRemoved;
 
     private final float mDisplayWidth;
 
+    private final Runnable mShowAnimationRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mLayout.animate()
+                    .alpha(1f)
+                    .setDuration(ANIMATION_DURATION_SHOW)
+                    .setInterpolator(mLinearOutSlowIn)
+                    .withLayer()
+                    .start();
+            mDialog.animate()
+                    .translationY(0)
+                    .setDuration(ANIMATION_DURATION_SHOW)
+                    .setInterpolator(mLinearOutSlowIn)
+                    .withLayer()
+                    .start();
+        }
+    };
+
     public FingerprintDialogView(Context context, Handler handler) {
         super(context);
         mHandler = handler;
@@ -87,8 +105,8 @@
         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
         mAnimationTranslationOffset = getResources()
                 .getDimension(R.dimen.fingerprint_dialog_animation_translation_offset);
-        mErrorTextColor = Color.parseColor(
-                getResources().getString(R.color.fingerprint_dialog_error_message_color));
+        mErrorColor = Color.parseColor(
+                getResources().getString(R.color.fingerprint_dialog_error_color));
         mTextColor = Color.parseColor(
                 getResources().getString(R.color.fingerprint_dialog_text_light_color));
         mFingerprintColor = Color.parseColor(
@@ -163,29 +181,29 @@
         mLastState = STATE_NONE;
         updateFingerprintIcon(STATE_FINGERPRINT);
 
-        title.setText(mBundle.getCharSequence(BiometricDialog.KEY_TITLE));
+        title.setText(mBundle.getCharSequence(BiometricPrompt.KEY_TITLE));
         title.setSelected(true);
 
-        final CharSequence subtitleText = mBundle.getCharSequence(BiometricDialog.KEY_SUBTITLE);
-        if (subtitleText == null) {
+        final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
+        if (TextUtils.isEmpty(subtitleText)) {
             subtitle.setVisibility(View.GONE);
         } else {
             subtitle.setVisibility(View.VISIBLE);
             subtitle.setText(subtitleText);
         }
 
-        final CharSequence descriptionText = mBundle.getCharSequence(BiometricDialog.KEY_DESCRIPTION);
-        if (descriptionText == null) {
-            subtitle.setVisibility(View.VISIBLE);
+        final CharSequence descriptionText = mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
+        if (TextUtils.isEmpty(descriptionText)) {
             description.setVisibility(View.GONE);
         } else {
-            description.setText(mBundle.getCharSequence(BiometricDialog.KEY_DESCRIPTION));
+            description.setVisibility(View.VISIBLE);
+            description.setText(descriptionText);
         }
 
-        negative.setText(mBundle.getCharSequence(BiometricDialog.KEY_NEGATIVE_TEXT));
+        negative.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
 
         final CharSequence positiveText =
-                mBundle.getCharSequence(BiometricDialog.KEY_POSITIVE_TEXT);
+                mBundle.getCharSequence(BiometricPrompt.KEY_POSITIVE_TEXT);
         positive.setText(positiveText); // needs to be set for marquee to work
         if (positiveText != null) {
             positive.setVisibility(View.VISIBLE);
@@ -193,26 +211,20 @@
             positive.setVisibility(View.GONE);
         }
 
-        // Dim the background and slide the dialog up
-        mDialog.setTranslationY(mAnimationTranslationOffset);
-        mLayout.setAlpha(0f);
-        postOnAnimation(new Runnable() {
-            @Override
-            public void run() {
-                mLayout.animate()
-                        .alpha(1f)
-                        .setDuration(ANIMATION_DURATION_SHOW)
-                        .setInterpolator(mLinearOutSlowIn)
-                        .withLayer()
-                        .start();
-                mDialog.animate()
-                        .translationY(0)
-                        .setDuration(ANIMATION_DURATION_SHOW)
-                        .setInterpolator(mLinearOutSlowIn)
-                        .withLayer()
-                        .start();
-            }
-        });
+        if (!mWasForceRemoved) {
+            // Dim the background and slide the dialog up
+            mDialog.setTranslationY(mAnimationTranslationOffset);
+            mLayout.setAlpha(0f);
+            postOnAnimation(mShowAnimationRunnable);
+        } else {
+            // Show the dialog immediately
+            mLayout.animate().cancel();
+            mDialog.animate().cancel();
+            mDialog.setAlpha(1.0f);
+            mDialog.setTranslationY(0);
+            mLayout.setAlpha(1.0f);
+        }
+        mWasForceRemoved = false;
     }
 
     private void setDismissesDialog(View v) {
@@ -225,10 +237,13 @@
     }
 
     public void startDismiss() {
+        mAnimatingAway = true;
+
         final Runnable endActionRunnable = new Runnable() {
             @Override
             public void run() {
                 mWindowManager.removeView(FingerprintDialogView.this);
+                mAnimatingAway = false;
             }
         };
 
@@ -252,6 +267,23 @@
         });
     }
 
+    /**
+     * Force remove the window, cancelling any animation that's happening. This should only be
+     * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method
+     * will cause the dialog to show without an animation the next time it's attached.
+     */
+    public void forceRemove() {
+        mLayout.animate().cancel();
+        mDialog.animate().cancel();
+        mWindowManager.removeView(FingerprintDialogView.this);
+        mAnimatingAway = false;
+        mWasForceRemoved = true;
+    }
+
+    public boolean isAnimatingAway() {
+        return mAnimatingAway;
+    }
+
     public void setBundle(Bundle bundle) {
         mBundle = bundle;
     }
@@ -268,10 +300,10 @@
         mHandler.removeMessages(FingerprintDialogImpl.MSG_CLEAR_MESSAGE);
         updateFingerprintIcon(STATE_FINGERPRINT_ERROR);
         mErrorText.setText(message);
-        mErrorText.setTextColor(mErrorTextColor);
+        mErrorText.setTextColor(mErrorColor);
         mErrorText.setContentDescription(message);
         mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_CLEAR_MESSAGE),
-                BiometricDialog.HIDE_DIALOG_DELAY);
+                BiometricPrompt.HIDE_DIALOG_DELAY);
     }
 
     public void showHelpMessage(String message) {
@@ -281,21 +313,17 @@
     public void showErrorMessage(String error) {
         showTemporaryMessage(error);
         mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG,
-                false /* userCanceled */), BiometricDialog.HIDE_DIALOG_DELAY);
+                false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
     }
 
     private void updateFingerprintIcon(int newState) {
-        Drawable icon  = getAnimationResForTransition(mLastState, newState);
+        Drawable icon  = getAnimationForTransition(mLastState, newState);
 
         if (icon == null) {
             Log.e(TAG, "Animation not found");
             return;
         }
 
-        if (newState == STATE_FINGERPRINT) {
-            icon.setColorFilter(mFingerprintColor, PorterDuff.Mode.SRC_IN);
-        }
-
         final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
                 ? (AnimatedVectorDrawable) icon
                 : null;
@@ -303,7 +331,7 @@
         final ImageView fingerprint_icon = mLayout.findViewById(R.id.fingerprint_icon);
         fingerprint_icon.setImageDrawable(icon);
 
-        if (animation != null) {
+        if (animation != null && shouldAnimateForTransition(mLastState, newState)) {
             animation.forceAnimationOnUI();
             animation.start();
         }
@@ -311,17 +339,33 @@
         mLastState = newState;
     }
 
-    private Drawable getAnimationResForTransition(int oldState, int newState) {
+    private boolean shouldAnimateForTransition(int oldState, int newState) {
+        if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) {
+            return false;
+        } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
+            return true;
+        } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
+            return true;
+        } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) {
+            // TODO(b/77328470): add animation when fingerprint is authenticated
+            return false;
+        }
+        return false;
+    }
+
+    private Drawable getAnimationForTransition(int oldState, int newState) {
         int iconRes;
         if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) {
-            iconRes = R.drawable.lockscreen_fingerprint_draw_on_animation;
+            iconRes = R.drawable.fingerprint_dialog_fp_to_error;
         } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
-            iconRes = R.drawable.lockscreen_fingerprint_fp_to_error_state_animation;
+            iconRes = R.drawable.fingerprint_dialog_fp_to_error;
         } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
-            iconRes = R.drawable.lockscreen_fingerprint_error_state_to_fp_animation;
+            iconRes = R.drawable.fingerprint_dialog_error_to_fp;
         } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) {
-            iconRes = R.drawable.lockscreen_fingerprint_draw_off_animation;
-        } else {
+            // TODO(b/77328470): add animation when fingerprint is authenticated
+            iconRes = R.drawable.fingerprint_dialog_error_to_fp;
+        }
+        else {
             return null;
         }
         return mContext.getDrawable(iconRes);
diff --git a/com/android/systemui/globalactions/GlobalActionsDialog.java b/com/android/systemui/globalactions/GlobalActionsDialog.java
index e171b53..a7975d7 100644
--- a/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -107,6 +107,7 @@
 
     static public final String SYSTEM_DIALOG_REASON_KEY = "reason";
     static public final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
+    static public final String SYSTEM_DIALOG_REASON_DREAM = "dream";
 
     private static final String TAG = "GlobalActionsDialog";
 
@@ -376,17 +377,13 @@
 
         mAdapter = new MyAdapter();
 
-        OnItemLongClickListener onItemLongClickListener = new OnItemLongClickListener() {
-            @Override
-            public boolean onItemLongClick(AdapterView<?> parent, View view, int position,
-                    long id) {
-                final Action action = mAdapter.getItem(position);
-                if (action instanceof LongPressAction) {
-                    mDialog.dismiss();
-                    return ((LongPressAction) action).onLongPress();
-                }
-                return false;
+        OnItemLongClickListener onItemLongClickListener = (parent, view, position, id) -> {
+            final Action action = mAdapter.getItem(position);
+            if (action instanceof LongPressAction) {
+                mDialog.dismiss();
+                return ((LongPressAction) action).onLongPress();
             }
+            return false;
         };
         ActionsDialog dialog = new ActionsDialog(mContext, this, mAdapter, onItemLongClickListener);
         dialog.setCanceledOnTouchOutside(false); // Handled by the custom class.
@@ -1236,7 +1233,7 @@
                     || Intent.ACTION_SCREEN_OFF.equals(action)) {
                 String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
                 if (!SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) {
-                    mHandler.sendEmptyMessage(MESSAGE_DISMISS);
+                    mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_DISMISS, reason));
                 }
             } else if (TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED.equals(action)) {
                 // Airplane mode can be changed after ECM exits if airplane toggle button
@@ -1287,7 +1284,11 @@
             switch (msg.what) {
                 case MESSAGE_DISMISS:
                     if (mDialog != null) {
-                        mDialog.dismiss();
+                        if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) {
+                            mDialog.dismissImmediately();
+                        } else {
+                            mDialog.dismiss();
+                        }
                         mDialog = null;
                     }
                     break;
@@ -1468,6 +1469,10 @@
                     .start();
         }
 
+        void dismissImmediately() {
+            super.dismiss();
+        }
+
         private float getAnimTranslation() {
             return getContext().getResources().getDimension(
                     com.android.systemui.R.dimen.global_actions_panel_width) / 2;
diff --git a/com/android/systemui/keyboard/KeyboardUI.java b/com/android/systemui/keyboard/KeyboardUI.java
index 9464105..ebd15f5 100644
--- a/com/android/systemui/keyboard/KeyboardUI.java
+++ b/com/android/systemui/keyboard/KeyboardUI.java
@@ -613,7 +613,7 @@
                                           int bluetoothProfile) { }
 
         @Override
-        public void onProfileAudioStateChanged(int bluetoothProfile, int state) { }
+        public void onAudioModeChanged() { }
     }
 
     private final class BluetoothErrorListener implements Utils.ErrorListener {
diff --git a/com/android/systemui/keyguard/KeyguardViewMediator.java b/com/android/systemui/keyguard/KeyguardViewMediator.java
index d6e59c7..5993c39 100644
--- a/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -145,6 +145,8 @@
     private static final String DELAYED_LOCK_PROFILE_ACTION =
             "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_LOCK";
 
+    private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
+
     // used for handler messages
     private static final int SHOW = 1;
     private static final int HIDE = 2;
@@ -357,7 +359,12 @@
             // ActivityManagerService) will not reconstruct the keyguard if it is already showing.
             synchronized (KeyguardViewMediator.this) {
                 resetKeyguardDonePendingLocked();
-                resetStateLocked();
+                if (mLockPatternUtils.isLockScreenDisabled(userId)) {
+                    // If we switching to a user that has keyguard disabled, dismiss keyguard.
+                    dismiss(null /* callback */, null /* message */);
+                } else {
+                    resetStateLocked();
+                }
                 adjustStatusBarLocked();
             }
         }
@@ -688,11 +695,15 @@
         mShowKeyguardWakeLock.setReferenceCounted(false);
 
         IntentFilter filter = new IntentFilter();
-        filter.addAction(DELAYED_KEYGUARD_ACTION);
-        filter.addAction(DELAYED_LOCK_PROFILE_ACTION);
         filter.addAction(Intent.ACTION_SHUTDOWN);
         mContext.registerReceiver(mBroadcastReceiver, filter);
 
+        final IntentFilter delayedActionFilter = new IntentFilter();
+        delayedActionFilter.addAction(DELAYED_KEYGUARD_ACTION);
+        delayedActionFilter.addAction(DELAYED_LOCK_PROFILE_ACTION);
+        mContext.registerReceiver(mDelayedLockBroadcastReceiver, delayedActionFilter,
+                SYSTEMUI_PERMISSION, null /* scheduler */);
+
         mKeyguardDisplayManager = new KeyguardDisplayManager(mContext, mViewMediatorCallback);
 
         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
@@ -1184,6 +1195,10 @@
         Trace.endSection();
     }
 
+    public boolean isHiding() {
+        return mHiding;
+    }
+
     /**
      * Handles SET_OCCLUDED message sent by setOccluded()
      */
@@ -1456,7 +1471,10 @@
         }
     }
 
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+    /**
+     * This broadcast receiver should be registered with the SystemUI permission.
+     */
+    private final BroadcastReceiver mDelayedLockBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             if (DELAYED_KEYGUARD_ACTION.equals(intent.getAction())) {
@@ -1478,7 +1496,14 @@
                         }
                     }
                 }
-            } else if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
+            }
+        }
+    };
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
                 synchronized (KeyguardViewMediator.this){
                     mShuttingDown = true;
                 }
diff --git a/com/android/systemui/pip/phone/PipTouchHandler.java b/com/android/systemui/pip/phone/PipTouchHandler.java
index a0bdcd0..1805f96 100644
--- a/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -295,6 +295,26 @@
                 final Rect toAdjustedBounds = mMenuState == MENU_STATE_FULL
                         ? expandedAdjustedBounds
                         : normalAdjustedBounds;
+                final Rect toMovementBounds = mMenuState == MENU_STATE_FULL
+                        ? expandedMovementBounds
+                        : normalMovementBounds;
+
+                // If the PIP window needs to shift to right above shelf/IME and it's already above
+                // that, don't move the PIP window.
+                if (toAdjustedBounds.bottom < mMovementBounds.bottom
+                        && animatingBounds.top < toAdjustedBounds.bottom) {
+                    return;
+                }
+
+                // If the PIP window needs to shift down due to dismissal of shelf/IME but it's way
+                // above the position as if shelf/IME shows, don't move the PIP window.
+                int movementBoundsAdjustment = toMovementBounds.bottom - mMovementBounds.bottom;
+                int offsetAdjustment = fromImeAdjustment ? mImeOffset : mShelfHeight;
+                if (toAdjustedBounds.bottom >= mMovementBounds.bottom
+                        && animatingBounds.top
+                        < toAdjustedBounds.bottom - movementBoundsAdjustment - offsetAdjustment) {
+                    return;
+                }
 
                 animateToOffset(animatingBounds, toAdjustedBounds);
             }
@@ -320,10 +340,6 @@
 
     private void animateToOffset(Rect animatingBounds, Rect toAdjustedBounds) {
         final Rect bounds = new Rect(animatingBounds);
-        if (toAdjustedBounds.bottom < mMovementBounds.bottom
-                && bounds.top < toAdjustedBounds.bottom) {
-            return;
-        }
         bounds.offset(0, toAdjustedBounds.bottom - bounds.top);
         // In landscape mode, PIP window can go offset while launching IME. We want to align the
         // the top of the PIP window with the top of the movement bounds in that case.
diff --git a/com/android/systemui/power/PowerNotificationWarnings.java b/com/android/systemui/power/PowerNotificationWarnings.java
index 49b00ce..40ce69b 100644
--- a/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/com/android/systemui/power/PowerNotificationWarnings.java
@@ -229,10 +229,11 @@
                         // Bump the notification when the bucket dropped.
                         .setWhen(mWarningTriggerTimeMs)
                         .setShowWhen(false)
-                        .setContentTitle(title)
                         .setContentText(contentText)
+                        .setContentTitle(title)
                         .setOnlyAlertOnce(true)
                         .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING))
+                        .setStyle(new Notification.BigTextStyle().bigText(contentText))
                         .setVisibility(Notification.VISIBILITY_PUBLIC);
         if (hasBatterySettings()) {
             nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS));
@@ -483,16 +484,24 @@
         d.setMessage(mContext.getString(R.string.auto_saver_enabled_text,
                 getLowBatteryAutoTriggerDefaultLevel()));
 
-        // Negative == "got it". Just close the dialog. Battery saver has already been enabled.
-        d.setNegativeButton(R.string.auto_saver_okay_action, null);
-        d.setPositiveButton(R.string.open_saver_setting_action, (dialog, which) ->
-                mContext.startActivity(actionBatterySaverSetting));
+        // "Got it". Just close the dialog. Automatic battery has been enabled already.
+        d.setPositiveButton(R.string.auto_saver_okay_action,
+                (dialog, which) -> onAutoSaverEnabledConfirmationClosed());
+
+        // "Settings" -> Opens the battery saver settings activity.
+        d.setNeutralButton(R.string.open_saver_setting_action, (dialog, which) -> {
+            mContext.startActivity(actionBatterySaverSetting);
+            onAutoSaverEnabledConfirmationClosed();
+        });
         d.setShowForAllUsers(true);
-        d.setOnDismissListener((dialog) -> mSaverEnabledConfirmation = null);
+        d.setOnDismissListener((dialog) -> onAutoSaverEnabledConfirmationClosed());
         d.show();
         mSaverEnabledConfirmation = d;
     }
 
+    private void onAutoSaverEnabledConfirmationClosed() {
+        mSaverEnabledConfirmation = null;
+    }
 
     private void setSaverMode(boolean mode, boolean needFirstTimeWarning) {
         BatterySaverUtils.setPowerSaveMode(mContext, mode, needFirstTimeWarning);
@@ -505,7 +514,7 @@
             autoTriggerThreshold = 15;
         }
 
-        BatterySaverUtils.scheduleAutoBatterySaver(mContext, autoTriggerThreshold);
+        BatterySaverUtils.ensureAutoBatterySaver(mContext, autoTriggerThreshold);
         showAutoSaverEnabledConfirmation();
     }
 
diff --git a/com/android/systemui/qs/QSContainerImpl.java b/com/android/systemui/qs/QSContainerImpl.java
index bfbfbf6..a9455f2 100644
--- a/com/android/systemui/qs/QSContainerImpl.java
+++ b/com/android/systemui/qs/QSContainerImpl.java
@@ -16,19 +16,19 @@
 
 package com.android.systemui.qs;
 
+import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
+
 import android.content.Context;
 import android.content.res.Configuration;
-import android.graphics.Canvas;
-import android.graphics.Path;
 import android.graphics.Point;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.FrameLayout;
 
-import com.android.settingslib.Utils;
 import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.qs.customize.QSCustomizer;
-import com.android.systemui.statusbar.ExpandableOutlineView;
+import com.android.systemui.statusbar.CommandQueue;
 
 /**
  * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
@@ -44,8 +44,13 @@
     protected float mQsExpansion;
     private QSCustomizer mQSCustomizer;
     private View mQSFooter;
+
     private View mBackground;
+    private View mBackgroundGradient;
+    private View mStatusBarBackground;
+
     private int mSideMargins;
+    private boolean mQsDisabled;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -60,6 +65,8 @@
         mQSCustomizer = findViewById(R.id.qs_customize);
         mQSFooter = findViewById(R.id.qs_footer);
         mBackground = findViewById(R.id.quick_settings_background);
+        mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
+        mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
         mSideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings);
 
         setClickable(true);
@@ -68,6 +75,23 @@
     }
 
     @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        // Hide the backgrounds when in landscape mode.
+        if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            mBackgroundGradient.setVisibility(View.INVISIBLE);
+            mStatusBarBackground.setVisibility(View.INVISIBLE);
+        } else {
+            mBackgroundGradient.setVisibility(View.VISIBLE);
+            mStatusBarBackground.setVisibility(View.VISIBLE);
+        }
+
+        updateResources();
+        mSizePoint.set(0, 0); // Will be retrieved on next measure pass.
+    }
+
+    @Override
     public boolean performClick() {
         // Want to receive clicks so missing QQS tiles doesn't cause collapse, but
         // don't want to do anything with them.
@@ -76,6 +100,16 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mQsDisabled) {
+            // Only show the status bar contents in QQS header when QS is disabled.
+            mHeader.measure(widthMeasureSpec, heightMeasureSpec);
+            LayoutParams layoutParams = (LayoutParams) mHeader.getLayoutParams();
+            int height = layoutParams.topMargin + layoutParams.bottomMargin
+                    + mHeader.getMeasuredHeight();
+            super.onMeasure(
+                    widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+            return;
+        }
         // Since we control our own bottom, be whatever size we want.
         // Otherwise the QSPanel ends up with 0 height when the window is only the
         // size of the status bar.
@@ -90,9 +124,8 @@
 
         // QSCustomizer will always be the height of the screen, but do this after
         // other measuring to avoid changing the height of the QS.
-        getDisplay().getRealSize(mSizePoint);
         mQSCustomizer.measure(widthMeasureSpec,
-                MeasureSpec.makeMeasureSpec(mSizePoint.y, MeasureSpec.EXACTLY));
+                MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
     }
 
     @Override
@@ -101,6 +134,23 @@
         updateExpansion();
     }
 
+    public void disable(int state1, int state2, boolean animate) {
+        final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
+        if (disabled == mQsDisabled) return;
+        mQsDisabled = disabled;
+        mBackgroundGradient.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+        mQSPanel.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+        mQSFooter.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+    }
+
+    private void updateResources() {
+        LayoutParams layoutParams = (LayoutParams) mQSPanel.getLayoutParams();
+        layoutParams.topMargin = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.quick_qs_offset_height);
+
+        mQSPanel.setLayoutParams(layoutParams);
+    }
+
     /**
      * Overrides the height of this view (post-layout), so that the content is clipped to that
      * height and the background is set to that height.
@@ -147,4 +197,11 @@
         lp.rightMargin = mSideMargins;
         lp.leftMargin = mSideMargins;
     }
+
+    private int getDisplayHeight() {
+        if (mSizePoint.y == 0) {
+            getDisplay().getRealSize(mSizePoint);
+        }
+        return mSizePoint.y;
+    }
 }
diff --git a/com/android/systemui/qs/QSFooter.java b/com/android/systemui/qs/QSFooter.java
index 3f3cea2..6c7eda7 100644
--- a/com/android/systemui/qs/QSFooter.java
+++ b/com/android/systemui/qs/QSFooter.java
@@ -69,4 +69,6 @@
      */
     @Nullable
     View getExpandView();
+
+    default void disable(int state1, int state2, boolean animate) {}
 }
diff --git a/com/android/systemui/qs/QSFooterImpl.java b/com/android/systemui/qs/QSFooterImpl.java
index 0fa6597..28dd26f 100644
--- a/com/android/systemui/qs/QSFooterImpl.java
+++ b/com/android/systemui/qs/QSFooterImpl.java
@@ -47,10 +47,8 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.R.dimen;
-import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.TouchAnimator.Builder;
-import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.MultiUserSwitch;
 import com.android.systemui.statusbar.phone.SettingsButton;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -62,8 +60,7 @@
 import com.android.systemui.tuner.TunerService;
 
 public class QSFooterImpl extends FrameLayout implements QSFooter,
-        OnClickListener, OnUserInfoChangedListener, EmergencyListener,
-        SignalCallback, CommandQueue.Callbacks {
+        OnClickListener, OnUserInfoChangedListener, EmergencyListener, SignalCallback {
 
     private ActivityStarter mActivityStarter;
     private UserInfoController mUserInfoController;
@@ -211,16 +208,9 @@
     }
 
     @Override
-    public void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
-    }
-
-    @Override
     @VisibleForTesting
     public void onDetachedFromWindow() {
         setListening(false);
-        SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this);
         super.onDetachedFromWindow();
     }
 
@@ -393,7 +383,7 @@
             if (TextUtils.equals(mInfo.typeContentDescription,
                     mContext.getString(R.string.data_connection_no_internet))
                 || TextUtils.equals(mInfo.typeContentDescription,
-                    mContext.getString(R.string.cell_data_off))) {
+                    mContext.getString(R.string.cell_data_off_content_description))) {
                 contentDescription.append(mInfo.typeContentDescription);
             }
             mMobileSignal.setContentDescription(contentDescription);
diff --git a/com/android/systemui/qs/QSFragment.java b/com/android/systemui/qs/QSFragment.java
index 018a635..cb068e3 100644
--- a/com/android/systemui/qs/QSFragment.java
+++ b/com/android/systemui/qs/QSFragment.java
@@ -14,9 +14,12 @@
 
 package com.android.systemui.qs;
 
+import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.app.Fragment;
+import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -35,12 +38,14 @@
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.R.id;
+import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 import com.android.systemui.statusbar.stack.StackStateAnimator;
 
-public class QSFragment extends Fragment implements QS {
+public class QSFragment extends Fragment implements QS, CommandQueue.Callbacks {
     private static final String TAG = "QS";
     private static final boolean DEBUG = false;
     private static final String EXTRA_EXPANDED = "expanded";
@@ -65,6 +70,7 @@
     private int mLayoutDirection;
     private QSFooter mFooter;
     private float mLastQSExpansion = -1;
+    private boolean mQsDisabled;
 
     @Override
     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -176,6 +182,17 @@
         }
     }
 
+    @Override
+    public void disable(int state1, int state2, boolean animate) {
+        final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
+        if (disabled == mQsDisabled) return;
+        mQsDisabled = disabled;
+        mContainer.disable(state1, state2, animate);
+        mHeader.disable(state1, state2, animate);
+        mFooter.disable(state1, state2, animate);
+        updateQsState();
+    }
+
     private void updateQsState() {
         final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
                 || mHeaderAnimating;
@@ -189,6 +206,9 @@
         mFooter.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
                 ? View.VISIBLE
                 : View.INVISIBLE);
+        if (mQsDisabled) {
+            mFooter.setVisibility(View.GONE);
+        }
         mFooter.setExpanded((mKeyguardShowing && !mHeaderAnimating)
                 || (mQsExpanded && !mStackScrollerOverscrolling));
         mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
@@ -258,6 +278,12 @@
         mHeader.setListening(listening);
         mFooter.setListening(listening);
         mQSPanel.setListening(mListening && mQsExpanded);
+        if (listening) {
+            SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
+        } else {
+            SysUiServiceProvider.getComponent(getContext(), CommandQueue.class)
+                    .removeCallbacks(this);
+        }
     }
 
     @Override
diff --git a/com/android/systemui/qs/QuickStatusBarHeader.java b/com/android/systemui/qs/QuickStatusBarHeader.java
index df65d1f..e2af90d 100644
--- a/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -26,6 +26,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.media.AudioManager;
@@ -54,8 +55,10 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
+import com.android.systemui.statusbar.policy.Clock;
 import com.android.systemui.statusbar.policy.DarkIconDispatcher;
 import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.policy.DateView;
 import com.android.systemui.statusbar.policy.NextAlarmController;
 
 import java.util.Locale;
@@ -65,7 +68,7 @@
  * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner
  * contents.
  */
-public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue.Callbacks,
+public class QuickStatusBarHeader extends RelativeLayout implements
         View.OnClickListener, NextAlarmController.NextAlarmChangeCallback {
     private static final String TAG = "QuickStatusBarHeader";
     private static final boolean DEBUG = false;
@@ -90,6 +93,7 @@
     private TouchAnimator mStatusIconsAlphaAnimator;
     private TouchAnimator mHeaderTextContainerAlphaAnimator;
 
+    private View mSystemIconsView;
     private View mQuickQsStatusIcons;
     private View mDate;
     private View mHeaderTextContainerView;
@@ -107,6 +111,9 @@
     private View mStatusSeparator;
     private ImageView mRingerModeIcon;
     private TextView mRingerModeTextView;
+    private BatteryMeterView mBatteryMeterView;
+    private Clock mClockView;
+    private DateView mDateView;
 
     private NextAlarmController mAlarmController;
     /** Counts how many times the long press tooltip has been shown to the user. */
@@ -138,6 +145,7 @@
         mHeaderQsPanel = findViewById(R.id.quick_qs_panel);
         mDate = findViewById(R.id.date);
         mDate.setOnClickListener(this);
+        mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons);
         mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
         mIconManager = new TintedIconManager(findViewById(R.id.statusIcons));
 
@@ -164,19 +172,21 @@
         // Set the correct tint for the status icons so they contrast
         mIconManager.setTint(fillColor);
 
-        BatteryMeterView battery = findViewById(R.id.battery);
-        battery.setForceShowPercent(true);
+        mBatteryMeterView = findViewById(R.id.battery);
+        mBatteryMeterView.setForceShowPercent(true);
+        mClockView = findViewById(R.id.clock);
+        mDateView = findViewById(R.id.date);
     }
 
     private void updateStatusText() {
         boolean ringerVisible = false;
         if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
             mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_vibrate);
-            mRingerModeTextView.setText(R.string.volume_ringer_status_vibrate);
+            mRingerModeTextView.setText(R.string.qs_status_phone_vibrate);
             ringerVisible = true;
         } else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) {
             mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_silent);
-            mRingerModeTextView.setText(R.string.volume_ringer_status_silent);
+            mRingerModeTextView.setText(R.string.qs_status_phone_muted);
             ringerVisible = true;
         }
         mRingerModeIcon.setVisibility(ringerVisible ? View.VISIBLE : View.GONE);
@@ -212,6 +222,13 @@
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         updateResources();
+
+        // Update color schemes in landscape to use wallpaperTextColor
+        boolean shouldUseWallpaperTextColor =
+                newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
+        mBatteryMeterView.useWallpaperTextColor(shouldUseWallpaperTextColor);
+        mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor);
+        mDateView.useWallpaperTextColor(shouldUseWallpaperTextColor);
     }
 
     @Override
@@ -221,11 +238,22 @@
     }
 
     private void updateResources() {
-        // Update height, especially due to landscape mode restricting space.
+        Resources resources = mContext.getResources();
+
+        // Update height for a few views, especially due to landscape mode restricting space.
         mHeaderTextContainerView.getLayoutParams().height =
-                mContext.getResources().getDimensionPixelSize(R.dimen.qs_header_tooltip_height);
+                resources.getDimensionPixelSize(R.dimen.qs_header_tooltip_height);
         mHeaderTextContainerView.setLayoutParams(mHeaderTextContainerView.getLayoutParams());
 
+        mSystemIconsView.getLayoutParams().height = resources.getDimensionPixelSize(
+                com.android.internal.R.dimen.quick_qs_offset_height);
+        mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams());
+
+        getLayoutParams().height = resources.getDimensionPixelSize(mQsDisabled
+                ? com.android.internal.R.dimen.quick_qs_offset_height
+                : com.android.internal.R.dimen.quick_qs_total_height);
+        setLayoutParams(getLayoutParams());
+
         updateStatusIconAlphaAnimator();
         updateHeaderTextContainerAlphaAnimator();
     }
@@ -293,20 +321,18 @@
                 TOOLTIP_NOT_YET_SHOWN_COUNT);
     }
 
-    @Override
     public void disable(int state1, int state2, boolean animate) {
         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
         if (disabled == mQsDisabled) return;
         mQsDisabled = disabled;
         mHeaderQsPanel.setDisabledByPolicy(disabled);
-        final int rawHeight = (int) getResources().getDimension(
-                com.android.internal.R.dimen.quick_qs_total_height);
-        getLayoutParams().height = disabled ? (rawHeight - mHeaderQsPanel.getHeight()) : rawHeight;
+        mHeaderTextContainerView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+        mQuickQsStatusIcons.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+        updateResources();
     }
 
     @Override
     public void onAttachedToWindow() {
-        SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
         Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager);
         requestApplyInsets();
     }
@@ -327,7 +353,6 @@
     @VisibleForTesting
     public void onDetachedFromWindow() {
         setListening(false);
-        SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this);
         Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager);
         super.onDetachedFromWindow();
     }
@@ -497,9 +522,8 @@
         mHeaderQsPanel.setHost(host, null /* No customization in header */);
 
         // Use SystemUI context to get battery meter colors, and let it use the default tint (white)
-        BatteryMeterView battery = findViewById(R.id.battery);
-        battery.setColorsFromContext(mHost.getContext());
-        battery.onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
+        mBatteryMeterView.setColorsFromContext(mHost.getContext());
+        mBatteryMeterView.onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
     }
 
     public void setCallback(Callback qsPanelCallback) {
diff --git a/com/android/systemui/qs/car/CarQSFooter.java b/com/android/systemui/qs/car/CarQSFooter.java
index 23d3ebb..24b5a34 100644
--- a/com/android/systemui/qs/car/CarQSFooter.java
+++ b/com/android/systemui/qs/car/CarQSFooter.java
@@ -29,7 +29,6 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.QSFooter;
 import com.android.systemui.qs.QSPanel;
-import com.android.systemui.statusbar.car.UserGridView;
 import com.android.systemui.statusbar.phone.MultiUserSwitch;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.UserInfoController;
diff --git a/com/android/systemui/qs/car/CarQSFragment.java b/com/android/systemui/qs/car/CarQSFragment.java
index 0ee6d1f..da21aa5 100644
--- a/com/android/systemui/qs/car/CarQSFragment.java
+++ b/com/android/systemui/qs/car/CarQSFragment.java
@@ -20,21 +20,20 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.app.Fragment;
+import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.GridLayoutManager;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.QSFooter;
-import com.android.systemui.statusbar.car.PageIndicator;
-import com.android.systemui.statusbar.car.UserGridView;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.car.UserGridRecyclerView;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -45,14 +44,12 @@
  * status bar, and a static row with access to the user switcher and settings.
  */
 public class CarQSFragment extends Fragment implements QS {
-    private ViewGroup mPanel;
     private View mHeader;
     private View mUserSwitcherContainer;
     private CarQSFooter mFooter;
     private View mFooterUserName;
     private View mFooterExpandIcon;
-    private UserGridView mUserGridView;
-    private PageIndicator mPageIndicator;
+    private UserGridRecyclerView mUserGridView;
     private AnimatorSet mAnimatorSet;
     private UserSwitchCallback mUserSwitchCallback;
 
@@ -65,7 +62,6 @@
     @Override
     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        mPanel = (ViewGroup) view;
         mHeader = view.findViewById(R.id.header);
         mFooter = view.findViewById(R.id.qs_footer);
         mFooterUserName = mFooter.findViewById(R.id.user_name);
@@ -75,16 +71,15 @@
 
         updateUserSwitcherHeight(0);
 
-        mUserGridView = view.findViewById(R.id.user_grid);
-        mUserGridView.init(null, Dependency.get(UserSwitcherController.class),
-                false /* overrideAlpha */);
-
-        mPageIndicator = view.findViewById(R.id.user_switcher_page_indicator);
-        mPageIndicator.setupWithViewPager(mUserGridView);
+        Context context = getContext();
+        mUserGridView = mUserSwitcherContainer.findViewById(R.id.user_grid);
+        GridLayoutManager layoutManager = new GridLayoutManager(context,
+                context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col));
+        mUserGridView.setLayoutManager(layoutManager);
+        mUserGridView.buildAdapter();
 
         mUserSwitchCallback = new UserSwitchCallback();
         mFooter.setUserSwitchCallback(mUserSwitchCallback);
-        mUserGridView.setUserSwitchCallback(mUserSwitchCallback);
     }
 
     @Override
@@ -111,13 +106,11 @@
     @Override
     public void setHeaderListening(boolean listening) {
         mFooter.setListening(listening);
-        mUserGridView.setListening(listening);
     }
 
     @Override
     public void setListening(boolean listening) {
         mFooter.setListening(listening);
-        mUserGridView.setListening(listening);
     }
 
     @Override
@@ -219,24 +212,6 @@
             mShowing = false;
             animateHeightChange(false /* opening */);
         }
-
-        public void resetShowing() {
-            if (mShowing) {
-                for (int i = 0; i < mUserGridView.getChildCount(); i++) {
-                    ViewGroup podContainer = (ViewGroup) mUserGridView.getChildAt(i);
-                    // Need to bring the last child to the front to maintain the order in the pod
-                    // container. Why? ¯\_(ツ)_/¯
-                    if (podContainer.getChildCount() > 0) {
-                        podContainer.getChildAt(podContainer.getChildCount() - 1).bringToFront();
-                    }
-                    // The alpha values are default to 0, so if the pods have been refreshed, they
-                    // need to be set to 1 when showing.
-                    for (int j = 0; j < podContainer.getChildCount(); j++) {
-                        podContainer.getChildAt(j).setAlpha(1f);
-                    }
-                }
-            }
-        }
     }
 
     private void updateUserSwitcherHeight(int height) {
@@ -260,27 +235,6 @@
         });
         allAnimators.add(heightAnimator);
 
-        // The user grid contains pod containers that each contain a number of pods.  Animate
-        // all pods to avoid any discrepancy/race conditions with possible changes during the
-        // animation.
-        int cascadeDelay = getResources().getInteger(
-                R.integer.car_user_switcher_anim_cascade_delay_ms);
-        for (int i = 0; i < mUserGridView.getChildCount(); i++) {
-            ViewGroup podContainer = (ViewGroup) mUserGridView.getChildAt(i);
-            for (int j = 0; j < podContainer.getChildCount(); j++) {
-                View pod = podContainer.getChildAt(j);
-                Animator podAnimator = AnimatorInflater.loadAnimator(getContext(),
-                        opening ? R.anim.car_user_switcher_open_pod_animation
-                                : R.anim.car_user_switcher_close_pod_animation);
-                // Add the cascading delay between pods
-                if (opening) {
-                    podAnimator.setStartDelay(podAnimator.getStartDelay() + j * cascadeDelay);
-                }
-                podAnimator.setTarget(pod);
-                allAnimators.add(podAnimator);
-            }
-        }
-
         Animator nameAnimator = AnimatorInflater.loadAnimator(getContext(),
                 opening ? R.anim.car_user_switcher_open_name_animation
                         : R.anim.car_user_switcher_close_name_animation);
@@ -293,12 +247,6 @@
         iconAnimator.setTarget(mFooterExpandIcon);
         allAnimators.add(iconAnimator);
 
-        Animator pageAnimator = AnimatorInflater.loadAnimator(getContext(),
-                opening ? R.anim.car_user_switcher_open_pages_animation
-                        : R.anim.car_user_switcher_close_pages_animation);
-        pageAnimator.setTarget(mPageIndicator);
-        allAnimators.add(pageAnimator);
-
         mAnimatorSet = new AnimatorSet();
         mAnimatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
diff --git a/com/android/systemui/qs/customize/CustomizeTileView.java b/com/android/systemui/qs/customize/CustomizeTileView.java
index eb95866..20e3cee 100644
--- a/com/android/systemui/qs/customize/CustomizeTileView.java
+++ b/com/android/systemui/qs/customize/CustomizeTileView.java
@@ -44,4 +44,9 @@
     public TextView getAppLabel() {
         return mSecondLine;
     }
+
+    @Override
+    protected boolean animationsEnabled() {
+        return false;
+    }
 }
diff --git a/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 0f83078..e7e756f 100644
--- a/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -16,6 +16,8 @@
 
 import static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -127,7 +129,6 @@
     }
 
     protected void setIcon(ImageView iv, QSTile.State state) {
-        updateIcon(iv, state);
         if (state.disabledByPolicy) {
             iv.setColorFilter(getContext().getColor(R.color.qs_tile_disabled_color));
         } else {
@@ -137,7 +138,7 @@
             int color = getColor(state.state);
             mState = state.state;
             if (iv.isShown() && mTint != 0) {
-                animateGrayScale(mTint, color, iv);
+                animateGrayScale(mTint, color, iv, () -> updateIcon(iv, state));
                 mTint = color;
             } else {
                 if (iv instanceof AlphaControlledSlashImageView) {
@@ -147,7 +148,10 @@
                     setTint(iv, color);
                 }
                 mTint = color;
+                updateIcon(iv, state);
             }
+        } else {
+            updateIcon(iv, state);
         }
     }
 
@@ -155,12 +159,13 @@
         return getColorForState(getContext(), state);
     }
 
-    public static void animateGrayScale(int fromColor, int toColor, ImageView iv) {
+    private void animateGrayScale(int fromColor, int toColor, ImageView iv,
+        final Runnable endRunnable) {
         if (iv instanceof AlphaControlledSlashImageView) {
             ((AlphaControlledSlashImageView)iv)
                     .setFinalImageTintList(ColorStateList.valueOf(toColor));
         }
-        if (ValueAnimator.areAnimatorsEnabled()) {
+        if (mAnimationEnabled && ValueAnimator.areAnimatorsEnabled()) {
             final float fromAlpha = Color.alpha(fromColor);
             final float toAlpha = Color.alpha(toColor);
             final float fromChannel = Color.red(fromColor);
@@ -175,10 +180,16 @@
 
                 setTint(iv, Color.argb(alpha, channel, channel, channel));
             });
-
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    endRunnable.run();
+                }
+            });
             anim.start();
         } else {
             setTint(iv, toColor);
+            endRunnable.run();
         }
     }
 
diff --git a/com/android/systemui/qs/tileimpl/QSTileBaseView.java b/com/android/systemui/qs/tileimpl/QSTileBaseView.java
index 09d928f..cc60f87 100644
--- a/com/android/systemui/qs/tileimpl/QSTileBaseView.java
+++ b/com/android/systemui/qs/tileimpl/QSTileBaseView.java
@@ -179,7 +179,7 @@
     protected void handleStateChanged(QSTile.State state) {
         int circleColor = getCircleColor(state.state);
         if (circleColor != mCircleColor) {
-            if (mBg.isShown()) {
+            if (mBg.isShown() && animationsEnabled()) {
                 ValueAnimator animator = ValueAnimator.ofArgb(mCircleColor, circleColor)
                         .setDuration(QS_ANIM_LENGTH);
                 animator.addUpdateListener(animation -> mBg.setImageTintList(ColorStateList.valueOf(
@@ -205,6 +205,10 @@
         }
     }
 
+    protected boolean animationsEnabled() {
+        return true;
+    }
+
     private int getCircleColor(int state) {
         switch (state) {
             case Tile.STATE_ACTIVE:
diff --git a/com/android/systemui/qs/tiles/CellularTile.java b/com/android/systemui/qs/tiles/CellularTile.java
index 2abe9d9..d6182c4 100644
--- a/com/android/systemui/qs/tiles/CellularTile.java
+++ b/com/android/systemui/qs/tiles/CellularTile.java
@@ -108,20 +108,21 @@
         }
         if (mDataController.isMobileDataEnabled()) {
             if (mKeyguardMonitor.isSecure() && !mKeyguardMonitor.canSkipBouncer()) {
-                mActivityStarter.postQSRunnableDismissingKeyguard(this::showDisableDialog);
+                mActivityStarter.postQSRunnableDismissingKeyguard(this::maybeShowDisableDialog);
             } else {
-                if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) {
-                    mDataController.setMobileDataEnabled(false);
-                } else {
-                    mUiHandler.post(this::showDisableDialog);
-                }
+                mUiHandler.post(this::maybeShowDisableDialog);
             }
         } else {
             mDataController.setMobileDataEnabled(true);
         }
     }
 
-    private void showDisableDialog() {
+    private void maybeShowDisableDialog() {
+        if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) {
+            // Directly turn off mobile data if the user has seen the dialog before.
+            mDataController.setMobileDataEnabled(false);
+            return;
+        }
         mHost.collapsePanels();
         String carrierName = mController.getMobileDataNetworkName();
         if (TextUtils.isEmpty(carrierName)) {
@@ -194,7 +195,18 @@
             state.state = Tile.STATE_INACTIVE;
             state.secondaryLabel = r.getString(R.string.cell_data_off);
         }
-        state.contentDescription = state.label + ", " + state.secondaryLabel;
+
+
+        // TODO(b/77881974): Instead of switching out the description via a string check for
+        // we need to have two strings provided by the MobileIconGroup.
+        final CharSequence contentDescriptionSuffix;
+        if (state.state == Tile.STATE_INACTIVE) {
+            contentDescriptionSuffix = r.getString(R.string.cell_data_off_content_description);
+        } else {
+            contentDescriptionSuffix = state.secondaryLabel;
+        }
+
+        state.contentDescription = state.label + ", " + contentDescriptionSuffix;
     }
 
     private CharSequence getMobileDataDescription(CallbackInfo cb) {
diff --git a/com/android/systemui/qs/tiles/DndTile.java b/com/android/systemui/qs/tiles/DndTile.java
index 7dcf5c0..16c2a75 100644
--- a/com/android/systemui/qs/tiles/DndTile.java
+++ b/com/android/systemui/qs/tiles/DndTile.java
@@ -143,26 +143,41 @@
     public void showDetail(boolean show) {
         int zenDuration = Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.ZEN_DURATION, 0);
-        switch (zenDuration) {
-            case Settings.Global.ZEN_DURATION_PROMPT:
-                mUiHandler.post(() -> {
-                    Dialog mDialog = new EnableZenModeDialog(mContext).createDialog();
-                    mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
-                    SystemUIDialog.setShowForAllUsers(mDialog, true);
-                    SystemUIDialog.registerDismissListener(mDialog);
-                    SystemUIDialog.setWindowOnTop(mDialog);
-                    mUiHandler.post(() -> mDialog.show());
-                    mHost.collapsePanels();
-                });
-                break;
-            case Settings.Global.ZEN_DURATION_FOREVER:
-                mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
-                break;
-            default:
-                Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration,
-                        ActivityManager.getCurrentUser(), true).id;
-                mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                        conditionId, TAG);
+        boolean showOnboarding = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.SHOW_ZEN_UPGRADE_NOTIFICATION, 0) != 0;
+        if (showOnboarding) {
+            // don't show on-boarding again or notification ever
+            Settings.Global.putInt(mContext.getContentResolver(),
+                    Global.SHOW_ZEN_UPGRADE_NOTIFICATION, 0);
+            // turn on DND
+            mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
+            // show on-boarding screen
+            Intent intent = new Intent(Settings.ZEN_MODE_ONBOARDING);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(intent, 0);
+        } else {
+            switch (zenDuration) {
+                case Settings.Global.ZEN_DURATION_PROMPT:
+                    mUiHandler.post(() -> {
+                        Dialog mDialog = new EnableZenModeDialog(mContext).createDialog();
+                        mDialog.getWindow().setType(
+                                WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+                        SystemUIDialog.setShowForAllUsers(mDialog, true);
+                        SystemUIDialog.registerDismissListener(mDialog);
+                        SystemUIDialog.setWindowOnTop(mDialog);
+                        mUiHandler.post(() -> mDialog.show());
+                        mHost.collapsePanels();
+                    });
+                    break;
+                case Settings.Global.ZEN_DURATION_FOREVER:
+                    mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
+                    break;
+                default:
+                    Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration,
+                            ActivityManager.getCurrentUser(), true).id;
+                    mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                            conditionId, TAG);
+            }
         }
     }
 
@@ -209,7 +224,7 @@
         state.slash.isSlashed = !state.value;
         state.label = getTileLabel();
         state.secondaryLabel = ZenModeConfig.getDescription(mContext,zen != Global.ZEN_MODE_OFF,
-                mController.getConfig());
+                mController.getConfig(), false);
         state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on);
         checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_ADJUST_VOLUME);
         switch (zen) {
diff --git a/com/android/systemui/qs/tiles/WifiTile.java b/com/android/systemui/qs/tiles/WifiTile.java
index 8a1e4da..d8f7b71 100644
--- a/com/android/systemui/qs/tiles/WifiTile.java
+++ b/com/android/systemui/qs/tiles/WifiTile.java
@@ -41,6 +41,7 @@
 import com.android.systemui.qs.QSDetailItems;
 import com.android.systemui.qs.QSDetailItems.Item;
 import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.tileimpl.QSIconViewImpl;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.NetworkController.AccessPointController;
@@ -60,6 +61,7 @@
 
     protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback();
     private final ActivityStarter mActivityStarter;
+    private boolean mExpectDisabled;
 
     public WifiTile(QSHost host) {
         super(host);
@@ -120,6 +122,15 @@
         // Immediately enter transient state when turning on wifi.
         refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
         mController.setWifiEnabled(!wifiEnabled);
+        mExpectDisabled = wifiEnabled;
+        if (mExpectDisabled) {
+            mHandler.postDelayed(() -> {
+                if (mExpectDisabled) {
+                    mExpectDisabled = false;
+                    refreshState();
+                }
+            }, QSIconViewImpl.QS_ANIM_LENGTH);
+        }
     }
 
     @Override
@@ -143,11 +154,13 @@
     @Override
     protected void handleUpdateState(SignalState state, Object arg) {
         if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg);
-        final CallbackInfo cb;
-        if (arg != null && arg instanceof CallbackInfo) {
-            cb = (CallbackInfo) arg;
-        } else {
-            cb = mSignalCallback.mInfo;
+        final CallbackInfo cb = mSignalCallback.mInfo;
+        if (mExpectDisabled) {
+            if (cb.enabled) {
+                return; // Ignore updates until disabled event occurs.
+            } else {
+                mExpectDisabled = false;
+            }
         }
         boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
         boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0) && (cb.ssid != null);
@@ -288,7 +301,7 @@
             if (isShowingDetail()) {
                 mDetailAdapter.updateItems();
             }
-            refreshState(mInfo);
+            refreshState();
         }
     }
 
diff --git a/com/android/systemui/recents/Recents.java b/com/android/systemui/recents/Recents.java
index 0f85c5b..8bb3c02 100644
--- a/com/android/systemui/recents/Recents.java
+++ b/com/android/systemui/recents/Recents.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS;
 
 import android.app.ActivityManager;
+import android.app.trust.TrustManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -51,6 +52,7 @@
 import com.android.systemui.OverviewProxyService;
 import com.android.systemui.R;
 import com.android.systemui.RecentsComponent;
+import com.android.systemui.SystemUIApplication;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.SystemUI;
 import com.android.systemui.recents.events.EventBus;
@@ -70,6 +72,7 @@
 import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
 
+import com.android.systemui.statusbar.phone.StatusBar;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -107,6 +110,7 @@
 
     private Handler mHandler;
     private RecentsImpl mImpl;
+    private TrustManager mTrustManager;
     private int mDraggingInRecentsCurrentUser;
 
     // Only For system user, this is the callbacks instance we return to each secondary user
@@ -235,6 +239,8 @@
             registerWithSystemUser();
         }
         putComponent(Recents.class, this);
+
+        mTrustManager = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
     }
 
     @Override
@@ -342,12 +348,28 @@
         // If connected to launcher service, let it handle the toggle logic
         IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
         if (overviewProxy != null) {
-            try {
-                overviewProxy.onOverviewToggle();
-                return;
-            } catch (RemoteException e) {
-                Log.e(TAG, "Cannot send toggle recents through proxy service.", e);
+            final Runnable toggleRecents = () -> {
+                try {
+                    if (mOverviewProxyService.getProxy() != null) {
+                        mOverviewProxyService.getProxy().onOverviewToggle();
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Cannot send toggle recents through proxy service.", e);
+                }
+            };
+            // Preload only if device for current user is unlocked
+            final StatusBar statusBar = getComponent(StatusBar.class);
+            if (statusBar != null && statusBar.isKeyguardShowing()) {
+                statusBar.executeRunnableDismissingKeyguard(() -> {
+                        // Flush trustmanager before checking device locked per user
+                        mTrustManager.reportKeyguardShowingChanged();
+                        mHandler.post(toggleRecents);
+                    }, null,  true /* dismissShade */, false /* afterKeyguardGone */,
+                    true /* deferred */);
+            } else {
+                toggleRecents.run();
             }
+            return;
         }
 
         int growTarget = getComponent(Divider.class).getView().growsRecents();
diff --git a/com/android/systemui/recents/RecentsImpl.java b/com/android/systemui/recents/RecentsImpl.java
index 19da3db..6fcb1c1 100644
--- a/com/android/systemui/recents/RecentsImpl.java
+++ b/com/android/systemui/recents/RecentsImpl.java
@@ -126,7 +126,7 @@
         @Override
         public void onTaskStackChangedBackground() {
             // Skip background preloading recents in SystemUI if the overview services is bound
-            if (Dependency.get(OverviewProxyService.class).getProxy() != null) {
+            if (Dependency.get(OverviewProxyService.class).isEnabled()) {
                 return;
             }
 
@@ -300,7 +300,7 @@
 
     public void onBootCompleted() {
         // Skip preloading tasks if we are already bound to the service
-        if (Dependency.get(OverviewProxyService.class).getProxy() != null) {
+        if (Dependency.get(OverviewProxyService.class).isEnabled()) {
             return;
         }
 
diff --git a/com/android/systemui/recents/RecentsOnboarding.java b/com/android/systemui/recents/RecentsOnboarding.java
index 75bc955..0d8aed4 100644
--- a/com/android/systemui/recents/RecentsOnboarding.java
+++ b/com/android/systemui/recents/RecentsOnboarding.java
@@ -21,19 +21,19 @@
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
 import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.RippleDrawable;
+import android.graphics.drawable.ShapeDrawable;
 import android.os.Build;
 import android.os.SystemProperties;
 import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -62,6 +62,7 @@
 
     private static final String TAG = "RecentsOnboarding";
     private static final boolean RESET_PREFS_FOR_DEBUG = false;
+    private static final boolean ONBOARDING_ENABLED = false;
     private static final long SHOW_DELAY_MS = 500;
     private static final long SHOW_HIDE_DURATION_MS = 300;
     // Don't show the onboarding until the user has launched this number of apps.
@@ -76,17 +77,13 @@
     private final View mLayout;
     private final TextView mTextView;
     private final ImageView mDismissView;
-    private final ColorDrawable mBackgroundDrawable;
-    private final int mDarkBackgroundColor;
-    private final int mLightBackgroundColor;
-    private final int mDarkContentColor;
-    private final int mLightContentColor;
-    private final RippleDrawable mDarkRipple;
-    private final RippleDrawable mLightRipple;
+    private final View mArrowView;
+    private final int mOnboardingToastColor;
+    private final int mOnboardingToastArrowRadius;
+    private int mNavBarHeight;
 
     private boolean mTaskListenerRegistered;
     private boolean mLayoutAttachedToWindow;
-    private boolean mBackgroundIsLight;
     private int mLastTaskId;
     private boolean mHasDismissed;
     private int mNumAppsLaunchedSinceDismiss;
@@ -159,24 +156,30 @@
         mLayout = LayoutInflater.from(mContext).inflate(R.layout.recents_onboarding, null);
         mTextView = mLayout.findViewById(R.id.onboarding_text);
         mDismissView = mLayout.findViewById(R.id.dismiss);
-        mDarkBackgroundColor = res.getColor(android.R.color.background_dark);
-        mLightBackgroundColor = res.getColor(android.R.color.background_light);
-        mDarkContentColor = res.getColor(R.color.primary_text_default_material_light);
-        mLightContentColor = res.getColor(R.color.primary_text_default_material_dark);
-        mDarkRipple = new RippleDrawable(res.getColorStateList(R.color.ripple_material_light),
-                null, null);
-        mLightRipple = new RippleDrawable(res.getColorStateList(R.color.ripple_material_dark),
-                null, null);
-        mBackgroundDrawable = new ColorDrawable(mDarkBackgroundColor);
+        mArrowView = mLayout.findViewById(R.id.arrow);
+
+        TypedValue typedValue = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+        mOnboardingToastColor = res.getColor(typedValue.resourceId);
+        mOnboardingToastArrowRadius = res.getDimensionPixelSize(
+                R.dimen.recents_onboarding_toast_arrow_corner_radius);
 
         mLayout.addOnAttachStateChangeListener(mOnAttachStateChangeListener);
-        mLayout.setBackground(mBackgroundDrawable);
         mDismissView.setOnClickListener(v -> {
             hide(true);
             mHasDismissed = true;
             mNumAppsLaunchedSinceDismiss = 0;
         });
 
+        ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams();
+        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+                arrowLp.width, arrowLp.height, false));
+        Paint arrowPaint = arrowDrawable.getPaint();
+        arrowPaint.setColor(mOnboardingToastColor);
+        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+        arrowPaint.setPathEffect(new CornerPathEffect(mOnboardingToastArrowRadius));
+        mArrowView.setBackground(arrowDrawable);
+
         if (RESET_PREFS_FOR_DEBUG) {
             Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, false);
             Prefs.putInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, 0);
@@ -184,6 +187,9 @@
     }
 
     public void onConnectedToLauncher() {
+        if (!ONBOARDING_ENABLED) {
+            return;
+        }
         boolean alreadySeenRecentsOnboarding = Prefs.getBoolean(mContext,
                 Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, false);
         if (!mTaskListenerRegistered && !alreadySeenRecentsOnboarding) {
@@ -231,6 +237,7 @@
         int orientation = mContext.getResources().getConfiguration().orientation;
         if (!mLayoutAttachedToWindow && orientation == Configuration.ORIENTATION_PORTRAIT) {
             mLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+
             mWindowManager.addView(mLayout, getWindowLayoutParams());
             int layoutHeight = mLayout.getHeight();
             if (layoutHeight == 0) {
@@ -278,29 +285,18 @@
         }
     }
 
-    public void setContentDarkIntensity(float contentDarkIntensity) {
-        boolean backgroundIsLight = contentDarkIntensity > 0.5f;
-        if (backgroundIsLight != mBackgroundIsLight) {
-            mBackgroundIsLight = backgroundIsLight;
-            mBackgroundDrawable.setColor(mBackgroundIsLight
-                    ? mLightBackgroundColor : mDarkBackgroundColor);
-            int contentColor = mBackgroundIsLight ? mDarkContentColor : mLightContentColor;
-            mTextView.setTextColor(contentColor);
-            mTextView.getCompoundDrawables()[3].setColorFilter(contentColor,
-                    PorterDuff.Mode.SRC_IN);
-            mDismissView.setColorFilter(contentColor);
-            mDismissView.setBackground(mBackgroundIsLight ? mDarkRipple : mLightRipple);
-        }
+    public void setNavBarHeight(int navBarHeight) {
+        mNavBarHeight = navBarHeight;
     }
 
     private WindowManager.LayoutParams getWindowLayoutParams() {
-        int flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
-                | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
+        int flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.WRAP_CONTENT,
-                WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG,
+                0, -mNavBarHeight / 2,
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                 flags,
                 PixelFormat.TRANSLUCENT);
         lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
diff --git a/com/android/systemui/recents/ScreenPinningRequest.java b/com/android/systemui/recents/ScreenPinningRequest.java
index 3dd6e35..bfbba7c 100644
--- a/com/android/systemui/recents/ScreenPinningRequest.java
+++ b/com/android/systemui/recents/ScreenPinningRequest.java
@@ -107,7 +107,7 @@
         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT,
-                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                         | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                 PixelFormat.TRANSLUCENT);
diff --git a/com/android/systemui/recents/TriangleShape.java b/com/android/systemui/recents/TriangleShape.java
new file mode 100644
index 0000000..de85c0f
--- /dev/null
+++ b/com/android/systemui/recents/TriangleShape.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 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.recents;
+
+import android.graphics.Outline;
+import android.graphics.Path;
+import android.graphics.drawable.shapes.PathShape;
+import android.support.annotation.NonNull;
+
+/**
+ * Wrapper around {@link android.graphics.drawable.shapes.PathShape}
+ * that creates a shape with a triangular path (pointing up or down).
+ */
+public class TriangleShape extends PathShape {
+    private Path mTriangularPath;
+
+    public TriangleShape(Path path, float stdWidth, float stdHeight) {
+        super(path, stdWidth, stdHeight);
+        mTriangularPath = path;
+    }
+
+    public static TriangleShape create(float width, float height, boolean isPointingUp) {
+        Path triangularPath = new Path();
+        if (isPointingUp) {
+            triangularPath.moveTo(0, height);
+            triangularPath.lineTo(width, height);
+            triangularPath.lineTo(width / 2, 0);
+            triangularPath.close();
+        } else {
+            triangularPath.moveTo(0, 0);
+            triangularPath.lineTo(width / 2, height);
+            triangularPath.lineTo(width, 0);
+            triangularPath.close();
+        }
+        return new TriangleShape(triangularPath, width, height);
+    }
+
+    @Override
+    public void getOutline(@NonNull Outline outline) {
+        outline.setConvexPath(mTriangularPath);
+    }
+}
diff --git a/com/android/systemui/shared/recents/model/IconLoader.java b/com/android/systemui/shared/recents/model/IconLoader.java
index 20d1418..78b1b26 100644
--- a/com/android/systemui/shared/recents/model/IconLoader.java
+++ b/com/android/systemui/shared/recents/model/IconLoader.java
@@ -15,10 +15,13 @@
  */
 package com.android.systemui.shared.recents.model;
 
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
+
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -108,10 +111,12 @@
         }
         if (desc.getIconResource() != 0) {
             try {
-                Context packageContext = mContext.createPackageContextAsUser(
-                        taskKey.getPackageName(), 0, UserHandle.of(userId));
-                return createBadgedDrawable(packageContext.getDrawable(desc.getIconResource()),
-                        userId, desc);
+                PackageManager pm = mContext.getPackageManager();
+                ApplicationInfo appInfo = pm.getApplicationInfo(taskKey.getPackageName(),
+                        MATCH_ANY_USER);
+                Resources res = pm.getResourcesForApplication(appInfo);
+                return createBadgedDrawable(res.getDrawable(desc.getIconResource(), null), userId,
+                        desc);
             } catch (Resources.NotFoundException|PackageManager.NameNotFoundException e) {
                 Log.e(TAG, "Could not find icon drawable from resource", e);
             }
diff --git a/com/android/systemui/shared/system/ActivityManagerWrapper.java b/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 1aad27f..ca5b034 100644
--- a/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -44,8 +44,10 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.Log;
@@ -53,6 +55,8 @@
 import android.view.IRecentsAnimationRunner;
 
 import android.view.RemoteAnimationTarget;
+
+import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -67,6 +71,9 @@
 
     private static final ActivityManagerWrapper sInstance = new ActivityManagerWrapper();
 
+    // Should match the values in PhoneWindowManager
+    public static final String CLOSE_SYSTEM_WINDOWS_REASON_RECENTS = "recentapps";
+
     private final PackageManager mPackageManager;
     private final BackgroundExecutor mBackgroundExecutor;
     private final TaskStackChangeListeners mTaskStackChangeListeners;
@@ -258,9 +265,9 @@
     /**
      * Cancels the remote recents animation started from {@link #startRecentsActivity}.
      */
-    public void cancelRecentsAnimation() {
+    public void cancelRecentsAnimation(boolean restoreHomeStackPosition) {
         try {
-            ActivityManager.getService().cancelRecentsAnimation();
+            ActivityManager.getService().cancelRecentsAnimation(restoreHomeStackPosition);
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to cancel recents animation", e);
         }
@@ -432,4 +439,21 @@
             return false;
         }
     }
+
+    /**
+     * Shows a voice session identified by {@code token}
+     * @return true if the session was shown, false otherwise
+     */
+    public boolean showVoiceSession(IBinder token, Bundle args, int flags) {
+        IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface(
+                ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+        if (service == null) {
+            return false;
+        }
+        try {
+            return service.showSessionFromSession(token, args, flags);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
 }
diff --git a/com/android/systemui/shared/system/NavigationBarCompat.java b/com/android/systemui/shared/system/NavigationBarCompat.java
index 79c1cb1..bff0d9b 100644
--- a/com/android/systemui/shared/system/NavigationBarCompat.java
+++ b/com/android/systemui/shared/system/NavigationBarCompat.java
@@ -17,10 +17,27 @@
 package com.android.systemui.shared.system;
 
 import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
+import sun.misc.Resource;
+
 public class NavigationBarCompat {
+    /**
+     * Touch slopes and thresholds for quick step operations. Drag slop is the point where the
+     * home button press/long press over are ignored and will start to drag when exceeded and the
+     * touch slop is when the respected operation will occur when exceeded. Touch slop must be
+     * larger than the drag slop.
+     */
+    public static final int QUICK_STEP_DRAG_SLOP_PX = convertDpToPixel(10);
+    public static final int QUICK_SCRUB_DRAG_SLOP_PX = convertDpToPixel(20);
+    public static final int QUICK_STEP_TOUCH_SLOP_PX = convertDpToPixel(40);
+    public static final int QUICK_SCRUB_TOUCH_SLOP_PX = convertDpToPixel(35);
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({HIT_TARGET_NONE, HIT_TARGET_BACK, HIT_TARGET_HOME, HIT_TARGET_OVERVIEW})
     public @interface HitTarget{}
@@ -42,7 +59,6 @@
      * Interaction type: whether the gesture to swipe up from the navigation bar will trigger
      * launcher to show overview
      */
-
     public static final int FLAG_DISABLE_SWIPE_UP = 0x1;
     /**
      * Interaction type: enable quick scrub interaction on the home button
@@ -58,4 +74,8 @@
      * Interaction type: show/hide the back button while this service is connected to launcher
      */
     public static final int FLAG_HIDE_BACK_BUTTON = 0x8;
+
+    private static int convertDpToPixel(float dp){
+        return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
+    }
 }
diff --git a/com/android/systemui/shared/system/PackageManagerWrapper.java b/com/android/systemui/shared/system/PackageManagerWrapper.java
index 6fa7db3..32e4bbf 100644
--- a/com/android/systemui/shared/system/PackageManagerWrapper.java
+++ b/com/android/systemui/shared/system/PackageManagerWrapper.java
@@ -18,23 +18,24 @@
 
 import android.app.AppGlobals;
 import android.content.ComponentName;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.RemoteException;
 
-import java.util.ArrayList;
 import java.util.List;
 
 public class PackageManagerWrapper {
 
-    private static final String TAG = "PackageManagerWrapper";
-
     private static final PackageManagerWrapper sInstance = new PackageManagerWrapper();
 
     private static final IPackageManager mIPackageManager = AppGlobals.getPackageManager();
 
+    public static final String ACTION_PREFERRED_ACTIVITY_CHANGED =
+            Intent.ACTION_PREFERRED_ACTIVITY_CHANGED;
+
     public static PackageManagerWrapper getInstance() {
         return sInstance;
     }
@@ -53,40 +54,15 @@
     }
 
     /**
-     * @return true if the packageName belongs to the current preferred home app on the device.
-     *
-     * If will also return false if there are multiple home apps and the user has not picked any
-     * preferred home, in which case the user would see a disambiguation screen on going to home.
+     * Report the set of 'Home' activity candidates, plus (if any) which of them
+     * is the current "always use this one" setting.
      */
-    public boolean isDefaultHomeActivity(String packageName) {
-        List<ResolveInfo> allHomeCandidates = new ArrayList<>();
-        ComponentName home;
+    public ComponentName getHomeActivities(List<ResolveInfo> allHomeCandidates) {
         try {
-            home = mIPackageManager.getHomeActivities(allHomeCandidates);
+            return mIPackageManager.getHomeActivities(allHomeCandidates);
         } catch (RemoteException e) {
             e.printStackTrace();
-            return false;
+            return null;
         }
-
-        if (home != null && packageName.equals(home.getPackageName())) {
-            return true;
-        }
-
-        // Find the launcher with the highest priority and return that component if there are no
-        // other home activity with the same priority.
-        int lastPriority = Integer.MIN_VALUE;
-        ComponentName lastComponent = null;
-        final int size = allHomeCandidates.size();
-        for (int i = 0; i < size; i++) {
-            final ResolveInfo ri = allHomeCandidates.get(i);
-            if (ri.priority > lastPriority) {
-                lastComponent = ri.activityInfo.getComponentName();
-                lastPriority = ri.priority;
-            } else if (ri.priority == lastPriority) {
-                // Two components found with same priority.
-                lastComponent = null;
-            }
-        }
-        return lastComponent != null && packageName.equals(lastComponent.getPackageName());
     }
 }
diff --git a/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
index 5fa6c79..80e226d 100644
--- a/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
+++ b/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -61,6 +61,14 @@
         }
     }
 
+    public void setSplitScreenMinimized(boolean minimized) {
+        try {
+            mAnimationController.setSplitScreenMinimized(minimized);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to set minimize dock", e);
+        }
+    }
+
     public void finish(boolean toHome) {
         try {
             mAnimationController.finish(toHome);
diff --git a/com/android/systemui/shared/system/ThreadedRendererCompat.java b/com/android/systemui/shared/system/ThreadedRendererCompat.java
new file mode 100644
index 0000000..bf88a29
--- /dev/null
+++ b/com/android/systemui/shared/system/ThreadedRendererCompat.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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.shared.system;
+
+import android.view.ThreadedRenderer;
+
+/**
+ * @see ThreadedRenderer
+ */
+public class ThreadedRendererCompat {
+
+    public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101;
+    public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102;
+    public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103;
+
+    public static void setContextPriority(int priority) {
+        ThreadedRenderer.setContextPriority(priority);
+    }
+}
diff --git a/com/android/systemui/shared/system/TransactionCompat.java b/com/android/systemui/shared/system/TransactionCompat.java
index c82c519..9975c41 100644
--- a/com/android/systemui/shared/system/TransactionCompat.java
+++ b/com/android/systemui/shared/system/TransactionCompat.java
@@ -101,6 +101,11 @@
         return this;
     }
 
+    public TransactionCompat setEarlyWakeup() {
+        mTransaction.setEarlyWakeup();
+        return this;
+    }
+
     public TransactionCompat setColor(SurfaceControlCompat surfaceControl, float[] color) {
         mTransaction.setColor(surfaceControl.mSurfaceControl, color);
         return this;
diff --git a/com/android/systemui/shared/system/WindowCallbacksCompat.java b/com/android/systemui/shared/system/WindowCallbacksCompat.java
new file mode 100644
index 0000000..b2b140e
--- /dev/null
+++ b/com/android/systemui/shared/system/WindowCallbacksCompat.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 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.shared.system;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.DisplayListCanvas;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowCallbacks;
+
+public class WindowCallbacksCompat {
+
+    private final WindowCallbacks mWindowCallbacks = new WindowCallbacks() {
+        @Override
+        public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+                Rect stableInsets) {
+            WindowCallbacksCompat.this.onWindowSizeIsChanging(newBounds, fullscreen, systemInsets,
+                    stableInsets);
+        }
+
+        @Override
+        public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen,
+                Rect systemInsets, Rect stableInsets, int resizeMode) {
+            WindowCallbacksCompat.this.onWindowDragResizeStart(initialBounds, fullscreen,
+                    systemInsets, stableInsets, resizeMode);
+        }
+
+        @Override
+        public void onWindowDragResizeEnd() {
+            WindowCallbacksCompat.this.onWindowDragResizeEnd();
+        }
+
+        @Override
+        public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) {
+            return WindowCallbacksCompat.this.onContentDrawn(offsetX, offsetY, sizeX, sizeY);
+        }
+
+        @Override
+        public void onRequestDraw(boolean reportNextDraw) {
+            WindowCallbacksCompat.this.onRequestDraw(reportNextDraw);
+        }
+
+        @Override
+        public void onPostDraw(DisplayListCanvas canvas) {
+            WindowCallbacksCompat.this.onPostDraw(canvas);
+        }
+    };
+
+    private final View mView;
+
+    public WindowCallbacksCompat(View view) {
+        mView = view;
+    }
+
+    public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) { }
+
+    public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets, int resizeMode) { }
+
+    public void onWindowDragResizeEnd() { }
+
+    public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) {
+        return false;
+    }
+
+    public void onRequestDraw(boolean reportNextDraw) {
+        if (reportNextDraw) {
+            reportDrawFinish();
+        }
+    }
+
+    public void onPostDraw(Canvas canvas) { }
+
+    public void reportDrawFinish() {
+        mView.getViewRootImpl().reportDrawFinish();
+    }
+
+    public boolean attach() {
+        ViewRootImpl root = mView.getViewRootImpl();
+        if (root != null) {
+            root.addWindowCallbacks(mWindowCallbacks);
+            root.requestInvalidateRootRenderNode();
+            return true;
+        }
+        return false;
+    }
+
+    public void detach() {
+        ViewRootImpl root = mView.getViewRootImpl();
+        if (root != null) {
+            root.removeWindowCallbacks(mWindowCallbacks);
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/AppOpsListener.java b/com/android/systemui/statusbar/AppOpsListener.java
index 2ec78cf..019c680 100644
--- a/com/android/systemui/statusbar/AppOpsListener.java
+++ b/com/android/systemui/statusbar/AppOpsListener.java
@@ -62,7 +62,7 @@
     public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
         mFsc.onAppOpChanged(code, uid, packageName, active);
         mPresenter.getHandler().post(() -> {
-          mEntryManager.updateNotificationsForAppOps(code, uid, packageName, active);
+          mEntryManager.updateNotificationsForAppOp(code, uid, packageName, active);
         });
     }
 }
diff --git a/com/android/systemui/statusbar/CommandQueue.java b/com/android/systemui/statusbar/CommandQueue.java
index 65037f9..6fd0aa6 100644
--- a/com/android/systemui/statusbar/CommandQueue.java
+++ b/com/android/systemui/statusbar/CommandQueue.java
@@ -18,7 +18,7 @@
 
 import android.content.ComponentName;
 import android.graphics.Rect;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.IBiometricPromptReceiver;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -160,7 +160,7 @@
 
         default void onRotationProposal(int rotation, boolean isValid) { }
 
-        default void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) { }
+        default void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) { }
         default void onFingerprintAuthenticated() { }
         default void onFingerprintHelp(String message) { }
         default void onFingerprintError(String error) { }
@@ -513,7 +513,7 @@
     }
 
     @Override
-    public void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) {
+    public void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = bundle;
@@ -759,7 +759,7 @@
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).showFingerprintDialog(
                                 (Bundle)((SomeArgs)msg.obj).arg1,
-                                (IBiometricDialogReceiver)((SomeArgs)msg.obj).arg2);
+                                (IBiometricPromptReceiver)((SomeArgs)msg.obj).arg2);
                     }
                     break;
                 case MSG_FINGERPRINT_AUTHENTICATED:
diff --git a/com/android/systemui/statusbar/ExpandableNotificationRow.java b/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 3ece2f9..87e6608 100644
--- a/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -35,6 +35,7 @@
 import android.graphics.drawable.AnimationDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.service.notification.StatusBarNotification;
@@ -100,13 +101,17 @@
 public class ExpandableNotificationRow extends ActivatableNotificationView
         implements PluginListener<NotificationMenuRowPlugin> {
 
+    private static final boolean DEBUG = false;
     private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
     private static final int COLORED_DIVIDER_ALPHA = 0x7B;
     private static final int MENU_VIEW_INDEX = 0;
     private static final String TAG = "ExpandableNotifRow";
 
+    /**
+     * Listener for when {@link ExpandableNotificationRow} is laid out.
+     */
     public interface LayoutListener {
-        public void onLayout();
+        void onLayout();
     }
 
     private LayoutListener mLayoutListener;
@@ -174,8 +179,11 @@
     private NotificationGuts mGuts;
     private NotificationData.Entry mEntry;
     private StatusBarNotification mStatusBarNotification;
-    private PackageManager mCachedPackageManager;
-    private PackageInfo mCachedPackageInfo;
+    /**
+     * Whether or not this row represents a system notification. Note that if this is {@code null},
+     * that means we were either unable to retrieve the info or have yet to retrieve the info.
+     */
+    private Boolean mIsSystemNotification;
     private String mAppName;
     private boolean mIsHeadsUp;
     private boolean mLastChronometerRunning = true;
@@ -271,7 +279,7 @@
                 public Float get(ExpandableNotificationRow object) {
                     return object.getTranslation();
                 }
-    };
+            };
     private OnClickListener mOnClickListener;
     private boolean mHeadsupDisappearRunning;
     private View mChildAfterViewWhenDismissed;
@@ -292,6 +300,33 @@
     private int mNotificationColorAmbient;
     private NotificationViewState mNotificationViewState;
 
+    private SystemNotificationAsyncTask mSystemNotificationAsyncTask =
+            new SystemNotificationAsyncTask();
+
+    /**
+     * Returns whether the given {@code statusBarNotification} is a system notification.
+     * <b>Note</b>, this should be run in the background thread if possible as it makes multiple IPC
+     * calls.
+     */
+    private static Boolean isSystemNotification(
+            Context context, StatusBarNotification statusBarNotification) {
+        PackageManager packageManager = StatusBar.getPackageManagerForUser(
+                context, statusBarNotification.getUser().getIdentifier());
+        Boolean isSystemNotification = null;
+
+        try {
+            PackageInfo packageInfo = packageManager.getPackageInfo(
+                    statusBarNotification.getPackageName(), PackageManager.GET_SIGNATURES);
+
+            isSystemNotification =
+                    com.android.settingslib.Utils.isSystemPackage(
+                            context.getResources(), packageManager, packageInfo);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "cacheIsSystemNotification: Could not find package info");
+        }
+        return isSystemNotification;
+    }
+
     @Override
     public boolean isGroupExpansionChanging() {
         if (isChildInGroup()) {
@@ -383,45 +418,43 @@
         mStatusBarNotification = entry.notification;
         mNotificationInflater.inflateNotificationViews();
 
-        perhapsCachePackageInfo();
+        cacheIsSystemNotification();
     }
 
     /**
-     * Caches the package manager and info objects which are expensive to obtain.
+     * Caches whether or not this row contains a system notification. Note, this is only cached
+     * once per notification as the packageInfo can't technically change for a notification row.
      */
-    private void perhapsCachePackageInfo() {
-        if (mCachedPackageInfo == null) {
-            mCachedPackageManager = StatusBar.getPackageManagerForUser(
-                    mContext, mStatusBarNotification.getUser().getIdentifier());
-            try {
-                mCachedPackageInfo = mCachedPackageManager.getPackageInfo(
-                        mStatusBarNotification.getPackageName(), PackageManager.GET_SIGNATURES);
-            } catch (PackageManager.NameNotFoundException e) {
-                Log.e(TAG, "perhapsCachePackageInfo: Could not find package info");
+    private void cacheIsSystemNotification() {
+        if (mIsSystemNotification == null) {
+            if (mSystemNotificationAsyncTask.getStatus() == AsyncTask.Status.PENDING) {
+                // Run async task once, only if it hasn't already been executed. Note this is
+                // executed in serial - no need to parallelize this small task.
+                mSystemNotificationAsyncTask.execute();
             }
         }
     }
 
     /**
-     * Returns whether this row is considered non-blockable (e.g. it's a non-blockable system notif,
-     * covers multiple channels, or is in a whitelist).
+     * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif
+     * or is in a whitelist).
      */
     public boolean getIsNonblockable() {
-        boolean isNonblockable;
-
-        isNonblockable = Dependency.get(NotificationBlockingHelperManager.class)
+        boolean isNonblockable = Dependency.get(NotificationBlockingHelperManager.class)
                 .isNonblockablePackage(mStatusBarNotification.getPackageName());
 
-        // Only bother with going through the children if the row is still blockable based on the
-        // number of unique channels.
-        if (!isNonblockable) {
-            isNonblockable = getNumUniqueChannels() > 1;
+        // If the SystemNotifAsyncTask hasn't finished running or retrieved a value, we'll try once
+        // again, but in-place on the main thread this time. This should rarely ever get called.
+        if (mIsSystemNotification == null) {
+            if (DEBUG) {
+                Log.d(TAG, "Retrieving isSystemNotification on main thread");
+            }
+            mSystemNotificationAsyncTask.cancel(true /* mayInterruptIfRunning */);
+            mIsSystemNotification = isSystemNotification(mContext, mStatusBarNotification);
         }
 
-        // Only bother with IPC if the package is still blockable.
-        if (!isNonblockable && mCachedPackageManager != null && mCachedPackageInfo != null) {
-            if (com.android.settingslib.Utils.isSystemPackage(
-                    mContext.getResources(), mCachedPackageManager, mCachedPackageInfo)) {
+        if (!isNonblockable && mIsSystemNotification != null) {
+            if (mIsSystemNotification) {
                 if (mEntry.channel != null
                         && !mEntry.channel.isBlockableSystem()) {
                     isNonblockable = true;
@@ -2828,4 +2861,21 @@
          */
         boolean onClick(View v, int x, int y, MenuItem item);
     }
+
+    /**
+     * Background task for executing IPCs to check if the notification is a system notification. The
+     * output is used for both the blocking helper and the notification info.
+     */
+    private class SystemNotificationAsyncTask extends AsyncTask<Void, Void, Boolean> {
+
+        @Override
+        protected Boolean doInBackground(Void... voids) {
+            return isSystemNotification(mContext, mStatusBarNotification);
+        }
+
+        @Override
+        protected void onPostExecute(Boolean result) {
+            mIsSystemNotification = result;
+        }
+    }
 }
diff --git a/com/android/systemui/statusbar/NotificationContentView.java b/com/android/systemui/statusbar/NotificationContentView.java
index b81e9af..4256cd6 100644
--- a/com/android/systemui/statusbar/NotificationContentView.java
+++ b/com/android/systemui/statusbar/NotificationContentView.java
@@ -26,6 +26,7 @@
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.NotificationHeaderView;
 import android.view.View;
 import android.view.ViewGroup;
@@ -79,7 +80,7 @@
     private RemoteInputView mHeadsUpRemoteInput;
 
     private SmartReplyConstants mSmartReplyConstants;
-    private SmartReplyView mExpandedSmartReplyView;
+    private SmartReplyLogger mSmartReplyLogger;
 
     private NotificationViewWrapper mContractedWrapper;
     private NotificationViewWrapper mExpandedWrapper;
@@ -152,6 +153,7 @@
         super(context, attrs);
         mHybridGroupManager = new HybridGroupManager(getContext(), this);
         mSmartReplyConstants = Dependency.get(SmartReplyConstants.class);
+        mSmartReplyLogger = Dependency.get(SmartReplyLogger.class);
         initView();
     }
 
@@ -1242,7 +1244,7 @@
         }
 
         applyRemoteInput(entry, hasRemoteInput);
-        applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices);
+        applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices, entry);
     }
 
     private void applyRemoteInput(NotificationData.Entry entry, boolean hasRemoteInput) {
@@ -1343,13 +1345,21 @@
         return null;
     }
 
-    private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent) {
-        mExpandedSmartReplyView = mExpandedChild == null ?
-                null : applySmartReplyView(mExpandedChild, remoteInput, pendingIntent);
+    private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent,
+            NotificationData.Entry entry) {
+        if (mExpandedChild != null) {
+            SmartReplyView view =
+                    applySmartReplyView(mExpandedChild, remoteInput, pendingIntent, entry);
+            if (view != null && remoteInput != null && remoteInput.getChoices() != null
+                    && remoteInput.getChoices().length > 0) {
+                mSmartReplyLogger.smartRepliesAdded(entry, remoteInput.getChoices().length);
+            }
+        }
     }
 
     private SmartReplyView applySmartReplyView(
-            View view, RemoteInput remoteInput, PendingIntent pendingIntent) {
+            View view, RemoteInput remoteInput, PendingIntent pendingIntent,
+            NotificationData.Entry entry) {
         View smartReplyContainerCandidate = view.findViewById(
                 com.android.internal.R.id.smart_reply_container);
         if (!(smartReplyContainerCandidate instanceof LinearLayout)) {
@@ -1371,7 +1381,8 @@
             }
         }
         if (smartReplyView != null) {
-            smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent);
+            smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent,
+                    mSmartReplyLogger, entry);
             smartReplyContainer.setVisibility(View.VISIBLE);
         }
         return smartReplyView;
@@ -1631,6 +1642,42 @@
         return null;
     }
 
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        float y = ev.getY();
+        // We still want to distribute touch events to the remote input even if it's outside the
+        // view boundary. We're therefore manually dispatching these events to the remote view
+        RemoteInputView riv = getRemoteInputForView(getViewForVisibleType(mVisibleType));
+        if (riv != null && riv.getVisibility() == VISIBLE) {
+            int inputStart = mUnrestrictedContentHeight - riv.getHeight();
+            if (y <= mUnrestrictedContentHeight && y >= inputStart) {
+                ev.offsetLocation(0, -inputStart);
+                return riv.dispatchTouchEvent(ev);
+            }
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
+    /**
+     * Overridden to make sure touches to the reply action bar actually go through to this view
+     */
+    @Override
+    public boolean pointInView(float localX, float localY, float slop) {
+        float top = mClipTopAmount;
+        float bottom = mUnrestrictedContentHeight;
+        return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
+                localY < (bottom + slop);
+    }
+
+    private RemoteInputView getRemoteInputForView(View child) {
+        if (child == mExpandedChild) {
+            return mExpandedRemoteInput;
+        } else if (child == mHeadsUpChild) {
+            return mHeadsUpRemoteInput;
+        }
+        return null;
+    }
+
     public int getExpandHeight() {
         int viewType = VISIBLE_TYPE_EXPANDED;
         if (mExpandedChild == null) {
diff --git a/com/android/systemui/statusbar/NotificationData.java b/com/android/systemui/statusbar/NotificationData.java
index 4b6ab64..ab46b39 100644
--- a/com/android/systemui/statusbar/NotificationData.java
+++ b/com/android/systemui/statusbar/NotificationData.java
@@ -383,8 +383,6 @@
         }
         mGroupManager.onEntryAdded(entry);
 
-        updateAppOps(entry);
-
         updateRankingAndSort(mRankingMap);
     }
 
@@ -403,25 +401,14 @@
         updateRankingAndSort(ranking);
     }
 
-    private void updateAppOps(Entry entry) {
-        final int uid = entry.notification.getUid();
-        final String pkg = entry.notification.getPackageName();
-        ArraySet<Integer> activeOps = mFsc.getAppOps(entry.notification.getUserId(), pkg);
-        if (activeOps != null) {
-            int N = activeOps.size();
-            for (int i = 0; i < N; i++) {
-                updateAppOp(activeOps.valueAt(i), uid, pkg, true);
-            }
-        }
-    }
-
-    public void updateAppOp(int appOp, int uid, String pkg, boolean showIcon) {
+    public void updateAppOp(int appOp, int uid, String pkg, String key, boolean showIcon) {
         synchronized (mEntries) {
             final int N = mEntries.size();
             for (int i = 0; i < N; i++) {
                 Entry entry = mEntries.valueAt(i);
                 if (uid == entry.notification.getUid()
-                    && pkg.equals(entry.notification.getPackageName())) {
+                        && pkg.equals(entry.notification.getPackageName())
+                        && key.equals(entry.key)) {
                     if (showIcon) {
                         entry.mActiveAppOps.add(appOp);
                     } else {
diff --git a/com/android/systemui/statusbar/NotificationEntryManager.java b/com/android/systemui/statusbar/NotificationEntryManager.java
index 45df450..849cfdd 100644
--- a/com/android/systemui/statusbar/NotificationEntryManager.java
+++ b/com/android/systemui/statusbar/NotificationEntryManager.java
@@ -43,6 +43,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.NotificationMessagingUtil;
@@ -665,6 +666,7 @@
         }
         // Add the expanded view and icon.
         mNotificationData.add(entry);
+        tagForeground(entry.notification);
         updateNotifications();
     }
 
@@ -726,6 +728,19 @@
         mPendingNotifications.put(key, shadeEntry);
     }
 
+    @VisibleForTesting
+    protected void tagForeground(StatusBarNotification notification) {
+        ArraySet<Integer> activeOps = mForegroundServiceController.getAppOps(
+                notification.getUserId(), notification.getPackageName());
+        if (activeOps != null) {
+            int N = activeOps.size();
+            for (int i = 0; i < N; i++) {
+                updateNotificationsForAppOp(activeOps.valueAt(i), notification.getUid(),
+                        notification.getPackageName(), true);
+            }
+        }
+    }
+
     @Override
     public void addNotification(StatusBarNotification notification,
             NotificationListenerService.RankingMap ranking) {
@@ -736,10 +751,11 @@
         }
     }
 
-    public void updateNotificationsForAppOps(int appOp, int uid, String pkg, boolean showIcon) {
-        if (mForegroundServiceController.getStandardLayoutKey(
-                UserHandle.getUserId(uid), pkg) != null) {
-            mNotificationData.updateAppOp(appOp, uid, pkg, showIcon);
+    public void updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon) {
+        String foregroundKey = mForegroundServiceController.getStandardLayoutKey(
+                UserHandle.getUserId(uid), pkg);
+        if (foregroundKey != null) {
+            mNotificationData.updateAppOp(appOp, uid, pkg, foregroundKey, showIcon);
             updateNotifications();
         }
     }
diff --git a/com/android/systemui/statusbar/NotificationInfo.java b/com/android/systemui/statusbar/NotificationInfo.java
index a93be00..81dd9e8 100644
--- a/com/android/systemui/statusbar/NotificationInfo.java
+++ b/com/android/systemui/statusbar/NotificationInfo.java
@@ -23,6 +23,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
 import android.app.INotificationManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -34,10 +35,12 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
@@ -63,10 +66,10 @@
     private INotificationManager mINotificationManager;
     private PackageManager mPm;
 
-    private String mPkg;
+    private String mPackageName;
     private String mAppName;
     private int mAppUid;
-    private int mNumNotificationChannels;
+    private int mNumUniqueChannelsInRow;
     private NotificationChannel mSingleNotificationChannel;
     private int mStartingUserImportance;
     private int mChosenImportance;
@@ -87,7 +90,7 @@
 
     private OnClickListener mOnKeepShowing = this::closeControls;
 
-    private OnClickListener mOnStopMinNotifications = v -> {
+    private OnClickListener mOnStopOrMinimizeNotifications = v -> {
         swapContent(false);
     };
 
@@ -120,16 +123,16 @@
             final INotificationManager iNotificationManager,
             final String pkg,
             final NotificationChannel notificationChannel,
-            final int numChannels,
+            final int numUniqueChannelsInRow,
             final StatusBarNotification sbn,
             final CheckSaveListener checkSaveListener,
             final OnSettingsClickListener onSettingsClick,
             final OnAppSettingsClickListener onAppSettingsClick,
             boolean isNonblockable)
             throws RemoteException {
-        bindNotification(pm, iNotificationManager, pkg, notificationChannel, numChannels, sbn,
-                checkSaveListener, onSettingsClick, onAppSettingsClick, isNonblockable,
-                false /* isBlockingHelper */,
+        bindNotification(pm, iNotificationManager, pkg, notificationChannel,
+                numUniqueChannelsInRow, sbn, checkSaveListener, onSettingsClick,
+                onAppSettingsClick, isNonblockable, false /* isBlockingHelper */,
                 false /* isUserSentimentNegative */);
     }
 
@@ -138,7 +141,7 @@
             INotificationManager iNotificationManager,
             String pkg,
             NotificationChannel notificationChannel,
-            int numChannels,
+            int numUniqueChannelsInRow,
             StatusBarNotification sbn,
             CheckSaveListener checkSaveListener,
             OnSettingsClickListener onSettingsClick,
@@ -148,12 +151,12 @@
             boolean isUserSentimentNegative)
             throws RemoteException {
         mINotificationManager = iNotificationManager;
-        mPkg = pkg;
-        mNumNotificationChannels = numChannels;
+        mPackageName = pkg;
+        mNumUniqueChannelsInRow = numUniqueChannelsInRow;
         mSbn = sbn;
         mPm = pm;
         mAppSettingsClickListener = onAppSettingsClick;
-        mAppName = mPkg;
+        mAppName = mPackageName;
         mCheckSaveListener = checkSaveListener;
         mOnSettingsClickListener = onSettingsClick;
         mSingleNotificationChannel = notificationChannel;
@@ -167,11 +170,11 @@
 
         int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
                 pkg, mAppUid, false /* includeDeleted */);
-        if (mNumNotificationChannels == 0) {
+        if (mNumUniqueChannelsInRow == 0) {
             throw new IllegalArgumentException("bindNotification requires at least one channel");
         } else  {
             // Special behavior for the Default channel if no other channels have been defined.
-            mIsSingleDefaultChannel = mNumNotificationChannels == 1
+            mIsSingleDefaultChannel = mNumUniqueChannelsInRow == 1
                     && mSingleNotificationChannel.getId().equals(
                             NotificationChannel.DEFAULT_CHANNEL_ID)
                     && numTotalChannels == 1;
@@ -187,7 +190,8 @@
         Drawable pkgicon = null;
         ApplicationInfo info;
         try {
-            info = mPm.getApplicationInfo(mPkg,
+            info = mPm.getApplicationInfo(
+                    mPackageName,
                     PackageManager.MATCH_UNINSTALLED_PACKAGES
                             | PackageManager.MATCH_DISABLED_COMPONENTS
                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
@@ -208,7 +212,7 @@
         if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
             final NotificationChannelGroup notificationChannelGroup =
                     mINotificationManager.getNotificationChannelGroupForPackage(
-                            mSingleNotificationChannel.getGroup(), mPkg, mAppUid);
+                            mSingleNotificationChannel.getGroup(), mPackageName, mAppUid);
             if (notificationChannelGroup != null) {
                 groupName = notificationChannelGroup.getName();
             }
@@ -232,7 +236,7 @@
             settingsButton.setOnClickListener(
                     (View view) -> {
                         mOnSettingsClickListener.onClick(view,
-                                mNumNotificationChannels > 1 ? null : mSingleNotificationChannel,
+                                mNumUniqueChannelsInRow > 1 ? null : mSingleNotificationChannel,
                                 appUidF);
                     });
         } else {
@@ -248,7 +252,7 @@
         } else {
             if (mNegativeUserSentiment) {
                 blockPrompt.setText(R.string.inline_blocking_helper);
-            }  else if (mIsSingleDefaultChannel || mNumNotificationChannels > 1) {
+            }  else if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
                 blockPrompt.setText(R.string.inline_keep_showing_app);
             } else {
                 blockPrompt.setText(R.string.inline_keep_showing);
@@ -258,7 +262,7 @@
 
     private void bindName() {
         final TextView channelName = findViewById(R.id.channel_name);
-        if (mIsSingleDefaultChannel || mNumNotificationChannels > 1) {
+        if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
             channelName.setVisibility(View.GONE);
         } else {
             channelName.setText(mSingleNotificationChannel.getName());
@@ -270,19 +274,26 @@
     }
 
     private void saveImportance() {
-        if (mIsNonblockable) {
-            return;
+        if (!mIsNonblockable) {
+            if (mCheckSaveListener != null) {
+                mCheckSaveListener.checkSave(this::updateImportance, mSbn);
+            } else {
+                updateImportance();
+            }
         }
+    }
+
+    /**
+     * Commits the updated importance values on the background thread.
+     */
+    private void updateImportance() {
         MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
                 mChosenImportance - mStartingUserImportance);
-        mSingleNotificationChannel.setImportance(mChosenImportance);
-        mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
-        try {
-            mINotificationManager.updateNotificationChannelForPackage(
-                    mPkg, mAppUid, mSingleNotificationChannel);
-        } catch (RemoteException e) {
-            // :(
-        }
+
+        Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
+        bgHandler.post(new UpdateImportanceRunnable(mINotificationManager, mPackageName, mAppUid,
+                mNumUniqueChannelsInRow == 1 ? mSingleNotificationChannel : null,
+                mStartingUserImportance, mChosenImportance));
     }
 
     private void bindButtons() {
@@ -292,9 +303,9 @@
         View minimize = findViewById(R.id.minimize);
 
         findViewById(R.id.undo).setOnClickListener(mOnUndo);
-        block.setOnClickListener(mOnStopMinNotifications);
+        block.setOnClickListener(mOnStopOrMinimizeNotifications);
         keep.setOnClickListener(mOnKeepShowing);
-        minimize.setOnClickListener(mOnStopMinNotifications);
+        minimize.setOnClickListener(mOnStopOrMinimizeNotifications);
 
         if (mIsNonblockable) {
             keep.setText(R.string.notification_done);
@@ -308,15 +319,15 @@
             minimize.setVisibility(GONE);
         }
 
-        // Set up app settings link
+        // Set up app settings link (i.e. Customize)
         TextView settingsLinkView = findViewById(R.id.app_settings);
-        Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
+        Intent settingsIntent = getAppSettingsIntent(mPm, mPackageName, mSingleNotificationChannel,
                 mSbn.getId(), mSbn.getTag());
-        if (settingsIntent != null
+        if (!mIsForBlockingHelper
+                && settingsIntent != null
                 && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
             settingsLinkView.setVisibility(VISIBLE);
-            settingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
-                    mSbn.getNotification().getSettingsText()));
+            settingsLinkView.setText(mContext.getString(R.string.notification_app_settings));
             settingsLinkView.setOnClickListener((View view) -> {
                 mAppSettingsClickListener.onClick(view, settingsIntent);
             });
@@ -415,12 +426,25 @@
         return intent;
     }
 
+    /**
+     * Closes the controls and commits the updated importance values (indirectly). If this view is
+     * being used to show the blocking helper, this will immediately dismiss the blocking helper and
+     * commit the updated importance.
+     *
+     * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the
+     * user does not have the ability to undo the action anymore. See {@link #swapContent(boolean)}
+     * for where undo is handled.
+     */
     @VisibleForTesting
     void closeControls(View v) {
         if (mIsForBlockingHelper) {
             NotificationBlockingHelperManager manager =
                     Dependency.get(NotificationBlockingHelperManager.class);
             manager.dismissCurrentBlockingHelper();
+
+            // Since this won't get a callback via gutsContainer.closeControls, save the new
+            // importance values immediately.
+            saveImportance();
         } else {
             int[] parentLoc = new int[2];
             int[] targetLoc = new int[2];
@@ -454,11 +478,7 @@
         // Save regardless of the importance so we can lock the importance field if the user wants
         // to keep getting notifications
         if (save) {
-            if (mCheckSaveListener != null) {
-                mCheckSaveListener.checkSave(this::saveImportance, mSbn);
-            } else {
-                saveImportance();
-            }
+            saveImportance();
         }
         return false;
     }
@@ -467,4 +487,48 @@
     public int getActualHeight() {
         return getHeight();
     }
+
+    /**
+     * Runnable to either update the given channel (with a new importance value) or, if no channel
+     * is provided, update notifications enabled state for the package.
+     */
+    private static class UpdateImportanceRunnable implements Runnable {
+        private final INotificationManager mINotificationManager;
+        private final String mPackageName;
+        private final int mAppUid;
+        private final @Nullable NotificationChannel mChannelToUpdate;
+        private final int mCurrentImportance;
+        private final int mNewImportance;
+
+
+        public UpdateImportanceRunnable(INotificationManager notificationManager,
+                String packageName, int appUid, @Nullable NotificationChannel channelToUpdate,
+                int currentImportance, int newImportance) {
+            mINotificationManager = notificationManager;
+            mPackageName = packageName;
+            mAppUid = appUid;
+            mChannelToUpdate = channelToUpdate;
+            mCurrentImportance = currentImportance;
+            mNewImportance = newImportance;
+        }
+
+        @Override
+        public void run() {
+            try {
+                if (mChannelToUpdate != null) {
+                    mChannelToUpdate.setImportance(mNewImportance);
+                    mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+                    mINotificationManager.updateNotificationChannelForPackage(
+                            mPackageName, mAppUid, mChannelToUpdate);
+                } else {
+                    // For notifications with more than one channel, update notification enabled
+                    // state. If the importance was lowered, we disable notifications.
+                    mINotificationManager.setNotificationsEnabledForPackage(
+                            mPackageName, mAppUid, mNewImportance >= mCurrentImportance);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Unable to update notification importance", e);
+            }
+        }
+    }
 }
diff --git a/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
index ccabb79..e24bf67 100644
--- a/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
+++ b/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -47,7 +47,6 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.List;
 
 /**
  * Handles keeping track of the current user, profiles, and various things related to hiding
@@ -352,7 +351,8 @@
             final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
                     mContext.getContentResolver(),
                     Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle);
-            final boolean allowedByDpm = adminAllowsUnredactedNotifications(userHandle);
+            final boolean allowedByDpm = adminAllowsKeyguardFeature(userHandle,
+                    DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS);
             final boolean allowed = allowedByUser && allowedByDpm;
             mUsersAllowingPrivateNotifications.append(userHandle, allowed);
             return allowed;
@@ -361,13 +361,13 @@
         return mUsersAllowingPrivateNotifications.get(userHandle);
     }
 
-    private boolean adminAllowsUnredactedNotifications(int userHandle) {
+    private boolean adminAllowsKeyguardFeature(int userHandle, int feature) {
         if (userHandle == UserHandle.USER_ALL) {
             return true;
         }
-        final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */,
-                userHandle);
-        return (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0;
+        final int dpmFlags =
+                mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */, userHandle);
+        return (dpmFlags & feature) == 0;
     }
 
     /**
@@ -389,14 +389,17 @@
      * "public" (secure & locked) mode?
      */
     private boolean userAllowsNotificationsInPublic(int userHandle) {
-        if (isCurrentProfile(userHandle)) {
+        if (isCurrentProfile(userHandle) && userHandle != mCurrentUserId) {
             return true;
         }
 
         if (mUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
-            final boolean allowed = 0 != Settings.Secure.getIntForUser(
+            final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
                     mContext.getContentResolver(),
                     Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, userHandle);
+            final boolean allowedByDpm = adminAllowsKeyguardFeature(userHandle,
+                    DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS);
+            final boolean allowed = allowedByUser && allowedByDpm;
             mUsersAllowingNotifications.append(userHandle, allowed);
             return allowed;
         }
@@ -428,7 +431,6 @@
                 Notification.VISIBILITY_PRIVATE;
     }
 
-
     private void updateCurrentProfilesCache() {
         synchronized (mCurrentProfiles) {
             mCurrentProfiles.clear();
diff --git a/com/android/systemui/statusbar/NotificationMediaManager.java b/com/android/systemui/statusbar/NotificationMediaManager.java
index 852239a..abc261e 100644
--- a/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -172,6 +172,14 @@
                 }
             }
 
+            if (mediaNotification != null) {
+                mMediaNotificationKey = mediaNotification.notification.getKey();
+                if (DEBUG_MEDIA) {
+                    Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
+                            + mMediaNotificationKey + " controller=" + mMediaController);
+                }
+            }
+
             if (controller != null && !sameSessions(mMediaController, controller)) {
                 // We have a new media session
                 clearCurrentMediaNotification();
@@ -183,13 +191,6 @@
                             + mMediaMetadata);
                 }
 
-                if (mediaNotification != null) {
-                    mMediaNotificationKey = mediaNotification.notification.getKey();
-                    if (DEBUG_MEDIA) {
-                        Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
-                                + mMediaNotificationKey + " controller=" + mMediaController);
-                    }
-                }
                 metaDataChanged = true;
             }
         }
diff --git a/com/android/systemui/statusbar/NotificationShelf.java b/com/android/systemui/statusbar/NotificationShelf.java
index 0112661..6364f5b 100644
--- a/com/android/systemui/statusbar/NotificationShelf.java
+++ b/com/android/systemui/statusbar/NotificationShelf.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar;
 
 import static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE;
-import static com.android.systemui.statusbar.phone.NotificationIconContainer.OVERFLOW_EARLY_AMOUNT;
 
 import android.content.Context;
 import android.content.res.Configuration;
@@ -26,6 +25,7 @@
 import android.os.SystemProperties;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.MathUtils;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
@@ -58,6 +58,8 @@
             = SystemProperties.getBoolean("debug.icon_scroll_animations", true);
     private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
     private static final String TAG = "NotificationShelf";
+    private static final long SHELF_IN_TRANSLATION_DURATION = 220;
+
     private ViewInvertHelper mViewInvertHelper;
     private boolean mDark;
     private NotificationIconContainer mShelfIcons;
@@ -65,6 +67,7 @@
     private int[] mTmp = new int[2];
     private boolean mHideBackground;
     private int mIconAppearTopPadding;
+    private int mShelfAppearTranslation;
     private int mStatusBarHeight;
     private int mStatusBarPaddingStart;
     private AmbientState mAmbientState;
@@ -120,6 +123,7 @@
         mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
         mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start);
         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
+        mShelfAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation);
 
         ViewGroup.LayoutParams layoutParams = getLayoutParams();
         layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
@@ -151,6 +155,18 @@
         updateInteractiveness();
     }
 
+    public void fadeInTranslating() {
+        float translation = mShelfIcons.getTranslationY();
+        mShelfIcons.setTranslationY(translation + mShelfAppearTranslation);
+        mShelfIcons.setAlpha(0);
+        mShelfIcons.animate()
+                .alpha(1)
+                .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+                .translationY(translation)
+                .setDuration(SHELF_IN_TRANSLATION_DURATION)
+                .start();
+    }
+
     @Override
     protected View getContentView() {
         return mShelfIcons;
@@ -175,12 +191,14 @@
             float viewEnd = lastViewState.yTranslation + lastViewState.height;
             mShelfState.copyFrom(lastViewState);
             mShelfState.height = getIntrinsicHeight();
-            mShelfState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - mShelfState.height,
+
+            float awakenTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - mShelfState.height,
                     getFullyClosedTranslation());
+            float darkTranslation = mAmbientState.getDarkTopPadding();
+            float yRatio = mAmbientState.hasPulsingNotifications() ?
+                    0 : mAmbientState.getDarkAmount();
+            mShelfState.yTranslation = MathUtils.lerp(awakenTranslation, darkTranslation, yRatio);
             mShelfState.zTranslation = ambientState.getBaseZHeight();
-            if (mAmbientState.isDark() && !mAmbientState.hasPulsingNotifications()) {
-                mShelfState.yTranslation = mAmbientState.getDarkTopPadding();
-            }
             float openedAmount = (mShelfState.yTranslation - getFullyClosedTranslation())
                     / (getIntrinsicHeight() * 2);
             openedAmount = Math.min(1.0f, openedAmount);
@@ -555,7 +573,9 @@
             iconState.translateContent = false;
         }
         float transitionAmount;
-        if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
+        if (mAmbientState.getDarkAmount() > 0 && !row.isInShelf()) {
+            transitionAmount = mAmbientState.isFullyDark() ? 1 : 0;
+        } else if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
                 || iconState.useLinearTransitionAmount) {
             transitionAmount = iconTransitionAmount;
         } else {
diff --git a/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index fd3a9d5..1637849 100644
--- a/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -323,8 +323,7 @@
             boolean showOnKeyguard = mLockscreenUserManager.shouldShowOnKeyguard(entry
                     .notification);
             if (suppressedSummary
-                    || (mLockscreenUserManager.isLockscreenPublicMode(userId)
-                    && !mLockscreenUserManager.shouldShowLockscreenNotifications())
+                    || mLockscreenUserManager.shouldHideNotifications(userId)
                     || (isLocked && !showOnKeyguard)) {
                 entry.row.setVisibility(View.GONE);
             } else {
diff --git a/com/android/systemui/statusbar/SmartReplyLogger.java b/com/android/systemui/statusbar/SmartReplyLogger.java
new file mode 100644
index 0000000..75dd77d
--- /dev/null
+++ b/com/android/systemui/statusbar/SmartReplyLogger.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 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.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import com.android.internal.statusbar.IStatusBarService;
+
+/**
+ * Handles reporting when smart replies are added to a notification
+ * and clicked upon.
+ */
+public class SmartReplyLogger {
+    protected IStatusBarService mBarService;
+
+    public SmartReplyLogger(Context context) {
+        mBarService = IStatusBarService.Stub.asInterface(
+                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+    }
+
+    public void smartReplySent(NotificationData.Entry entry, int replyIndex) {
+        try {
+            mBarService.onNotificationSmartReplySent(entry.notification.getKey(),
+                    replyIndex);
+        } catch (RemoteException e) {
+            // Nothing to do, system going down
+        }
+    }
+
+    public void smartRepliesAdded(final NotificationData.Entry entry, int replyCount) {
+        try {
+            mBarService.onNotificationSmartRepliesAdded(entry.notification.getKey(),
+                    replyCount);
+        } catch (RemoteException e) {
+            // Nothing to do, system going down
+        }
+    }
+}
diff --git a/com/android/systemui/statusbar/StatusBarMobileView.java b/com/android/systemui/statusbar/StatusBarMobileView.java
index b7620f3..51b4239 100644
--- a/com/android/systemui/statusbar/StatusBarMobileView.java
+++ b/com/android/systemui/statusbar/StatusBarMobileView.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar;
 
 import static com.android.systemui.statusbar.policy.DarkIconDispatcher.getTint;
+import static com.android.systemui.statusbar.policy.DarkIconDispatcher.isInArea;
 
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -141,12 +142,14 @@
         if (mState.strengthId != state.strengthId) {
             mMobileDrawable.setLevel(state.strengthId);
         }
-        if (mState.typeId != state.typeId && state.typeId != 0) {
-            mMobileType.setContentDescription(state.typeContentDescription);
-            mMobileType.setImageResource(state.typeId);
-            mMobileType.setVisibility(View.VISIBLE);
-        } else {
-            mMobileType.setVisibility(View.GONE);
+        if (mState.typeId != state.typeId) {
+            if (state.typeId != 0) {
+                mMobileType.setContentDescription(state.typeContentDescription);
+                mMobileType.setImageResource(state.typeId);
+                mMobileType.setVisibility(View.VISIBLE);
+            } else {
+                mMobileType.setVisibility(View.GONE);
+            }
         }
 
         mMobileRoaming.setVisibility(state.roaming ? View.VISIBLE : View.GONE);
@@ -161,6 +164,9 @@
 
     @Override
     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
+        if (!isInArea(area, this)) {
+            return;
+        }
         mMobileDrawable.setDarkIntensity(darkIntensity);
         ColorStateList color = ColorStateList.valueOf(getTint(area, this, tint));
         mIn.setImageTintList(color);
diff --git a/com/android/systemui/statusbar/StatusBarWifiView.java b/com/android/systemui/statusbar/StatusBarWifiView.java
index afd373e..62cd16f 100644
--- a/com/android/systemui/statusbar/StatusBarWifiView.java
+++ b/com/android/systemui/statusbar/StatusBarWifiView.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar;
 
 import static com.android.systemui.statusbar.policy.DarkIconDispatcher.getTint;
+import static com.android.systemui.statusbar.policy.DarkIconDispatcher.isInArea;
 
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -175,6 +176,9 @@
 
     @Override
     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
+        if (!isInArea(area, this)) {
+            return;
+        }
         mDarkIntensity = darkIntensity;
         Drawable d = mWifiIcon.getDrawable();
         if (d instanceof NeutralGoodDrawable) {
diff --git a/com/android/systemui/statusbar/car/CarFacetButton.java b/com/android/systemui/statusbar/car/CarFacetButton.java
index 5f3e2e3..46f8863 100644
--- a/com/android/systemui/statusbar/car/CarFacetButton.java
+++ b/com/android/systemui/statusbar/car/CarFacetButton.java
@@ -4,6 +4,7 @@
 import android.content.Intent;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -37,11 +38,17 @@
     private AlphaOptimizedImageButton mIcon;
     private AlphaOptimizedImageButton mMoreIcon;
     private boolean mSelected = false;
+    private String[] mComponentNames;
     /** App categories that are to be used with this widget */
     private String[] mFacetCategories;
     /** App packages that are allowed to be used with this widget */
     private String[] mFacetPackages;
     private int mIconResourceId;
+    /**
+     * If defined in the xml this will be the icon that's rendered when the button is marked as
+     * selected
+     */
+    private int mSelectedIconResourceId;
     private boolean mUseMoreIcon = true;
     private float mSelectedAlpha = 1f;
     private float mUnselectedAlpha = 1f;
@@ -70,6 +77,8 @@
         String longPressIntentString = typedArray.getString(R.styleable.CarFacetButton_longIntent);
         String categoryString = typedArray.getString(R.styleable.CarFacetButton_categories);
         String packageString = typedArray.getString(R.styleable.CarFacetButton_packages);
+        String componentNameString =
+                typedArray.getString(R.styleable.CarFacetButton_componentNames);
         try {
             final Intent intent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
             intent.putExtra(EXTRA_FACET_ID, Integer.toString(getId()));
@@ -82,17 +91,20 @@
                 mFacetCategories = categoryString.split(FACET_FILTER_DELIMITER);
                 intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories);
             }
+            if (componentNameString != null) {
+                mComponentNames = componentNameString.split(FACET_FILTER_DELIMITER);
+            }
 
             setOnClickListener(v -> {
                 intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, mSelected);
-                mContext.startActivity(intent);
+                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
             });
 
             if (longPressIntentString != null) {
                 final Intent longPressIntent = Intent.parseUri(longPressIntentString,
                         Intent.URI_INTENT_SCHEME);
                 setOnLongClickListener(v -> {
-                    mContext.startActivity(longPressIntent);
+                    mContext.startActivityAsUser(longPressIntent, UserHandle.CURRENT);
                     return true;
                 });
             }
@@ -112,10 +124,9 @@
         mIcon.setClickable(false);
         mIcon.setAlpha(mUnselectedAlpha);
         mIconResourceId = styledAttributes.getResourceId(R.styleable.CarFacetButton_icon, 0);
-        if (mIconResourceId == 0)  {
-            throw new RuntimeException("specified icon resource was not found and is required");
-        }
         mIcon.setImageResource(mIconResourceId);
+        mSelectedIconResourceId = styledAttributes.getResourceId(
+                R.styleable.CarFacetButton_selectedIcon, mIconResourceId);
 
         mMoreIcon = findViewById(R.id.car_nav_button_more_icon);
         mMoreIcon.setClickable(false);
@@ -144,6 +155,13 @@
         return mFacetPackages;
     }
 
+    public String[] getComponentName() {
+        if (mComponentNames == null) {
+            return new String[0];
+        }
+        return mComponentNames;
+    }
+
     /**
      * Updates the alpha of the icons to "selected" and shows the "More icon"
      * @param selected true if the view must be selected, false otherwise
@@ -161,22 +179,10 @@
      */
     public void setSelected(boolean selected, boolean showMoreIcon) {
         mSelected = selected;
-        if (selected) {
-            if (mUseMoreIcon) {
-                mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : GONE);
-            }
-            mIcon.setAlpha(mSelectedAlpha);
-        } else {
-            mMoreIcon.setVisibility(GONE);
-            mIcon.setAlpha(mUnselectedAlpha);
-        }
-    }
-
-    public void setIcon(Drawable d) {
-        if (d != null) {
-            mIcon.setImageDrawable(d);
-        } else {
-            mIcon.setImageResource(mIconResourceId);
+        mIcon.setAlpha(mSelected ? mSelectedAlpha : mUnselectedAlpha);
+        mIcon.setImageResource(mSelected ? mSelectedIconResourceId : mIconResourceId);
+        if (mUseMoreIcon) {
+            mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : GONE);
         }
     }
 }
diff --git a/com/android/systemui/statusbar/car/CarFacetButtonController.java b/com/android/systemui/statusbar/car/CarFacetButtonController.java
index b7d501e..2d30ce1 100644
--- a/com/android/systemui/statusbar/car/CarFacetButtonController.java
+++ b/com/android/systemui/statusbar/car/CarFacetButtonController.java
@@ -1,10 +1,12 @@
 package com.android.systemui.statusbar.car;
 
 import android.app.ActivityManager;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.util.Log;
 
 import java.util.HashMap;
 import java.util.List;
@@ -19,6 +21,7 @@
 
     protected HashMap<String, CarFacetButton> mButtonsByCategory = new HashMap<>();
     protected HashMap<String, CarFacetButton> mButtonsByPackage = new HashMap<>();
+    protected HashMap<String, CarFacetButton> mButtonsByComponentName = new HashMap<>();
     protected CarFacetButton mSelectedFacetButton;
     protected Context mContext;
 
@@ -34,28 +37,32 @@
      */
     public void addFacetButton(CarFacetButton facetButton) {
         String[] categories = facetButton.getCategories();
-        for (int j = 0; j < categories.length; j++) {
-            String category = categories[j];
-            mButtonsByCategory.put(category, facetButton);
+        for (int i = 0; i < categories.length; i++) {
+            mButtonsByCategory.put(categories[i], facetButton);
         }
 
         String[] facetPackages = facetButton.getFacetPackages();
-        for (int j = 0; j < facetPackages.length; j++) {
-            String facetPackage = facetPackages[j];
-            mButtonsByPackage.put(facetPackage, facetButton);
+        for (int i = 0; i < facetPackages.length; i++) {
+            mButtonsByPackage.put(facetPackages[i], facetButton);
+        }
+        String[] componentNames = facetButton.getComponentName();
+        for (int i = 0; i < componentNames.length; i++) {
+            mButtonsByComponentName.put(componentNames[i], facetButton);
         }
     }
 
     public void removeAll() {
         mButtonsByCategory.clear();
         mButtonsByPackage.clear();
+        mButtonsByComponentName.clear();
         mSelectedFacetButton = null;
     }
 
     /**
      * This will unselect the currently selected CarFacetButton and determine which one should be
      * selected next. It does this by reading the properties on the CarFacetButton and seeing if
-     * they are a match with the supplied taskino.
+     * they are a match with the supplied taskInfo.
+     * Order of selection detection ComponentName, PackageName, Category
      * @param taskInfo of the currently running application
      */
     public void taskChanged(ActivityManager.RunningTaskInfo taskInfo) {
@@ -69,7 +76,10 @@
         if (mSelectedFacetButton != null) {
             mSelectedFacetButton.setSelected(false);
         }
-        CarFacetButton facetButton = mButtonsByPackage.get(packageName);
+        CarFacetButton facetButton = findFacetButtongByComponentName(taskInfo.topActivity);
+        if (facetButton == null) {
+            facetButton =  mButtonsByPackage.get(packageName);
+        }
         if (facetButton != null) {
             facetButton.setSelected(true);
             mSelectedFacetButton = facetButton;
@@ -83,6 +93,12 @@
         }
     }
 
+    private CarFacetButton findFacetButtongByComponentName(ComponentName componentName) {
+        CarFacetButton button = mButtonsByComponentName.get(componentName.flattenToShortString());
+        return (button != null) ? button :
+                mButtonsByComponentName.get(componentName.flattenToString());
+    }
+
     protected String getPackageCategory(String packageName) {
         PackageManager pm = mContext.getPackageManager();
         Set<String> supportedCategories = mButtonsByCategory.keySet();
diff --git a/com/android/systemui/statusbar/car/CarNavigationBarView.java b/com/android/systemui/statusbar/car/CarNavigationBarView.java
index e73b173..b2cef16 100644
--- a/com/android/systemui/statusbar/car/CarNavigationBarView.java
+++ b/com/android/systemui/statusbar/car/CarNavigationBarView.java
@@ -25,7 +25,9 @@
 import android.widget.TextView;
 
 import com.android.keyguard.AlphaOptimizedImageButton;
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
 
 /**
  * A custom navigation bar for the automotive use case.
@@ -52,6 +54,17 @@
         if (mNotificationsButton != null) {
             mNotificationsButton.setOnClickListener(this::onNotificationsClick);
         }
+        View mStatusIcons = findViewById(R.id.statusIcons);
+        if (mStatusIcons != null) {
+            // Attach the controllers for Status icons such as wifi and bluetooth if the standard
+            // container is in the view.
+            StatusBarIconController.DarkIconManager mDarkIconManager =
+                    new StatusBarIconController.DarkIconManager(
+                            mStatusIcons.findViewById(R.id.statusIcons));
+            mDarkIconManager.setShouldLog(true);
+            Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
+        }
+
     }
 
     void setStatusBar(CarStatusBar carStatusBar) {
diff --git a/com/android/systemui/statusbar/car/CarNavigationButton.java b/com/android/systemui/statusbar/car/CarNavigationButton.java
index 0cdaec1..084c136 100644
--- a/com/android/systemui/statusbar/car/CarNavigationButton.java
+++ b/com/android/systemui/statusbar/car/CarNavigationButton.java
@@ -3,7 +3,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.TypedArray;
+import android.os.UserHandle;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.widget.ImageView;
 
 import com.android.systemui.R;
@@ -17,23 +19,34 @@
  */
 public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImageButton {
 
-    private static final float SELECTED_ALPHA = 1;
-    private static final float UNSELECTED_ALPHA = 0.7f;
-
+    private static final String TAG = "CarNavigationButton";
     private Context mContext;
-    private String mIntent = null;
-    private String mLongIntent = null;
-    private boolean mBroadcastIntent = false;
+    private String mIntent;
+    private String mLongIntent;
+    private boolean mBroadcastIntent;
     private boolean mSelected = false;
+    private float mSelectedAlpha = 1f;
+    private float mUnselectedAlpha = 1f;
+    private int mSelectedIconResourceId;
+    private int mIconResourceId;
 
 
     public CarNavigationButton(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
-        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CarNavigationButton);
+        TypedArray typedArray = context.obtainStyledAttributes(
+                attrs, R.styleable.CarNavigationButton);
         mIntent = typedArray.getString(R.styleable.CarNavigationButton_intent);
         mLongIntent = typedArray.getString(R.styleable.CarNavigationButton_longIntent);
         mBroadcastIntent = typedArray.getBoolean(R.styleable.CarNavigationButton_broadcast, false);
+        mSelectedAlpha = typedArray.getFloat(
+                R.styleable.CarNavigationButton_selectedAlpha, mSelectedAlpha);
+        mUnselectedAlpha = typedArray.getFloat(
+                R.styleable.CarNavigationButton_unselectedAlpha, mUnselectedAlpha);
+        mIconResourceId = typedArray.getResourceId(
+                com.android.internal.R.styleable.ImageView_src, 0);
+        mSelectedIconResourceId = typedArray.getResourceId(
+                R.styleable.CarNavigationButton_selectedIcon, mIconResourceId);
     }
 
 
@@ -45,17 +58,20 @@
     public void onFinishInflate() {
         super.onFinishInflate();
         setScaleType(ImageView.ScaleType.CENTER);
-        setAlpha(UNSELECTED_ALPHA);
+        setAlpha(mUnselectedAlpha);
         try {
             if (mIntent != null) {
                 final Intent intent = Intent.parseUri(mIntent, Intent.URI_INTENT_SCHEME);
-                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                 setOnClickListener(v -> {
-                    if (mBroadcastIntent) {
-                        mContext.sendBroadcast(intent);
-                        return;
+                    try {
+                        if (mBroadcastIntent) {
+                            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+                            return;
+                        }
+                        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to launch intent", e);
                     }
-                    mContext.startActivity(intent);
                 });
             }
         } catch (URISyntaxException e) {
@@ -65,9 +81,13 @@
         try {
             if (mLongIntent != null) {
                 final Intent intent = Intent.parseUri(mLongIntent, Intent.URI_INTENT_SCHEME);
-                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                 setOnLongClickListener(v -> {
-                    mContext.startActivity(intent);
+                    try {
+                        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to launch intent", e);
+                    }
+                    // consume event either way
                     return true;
                 });
             }
@@ -82,6 +102,7 @@
     public void setSelected(boolean selected) {
         super.setSelected(selected);
         mSelected = selected;
-        setAlpha(mSelected ? SELECTED_ALPHA : UNSELECTED_ALPHA);
+        setAlpha(mSelected ? mSelectedAlpha : mUnselectedAlpha);
+        setImageResource(mSelected ? mSelectedIconResourceId : mIconResourceId);
     }
 }
diff --git a/com/android/systemui/statusbar/car/CarStatusBar.java b/com/android/systemui/statusbar/car/CarStatusBar.java
index 3fb1137..008794c 100644
--- a/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -418,8 +418,7 @@
                 Dependency.get(UserSwitcherController.class);
         if (userSwitcherController.useFullscreenUserSwitcher()) {
             mFullscreenUserSwitcher = new FullscreenUserSwitcher(this,
-                    userSwitcherController,
-                    mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub));
+                    mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub), mContext);
         } else {
             super.createUserSwitcher();
         }
diff --git a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
index bc353f2..5a02d18 100644
--- a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
+++ b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
@@ -18,14 +18,15 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.content.res.Resources;
+import android.content.Context;
 import android.view.View;
 import android.view.ViewStub;
 import android.widget.ProgressBar;
 
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
 
 /**
  * Manages the fullscreen user switcher.
@@ -33,42 +34,59 @@
 public class FullscreenUserSwitcher {
     private final View mContainer;
     private final View mParent;
-    private final UserGridView mUserGridView;
-    private final UserSwitcherController mUserSwitcherController;
+    private final UserGridRecyclerView mUserGridView;
     private final ProgressBar mSwitchingUsers;
     private final int mShortAnimDuration;
+    private final StatusBar mStatusBar;
 
     private boolean mShowing;
 
-    public FullscreenUserSwitcher(StatusBar statusBar,
-            UserSwitcherController userSwitcherController,
-            ViewStub containerStub) {
-        mUserSwitcherController = userSwitcherController;
+    public FullscreenUserSwitcher(StatusBar statusBar, ViewStub containerStub, Context context) {
+        mStatusBar = statusBar;
         mParent = containerStub.inflate();
         mContainer = mParent.findViewById(R.id.container);
         mUserGridView = mContainer.findViewById(R.id.user_grid);
-        mUserGridView.init(statusBar, mUserSwitcherController, true /* overrideAlpha */);
-        mUserGridView.setUserSelectionListener(record -> {
-            if (!record.isCurrent) {
-                toggleSwitchInProgress(true);
-            }
-        });
+        GridLayoutManager layoutManager = new GridLayoutManager(context,
+                context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col));
+        mUserGridView.setLayoutManager(layoutManager);
+        mUserGridView.buildAdapter();
+        mUserGridView.setUserSelectionListener(this::onUserSelected);
 
-        PageIndicator pageIndicator = mContainer.findViewById(R.id.user_switcher_page_indicator);
-        pageIndicator.setupWithViewPager(mUserGridView);
-
-        Resources res = mContainer.getResources();
-        mShortAnimDuration = res.getInteger(android.R.integer.config_shortAnimTime);
-
-        mContainer.findViewById(R.id.start_driving).setOnClickListener(v -> {
-            automaticallySelectUser();
-        });
+        mShortAnimDuration = mContainer.getResources()
+            .getInteger(android.R.integer.config_shortAnimTime);
 
         mSwitchingUsers = mParent.findViewById(R.id.switching_users);
     }
 
+    public void show() {
+        if (mShowing) {
+            return;
+        }
+        mShowing = true;
+        mParent.setVisibility(View.VISIBLE);
+    }
+
+    public void hide() {
+        mShowing = false;
+        toggleSwitchInProgress(false);
+        mParent.setVisibility(View.GONE);
+    }
+
     public void onUserSwitched(int newUserId) {
-        mUserGridView.onUserSwitched(newUserId);
+        mParent.post(this::showOfflineAuthUi);
+    }
+
+    private void onUserSelected(UserGridRecyclerView.UserRecord record) {
+        if (record.mIsForeground) {
+            showOfflineAuthUi();
+            return;
+        }
+        toggleSwitchInProgress(true);
+    }
+
+    private void showOfflineAuthUi() {
+        mStatusBar.executeRunnableDismissingKeyguard(null/* runnable */, null /* cancelAction */,
+                true /* dismissShade */, true /* afterKeyguardGone */, true /* deferred */);
     }
 
     private void toggleSwitchInProgress(boolean inProgress) {
@@ -101,24 +119,4 @@
                 }
             });
     }
-
-    public void show() {
-        if (mShowing) {
-            return;
-        }
-        mShowing = true;
-        mParent.setVisibility(View.VISIBLE);
-    }
-
-    public void hide() {
-        mShowing = false;
-        toggleSwitchInProgress(false);
-        mParent.setVisibility(View.GONE);
-    }
-
-    private void automaticallySelectUser() {
-        // TODO: Switch according to some policy. This implementation just tries to drop the
-        //       keyguard for the current user.
-        mUserGridView.showOfflineAuthUi();
-    }
 }
diff --git a/com/android/systemui/statusbar/car/UserGridRecyclerView.java b/com/android/systemui/statusbar/car/UserGridRecyclerView.java
new file mode 100644
index 0000000..5ad08ac
--- /dev/null
+++ b/com/android/systemui/statusbar/car/UserGridRecyclerView.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2018 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.car;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.drawable.GradientDrawable;
+import android.os.AsyncTask;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.settingslib.users.UserManagerHelper;
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays a GridLayout with icons for the users in the system to allow switching between users.
+ * One of the uses of this is for the lock screen in auto.
+ */
+public class UserGridRecyclerView extends RecyclerView implements
+        UserManagerHelper.OnUsersUpdateListener {
+    private UserSelectionListener mUserSelectionListener;
+    private UserAdapter mAdapter;
+    private UserManagerHelper mUserManagerHelper;
+    private Context mContext;
+
+    public UserGridRecyclerView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        super.setHasFixedSize(true);
+        mContext = context;
+        mUserManagerHelper = new UserManagerHelper(mContext);
+    }
+
+    /**
+     * Register listener for any update to the users
+     */
+    @Override
+    public void onFinishInflate() {
+        mUserManagerHelper.registerOnUsersUpdateListener(this);
+    }
+
+    /**
+     * Unregisters listener checking for any change to the users
+     */
+    @Override
+    public void onDetachedFromWindow() {
+        mUserManagerHelper.unregisterOnUsersUpdateListener();
+    }
+
+    /**
+     * Initializes the adapter that populates the grid layout
+     *
+     * @return the adapter
+     */
+    public void buildAdapter() {
+        List<UserRecord> userRecords = createUserRecords(mUserManagerHelper
+                .getAllUsers());
+        mAdapter = new UserAdapter(mContext, userRecords);
+        super.setAdapter(mAdapter);
+    }
+
+    private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) {
+        List<UserRecord> userRecords = new ArrayList<>();
+        for (UserInfo userInfo : userInfoList) {
+            boolean isForeground = mUserManagerHelper.getForegroundUserId() == userInfo.id;
+            UserRecord record = new UserRecord(userInfo, false /* isGuest */,
+                    false /* isAddUser */, isForeground);
+            userRecords.add(record);
+        }
+
+        // Add guest user record if the foreground user is not a guest
+        if (!mUserManagerHelper.foregroundUserIsGuestUser()) {
+            userRecords.add(addGuestUserRecord());
+        }
+
+        // Add add user record if the foreground user can add users
+        if (mUserManagerHelper.foregroundUserCanAddUsers()) {
+            userRecords.add(addUserRecord());
+        }
+
+        return userRecords;
+    }
+
+    /**
+     * Create guest user record
+     */
+    private UserRecord addGuestUserRecord() {
+        UserInfo userInfo = new UserInfo();
+        userInfo.name = mContext.getString(R.string.car_guest);
+        return new UserRecord(userInfo, true /* isGuest */,
+                false /* isAddUser */, false /* isForeground */);
+    }
+
+    /**
+     * Create add user record
+     */
+    private UserRecord addUserRecord() {
+        UserInfo userInfo = new UserInfo();
+        userInfo.name = mContext.getString(R.string.car_add_user);
+        return new UserRecord(userInfo, false /* isGuest */,
+                true /* isAddUser */, false /* isForeground */);
+    }
+
+    public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
+        mUserSelectionListener = userSelectionListener;
+    }
+
+    @Override
+    public void onUsersUpdate() {
+        mAdapter.clearUsers();
+        mAdapter.updateUsers(createUserRecords(mUserManagerHelper.getAllUsers()));
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Adapter to populate the grid layout with the available user profiles
+     */
+    public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder> {
+
+        private final Context mContext;
+        private List<UserRecord> mUsers;
+        private final int mPodImageAvatarWidth;
+        private final int mPodImageAvatarHeight;
+        private final Resources mRes;
+        private final String mGuestName;
+        private final String mNewUserName;
+
+        public UserAdapter(Context context, List<UserRecord> users) {
+            mRes = context.getResources();
+            mContext = context;
+            updateUsers(users);
+            mPodImageAvatarWidth = mRes.getDimensionPixelSize(
+                    R.dimen.car_fullscreen_user_pod_image_avatar_width);
+            mPodImageAvatarHeight = mRes.getDimensionPixelSize(
+                    R.dimen.car_fullscreen_user_pod_image_avatar_height);
+            mGuestName = mRes.getString(R.string.car_guest);
+            mNewUserName = mRes.getString(R.string.car_new_user);
+        }
+
+        public void clearUsers() {
+            mUsers.clear();
+        }
+
+        public void updateUsers(List<UserRecord> users) {
+            mUsers = users;
+        }
+
+        @Override
+        public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(mContext)
+                    .inflate(R.layout.car_fullscreen_user_pod, parent, false);
+            view.setAlpha(1f);
+            view.bringToFront();
+            return new UserAdapterViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(UserAdapterViewHolder holder, int position) {
+            UserRecord userRecord = mUsers.get(position);
+            holder.mUserAvatarImageView.setImageBitmap(getDefaultUserIcon(userRecord));
+            holder.mUserNameTextView.setText(userRecord.mInfo.name);
+            holder.mView.setOnClickListener(v -> {
+                if (userRecord == null) {
+                    return;
+                }
+
+                // Notify the listener which user was selected
+                if (mUserSelectionListener != null) {
+                    mUserSelectionListener.onUserSelected(userRecord);
+                }
+
+                // If the user selects Guest, switch to Guest profile
+                if (userRecord.mIsGuest) {
+                    mUserManagerHelper.switchToGuest(mGuestName);
+                    return;
+                }
+
+                // If the user wants to add a user, start task to add new user
+                if (userRecord.mIsAddUser) {
+                    new AddNewUserTask().execute(mNewUserName);
+                    return;
+                }
+
+                // If the user doesn't want to be a guest or add a user, switch to the user selected
+                mUserManagerHelper.switchToUser(userRecord.mInfo);
+            });
+
+        }
+
+        private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> {
+
+            @Override
+            protected UserInfo doInBackground(String... userNames) {
+                return mUserManagerHelper.createNewUser(userNames[0]);
+            }
+
+            @Override
+            protected void onPreExecute() {
+            }
+
+            @Override
+            protected void onPostExecute(UserInfo user) {
+                if (user != null) {
+                    mUserManagerHelper.switchToUser(user);
+                }
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mUsers.size();
+        }
+
+        /**
+         * Returns the default user icon.  This icon is a circle with a letter in it.  The letter is
+         * the first character in the username.
+         *
+         * @param record the profile of the user for which the icon should be created
+         */
+        private Bitmap getDefaultUserIcon(UserRecord record) {
+            CharSequence displayText;
+            boolean isAddUserText = false;
+            if (record.mIsAddUser) {
+                displayText = "+";
+                isAddUserText = true;
+            } else {
+                displayText = record.mInfo.name.subSequence(0, 1);
+            }
+            Bitmap out = Bitmap.createBitmap(mPodImageAvatarWidth, mPodImageAvatarHeight,
+                    Bitmap.Config.ARGB_8888);
+            Canvas canvas = new Canvas(out);
+
+            // Draw the circle background.
+            GradientDrawable shape = new GradientDrawable();
+            shape.setShape(GradientDrawable.RADIAL_GRADIENT);
+            shape.setGradientRadius(1.0f);
+            shape.setColor(mContext.getColor(R.color.car_user_switcher_no_user_image_bgcolor));
+            shape.setBounds(0, 0, mPodImageAvatarWidth, mPodImageAvatarHeight);
+            shape.draw(canvas);
+
+            // Draw the letter in the center.
+            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+            paint.setColor(mContext.getColor(R.color.car_user_switcher_no_user_image_fgcolor));
+            paint.setTextAlign(Align.CENTER);
+            if (isAddUserText) {
+                paint.setTextSize(mRes.getDimensionPixelSize(
+                        R.dimen.car_touch_target_size));
+            } else {
+                paint.setTextSize(mRes.getDimensionPixelSize(
+                        R.dimen.car_fullscreen_user_pod_icon_text_size));
+            }
+
+            Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
+            // The Y coordinate is measured by taking half the height of the pod, but that would
+            // draw the character putting the bottom of the font in the middle of the pod.  To
+            // correct this, half the difference between the top and bottom distance metrics of the
+            // font gives the offset of the font.  Bottom is a positive value, top is negative, so
+            // the different is actually a sum.  The "half" operation is then factored out.
+            canvas.drawText(displayText.toString(), mPodImageAvatarWidth / 2,
+                    (mPodImageAvatarHeight - (metrics.bottom + metrics.top)) / 2, paint);
+
+            return out;
+        }
+
+        public class UserAdapterViewHolder extends RecyclerView.ViewHolder {
+
+            public ImageView mUserAvatarImageView;
+            public TextView mUserNameTextView;
+            public View mView;
+
+            public UserAdapterViewHolder(View view) {
+                super(view);
+                mView = view;
+                mUserAvatarImageView = (ImageView) view.findViewById(R.id.user_avatar);
+                mUserNameTextView = (TextView) view.findViewById(R.id.user_name);
+            }
+        }
+    }
+
+    /**
+     * Object wrapper class for the userInfo.  Use it to distinguish if a profile is a
+     * guest profile, add user profile, or the foreground user.
+     */
+    public static final class UserRecord {
+
+        public final UserInfo mInfo;
+        public final boolean mIsGuest;
+        public final boolean mIsAddUser;
+        public final boolean mIsForeground;
+
+        public UserRecord(UserInfo userInfo, boolean isGuest, boolean isAddUser,
+                boolean isForeground) {
+            mInfo = userInfo;
+            mIsGuest = isGuest;
+            mIsAddUser = isAddUser;
+            mIsForeground = isForeground;
+        }
+    }
+
+    /**
+     * Listener used to notify when a user has been selected
+     */
+    interface UserSelectionListener {
+
+        void onUserSelected(UserRecord record);
+    }
+}
diff --git a/com/android/systemui/statusbar/car/UserGridView.java b/com/android/systemui/statusbar/car/UserGridView.java
deleted file mode 100644
index 1bd820d..0000000
--- a/com/android/systemui/statusbar/car/UserGridView.java
+++ /dev/null
@@ -1,364 +0,0 @@
-/*
- * 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
- */
-
-package com.android.systemui.statusbar.car;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.ViewPager;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.R;
-import com.android.systemui.qs.car.CarQSFragment;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.UserInfoController;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Vector;
-
-/**
- * Displays a ViewPager with icons for the users in the system to allow switching between users.
- * One of the uses of this is for the lock screen in auto.
- */
-public class UserGridView extends ViewPager implements
-        UserInfoController.OnUserInfoChangedListener {
-    private StatusBar mStatusBar;
-    private UserSwitcherController mUserSwitcherController;
-    private Adapter mAdapter;
-    private UserSelectionListener mUserSelectionListener;
-    private UserInfoController mUserInfoController;
-    private Vector mUserContainers;
-    private int mContainerWidth;
-    private boolean mOverrideAlpha;
-    private CarQSFragment.UserSwitchCallback mUserSwitchCallback;
-
-    public UserGridView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public void init(StatusBar statusBar, UserSwitcherController userSwitcherController,
-            boolean overrideAlpha) {
-        mStatusBar = statusBar;
-        mUserSwitcherController = userSwitcherController;
-        mAdapter = new Adapter(mUserSwitcherController);
-        mUserInfoController = Dependency.get(UserInfoController.class);
-        mOverrideAlpha = overrideAlpha;
-        // Whenever the container width changes, the containers must be refreshed. Instead of
-        // doing an initial refreshContainers() to populate the containers, this listener will
-        // refresh them on layout change because that affects how the users are split into
-        // containers. Furthermore, at this point, the container width is unknown, so
-        // refreshContainers() cannot populate any containers.
-        addOnLayoutChangeListener(
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                    int newWidth = Math.max(left - right, right - left);
-                    if (mContainerWidth != newWidth) {
-                        mContainerWidth = newWidth;
-                        refreshContainers();
-                    }
-                });
-    }
-
-    private void refreshContainers() {
-        mUserContainers = new Vector();
-
-        Context context = getContext();
-        LayoutInflater inflater = LayoutInflater.from(context);
-
-        for (int i = 0; i < mAdapter.getCount(); i++) {
-            ViewGroup pods = (ViewGroup) inflater.inflate(
-                    R.layout.car_fullscreen_user_pod_container, null);
-
-            int iconsPerPage = mAdapter.getIconsPerPage();
-            int limit = Math.min(mUserSwitcherController.getUsers().size(), (i + 1) * iconsPerPage);
-            for (int j = i * iconsPerPage; j < limit; j++) {
-                View v = mAdapter.makeUserPod(inflater, context, j, pods);
-                if (mOverrideAlpha) {
-                    v.setAlpha(1f);
-                }
-                pods.addView(v);
-                // This is hacky, but the dividers on the pod container LinearLayout don't seem
-                // to work for whatever reason.  Instead, set a right margin on the pod if it's not
-                // the right-most pod and there is more than one pod in the container.
-                if (i < limit - 1 && limit > 1) {
-                    ViewGroup.MarginLayoutParams params =
-                            (ViewGroup.MarginLayoutParams) v.getLayoutParams();
-                    params.setMargins(0, 0, getResources().getDimensionPixelSize(
-                            R.dimen.car_fullscreen_user_pod_margin_between), 0);
-                    v.setLayoutParams(params);
-                }
-            }
-            mUserContainers.add(pods);
-        }
-
-        mAdapter = new Adapter(mUserSwitcherController);
-        setAdapter(mAdapter);
-    }
-
-    @Override
-    public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
-        refreshContainers();
-    }
-
-    public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) {
-        mUserSwitchCallback = callback;
-    }
-
-    public void onUserSwitched(int newUserId) {
-        // Bring up security view after user switch is completed.
-        post(this::showOfflineAuthUi);
-    }
-
-    public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
-        mUserSelectionListener = userSelectionListener;
-    }
-
-    public void setListening(boolean listening) {
-        if (listening) {
-            mUserInfoController.addCallback(this);
-        } else {
-            mUserInfoController.removeCallback(this);
-        }
-    }
-
-    void showOfflineAuthUi() {
-        // TODO: Show keyguard UI in-place.
-        mStatusBar.executeRunnableDismissingKeyguard(null, null, true, true, true);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        // Wrap content doesn't work in ViewPagers, so simulate the behavior in code.
-        int height = 0;
-        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
-            height = MeasureSpec.getSize(heightMeasureSpec);
-        } else {
-            for (int i = 0; i < getChildCount(); i++) {
-                View child = getChildAt(i);
-                child.measure(widthMeasureSpec,
-                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
-                height = Math.max(child.getMeasuredHeight(), height);
-            }
-
-            // Respect the AT_MOST request from parent.
-            if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
-                height = Math.min(MeasureSpec.getSize(heightMeasureSpec), height);
-            }
-        }
-        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
-
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-
-    /**
-     * This is a ViewPager.PagerAdapter which deletegates the work to a
-     * UserSwitcherController.BaseUserAdapter. Java doesn't support multiple inheritance so we have
-     * to use composition instead to achieve the same goal since both the base classes are abstract
-     * classes and not interfaces.
-     */
-    private final class Adapter extends PagerAdapter {
-        private final int mPodWidth;
-        private final int mPodMarginBetween;
-        private final int mPodImageAvatarWidth;
-        private final int mPodImageAvatarHeight;
-
-        private final WrappedBaseUserAdapter mUserAdapter;
-
-        public Adapter(UserSwitcherController controller) {
-            super();
-            mUserAdapter = new WrappedBaseUserAdapter(controller, this);
-
-            Resources res = getResources();
-            mPodWidth = res.getDimensionPixelSize(R.dimen.car_fullscreen_user_pod_width);
-            mPodMarginBetween = res.getDimensionPixelSize(
-                    R.dimen.car_fullscreen_user_pod_margin_between);
-            mPodImageAvatarWidth = res.getDimensionPixelSize(
-                    R.dimen.car_fullscreen_user_pod_image_avatar_width);
-            mPodImageAvatarHeight = res.getDimensionPixelSize(
-                    R.dimen.car_fullscreen_user_pod_image_avatar_height);
-        }
-
-        @Override
-        public void destroyItem(ViewGroup container, int position, Object object) {
-            container.removeView((View) object);
-        }
-
-        private int getIconsPerPage() {
-            // We need to know how many pods we need in this page. Each pod has its own width and
-            // a margin between them. We can then divide the measured width of the parent by the
-            // sum of pod width and margin to get the number of pods that will completely fit.
-            // There is one less margin than the number of pods (eg. for 5 pods, there are 4
-            // margins), so need to add the margin to the measured width to account for that.
-            return (mContainerWidth + mPodMarginBetween) /
-                    (mPodWidth + mPodMarginBetween);
-        }
-
-        @Override
-        public void finishUpdate(ViewGroup container) {
-            if (mUserSwitchCallback != null) {
-                mUserSwitchCallback.resetShowing();
-            }
-        }
-
-        @Override
-        public Object instantiateItem(ViewGroup container, int position) {
-            if (position < mUserContainers.size()) {
-                container.addView((View) mUserContainers.get(position));
-                return mUserContainers.get(position);
-            } else {
-                return null;
-            }
-        }
-
-        /**
-         * Returns the default user icon.  This icon is a circle with a letter in it.  The letter is
-         * the first character in the username.
-         *
-         * @param userName the username of the user for which the icon is to be created
-         */
-        private Bitmap getDefaultUserIcon(CharSequence userName) {
-            CharSequence displayText = userName.subSequence(0, 1);
-            Bitmap out = Bitmap.createBitmap(mPodImageAvatarWidth, mPodImageAvatarHeight,
-                    Bitmap.Config.ARGB_8888);
-            Canvas canvas = new Canvas(out);
-
-            // Draw the circle background.
-            GradientDrawable shape = new GradientDrawable();
-            shape.setShape(GradientDrawable.RADIAL_GRADIENT);
-            shape.setGradientRadius(1.0f);
-            shape.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_bgcolor));
-            shape.setBounds(0, 0, mPodImageAvatarWidth, mPodImageAvatarHeight);
-            shape.draw(canvas);
-
-            // Draw the letter in the center.
-            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
-            paint.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_fgcolor));
-            paint.setTextAlign(Align.CENTER);
-            paint.setTextSize(getResources().getDimensionPixelSize(
-                    R.dimen.car_fullscreen_user_pod_icon_text_size));
-            Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
-            // The Y coordinate is measured by taking half the height of the pod, but that would
-            // draw the character putting the bottom of the font in the middle of the pod.  To
-            // correct this, half the difference between the top and bottom distance metrics of the
-            // font gives the offset of the font.  Bottom is a positive value, top is negative, so
-            // the different is actually a sum.  The "half" operation is then factored out.
-            canvas.drawText(displayText.toString(), mPodImageAvatarWidth / 2,
-                    (mPodImageAvatarHeight - (metrics.bottom + metrics.top)) / 2, paint);
-
-            return out;
-        }
-
-        private View makeUserPod(LayoutInflater inflater, Context context,
-                int position, ViewGroup parent) {
-            final UserSwitcherController.UserRecord record = mUserAdapter.getItem(position);
-            View view = inflater.inflate(R.layout.car_fullscreen_user_pod, parent, false);
-
-            TextView nameView = view.findViewById(R.id.user_name);
-            if (record != null) {
-                nameView.setText(mUserAdapter.getName(context, record));
-                view.setActivated(record.isCurrent);
-            } else {
-                nameView.setText(context.getString(R.string.unknown_user_label));
-            }
-
-            ImageView iconView = (ImageView) view.findViewById(R.id.user_avatar);
-            if (record == null || (record.picture == null && !record.isAddUser)) {
-                iconView.setImageBitmap(getDefaultUserIcon(nameView.getText()));
-            } else if (record.isAddUser) {
-                Drawable icon = context.getDrawable(R.drawable.ic_add_circle_qs);
-                icon.setTint(context.getColor(R.color.car_user_switcher_no_user_image_bgcolor));
-                iconView.setImageDrawable(icon);
-            } else {
-                iconView.setImageBitmap(record.picture);
-            }
-
-            iconView.setOnClickListener(v -> {
-                if (record == null) {
-                    return;
-                }
-
-                if (mUserSelectionListener != null) {
-                    mUserSelectionListener.onUserSelected(record);
-                }
-
-                if (record.isCurrent) {
-                    showOfflineAuthUi();
-                } else {
-                    mUserSwitcherController.switchTo(record);
-                }
-            });
-
-            return view;
-        }
-
-        @Override
-        public int getCount() {
-            int iconsPerPage = getIconsPerPage();
-            if (iconsPerPage == 0) {
-                return 0;
-            }
-            return (int) Math.ceil((double) mUserAdapter.getCount() / getIconsPerPage());
-        }
-
-        public void refresh() {
-            mUserAdapter.refresh();
-        }
-
-        @Override
-        public boolean isViewFromObject(View view, Object object) {
-            return view == object;
-        }
-    }
-
-    private final class WrappedBaseUserAdapter extends UserSwitcherController.BaseUserAdapter {
-        private final Adapter mContainer;
-
-        public WrappedBaseUserAdapter(UserSwitcherController controller, Adapter container) {
-            super(controller);
-            mContainer = container;
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            throw new UnsupportedOperationException("unused");
-        }
-
-        @Override
-        public void notifyDataSetChanged() {
-            super.notifyDataSetChanged();
-            mContainer.notifyDataSetChanged();
-        }
-    }
-
-    interface UserSelectionListener {
-        void onUserSelected(UserSwitcherController.UserRecord record);
-    };
-}
diff --git a/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
index 3bbfe3c..b8bce95 100644
--- a/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
+++ b/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
@@ -238,6 +238,7 @@
                 t.deferTransactionUntilSurface(app.leash, systemUiSurface,
                         systemUiSurface.getNextFrameNumber());
             }
+            t.setEarlyWakeup();
             t.apply();
         }
 
diff --git a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
index 75b31c5..9fcb090 100644
--- a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
+++ b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -14,6 +14,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.app.StatusBarManager.DISABLE_CLOCK;
 import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS;
 import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO;
 
@@ -96,6 +97,7 @@
         mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area);
         mClockView = mStatusBar.findViewById(R.id.clock);
         showSystemIconArea(false);
+        showClock(false);
         initEmergencyCryptkeeperText();
         initOperatorName();
     }
@@ -163,6 +165,13 @@
                 showNotificationIconArea(animate);
             }
         }
+        if ((diff1 & DISABLE_CLOCK) != 0) {
+            if ((state1 & DISABLE_CLOCK) != 0) {
+                hideClock(animate);
+            } else {
+                showClock(animate);
+            }
+        }
     }
 
     protected int adjustDisableFlags(int state) {
@@ -171,6 +180,7 @@
                 && shouldHideNotificationIcons()) {
             state |= DISABLE_NOTIFICATION_ICONS;
             state |= DISABLE_SYSTEM_INFO;
+            state |= DISABLE_CLOCK;
         }
         if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) {
             if (mNetworkController.hasEmergencyCryptKeeperText()) {
@@ -195,11 +205,17 @@
 
     public void hideSystemIconArea(boolean animate) {
         animateHide(mSystemIconArea, animate);
-        animateHide(mClockView, animate);
     }
 
     public void showSystemIconArea(boolean animate) {
         animateShow(mSystemIconArea, animate);
+    }
+
+    public void hideClock(boolean animate) {
+        animateHide(mClockView, animate);
+    }
+
+    public void showClock(boolean animate) {
         animateShow(mClockView, animate);
     }
 
diff --git a/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index df2b817..60a3474 100644
--- a/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -159,6 +159,11 @@
      */
     public void onFullyShown() {
         mFalsingManager.onBouncerShown();
+        if (mKeyguardView == null) {
+            Log.wtf(TAG, "onFullyShown when view was null");
+        } else {
+            mKeyguardView.onResume();
+        }
     }
 
     /**
@@ -180,7 +185,6 @@
         @Override
         public void run() {
             mRoot.setVisibility(View.VISIBLE);
-            mKeyguardView.onResume();
             showPromptReason(mBouncerPromptReason);
             final CharSequence customMessage = mCallback.consumeCustomMessage();
             if (customMessage != null) {
@@ -296,7 +300,7 @@
 
     public boolean isShowing() {
         return (mShowingSoon || (mRoot != null && mRoot.getVisibility() == View.VISIBLE))
-                && mExpansion == 0;
+                && mExpansion == 0 && !isAnimatingAway();
     }
 
     /**
diff --git a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index 3d7067d..1fb1ddd 100644
--- a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -99,6 +99,11 @@
     private int mBurnInPreventionOffsetY;
 
     /**
+     * Clock vertical padding when pulsing.
+     */
+    private int mPulsingPadding;
+
+    /**
      * Doze/AOD transition amount.
      */
     private float mDarkAmount;
@@ -109,9 +114,9 @@
     private boolean mCurrentlySecure;
 
     /**
-     * If notification panel view currently has a touch.
+     * Dozing and receiving a notification (AOD notification.)
      */
-    private boolean mTracking;
+    private boolean mPulsing;
 
     /**
      * Distance in pixels between the top of the screen and the first view of the bouncer.
@@ -130,11 +135,13 @@
                 R.dimen.burn_in_prevention_offset_x);
         mBurnInPreventionOffsetY = res.getDimensionPixelSize(
                 R.dimen.burn_in_prevention_offset_y);
+        mPulsingPadding = res.getDimensionPixelSize(
+                R.dimen.widget_pulsing_bottom_padding);
     }
 
     public void setup(int minTopMargin, int maxShadeBottom, int notificationStackHeight,
             float expandedHeight, float maxPanelHeight, int parentHeight, int keyguardStatusHeight,
-            float dark, boolean secure, boolean tracking, int bouncerTop) {
+            float dark, boolean secure, boolean pulsing, int bouncerTop) {
         mMinTopMargin = minTopMargin + mContainerTopPadding;
         mMaxShadeBottom = maxShadeBottom;
         mNotificationStackHeight = notificationStackHeight;
@@ -144,7 +151,7 @@
         mKeyguardStatusHeight = keyguardStatusHeight;
         mDarkAmount = dark;
         mCurrentlySecure = secure;
-        mTracking = tracking;
+        mPulsing = pulsing;
         mBouncerTop = bouncerTop;
     }
 
@@ -152,7 +159,7 @@
         final int y = getClockY();
         result.clockY = y;
         result.clockAlpha = getClockAlpha(y);
-        result.stackScrollerPadding = y + mKeyguardStatusHeight;
+        result.stackScrollerPadding = y + (mPulsing ? 0 : mKeyguardStatusHeight);
         result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
     }
 
@@ -194,9 +201,13 @@
 
     private int getClockY() {
         // Dark: Align the bottom edge of the clock at about half of the screen:
-        final float clockYDark = getMaxClockY() + burnInPreventionOffsetY();
-        final float clockYRegular = getExpandedClockPosition();
-        final boolean hasEnoughSpace = mMinTopMargin + mKeyguardStatusHeight < mBouncerTop;
+        float clockYDark = getMaxClockY() + burnInPreventionOffsetY();
+        if (mPulsing) {
+            clockYDark -= mPulsingPadding;
+        }
+
+        float clockYRegular = getExpandedClockPosition();
+        boolean hasEnoughSpace = mMinTopMargin + mKeyguardStatusHeight < mBouncerTop;
         float clockYTarget = mCurrentlySecure && hasEnoughSpace ?
                 mMinTopMargin : -mKeyguardStatusHeight;
 
diff --git a/com/android/systemui/statusbar/phone/KeyguardDismissHandler.java b/com/android/systemui/statusbar/phone/KeyguardDismissHandler.java
new file mode 100644
index 0000000..759a0d1
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardDismissHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 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.phone;
+
+import android.annotation.Nullable;
+
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
+
+
+/** Executes actions that require the screen to be unlocked. */
+public interface KeyguardDismissHandler {
+    /** Executes an action that requres the screen to be unlocked. */
+    void dismissKeyguardThenExecute(
+            OnDismissAction action, @Nullable Runnable cancelAction, boolean afterKeyguardGone);
+}
diff --git a/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java b/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
new file mode 100644
index 0000000..c38b0b6
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 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.phone;
+
+import android.util.Log;
+
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
+
+/**
+ * Executes actions that require the screen to be unlocked. Delegates the actual handling to an
+ * implementation passed via {@link #setDismissHandler}.
+ */
+public class KeyguardDismissUtil implements KeyguardDismissHandler {
+    private static final String TAG = "KeyguardDismissUtil";
+
+    private volatile KeyguardDismissHandler mDismissHandler;
+
+    /** Sets the actual {@link DismissHandler} implementation. */
+    public void setDismissHandler(KeyguardDismissHandler dismissHandler) {
+        mDismissHandler = dismissHandler;
+    }
+
+    /**
+     * Executes an action that requres the screen to be unlocked.
+     *
+     * <p>Must be called after {@link #setDismissHandler}.
+     */
+    @Override
+    public void dismissKeyguardThenExecute(
+            OnDismissAction action, Runnable cancelAction, boolean afterKeyguardGone) {
+        KeyguardDismissHandler dismissHandler = mDismissHandler;
+        if (dismissHandler == null) {
+            Log.wtf(TAG, "KeyguardDismissHandler not set.");
+            action.onDismiss();
+            return;
+        }
+        dismissHandler.dismissKeyguardThenExecute(action, cancelAction, afterKeyguardGone);
+    }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index 58f8baa..ca6d596 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -103,6 +103,7 @@
 import java.io.PrintWriter;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 
 /**
  * Fragment containing the NavigationBarFragment. Contains logic for what happens
@@ -1109,8 +1110,11 @@
         public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
             // Only hide the icon if the top task changes its requestedOrientation
             // Launcher can alter its requestedOrientation while it's not on top, don't hide on this
-            final boolean top = ActivityManagerWrapper.getInstance().getRunningTask().id == taskId;
-            if (top) setRotateSuggestionButtonState(false);
+            Optional.ofNullable(ActivityManagerWrapper.getInstance())
+                    .map(ActivityManagerWrapper::getRunningTask)
+                    .ifPresent(a -> {
+                        if (a.id == taskId) setRotateSuggestionButtonState(false);
+                    });
         }
     }
 
diff --git a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 9894235..91cf8f0 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -25,6 +25,7 @@
 import android.view.Display.Mode;
 import android.view.Gravity;
 import android.view.LayoutInflater;
+import android.view.Surface;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -80,6 +81,7 @@
     private static final String WEIGHT_CENTERED_SUFFIX = "WC";
 
     private final List<NavBarButtonProvider> mPlugins = new ArrayList<>();
+    private final Display mDisplay;
 
     protected LayoutInflater mLayoutInflater;
     protected LayoutInflater mLandscapeInflater;
@@ -99,9 +101,9 @@
     public NavigationBarInflaterView(Context context, AttributeSet attrs) {
         super(context, attrs);
         createInflaters();
-        Display display = ((WindowManager)
+        mDisplay = ((WindowManager)
                 context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
-        Mode displayMode = display.getMode();
+        Mode displayMode = mDisplay.getMode();
         isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
     }
 
@@ -173,6 +175,17 @@
         }
     }
 
+    public void updateButtonDispatchersCurrentView() {
+        if (mButtonDispatchers != null) {
+            final int rotation = mDisplay.getRotation();
+            final View view = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+                    ? mRot0 : mRot90;
+            for (int i = 0; i < mButtonDispatchers.size(); i++) {
+                mButtonDispatchers.valueAt(i).setCurrentView(view);
+            }
+        }
+    }
+
     public void setAlternativeOrder(boolean alternativeOrder) {
         if (alternativeOrder != mAlternativeOrder) {
             mAlternativeOrder = alternativeOrder;
@@ -239,6 +252,8 @@
 
         inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);
         inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
+
+        updateButtonDispatchersCurrentView();
     }
 
     private void addGravitySpacer(LinearLayout layout) {
diff --git a/com/android/systemui/statusbar/phone/NavigationBarView.java b/com/android/systemui/statusbar/phone/NavigationBarView.java
index f216695..d79f308 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -39,7 +39,6 @@
 import android.os.Handler;
 import android.os.Message;
 import android.os.SystemProperties;
-import android.os.VibrationEffect;
 import android.support.annotation.ColorInt;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -69,7 +68,6 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.NavigationBarCompat;
 import com.android.systemui.stackdivider.Divider;
-import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.DeadZone;
 import com.android.systemui.statusbar.policy.KeyButtonDrawable;
 import com.android.systemui.statusbar.policy.TintedKeyButtonDrawable;
@@ -124,7 +122,7 @@
     private TintedKeyButtonDrawable mRotateSuggestionIcon;
 
     private GestureHelper mGestureHelper;
-    private DeadZone mDeadZone;
+    private final DeadZone mDeadZone;
     private final NavigationBarTransitions mBarTransitions;
     private final OverviewProxyService mOverviewProxyService;
 
@@ -150,7 +148,6 @@
     private Divider mDivider;
     private RecentsOnboarding mRecentsOnboarding;
     private NotificationPanelView mPanelView;
-    private final VibratorHelper mVibratorHelper;
 
     private int mRotateBtnStyle = R.style.RotateButtonCCWStart90;
 
@@ -246,7 +243,6 @@
 
         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
         mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService);
-        mVibratorHelper = Dependency.get(VibratorHelper.class);
 
         mConfiguration = new Configuration();
         mConfiguration.updateFrom(context.getResources().getConfiguration());
@@ -263,6 +259,7 @@
                 new ButtonDispatcher(R.id.accessibility_button));
         mButtonDispatchers.put(R.id.rotate_suggestion,
                 new ButtonDispatcher(R.id.rotate_suggestion));
+        mDeadZone = new DeadZone(this);
     }
 
     public BarTransitions getBarTransitions() {
@@ -297,6 +294,10 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mDeadZone.onTouchEvent(event)) {
+            // Consumed the touch event
+            return true;
+        }
         switch (event.getActionMasked()) {
             case ACTION_DOWN:
                 int x = (int) event.getX();
@@ -309,9 +310,6 @@
                 } else if (mRecentsButtonBounds.contains(x, y)) {
                     mDownHitTarget = HIT_TARGET_OVERVIEW;
                 }
-
-                // Vibrate tick whenever down occurs on navigation bar
-                mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
                 break;
         }
         return mGestureHelper.onInterceptTouchEvent(event);
@@ -319,6 +317,10 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
+        if (mDeadZone.onTouchEvent(event)) {
+            // Consumed the touch event
+            return true;
+        }
         if (mGestureHelper.onTouchEvent(event)) {
             return true;
         }
@@ -389,7 +391,7 @@
 
     public boolean isQuickScrubEnabled() {
         return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", true)
-                && mOverviewProxyService.getProxy() != null && isOverviewEnabled()
+                && mOverviewProxyService.isEnabled() && isOverviewEnabled()
                 && ((mOverviewProxyService.getInteractionFlags() & FLAG_DISABLE_QUICK_SCRUB) == 0);
     }
 
@@ -587,7 +589,7 @@
         // recents buttons when disconnected from launcher service in screen pinning mode,
         // as they are used for exiting.
         final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
-        if (mOverviewProxyService.getProxy() != null) {
+        if (mOverviewProxyService.isEnabled()) {
             // Use interaction flags to show/hide navigation buttons but will be shown if required
             // to exit screen pinning.
             final int flags = mOverviewProxyService.getInteractionFlags();
@@ -810,14 +812,12 @@
         if (mGestureHelper != null) {
             mGestureHelper.onDarkIntensityChange(intensity);
         }
-        if (mRecentsOnboarding != null) {
-            mRecentsOnboarding.setContentDarkIntensity(intensity);
-        }
     }
 
     @Override
     protected void onDraw(Canvas canvas) {
         mGestureHelper.onDraw(canvas);
+        mDeadZone.onDraw(canvas);
         super.onDraw(canvas);
     }
 
@@ -828,6 +828,7 @@
         updateButtonLocationOnScreen(getHomeButton(), mHomeButtonBounds);
         updateButtonLocationOnScreen(getRecentsButton(), mRecentsButtonBounds);
         mGestureHelper.onLayout(changed, left, top, right, bottom);
+        mRecentsOnboarding.setNavBarHeight(getMeasuredHeight());
     }
 
     private void updateButtonLocationOnScreen(ButtonDispatcher button, Rect buttonBounds) {
@@ -870,9 +871,7 @@
         mCurrentView = mRotatedViews[rot];
         mCurrentView.setVisibility(View.VISIBLE);
         mNavigationInflaterView.setAlternativeOrder(rot == Surface.ROTATION_90);
-        for (int i = 0; i < mButtonDispatchers.size(); i++) {
-            mButtonDispatchers.valueAt(i).setCurrentView(mCurrentView);
-        }
+        mNavigationInflaterView.updateButtonDispatchersCurrentView();
         updateLayoutTransitionsEnabled();
         mCurrentRotation = rot;
     }
@@ -889,10 +888,8 @@
     public void reorient() {
         updateCurrentView();
 
-        mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
-
         ((NavigationBarFrame) getRootView()).setDeadZone(mDeadZone);
-        mDeadZone.setDisplayRotation(mCurrentRotation);
+        mDeadZone.onConfigurationChanged(mCurrentRotation);
 
         // force the low profile & disabled states into compliance
         mBarTransitions.init();
diff --git a/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index b6a11f7..6bc19ea 100644
--- a/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -125,7 +125,14 @@
         } else {
             mTintArea.set(tintArea);
         }
-        mIconTint = iconTint;
+        if (mNotificationIconArea != null) {
+            if (DarkIconDispatcher.isInArea(tintArea, mNotificationIconArea)) {
+                mIconTint = iconTint;
+            }
+        } else {
+            mIconTint = iconTint;
+        }
+
         applyNotificationIconsTint();
     }
 
diff --git a/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 5517434..8c257fe 100644
--- a/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -151,6 +151,7 @@
     private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
     // Keep track of the last visible icon so collapsed container can report on its location
     private IconState mLastVisibleIconState;
+    private IconState mFirstVisibleIconState;
     private float mVisualOverflowStart;
     // Keep track of overflow in range [0, 3]
     private int mNumDots;
@@ -159,7 +160,6 @@
     private int[] mAbsolutePosition = new int[2];
     private View mIsolatedIconForAnimation;
 
-
     public NotificationIconContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
         initDimens();
@@ -192,10 +192,15 @@
             paint.setColor(Color.BLUE);
             canvas.drawLine(end, 0, end, height, paint);
 
-            paint.setColor(Color.BLACK);
+            paint.setColor(Color.GREEN);
             int lastIcon = (int) mLastVisibleIconState.xTranslation;
             canvas.drawLine(lastIcon, 0, lastIcon, height, paint);
 
+            if (mFirstVisibleIconState != null) {
+                int firstIcon = (int) mFirstVisibleIconState.xTranslation;
+                canvas.drawLine(firstIcon, 0, firstIcon, height, paint);
+            }
+
             paint.setColor(Color.RED);
             canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint);
 
@@ -210,6 +215,7 @@
         super.onConfigurationChanged(newConfig);
         initDimens();
     }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         float centerY = getHeight() / 2.0f;
@@ -364,11 +370,15 @@
         float layoutEnd = getLayoutEnd();
         float overflowStart = getMaxOverflowStart();
         mVisualOverflowStart = 0;
+        mFirstVisibleIconState = null;
         boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
         for (int i = 0; i < childCount; i++) {
             View view = getChildAt(i);
             IconState iconState = mIconStates.get(view);
             iconState.xTranslation = translationX;
+            if (mFirstVisibleIconState == null) {
+                mFirstVisibleIconState = iconState;
+            }
             boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
                     && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
             boolean noOverflowAfter = i == childCount - 1;
@@ -417,10 +427,16 @@
         } else if (childCount > 0) {
             View lastChild = getChildAt(childCount - 1);
             mLastVisibleIconState = mIconStates.get(lastChild);
+            mFirstVisibleIconState = mIconStates.get(getChildAt(0));
         }
         boolean center = mDark;
         if (center && translationX < getLayoutEnd()) {
-            float delta = (getLayoutEnd() - translationX) / 2;
+            float initialTranslation =
+                    mFirstVisibleIconState == null ? 0 : mFirstVisibleIconState.xTranslation;
+            float contentWidth = getFinalTranslationX() - initialTranslation;
+            float availableSpace = getLayoutEnd() - getActualPaddingStart();
+            float delta = (availableSpace - contentWidth) / 2;
+
             if (firstOverflowIndex != -1) {
                 // If we have an overflow, only count those half for centering because the dots
                 // don't have a lot of visual weight.
diff --git a/com/android/systemui/statusbar/phone/NotificationPanelView.java b/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 27ca0d1..351633b 100644
--- a/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -238,6 +238,7 @@
     private boolean mIsFullWidth;
     private float mDarkAmount;
     private float mDarkAmountTarget;
+    private boolean mPulsing;
     private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
     private boolean mNoVisibleNotifications = true;
     private ValueAnimator mDarkAnimator;
@@ -477,7 +478,7 @@
                     mKeyguardStatusView.getHeight(),
                     mDarkAmount,
                     mStatusBar.isKeyguardCurrentlySecure(),
-                    mTracking,
+                    mPulsing,
                     mBouncerTop);
             mClockPositionAlgorithm.run(mClockPositionResult);
             if (animate || mClockAnimator != null) {
@@ -659,6 +660,14 @@
         expand(true /* animate */);
     }
 
+    public void expandWithoutQs() {
+        if (isQsExpanded()) {
+            flingSettings(0 /* velocity */, false /* expand */);
+        } else {
+            expand(true /* animate */);
+        }
+    }
+
     @Override
     public void fling(float vel, boolean expand) {
         GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
@@ -2681,14 +2690,8 @@
         positionClockAndNotifications();
     }
 
-    public void setNoVisibleNotifications(boolean noNotifications) {
-        mNoVisibleNotifications = noNotifications;
-        if (mQs != null) {
-            mQs.setHasNotifications(!noNotifications);
-        }
-    }
-
     public void setPulsing(boolean pulsing) {
+        mPulsing = pulsing;
         mKeyguardStatusView.setPulsing(pulsing);
         positionClockAndNotifications();
         mNotificationStackScroller.setPulsing(pulsing, mKeyguardStatusView.getLocationOnScreen()[1]
diff --git a/com/android/systemui/statusbar/phone/PanelView.java b/com/android/systemui/statusbar/phone/PanelView.java
index 04cb620..304a499 100644
--- a/com/android/systemui/statusbar/phone/PanelView.java
+++ b/com/android/systemui/statusbar/phone/PanelView.java
@@ -488,7 +488,7 @@
                 mUpdateFlingVelocity = vel;
             }
         } else if (mPanelClosedOnDown && !mHeadsUpManager.hasPinnedHeadsUp() && !mTracking
-                && !mStatusBar.isBouncerShowing()) {
+                && !mStatusBar.isBouncerShowing() && !mStatusBar.isKeyguardFadingAway()) {
             long timePassed = SystemClock.uptimeMillis() - mDownTime;
             if (timePassed < ViewConfiguration.getLongPressTimeout()) {
                 // Lets show the user that he can actually expand the panel
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
index 12bdfc6..a7d5aca 100644
--- a/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
@@ -43,10 +43,9 @@
     }
 
     public void init() {
-        mLeftSide = mView.findViewById(R.id.notification_icon_area);
+        mLeftSide = mView.findViewById(R.id.status_bar_left_side);
         mStatusIcons = mView.findViewById(R.id.statusIcons);
         mBattery = mView.findViewById(R.id.battery);
-        mClock = mView.findViewById(R.id.clock);
         applyModeBackground(-1, getMode(), false /*animate*/);
         applyMode(getMode(), false /*animate*/);
     }
@@ -89,8 +88,7 @@
             anims.playTogether(
                     animateTransitionTo(mLeftSide, newAlpha),
                     animateTransitionTo(mStatusIcons, newAlpha),
-                    animateTransitionTo(mBattery, newAlphaBC),
-                    animateTransitionTo(mClock, newAlphaBC)
+                    animateTransitionTo(mBattery, newAlphaBC)
                     );
             if (isLightsOut(mode)) {
                 anims.setDuration(LIGHTS_OUT_DURATION);
@@ -101,7 +99,6 @@
             mLeftSide.setAlpha(newAlpha);
             mStatusIcons.setAlpha(newAlpha);
             mBattery.setAlpha(newAlphaBC);
-            mClock.setAlpha(newAlphaBC);
         }
     }
 }
\ No newline at end of file
diff --git a/com/android/systemui/statusbar/phone/QuickStepController.java b/com/android/systemui/statusbar/phone/QuickStepController.java
index a51cd93..d3790d4 100644
--- a/com/android/systemui/statusbar/phone/QuickStepController.java
+++ b/com/android/systemui/statusbar/phone/QuickStepController.java
@@ -51,6 +51,10 @@
 import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
 import static com.android.systemui.OverviewProxyService.TAG_OPS;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_SCRUB_DRAG_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_SCRUB_TOUCH_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_DRAG_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_TOUCH_SLOP_PX;
 
 /**
  * Class to detect gestures on the navigation bar and implement quick scrub.
@@ -69,6 +73,7 @@
     private float mTranslation;
     private int mTouchDownX;
     private int mTouchDownY;
+    private boolean mDragScrubActive;
     private boolean mDragPositive;
     private boolean mIsVertical;
     private boolean mIsRTL;
@@ -82,7 +87,6 @@
     private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
     private final Rect mTrackRect = new Rect();
     private final Paint mTrackPaint = new Paint();
-    private final int mScrollTouchSlop;
     private final OverviewProxyService mOverviewEventSender;
     private final int mTrackThickness;
     private final int mTrackPadding;
@@ -115,6 +119,7 @@
         @Override
         public void onAnimationEnd(Animator animation) {
             mQuickScrubActive = false;
+            mDragScrubActive = false;
             mTranslation = 0;
             mQuickScrubEndAnimator.setCurrentPlayTime(mQuickScrubEndAnimator.getDuration());
             mHomeButtonView = null;
@@ -123,7 +128,6 @@
 
     public QuickStepController(Context context) {
         mContext = context;
-        mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mOverviewEventSender = Dependency.get(OverviewProxyService.class);
         mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness);
         mTrackPadding = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_edge_padding);
@@ -168,8 +172,8 @@
     }
 
     private boolean handleTouchEvent(MotionEvent event) {
-        if (!mNavigationBarView.isQuickScrubEnabled()
-                && !mNavigationBarView.isQuickStepSwipeUpEnabled()) {
+        if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled()
+                && !mNavigationBarView.isQuickStepSwipeUpEnabled())) {
             mNavigationBarView.getHomeButton().setDelayTouchFeedback(false /* delay */);
             return false;
         }
@@ -177,8 +181,8 @@
 
         final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
         final boolean homePressed = mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME;
-        int action = event.getAction();
-        switch (action & MotionEvent.ACTION_MASK) {
+        int action = event.getActionMasked();
+        switch (action) {
             case MotionEvent.ACTION_DOWN: {
                 int x = (int) event.getX();
                 int y = (int) event.getY();
@@ -199,28 +203,29 @@
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
-                if (mQuickStepStarted || !mAllowGestureDetection){
+                if (mQuickStepStarted || !mAllowGestureDetection || mHomeButtonView == null){
                     break;
                 }
                 int x = (int) event.getX();
                 int y = (int) event.getY();
                 int xDiff = Math.abs(x - mTouchDownX);
                 int yDiff = Math.abs(y - mTouchDownY);
-                boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
-                boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
-                boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
+
+                boolean exceededScrubTouchSlop, exceededSwipeUpTouchSlop, exceededScrubDragSlop;
                 int pos, touchDown, offset, trackSize;
 
                 if (mIsVertical) {
-                    exceededTouchSlop = exceededTouchSlopY;
-                    exceededPerpendicularTouchSlop = exceededTouchSlopX;
+                    exceededScrubTouchSlop = yDiff > QUICK_STEP_TOUCH_SLOP_PX && yDiff > xDiff;
+                    exceededSwipeUpTouchSlop = xDiff > QUICK_STEP_DRAG_SLOP_PX && xDiff > yDiff;
+                    exceededScrubDragSlop = yDiff > QUICK_SCRUB_DRAG_SLOP_PX && yDiff > xDiff;
                     pos = y;
                     touchDown = mTouchDownY;
                     offset = pos - mTrackRect.top;
                     trackSize = mTrackRect.height();
                 } else {
-                    exceededTouchSlop = exceededTouchSlopX;
-                    exceededPerpendicularTouchSlop = exceededTouchSlopY;
+                    exceededScrubTouchSlop = xDiff > QUICK_STEP_TOUCH_SLOP_PX && xDiff > yDiff;
+                    exceededSwipeUpTouchSlop = yDiff > QUICK_SCRUB_TOUCH_SLOP_PX && yDiff > xDiff;
+                    exceededScrubDragSlop = xDiff > QUICK_SCRUB_DRAG_SLOP_PX && xDiff > yDiff;
                     pos = x;
                     touchDown = mTouchDownX;
                     offset = pos - mTrackRect.left;
@@ -228,7 +233,7 @@
                 }
                 // Decide to start quickstep if dragging away from the navigation bar, otherwise in
                 // the parallel direction, decide to start quickscrub. Only one may run.
-                if (!mQuickScrubActive && exceededPerpendicularTouchSlop) {
+                if (!mQuickScrubActive && exceededSwipeUpTouchSlop) {
                     if (mNavigationBarView.isQuickStepSwipeUpEnabled()) {
                         startQuickStep(event);
                     }
@@ -244,29 +249,38 @@
                     offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
                 }
 
-                // Control the button movement
-                if (!mQuickScrubActive && exceededTouchSlop) {
-                    boolean allowDrag = !mDragPositive
-                            ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
-                    if (allowDrag) {
+                final boolean allowDrag = !mDragPositive
+                        ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
+                if (allowDrag) {
+                    // Passing the drag slop is for visual feedback and will not initiate anything
+                    if (!mDragScrubActive && exceededScrubDragSlop) {
                         mDownOffset = offset;
+                        mDragScrubActive = true;
+                    }
+
+                    // Passing the drag slop then touch slop will start quick step
+                    if (!mQuickScrubActive && exceededScrubTouchSlop) {
                         homeButton.abortCurrentGesture();
                         startQuickScrub();
                     }
                 }
-                if (mQuickScrubActive && (mDragPositive && offset >= 0
+
+                if ((mQuickScrubActive || mDragScrubActive) && (mDragPositive && offset >= 0
                         || !mDragPositive && offset <= 0)) {
-                    float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
                     mTranslation = !mDragPositive
-                        ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
-                        : Utilities.clamp(offset - mDownOffset, 0, trackSize);
-                    try {
-                        mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
-                        if (DEBUG_OVERVIEW_PROXY) {
-                            Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+                            ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
+                            : Utilities.clamp(offset - mDownOffset, 0, trackSize);
+                    if (mQuickScrubActive) {
+                        float scrubFraction =
+                                Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
+                        try {
+                            mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
+                            if (DEBUG_OVERVIEW_PROXY) {
+                                Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+                            }
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Failed to send progress of quick scrub.", e);
                         }
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Failed to send progress of quick scrub.", e);
                     }
                     if (mIsVertical) {
                         mHomeButtonView.setTranslationY(mTranslation);
@@ -283,7 +297,9 @@
         }
 
         // Proxy motion events to launcher if not handled by quick scrub
-        if (!mQuickScrubActive && mAllowGestureDetection) {
+        // Proxy motion events up/cancel that would be sent after long press on any nav button
+        if (!mQuickScrubActive && (mAllowGestureDetection || action == MotionEvent.ACTION_CANCEL
+                || action == MotionEvent.ACTION_UP)) {
             proxyMotionEvents(event);
         }
         return mQuickScrubActive || mQuickStepStarted;
@@ -370,10 +386,14 @@
         mOverviewEventSender.notifyQuickStepStarted();
         mNavigationBarView.getHomeButton().abortCurrentGesture();
         mHandler.removeCallbacksAndMessages(null);
+
+        if (mDragScrubActive) {
+            animateEnd();
+        }
     }
 
     private void startQuickScrub() {
-        if (!mQuickScrubActive) {
+        if (!mQuickScrubActive && mDragScrubActive) {
             mQuickScrubActive = true;
             mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light);
             mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark);
@@ -391,15 +411,17 @@
     }
 
     private void endQuickScrub(boolean animate) {
-        if (mQuickScrubActive) {
+        if (mQuickScrubActive || mDragScrubActive) {
             animateEnd();
-            try {
-                mOverviewEventSender.getProxy().onQuickScrubEnd();
-                if (DEBUG_OVERVIEW_PROXY) {
-                    Log.d(TAG_OPS, "Quick Scrub End");
+            if (mQuickScrubActive) {
+                try {
+                    mOverviewEventSender.getProxy().onQuickScrubEnd();
+                    if (DEBUG_OVERVIEW_PROXY) {
+                        Log.d(TAG_OPS, "Quick Scrub End");
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to send end of quick scrub.", e);
                 }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to send end of quick scrub.", e);
             }
         }
         if (mHomeButtonView != null && !animate) {
diff --git a/com/android/systemui/statusbar/phone/ScrimController.java b/com/android/systemui/statusbar/phone/ScrimController.java
index 2c025b5..cc143bb 100644
--- a/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/com/android/systemui/statusbar/phone/ScrimController.java
@@ -68,8 +68,14 @@
     private static final String TAG = "ScrimController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
+    /**
+     * General scrim animation duration.
+     */
     public static final long ANIMATION_DURATION = 220;
-
+    /**
+     * Longer duration, currently only used when going to AOD.
+     */
+    public static final long ANIMATION_DURATION_LONG = 1000;
     /**
      * When both scrims have 0 alpha.
      */
@@ -85,7 +91,7 @@
     /**
      * Default alpha value for most scrims.
      */
-    public static final float GRADIENT_SCRIM_ALPHA = 0.45f;
+    public static final float GRADIENT_SCRIM_ALPHA = 0.70f;
     /**
      * A scrim varies its opacity based on a busyness factor, for example
      * how many notifications are currently visible.
@@ -105,7 +111,6 @@
     private final Context mContext;
     protected final ScrimView mScrimBehind;
     protected final ScrimView mScrimInFront;
-    private final LightBarController mLightBarController;
     private final UnlockMethodCache mUnlockMethodCache;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final DozeParameters mDozeParameters;
@@ -139,6 +144,8 @@
     private int mCurrentBehindTint;
     private boolean mWallpaperVisibilityTimedOut;
     private int mScrimsVisibility;
+    private final Consumer<GradientColors> mScrimInFrontColorListener;
+    private final Consumer<Float> mScrimBehindAlphaListener;
     private final Consumer<Integer> mScrimVisibleListener;
     private boolean mBlankScreen;
     private boolean mScreenBlankingCallbackCalled;
@@ -155,17 +162,20 @@
     private boolean mWakeLockHeld;
     private boolean mKeyguardOccluded;
 
-    public ScrimController(LightBarController lightBarController, ScrimView scrimBehind,
-            ScrimView scrimInFront, Consumer<Integer> scrimVisibleListener,
-            DozeParameters dozeParameters, AlarmManager alarmManager) {
+    public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront,
+            Consumer<Float> scrimBehindAlphaListener,
+            Consumer<GradientColors> scrimInFrontColorListener,
+            Consumer<Integer> scrimVisibleListener, DozeParameters dozeParameters,
+            AlarmManager alarmManager) {
         mScrimBehind = scrimBehind;
         mScrimInFront = scrimInFront;
+        mScrimBehindAlphaListener = scrimBehindAlphaListener;
+        mScrimInFrontColorListener = scrimInFrontColorListener;
         mScrimVisibleListener = scrimVisibleListener;
         mContext = scrimBehind.getContext();
         mUnlockMethodCache = UnlockMethodCache.getInstance(mContext);
         mDarkenWhileDragging = !mUnlockMethodCache.canSkipBouncer();
         mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
-        mLightBarController = lightBarController;
         mScrimBehindAlphaResValue = mContext.getResources().getFloat(R.dimen.scrim_behind_alpha);
         mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout,
                 "hide_aod_wallpaper", new Handler());
@@ -190,6 +200,9 @@
         }
         mState = ScrimState.UNINITIALIZED;
 
+        mScrimBehind.setDefaultFocusHighlightEnabled(false);
+        mScrimInFront.setDefaultFocusHighlightEnabled(false);
+
         updateScrims();
     }
 
@@ -361,6 +374,8 @@
 
             setOrAdaptCurrentAnimation(mScrimBehind);
             setOrAdaptCurrentAnimation(mScrimInFront);
+
+            mScrimBehindAlphaListener.accept(mScrimBehind.getViewAlpha());
         }
     }
 
@@ -389,7 +404,7 @@
             // Darken scrim as you pull down the shade when unlocked
             float behindFraction = getInterpolatedFraction();
             behindFraction = (float) Math.pow(behindFraction, 0.8f);
-            mCurrentBehindAlpha = behindFraction * mScrimBehindAlphaKeyguard;
+            mCurrentBehindAlpha = behindFraction * GRADIENT_SCRIM_ALPHA_BUSY;
             mCurrentInFrontAlpha = 0;
         } else if (mState == ScrimState.KEYGUARD) {
             // Either darken of make the scrim transparent when you
@@ -469,7 +484,7 @@
             float minOpacity = ColorUtils.calculateMinimumBackgroundAlpha(textColor, mainColor,
                     4.5f /* minimumContrast */) / 255f;
             mScrimBehindAlpha = Math.max(mScrimBehindAlphaResValue, minOpacity);
-            mLightBarController.setScrimColor(mScrimInFront.getColors());
+            mScrimInFrontColorListener.accept(mScrimInFront.getColors());
         }
 
         // We want to override the back scrim opacity for the AOD state
@@ -528,8 +543,8 @@
         if (alpha == 0f) {
             scrim.setClickable(false);
         } else {
-            // Eat touch events (unless dozing).
-            scrim.setClickable(!mState.isLowPowerState());
+            // Eat touch events (unless dozing or pulsing).
+            scrim.setClickable(mState != ScrimState.AOD && mState != ScrimState.PULSING);
         }
         updateScrim(scrim, alpha);
     }
@@ -696,9 +711,8 @@
             }
         }
 
-        // TODO factor mLightBarController out of this class
         if (scrim == mScrimBehind) {
-            mLightBarController.setScrimAlpha(alpha);
+            mScrimBehindAlphaListener.accept(alpha);
         }
 
         final boolean wantsAlphaUpdate = alpha != currentAlpha;
@@ -807,7 +821,7 @@
     @VisibleForTesting
     protected WakeLock createWakeLock() {
          return new DelayedWakeLock(getHandler(),
-                WakeLock.createPartial(mContext, "Doze"));
+                WakeLock.createPartial(mContext, "Scrims"));
     }
 
     @Override
diff --git a/com/android/systemui/statusbar/phone/ScrimState.java b/com/android/systemui/statusbar/phone/ScrimState.java
index f4b6c38..bbdaa99 100644
--- a/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/com/android/systemui/statusbar/phone/ScrimState.java
@@ -111,9 +111,10 @@
             mCurrentInFrontAlpha = alwaysOnEnabled ? mAodFrontScrimAlpha : 1f;
             mCurrentInFrontTint = Color.BLACK;
             mCurrentBehindTint = Color.BLACK;
-            // DisplayPowerManager will blank the screen for us, we just need
-            // to set our state.
-            mAnimateChange = !mDisplayRequiresBlanking;
+            mAnimationDuration = ScrimController.ANIMATION_DURATION_LONG;
+            // DisplayPowerManager may blank the screen for us,
+            // in this case we just need to set our state.
+            mAnimateChange = mDozeParameters.shouldControlScreenOff();
         }
 
         @Override
diff --git a/com/android/systemui/statusbar/phone/StatusBar.java b/com/android/systemui/statusbar/phone/StatusBar.java
index 750d2a5..a3da807 100644
--- a/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/com/android/systemui/statusbar/phone/StatusBar.java
@@ -213,6 +213,7 @@
 import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
@@ -915,8 +916,14 @@
 
         ScrimView scrimBehind = mStatusBarWindow.findViewById(R.id.scrim_behind);
         ScrimView scrimInFront = mStatusBarWindow.findViewById(R.id.scrim_in_front);
-        mScrimController = SystemUIFactory.getInstance().createScrimController(mLightBarController,
+        mScrimController = SystemUIFactory.getInstance().createScrimController(
                 scrimBehind, scrimInFront, mLockscreenWallpaper,
+                scrimBehindAlpha -> {
+                    mLightBarController.setScrimAlpha(scrimBehindAlpha);
+                },
+                scrimInFrontColor -> {
+                    mLightBarController.setScrimColor(scrimInFrontColor);
+                },
                 scrimsVisible -> {
                     if (mStatusBarWindowManager != null) {
                         mStatusBarWindowManager.setScrimsVisibility(scrimsVisible);
@@ -1186,12 +1193,10 @@
 
     public void manageNotifications() {
         Intent intent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        startActivity(intent, true, true);
+        startActivity(intent, true, true, Intent.FLAG_ACTIVITY_SINGLE_TOP);
     }
 
     public void clearAllNotifications() {
-
         // animate-swipe all dismissable notifications, then animate the shade closed
         int numChildren = mStackScroller.getChildCount();
 
@@ -1300,6 +1305,8 @@
 
         mKeyguardViewMediatorCallback = keyguardViewMediator.getViewMediatorCallback();
         mLightBarController.setFingerprintUnlockController(mFingerprintUnlockController);
+        Dependency.get(KeyguardDismissUtil.class).setDismissHandler(
+                this::dismissKeyguardThenExecute);
         Trace.endSection();
     }
 
@@ -1425,11 +1432,15 @@
     }
 
     public void addQsTile(ComponentName tile) {
-        mQSPanel.getHost().addTile(tile);
+        if (mQSPanel != null && mQSPanel.getHost() != null) {
+            mQSPanel.getHost().addTile(tile);
+        }
     }
 
     public void remQsTile(ComponentName tile) {
-        mQSPanel.getHost().removeTile(tile);
+        if (mQSPanel != null && mQSPanel.getHost() != null) {
+            mQSPanel.getHost().removeTile(tile);
+        }
     }
 
     public void clickTile(ComponentName tile) {
@@ -1439,7 +1450,8 @@
     @VisibleForTesting
     protected void updateFooter() {
         boolean showFooterView = mState != StatusBarState.KEYGUARD
-                && mEntryManager.getNotificationData().getActiveNotifications().size() != 0;
+                && mEntryManager.getNotificationData().getActiveNotifications().size() != 0
+                && !mRemoteInputManager.getController().isRemoteInputActive();
         boolean showDismissView = mClearAllEnabled && mState != StatusBarState.KEYGUARD
                 && hasActiveClearableNotifications();
 
@@ -1824,6 +1836,11 @@
         return new StatusBar.H();
     }
 
+    private void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade,
+            int flags) {
+        startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, flags);
+    }
+
     @Override
     public void startActivity(Intent intent, boolean dismissShade) {
         startActivityDismissingKeyguard(intent, false, dismissShade);
@@ -1837,7 +1854,7 @@
     @Override
     public void startActivity(Intent intent, boolean dismissShade, Callback callback) {
         startActivityDismissingKeyguard(intent, false, dismissShade,
-                false /* disallowEnterPictureInPictureWhileLaunching */, callback);
+                false /* disallowEnterPictureInPictureWhileLaunching */, callback, 0);
     }
 
     public void setQsExpanded(boolean expanded) {
@@ -2043,11 +2060,19 @@
     }
 
     /**
+     * Decides if the status bar (clock + notifications + signal cluster) should be visible
+     * or not when showing the bouncer.
+     *
+     * We want to hide it when:
+     * • User swipes up on the keyguard
+     * • Locked activity that doesn't show a status bar requests the bouncer
+     *
      * @param animate should the change of the icons be animated.
      */
     private void updateHideIconsForBouncer(boolean animate) {
-        boolean shouldHideIconsForBouncer = !mPanelExpanded && mTopHidesStatusBar && mIsOccluded
-                && (mBouncerShowing || mStatusBarWindowHidden);
+        boolean hideBecauseApp = mTopHidesStatusBar && mIsOccluded;
+        boolean hideBecauseKeyguard = !mPanelExpanded && !mIsOccluded && mBouncerShowing;
+        boolean shouldHideIconsForBouncer = hideBecauseApp || hideBecauseKeyguard;
         if (mHideIconsForBouncer != shouldHideIconsForBouncer) {
             mHideIconsForBouncer = shouldHideIconsForBouncer;
             if (!shouldHideIconsForBouncer && mBouncerWasShowingWhenHidden) {
@@ -2286,7 +2311,7 @@
             return ;
         }
 
-        mNotificationPanel.expand(true /* animate */);
+        mNotificationPanel.expandWithoutQs();
 
         if (false) postStartTracing();
     }
@@ -2811,6 +2836,7 @@
                             boolean remoteInputActive) {
                         mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
                         entry.row.notifyHeightChanged(true /* needsAnimation */);
+                        updateFooter();
                     }
                     public void lockScrollTo(NotificationData.Entry entry) {
                         mStackScroller.lockScrollTo(entry.row);
@@ -2851,14 +2877,20 @@
     }
 
     public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
-            boolean dismissShade) {
+            boolean dismissShade, int flags) {
         startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade,
-                false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+                false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */,
+                flags);
+    }
+
+    public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+            boolean dismissShade) {
+        startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, 0);
     }
 
     public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
             final boolean dismissShade, final boolean disallowEnterPictureInPictureWhileLaunching,
-            final Callback callback) {
+            final Callback callback, int flags) {
         if (onlyProvisioned && !isDeviceProvisioned()) return;
 
         final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
@@ -2867,6 +2899,7 @@
             mAssistManager.hideAssist();
             intent.setFlags(
                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+            intent.addFlags(flags);
             int result = ActivityManager.START_CANCELED;
             ActivityOptions options = new ActivityOptions(getActivityOptions(
                     null /* remoteAnimation */));
@@ -3884,7 +3917,11 @@
     }
 
     public boolean onBackPressed() {
-        if (mStatusBarKeyguardViewManager.onBackPressed()) {
+        boolean isScrimmedBouncer = mScrimController.getState() == ScrimState.BOUNCER_SCRIMMED;
+        if (mStatusBarKeyguardViewManager.onBackPressed(isScrimmedBouncer /* hideImmediately */)) {
+            if (!isScrimmedBouncer) {
+                mNotificationPanel.expandWithoutQs();
+            }
             return true;
         }
         if (mNotificationPanel.isQsExpanded()) {
@@ -3915,7 +3952,8 @@
     }
 
     private void showBouncerIfKeyguard() {
-        if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+        if ((mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)
+                && !mKeyguardViewMediator.isHiding()) {
             showBouncer(true /* animated */);
         }
     }
@@ -4547,7 +4585,7 @@
         if (!mStatusBarKeyguardViewManager.isShowing()) {
             startActivityDismissingKeyguard(KeyguardBottomAreaView.INSECURE_CAMERA_INTENT,
                     false /* onlyProvisioned */, true /* dismissShade */,
-                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0);
         } else {
             if (!mDeviceInteractive) {
                 // Avoid flickering of the scrim when we instant launch the camera and the bouncer
@@ -4979,6 +5017,14 @@
 
     @Override
     public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) {
+        RemoteInputController controller = mRemoteInputManager.getController();
+        if (controller.isRemoteInputActive(row.getEntry())
+                && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
+            // We have an active remote input typed and the user clicked on the notification.
+            // this was probably unintentional, so we're closing the edit text instead.
+            controller.closeRemoteInputs();
+            return;
+        }
         Notification notification = sbn.getNotification();
         final PendingIntent intent = notification.contentIntent != null
                 ? notification.contentIntent
@@ -5042,12 +5088,7 @@
                     Intent fillInIntent = null;
                     Entry entry = row.getEntry();
                     CharSequence remoteInputText = null;
-                    RemoteInputController controller = mRemoteInputManager.getController();
-                    if (controller.isRemoteInputActive(entry)) {
-                        remoteInputText = row.getActiveRemoteInputText();
-                    }
-                    if (TextUtils.isEmpty(remoteInputText)
-                            && !TextUtils.isEmpty(entry.remoteInputText)) {
+                    if (!TextUtils.isEmpty(entry.remoteInputText)) {
                         remoteInputText = entry.remoteInputText;
                     }
                     if (!TextUtils.isEmpty(remoteInputText)
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 510af03..b4e7575 100644
--- a/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -306,17 +306,6 @@
         mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
     }
 
-    /**
-     * For mobile essentially (an array of holders in one slot)
-     */
-    private void handleSet(int slotIndex, List<StatusBarIconHolder> holders) {
-        for (StatusBarIconHolder holder : holders) {
-            int viewIndex = getViewIndex(slotIndex, holder.getTag());
-            mIconLogger.onIconVisibility(getSlotName(slotIndex), holder.isVisible());
-            mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
-        }
-    }
-
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println(TAG + " state:");
diff --git a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 6b6ea10..670c68f 100644
--- a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -89,7 +89,6 @@
     protected boolean mFirstUpdate = true;
     protected boolean mLastShowing;
     protected boolean mLastOccluded;
-    private boolean mLastTracking;
     private boolean mLastBouncerShowing;
     private boolean mLastBouncerDismissible;
     protected boolean mLastRemoteInputActive;
@@ -152,28 +151,19 @@
         // • The user quickly taps on the display and we show "swipe up to unlock."
         // • Keyguard will be dismissed by an action. a.k.a: FLAG_DISMISS_KEYGUARD_ACTIVITY
         // • Full-screen user switcher is displayed.
-        final boolean noLongerTracking = mLastTracking != tracking && !tracking;
         if (mOccluded || mNotificationPanelView.isUnlockHintRunning()
                 || mBouncer.willDismissWithAction()
                 || mStatusBar.isFullScreenUserSwitcherState()) {
             mBouncer.setExpansion(0);
         } else if (mShowing && mStatusBar.isKeyguardCurrentlySecure() && !mDozing) {
             mBouncer.setExpansion(expansion);
-            if (expansion == 1) {
-                mBouncer.onFullyHidden();
-            } else if (!mBouncer.isShowing() && !mBouncer.isAnimatingAway()) {
+            if (expansion != 1 && tracking && !mBouncer.isShowing()
+                    && !mBouncer.isAnimatingAway()) {
                 mBouncer.show(false /* resetSecuritySelection */, false /* animated */);
-            } else if (noLongerTracking) {
-                // Notify that falsing manager should stop its session when user stops touching,
-                // even before the animation ends, to guarantee that we're not recording sensitive
-                // data.
-                mBouncer.onFullyShown();
-            }
-            if (expansion == 0 || expansion == 1) {
+            } else if (expansion == 0 || expansion == 1) {
                 updateStates();
             }
         }
-        mLastTracking = tracking;
     }
 
     /**
@@ -522,12 +512,15 @@
     /**
      * Notifies this manager that the back button has been pressed.
      *
+     * @param hideImmediately Hide bouncer when {@code true}, keep it around otherwise.
+     *                        Non-scrimmed bouncers have a special animation tied to the expansion
+     *                        of the notification panel.
      * @return whether the back press has been handled
      */
-    public boolean onBackPressed() {
+    public boolean onBackPressed(boolean hideImmediately) {
         if (mBouncer.isShowing()) {
             mStatusBar.endAffordanceLaunch();
-            reset(true /* hideBouncerWhenShowing */);
+            reset(hideImmediately);
             return true;
         }
         return false;
@@ -595,6 +588,11 @@
         if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
             mStatusBarWindowManager.setBouncerShowing(bouncerShowing);
             mStatusBar.setBouncerShowing(bouncerShowing);
+            if (bouncerShowing) {
+                mBouncer.onFullyShown();
+            } else {
+                mBouncer.onFullyHidden();
+            }
         }
 
         KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
diff --git a/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index c5a3a0d..94ac4f6 100644
--- a/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -414,7 +414,7 @@
 
         @Override public String toString() {
             return "MobileIconState(subId=" + subId + ", strengthId=" + strengthId + ", roaming="
-                    + roaming + ", visible=" + visible + ")";
+                    + roaming + ", typeId=" + typeId + ", visible=" + visible + ")";
         }
     }
 }
diff --git a/com/android/systemui/statusbar/phone/SystemUIDialog.java b/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 378dad7..6a8d3a5 100644
--- a/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -69,6 +69,10 @@
         setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick);
     }
 
+    public void setNeutralButton(int resId, OnClickListener onClick) {
+        setButton(BUTTON_NEUTRAL, mContext.getString(resId), onClick);
+    }
+
     public static void setShowForAllUsers(Dialog dialog, boolean show) {
         if (show) {
             dialog.getWindow().getAttributes().privateFlags |=
diff --git a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index 94db95a..cd17cfc 100644
--- a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -280,7 +280,7 @@
     public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
 
     @Override
-    public void onProfileAudioStateChanged(int bluetoothProfile, int state) {}
+    public void onAudioModeChanged() {}
 
     private ActuallyCachedState getCachedState(CachedBluetoothDevice device) {
         ActuallyCachedState state = mCachedState.get(device);
diff --git a/com/android/systemui/statusbar/policy/Clock.java b/com/android/systemui/statusbar/policy/Clock.java
index 4c92d01..9aa8044 100644
--- a/com/android/systemui/statusbar/policy/Clock.java
+++ b/com/android/systemui/statusbar/policy/Clock.java
@@ -16,9 +16,6 @@
 
 package com.android.systemui.statusbar.policy;
 
-import libcore.icu.LocaleData;
-
-import android.app.ActivityManager;
 import android.app.StatusBarManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -40,11 +37,13 @@
 import android.view.View;
 import android.widget.TextView;
 
+import com.android.settingslib.Utils;
 import com.android.systemui.DemoMode;
 import com.android.systemui.Dependency;
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
 import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.settings.CurrentUserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -52,6 +51,8 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 
+import libcore.icu.LocaleData;
+
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Locale;
@@ -65,6 +66,9 @@
 
     public static final String CLOCK_SECONDS = "clock_seconds";
 
+    private final CurrentUserTracker mCurrentUserTracker;
+    private int mCurrentUserId;
+
     private boolean mClockVisibleByPolicy = true;
     private boolean mClockVisibleByUser = true;
 
@@ -84,6 +88,17 @@
     private boolean mShowSeconds;
     private Handler mSecondsHandler;
 
+    /**
+     * Whether we should use colors that adapt based on wallpaper/the scrim behind quick settings
+     * for text.
+     */
+    private boolean mUseWallpaperTextColor;
+
+    /**
+     * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized.
+     */
+    private int mNonAdaptedColor;
+
     public Clock(Context context) {
         this(context, null);
     }
@@ -101,9 +116,16 @@
         try {
             mAmPmStyle = a.getInt(R.styleable.Clock_amPmStyle, AM_PM_STYLE_GONE);
             mShowDark = a.getBoolean(R.styleable.Clock_showDark, true);
+            mNonAdaptedColor = getCurrentTextColor();
         } finally {
             a.recycle();
         }
+        mCurrentUserTracker = new CurrentUserTracker(context) {
+            @Override
+            public void onUserSwitched(int newUserId) {
+                mCurrentUserId = newUserId;
+            }
+        };
     }
 
     @Override
@@ -128,6 +150,8 @@
             if (mShowDark) {
                 Dependency.get(DarkIconDispatcher.class).addDarkReceiver(this);
             }
+            mCurrentUserTracker.startTracking();
+            mCurrentUserId = mCurrentUserTracker.getCurrentUserId();
         }
 
         // NOTE: It's safe to do these after registering the receiver since the receiver always runs
@@ -153,6 +177,7 @@
             if (mShowDark) {
                 Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(this);
             }
+            mCurrentUserTracker.stopTracking();
         }
     }
 
@@ -227,7 +252,10 @@
 
     @Override
     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
-        setTextColor(DarkIconDispatcher.getTint(area, this, tint));
+        mNonAdaptedColor = DarkIconDispatcher.getTint(area, this, tint);
+        if (!mUseWallpaperTextColor) {
+            setTextColor(mNonAdaptedColor);
+        }
     }
 
     @Override
@@ -242,6 +270,25 @@
                 0);
     }
 
+    /**
+     * Sets whether the clock uses the wallpaperTextColor. If we're not using it, we'll revert back
+     * to dark-mode-based/tinted colors.
+     *
+     * @param shouldUseWallpaperTextColor whether we should use wallpaperTextColor for text color
+     */
+    public void useWallpaperTextColor(boolean shouldUseWallpaperTextColor) {
+        if (shouldUseWallpaperTextColor == mUseWallpaperTextColor) {
+            return;
+        }
+        mUseWallpaperTextColor = shouldUseWallpaperTextColor;
+
+        if (mUseWallpaperTextColor) {
+            setTextColor(Utils.getColorAttr(mContext, R.attr.wallpaperTextColor));
+        } else {
+            setTextColor(mNonAdaptedColor);
+        }
+    }
+
     private void updateShowSeconds() {
         if (mShowSeconds) {
             // Wait until we have a display to start trying to show seconds.
@@ -267,7 +314,7 @@
 
     private final CharSequence getSmallTime() {
         Context context = getContext();
-        boolean is24 = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser());
+        boolean is24 = DateFormat.is24HourFormat(context, mCurrentUserId);
         LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
 
         final char MAGIC1 = '\uEF00';
@@ -357,8 +404,7 @@
             } else if (hhmm != null && hhmm.length() == 4) {
                 int hh = Integer.parseInt(hhmm.substring(0, 2));
                 int mm = Integer.parseInt(hhmm.substring(2));
-                boolean is24 = DateFormat.is24HourFormat(
-                        getContext(), ActivityManager.getCurrentUser());
+                boolean is24 = DateFormat.is24HourFormat(getContext(), mCurrentUserId);
                 if (is24) {
                     mCalendar.set(Calendar.HOUR_OF_DAY, hh);
                 } else {
diff --git a/com/android/systemui/statusbar/policy/DateView.java b/com/android/systemui/statusbar/policy/DateView.java
index 74a30fa..ef630c7 100644
--- a/com/android/systemui/statusbar/policy/DateView.java
+++ b/com/android/systemui/statusbar/policy/DateView.java
@@ -27,6 +27,7 @@
 import android.util.AttributeSet;
 import android.widget.TextView;
 
+import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 
@@ -42,6 +43,17 @@
     private String mLastText;
     private String mDatePattern;
 
+    /**
+     * Whether we should use colors that adapt based on wallpaper/the scrim behind quick settings
+     * for text.
+     */
+    private boolean mUseWallpaperTextColor;
+
+    /**
+     * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized.
+     */
+    private int mNonAdaptedTextColor;
+
     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -62,6 +74,7 @@
 
     public DateView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mNonAdaptedTextColor = getCurrentTextColor();
         TypedArray a = context.getTheme().obtainStyledAttributes(
                 attrs,
                 R.styleable.DateView,
@@ -117,6 +130,25 @@
         }
     }
 
+    /**
+     * Sets whether the date view uses the wallpaperTextColor. If we're not using it, we'll revert
+     * back to dark-mode-based/tinted colors.
+     *
+     * @param shouldUseWallpaperTextColor whether we should use wallpaperTextColor for text color
+     */
+    public void useWallpaperTextColor(boolean shouldUseWallpaperTextColor) {
+        if (shouldUseWallpaperTextColor == mUseWallpaperTextColor) {
+            return;
+        }
+        mUseWallpaperTextColor = shouldUseWallpaperTextColor;
+
+        if (mUseWallpaperTextColor) {
+            setTextColor(Utils.getColorAttr(mContext, R.attr.wallpaperTextColor));
+        } else {
+            setTextColor(mNonAdaptedTextColor);
+        }
+    }
+
     public void setDatePattern(String pattern) {
         if (TextUtils.equals(pattern, mDatePattern)) {
             return;
diff --git a/com/android/systemui/statusbar/policy/DeadZone.java b/com/android/systemui/statusbar/policy/DeadZone.java
index 06040e2..4a11754 100644
--- a/com/android/systemui/statusbar/policy/DeadZone.java
+++ b/com/android/systemui/statusbar/policy/DeadZone.java
@@ -17,18 +17,16 @@
 package com.android.systemui.statusbar.policy;
 
 import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
+import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.os.SystemClock;
-import android.util.AttributeSet;
 import android.util.Slog;
 import android.view.MotionEvent;
 import android.view.Surface;
-import android.view.View;
 
 import com.android.systemui.R;
 import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.phone.NavigationBarView;
 import com.android.systemui.statusbar.phone.StatusBar;
 
 /**
@@ -38,7 +36,7 @@
  * outside the navigation bar (since this is when accidental taps are more likely), then contracts
  * back over time (since a later tap might be intended for the top of the bar).
  */
-public class DeadZone extends View {
+public class DeadZone {
     public static final String TAG = "DeadZone";
 
     public static final boolean DEBUG = false;
@@ -47,6 +45,7 @@
 
     private static final boolean CHATTY = true; // print to logcat when we eat a click
     private final StatusBar mStatusBar;
+    private final NavigationBarView mNavigationBarView;
 
     private boolean mShouldFlash;
     private float mFlashFrac = 0f;
@@ -67,31 +66,11 @@
         }
     };
 
-    public DeadZone(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public DeadZone(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs);
-
-        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DeadZone,
-                defStyle, 0);
-
-        mHold = a.getInteger(R.styleable.DeadZone_holdTime, 0);
-        mDecay = a.getInteger(R.styleable.DeadZone_decayTime, 0);
-
-        mSizeMin = a.getDimensionPixelSize(R.styleable.DeadZone_minSize, 0);
-        mSizeMax = a.getDimensionPixelSize(R.styleable.DeadZone_maxSize, 0);
-
-        int index = a.getInt(R.styleable.DeadZone_orientation, -1);
-        mVertical = (index == VERTICAL);
-
-        if (DEBUG)
-            Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
-                    + (mVertical ? " vertical" : " horizontal"));
-
-        setFlashOnTouchCapture(context.getResources().getBoolean(R.bool.config_dead_zone_flash));
-        mStatusBar = SysUiServiceProvider.getComponent(context, StatusBar.class);
+    public DeadZone(NavigationBarView view) {
+        mNavigationBarView = view;
+        mStatusBar = SysUiServiceProvider.getComponent(mNavigationBarView.getContext(),
+                StatusBar.class);
+        onConfigurationChanged(HORIZONTAL);
     }
 
     static float lerp(float a, float b, float f) {
@@ -112,11 +91,29 @@
     public void setFlashOnTouchCapture(boolean dbg) {
         mShouldFlash = dbg;
         mFlashFrac = 0f;
-        postInvalidate();
+        mNavigationBarView.postInvalidate();
+    }
+
+    public void onConfigurationChanged(int rotation) {
+        mDisplayRotation = rotation;
+
+        final Resources res = mNavigationBarView.getResources();
+        mHold = res.getInteger(R.integer.navigation_bar_deadzone_hold);
+        mDecay = res.getInteger(R.integer.navigation_bar_deadzone_decay);
+
+        mSizeMin = res.getDimensionPixelSize(R.dimen.navigation_bar_deadzone_size);
+        mSizeMax = res.getDimensionPixelSize(R.dimen.navigation_bar_deadzone_size_max);
+        int index = res.getInteger(R.integer.navigation_bar_deadzone_orientation);
+        mVertical = (index == VERTICAL);
+
+        if (DEBUG) {
+            Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
+                    + (mVertical ? " vertical" : " horizontal"));
+        }
+        setFlashOnTouchCapture(res.getBoolean(R.bool.config_dead_zone_flash));
     }
 
     // I made you a touch event...
-    @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (DEBUG) {
             Slog.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
@@ -143,7 +140,7 @@
             final boolean consumeEvent;
             if (mVertical) {
                 if (mDisplayRotation == Surface.ROTATION_270) {
-                    consumeEvent = event.getX() > getWidth() - size;
+                    consumeEvent = event.getX() > mNavigationBarView.getWidth() - size;
                 } else {
                     consumeEvent = event.getX() < size;
                 }
@@ -155,8 +152,8 @@
                     Slog.v(TAG, "consuming errant click: (" + event.getX() + "," + event.getY() + ")");
                 }
                 if (mShouldFlash) {
-                    post(mDebugFlash);
-                    postInvalidate();
+                    mNavigationBarView.post(mDebugFlash);
+                    mNavigationBarView.postInvalidate();
                 }
                 return true; // ...but I eated it
             }
@@ -168,19 +165,18 @@
         mLastPokeTime = event.getEventTime();
         if (DEBUG)
             Slog.v(TAG, "poked! size=" + getSize(mLastPokeTime));
-        if (mShouldFlash) postInvalidate();
+        if (mShouldFlash) mNavigationBarView.postInvalidate();
     }
 
     public void setFlash(float f) {
         mFlashFrac = f;
-        postInvalidate();
+        mNavigationBarView.postInvalidate();
     }
 
     public float getFlash() {
         return mFlashFrac;
     }
 
-    @Override
     public void onDraw(Canvas can) {
         if (!mShouldFlash || mFlashFrac <= 0f) {
             return;
@@ -202,10 +198,6 @@
 
         if (DEBUG && size > mSizeMin)
             // crazy aggressive redrawing here, for debugging only
-            postInvalidateDelayed(100);
-    }
-
-    public void setDisplayRotation(int rotation) {
-        mDisplayRotation = rotation;
+            mNavigationBarView.postInvalidateDelayed(100);
     }
 }
diff --git a/com/android/systemui/statusbar/policy/KeyButtonView.java b/com/android/systemui/statusbar/policy/KeyButtonView.java
index 5d7e938..1b02e15 100644
--- a/com/android/systemui/statusbar/policy/KeyButtonView.java
+++ b/com/android/systemui/statusbar/policy/KeyButtonView.java
@@ -28,7 +28,6 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.os.VibrationEffect;
 import android.util.AttributeSet;
 import android.util.TypedValue;
 import android.view.HapticFeedbackConstants;
@@ -50,10 +49,12 @@
 import com.android.systemui.R;
 import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.statusbar.VibratorHelper;
 
+import static android.view.KeyEvent.KEYCODE_HOME;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_SCRUB_TOUCH_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_TOUCH_SLOP_PX;
 
 public class KeyButtonView extends ImageView implements ButtonInterface {
     private static final String TAG = KeyButtonView.class.getSimpleName();
@@ -62,9 +63,9 @@
     private int mContentDescriptionRes;
     private long mDownTime;
     private int mCode;
-    private int mTouchSlop;
     private int mTouchDownX;
     private int mTouchDownY;
+    private boolean mIsVertical;
     private boolean mSupportsLongpress = true;
     private AudioManager mAudioManager;
     private boolean mGestureAborted;
@@ -72,7 +73,6 @@
     private OnClickListener mOnClickListener;
     private final KeyButtonRipple mRipple;
     private final OverviewProxyService mOverviewProxyService;
-    private final VibratorHelper mVibratorHelper;
     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
 
     private final Runnable mCheckLongPress = new Runnable() {
@@ -115,11 +115,9 @@
         a.recycle();
 
         setClickable(true);
-        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
 
         mRipple = new KeyButtonRipple(context, this);
-        mVibratorHelper = Dependency.get(VibratorHelper.class);
         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
         setBackground(mRipple);
     }
@@ -200,7 +198,7 @@
     }
 
     public boolean onTouchEvent(MotionEvent ev) {
-        final boolean isProxyConnected = mOverviewProxyService.getProxy() != null;
+        final boolean showSwipeUI = mOverviewProxyService.shouldShowSwipeUpUI();
         final int action = ev.getAction();
         int x, y;
         if (action == MotionEvent.ACTION_DOWN) {
@@ -226,7 +224,7 @@
                     // Provide the same haptic feedback that the system offers for virtual keys.
                     performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                 }
-                if (!isProxyConnected) {
+                if (!showSwipeUI) {
                     playSoundEffect(SoundEffectConstants.CLICK);
                 }
                 removeCallbacks(mCheckLongPress);
@@ -235,8 +233,11 @@
             case MotionEvent.ACTION_MOVE:
                 x = (int)ev.getRawX();
                 y = (int)ev.getRawY();
-                boolean exceededTouchSlopX = Math.abs(x - mTouchDownX) > mTouchSlop;
-                boolean exceededTouchSlopY = Math.abs(y - mTouchDownY) > mTouchSlop;
+
+                boolean exceededTouchSlopX = Math.abs(x - mTouchDownX) >
+                        (mIsVertical ? QUICK_SCRUB_TOUCH_SLOP_PX : QUICK_STEP_TOUCH_SLOP_PX);
+                boolean exceededTouchSlopY = Math.abs(y - mTouchDownY) >
+                        (mIsVertical ? QUICK_STEP_TOUCH_SLOP_PX : QUICK_SCRUB_TOUCH_SLOP_PX);
                 if (exceededTouchSlopX || exceededTouchSlopY) {
                     // When quick step is enabled, prevent animating the ripple triggered by
                     // setPressed and decide to run it on touch up
@@ -255,11 +256,10 @@
                 final boolean doIt = isPressed() && !mLongClicked;
                 setPressed(false);
                 final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
-                if (isProxyConnected) {
+                if (showSwipeUI) {
                     if (doIt) {
-                        if (doHapticFeedback) {
-                            mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
-                        }
+                        // Apply haptic feedback on touch up since there is none on touch down
+                        performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                         playSoundEffect(SoundEffectConstants.CLICK);
                     }
                 } else if (doHapticFeedback && !mLongClicked) {
@@ -270,8 +270,12 @@
                 if (mCode != 0) {
                     if (doIt) {
                         // If there was a pending remote recents animation, then we need to
-                        // cancel the animation now before we handle the button itself
-                        ActivityManagerWrapper.getInstance().cancelRecentsAnimation();
+                        // cancel the animation now before we handle the button itself. In the case
+                        // where we are going home and the recents animation has already started,
+                        // just cancel the recents animation, leaving the home stack in place
+                        boolean isHomeKey = mCode == KEYCODE_HOME;
+                        ActivityManagerWrapper.getInstance().cancelRecentsAnimation(!isHomeKey);
+
                         sendEvent(KeyEvent.ACTION_UP, 0);
                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                     } else {
@@ -342,7 +346,7 @@
 
     @Override
     public void setVertical(boolean vertical) {
-        //no op
+        mIsVertical = vertical;
     }
 }
 
diff --git a/com/android/systemui/statusbar/policy/SmartReplyView.java b/com/android/systemui/statusbar/policy/SmartReplyView.java
index 790135f..4c79ee3 100644
--- a/com/android/systemui/statusbar/policy/SmartReplyView.java
+++ b/com/android/systemui/statusbar/policy/SmartReplyView.java
@@ -20,8 +20,12 @@
 import android.widget.Button;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.SmartReplyLogger;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 
 import java.text.BreakIterator;
 import java.util.Comparator;
@@ -42,6 +46,7 @@
     private static final int SQUEEZE_FAILED = -1;
 
     private final SmartReplyConstants mConstants;
+    private final KeyguardDismissUtil mKeyguardDismissUtil;
 
     /** Spacing to be applied between views. */
     private final int mSpacing;
@@ -62,6 +67,7 @@
     public SmartReplyView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mConstants = Dependency.get(SmartReplyConstants.class);
+        mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
 
         int spacing = 0;
         int singleLineButtonPaddingHorizontal = 0;
@@ -105,14 +111,16 @@
                 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
     }
 
-    public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent) {
+    public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent,
+            SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
         removeAllViews();
         if (remoteInput != null && pendingIntent != null) {
             CharSequence[] choices = remoteInput.getChoices();
             if (choices != null) {
-                for (CharSequence choice : choices) {
+                for (int i = 0; i < choices.length; ++i) {
                     Button replyButton = inflateReplyButton(
-                            getContext(), this, choice, remoteInput, pendingIntent);
+                            getContext(), this, i, choices[i], remoteInput, pendingIntent,
+                            smartReplyLogger, entry);
                     addView(replyButton);
                 }
             }
@@ -126,12 +134,14 @@
     }
 
     @VisibleForTesting
-    static Button inflateReplyButton(Context context, ViewGroup root, CharSequence choice,
-            RemoteInput remoteInput, PendingIntent pendingIntent) {
+    Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
+            CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
+            SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
         Button b = (Button) LayoutInflater.from(context).inflate(
                 R.layout.smart_reply_button, root, false);
         b.setText(choice);
-        b.setOnClickListener(view -> {
+
+        OnDismissAction action = () -> {
             Bundle results = new Bundle();
             results.putString(remoteInput.getResultKey(), choice.toString());
             Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -142,6 +152,13 @@
             } catch (PendingIntent.CanceledException e) {
                 Log.w(TAG, "Unable to send smart reply", e);
             }
+            smartReplyLogger.smartReplySent(entry, replyIndex);
+            return false; // do not defer
+        };
+
+        b.setOnClickListener(view -> {
+            mKeyguardDismissUtil.dismissKeyguardThenExecute(
+                    action, null /* cancelAction */, false /* afterKeyguardGone */);
         });
         return b;
     }
diff --git a/com/android/systemui/statusbar/policy/TelephonyIcons.java b/com/android/systemui/statusbar/policy/TelephonyIcons.java
index 7e6fe02..bd76820 100644
--- a/com/android/systemui/statusbar/policy/TelephonyIcons.java
+++ b/com/android/systemui/statusbar/policy/TelephonyIcons.java
@@ -208,7 +208,7 @@
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
-            R.string.cell_data_off,
+            R.string.cell_data_off_content_description,
             0,
             false);
 }
diff --git a/com/android/systemui/statusbar/stack/AmbientState.java b/com/android/systemui/statusbar/stack/AmbientState.java
index 7c1c566..91a4b07 100644
--- a/com/android/systemui/statusbar/stack/AmbientState.java
+++ b/com/android/systemui/statusbar/stack/AmbientState.java
@@ -70,8 +70,8 @@
     private int mIntrinsicPadding;
     private int mExpandAnimationTopChange;
     private ExpandableNotificationRow mExpandingNotification;
-    private boolean mFullyDark;
     private int mDarkTopPadding;
+    private float mDarkAmount;
 
     public AmbientState(Context context) {
         reload(context);
@@ -149,6 +149,16 @@
         mDark = dark;
     }
 
+    /** Dark ratio of the status bar **/
+    public void setDarkAmount(float darkAmount) {
+        mDarkAmount = darkAmount;
+    }
+
+    /** Returns the dark ratio of the status bar */
+    public float getDarkAmount() {
+        return mDarkAmount;
+    }
+
     public void setHideSensitive(boolean hideSensitive) {
         mHideSensitive = hideSensitive;
     }
@@ -413,17 +423,10 @@
     }
 
     /**
-     * {@see isFullyDark}
-     */
-    public void setFullyDark(boolean fullyDark) {
-        mFullyDark = fullyDark;
-    }
-
-    /**
      * @return {@code true } when shade is completely dark: in AOD or ambient display.
      */
     public boolean isFullyDark() {
-        return mFullyDark;
+        return mDarkAmount == 1;
     }
 
     public void setDarkTopPadding(int darkTopPadding) {
diff --git a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 375e860..bc5a848 100644
--- a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -96,6 +96,7 @@
 import com.android.systemui.statusbar.notification.FakeShadowView;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
+import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.ScrimController;
@@ -269,8 +270,6 @@
      */
     private boolean mOnlyScrollingInThisMotion;
     private boolean mDisallowDismissInThisMotion;
-    private boolean mInterceptDelegateEnabled;
-    private boolean mDelegateToScrollView;
     private boolean mDisallowScrollingInThisMotion;
     private long mGoToFullShadeDelay;
     private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
@@ -562,17 +561,17 @@
             return;
         }
 
-        final int color;
-        if (mAmbientState.isDark()) {
-            color = Color.WHITE;
-        } else {
-            float alpha =
-                    BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
-            alpha *= 1f - mDarkAmount;
-            // We need to manually blend in the background color
-            int scrimColor = mScrimController.getBackgroundColor();
-            color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
-        }
+        float alpha =
+                BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
+        alpha *= 1f - mDarkAmount;
+        // We need to manually blend in the background color.
+        int scrimColor = mScrimController.getBackgroundColor();
+        int awakeColor = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
+
+        // Interpolate between semi-transparent notification panel background color
+        // and white AOD separator.
+        float colorInterpolation = Interpolators.DECELERATE_QUINT.getInterpolation(mDarkAmount);
+        int color = ColorUtils.blendARGB(awakeColor, Color.WHITE, colorInterpolation);
 
         if (mCachedBackgroundColor != color) {
             mCachedBackgroundColor = color;
@@ -3023,6 +3022,11 @@
     public void setAnimationsEnabled(boolean animationsEnabled) {
         mAnimationsEnabled = animationsEnabled;
         updateNotificationAnimationStates();
+        if (!animationsEnabled) {
+            mSwipedOutViews.clear();
+            mChildrenToRemoveAnimated.clear();
+            clearTemporaryViewsInGroup(this);
+        }
     }
 
     private void updateNotificationAnimationStates() {
@@ -3090,6 +3094,21 @@
     @Override
     public void changeViewPosition(View child, int newIndex) {
         int currentIndex = indexOfChild(child);
+
+        if (currentIndex == -1) {
+            boolean isTransient = false;
+            if (child instanceof ExpandableNotificationRow
+                    && ((ExpandableNotificationRow)child).getTransientContainer() != null) {
+                isTransient = true;
+            }
+            Log.e(TAG, "Attempting to re-position "
+                    + (isTransient ? "transient" : "")
+                    + " view {"
+                    + child
+                    + "}");
+            return;
+        }
+
         if (child != null && child.getParent() == this && currentIndex != newIndex) {
             mChangePositionInProgress = true;
             ((ExpandableView)child).setChangingPosition(true);
@@ -3569,17 +3588,17 @@
 
     private void clearTemporaryViews() {
         // lets make sure nothing is in the overlay / transient anymore
-        clearTemporaryViews(this);
+        clearTemporaryViewsInGroup(this);
         for (int i = 0; i < getChildCount(); i++) {
             ExpandableView child = (ExpandableView) getChildAt(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-                clearTemporaryViews(row.getChildrenContainer());
+                clearTemporaryViewsInGroup(row.getChildrenContainer());
             }
         }
     }
 
-    private void clearTemporaryViews(ViewGroup viewGroup) {
+    private void clearTemporaryViewsInGroup(ViewGroup viewGroup) {
         while (viewGroup != null && viewGroup.getTransientViewCount() != 0) {
             viewGroup.removeTransientView(viewGroup.getTransientView(0));
         }
@@ -3922,12 +3941,11 @@
         requestChildrenUpdate();
         applyCurrentBackgroundBounds();
         updateWillNotDraw();
-        updateAntiBurnInTranslation();
         notifyHeightChangeListener(mShelf);
     }
 
     private void updateAntiBurnInTranslation() {
-        setTranslationX(mAmbientState.isDark() ? mAntiBurnInOffsetX : 0);
+        setTranslationX(mAntiBurnInOffsetX * mDarkAmount);
     }
 
     /**
@@ -3942,12 +3960,18 @@
 
     private void setDarkAmount(float darkAmount) {
         mDarkAmount = darkAmount;
-        final boolean fullyDark = darkAmount == 1;
-        if (mAmbientState.isFullyDark() != fullyDark) {
-            mAmbientState.setFullyDark(fullyDark);
+        boolean wasFullyDark = mAmbientState.isFullyDark();
+        mAmbientState.setDarkAmount(darkAmount);
+        if (mAmbientState.isFullyDark() != wasFullyDark) {
             updateContentHeight();
+            DozeParameters dozeParameters = DozeParameters.getInstance(mContext);
+            if (mAmbientState.isFullyDark() && dozeParameters.shouldControlScreenOff()) {
+                mShelf.fadeInTranslating();
+            }
         }
         updateBackgroundDimming();
+        updateAntiBurnInTranslation();
+        requestChildrenUpdate();
     }
 
     public float getDarkAmount() {
diff --git a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index a8d2d98..f4d7f8d 100644
--- a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -276,8 +276,6 @@
                 if (i >= firstHiddenIndex) {
                     // we need normal padding now, to be in sync with what the stack calculates
                     lastView = null;
-                    ExpandableViewState viewState = resultState.getViewStateForView(v);
-                    viewState.hidden = true;
                 }
                 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
                 float increasedPadding = v.getIncreasedPaddingAmount();
diff --git a/com/android/systemui/util/wakelock/DelayedWakeLock.java b/com/android/systemui/util/wakelock/DelayedWakeLock.java
index 5ec3dff..a901e88 100644
--- a/com/android/systemui/util/wakelock/DelayedWakeLock.java
+++ b/com/android/systemui/util/wakelock/DelayedWakeLock.java
@@ -23,7 +23,7 @@
  */
 public class DelayedWakeLock implements WakeLock {
 
-    private static final long RELEASE_DELAY_MS = 120;
+    private static final long RELEASE_DELAY_MS = 140;
 
     private final Handler mHandler;
     private final WakeLock mInner;
diff --git a/com/android/systemui/volume/Events.java b/com/android/systemui/volume/Events.java
index 2c85bb6..ca55e1f 100644
--- a/com/android/systemui/volume/Events.java
+++ b/com/android/systemui/volume/Events.java
@@ -52,26 +52,28 @@
     public static final int EVENT_MUTE_CHANGED = 15;  // (stream|int) (muted|bool)
     public static final int EVENT_TOUCH_LEVEL_DONE = 16;  // (stream|int) (level|bool)
     public static final int EVENT_ZEN_CONFIG_CHANGED = 17; // (allow/disallow|string)
+    public static final int EVENT_RINGER_TOGGLE = 18; // (ringer_mode)
 
     private static final String[] EVENT_TAGS = {
-        "show_dialog",
-        "dismiss_dialog",
-        "active_stream_changed",
-        "expand",
-        "key",
-        "collection_started",
-        "collection_stopped",
-        "icon_click",
-        "settings_click",
-        "touch_level_changed",
-        "level_changed",
-        "internal_ringer_mode_changed",
-        "external_ringer_mode_changed",
-        "zen_mode_changed",
-        "suppressor_changed",
-        "mute_changed",
-        "touch_level_done",
-        "zen_mode_config_changed",
+            "show_dialog",
+            "dismiss_dialog",
+            "active_stream_changed",
+            "expand",
+            "key",
+            "collection_started",
+            "collection_stopped",
+            "icon_click",
+            "settings_click",
+            "touch_level_changed",
+            "level_changed",
+            "internal_ringer_mode_changed",
+            "external_ringer_mode_changed",
+            "zen_mode_changed",
+            "suppressor_changed",
+            "mute_changed",
+            "touch_level_done",
+            "zen_mode_config_changed",
+            "ringer_toggle"
     };
 
     public static final int DISMISS_REASON_UNKNOWN = 0;
@@ -112,6 +114,7 @@
     public static Callback sCallback;
 
     public static void writeEvent(Context context, int tag, Object... list) {
+        MetricsLogger logger = new MetricsLogger();
         final long time = System.currentTimeMillis();
         final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[tag]);
         if (list != null && list.length > 0) {
@@ -139,7 +142,7 @@
                     break;
                 case EVENT_ICON_CLICK:
                     MetricsLogger.action(context, MetricsEvent.ACTION_VOLUME_ICON,
-                            (Integer) list[1]);
+                            (Integer) list[0]);
                     sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ')
                             .append(iconStateToString((Integer) list[1]));
                     break;
@@ -155,10 +158,16 @@
                     break;
                 case EVENT_KEY:
                     MetricsLogger.action(context, MetricsEvent.ACTION_VOLUME_KEY,
-                            (Integer) list[1]);
+                            (Integer) list[0]);
                     sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ')
                             .append(list[1]);
                     break;
+                case EVENT_RINGER_TOGGLE:
+                    logger.action(MetricsEvent.ACTION_VOLUME_RINGER_TOGGLE, (Integer) list[0]);
+                    break;
+                case EVENT_SETTINGS_CLICK:
+                    logger.action(MetricsEvent.ACTION_VOLUME_SETTINGS);
+                    break;
                 case EVENT_EXTERNAL_RINGER_MODE_CHANGED:
                     MetricsLogger.action(context, MetricsEvent.ACTION_RINGER_MODE,
                             (Integer) list[0]);
diff --git a/com/android/systemui/volume/VolumeDialogImpl.java b/com/android/systemui/volume/VolumeDialogImpl.java
index 6f71e55..00874e3 100644
--- a/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/com/android/systemui/volume/VolumeDialogImpl.java
@@ -212,8 +212,7 @@
                     .setDuration(300)
                     .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
                     .withEndAction(() -> {
-                        mWindow.getDecorView().requestAccessibilityFocus();
-                        if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, true)) {
+                        if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
                             mRingerIcon.postOnAnimationDelayed(mSinglePress, 1500);
                         }
                     })
@@ -302,15 +301,8 @@
         if (D.BUG) Slog.d(TAG, "Adding row for stream " + stream);
         VolumeRow row = new VolumeRow();
         initRow(row, stream, iconRes, iconMuteRes, important, defaultStream);
-        if (dynamic && mRows.size() > 2) {
-            // Dynamic Streams should be the first in the list, so they're shown to start of
-            // everything except a11y
-            mDialogRowsView.addView(row.view, 1);
-            mRows.add(1, row);
-        } else {
-            mDialogRowsView.addView(row.view);
-            mRows.add(row);
-        }
+        mDialogRowsView.addView(row.view);
+        mRows.add(row);
     }
 
     private void addExistingRows() {
@@ -423,6 +415,7 @@
         mSettingsView.setVisibility(
                 mDeviceProvisionedController.isDeviceProvisioned() ? VISIBLE : GONE);
         mSettingsIcon.setOnClickListener(v -> {
+            Events.writeEvent(mContext, Events.EVENT_SETTINGS_CLICK);
             Intent intent = new Intent(Settings.ACTION_SOUND_SETTINGS);
             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             dismissH(DISMISS_REASON_SETTINGS_CLICKED);
@@ -432,8 +425,6 @@
 
     public void initRingerH() {
         mRingerIcon.setOnClickListener(v -> {
-            Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, AudioManager.STREAM_RING,
-                    mRingerIcon.getTag());
             Prefs.putBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, true);
             final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
             if (ss == null) {
@@ -457,6 +448,7 @@
                     mController.setStreamVolume(AudioManager.STREAM_RING, 1);
                 }
             }
+            Events.writeEvent(mContext, Events.EVENT_RINGER_TOGGLE, newRingerMode);
             updateRingerH();
             provideTouchFeedbackH(newRingerMode);
             mController.setRingerMode(newRingerMode, false);
@@ -604,7 +596,8 @@
             return activeRow.stream == STREAM_RING
                     || activeRow.stream == STREAM_ALARM
                     || activeRow.stream == STREAM_VOICE_CALL
-                    || activeRow.stream == STREAM_ACCESSIBILITY;
+                    || activeRow.stream == STREAM_ACCESSIBILITY
+                    || mDynamic.get(activeRow.stream);
         }
 
         return false;
diff --git a/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
index 7c9aede..3d5476d 100644
--- a/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
+++ b/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2012 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.
@@ -16,24 +16,55 @@
 
 package com.android.uiautomator.testrunner;
 
-import android.app.Instrumentation;
+import android.content.Context;
 import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.SystemClock;
-import android.test.InstrumentationTestCase;
+import android.view.inputmethod.InputMethodInfo;
 
-import com.android.uiautomator.core.InstrumentationUiAutomatorBridge;
+import com.android.internal.view.IInputMethodManager;
 import com.android.uiautomator.core.UiDevice;
 
+import junit.framework.TestCase;
+
+import java.util.List;
+
 /**
- * UI Automator test case that is executed on the device.
+ * UI automation test should extend this class. This class provides access
+ * to the following:
+ * {@link UiDevice} instance
+ * {@link Bundle} for command line parameters.
+ * @since API Level 16
  * @deprecated New tests should be written using UI Automator 2.0 which is available as part of the
  * Android Testing Support Library.
  */
 @Deprecated
-public class UiAutomatorTestCase extends InstrumentationTestCase {
+public class UiAutomatorTestCase extends TestCase {
 
+    private static final String DISABLE_IME = "disable_ime";
+    private static final String DUMMY_IME_PACKAGE = "com.android.testing.dummyime";
+    private UiDevice mUiDevice;
     private Bundle mParams;
     private IAutomationSupport mAutomationSupport;
+    private boolean mShouldDisableIme = false;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mShouldDisableIme = "true".equals(mParams.getString(DISABLE_IME));
+        if (mShouldDisableIme) {
+            setDummyIme();
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mShouldDisableIme) {
+            restoreActiveIme();
+        }
+        super.tearDown();
+    }
 
     /**
      * Get current instance of {@link UiDevice}. Works similar to calling the static
@@ -41,7 +72,7 @@
      * @since API Level 16
      */
     public UiDevice getUiDevice() {
-        return UiDevice.getInstance();
+        return mUiDevice;
     }
 
     /**
@@ -54,43 +85,34 @@
         return mParams;
     }
 
-    void setAutomationSupport(IAutomationSupport automationSupport) {
-        mAutomationSupport = automationSupport;
-    }
-
     /**
      * Provides support for running tests to report interim status
      *
      * @return IAutomationSupport
      * @since API Level 16
-     * @deprecated Use {@link Instrumentation#sendStatus(int, Bundle)} instead
      */
     public IAutomationSupport getAutomationSupport() {
-        if (mAutomationSupport == null) {
-            mAutomationSupport = new InstrumentationAutomationSupport(getInstrumentation());
-        }
         return mAutomationSupport;
     }
 
     /**
-     * Initializes this test case.
-     *
-     * @param params Instrumentation arguments.
+     * package private
+     * @param uiDevice
      */
-    void initialize(Bundle params) {
+    void setUiDevice(UiDevice uiDevice) {
+        mUiDevice = uiDevice;
+    }
+
+    /**
+     * package private
+     * @param params
+     */
+    void setParams(Bundle params) {
         mParams = params;
+    }
 
-        // check if this is a monkey test mode
-        String monkeyVal = mParams.getString("monkey");
-        if (monkeyVal != null) {
-            // only if the monkey key is specified, we alter the state of monkey
-            // else we should leave things as they are.
-            getInstrumentation().getUiAutomation().setRunAsMonkey(Boolean.valueOf(monkeyVal));
-        }
-
-        UiDevice.getInstance().initialize(new InstrumentationUiAutomatorBridge(
-                getInstrumentation().getContext(),
-                getInstrumentation().getUiAutomation()));
+    void setAutomationSupport(IAutomationSupport automationSupport) {
+        mAutomationSupport = automationSupport;
     }
 
     /**
@@ -101,4 +123,28 @@
     public void sleep(long ms) {
         SystemClock.sleep(ms);
     }
+
+    private void setDummyIme() throws RemoteException {
+        IInputMethodManager im = IInputMethodManager.Stub.asInterface(ServiceManager
+                .getService(Context.INPUT_METHOD_SERVICE));
+        List<InputMethodInfo> infos = im.getInputMethodList();
+        String id = null;
+        for (InputMethodInfo info : infos) {
+            if (DUMMY_IME_PACKAGE.equals(info.getComponent().getPackageName())) {
+                id = info.getId();
+            }
+        }
+        if (id == null) {
+            throw new RuntimeException(String.format(
+                    "Required testing fixture missing: IME package (%s)", DUMMY_IME_PACKAGE));
+        }
+        im.setInputMethod(null, id);
+    }
+
+    private void restoreActiveIme() throws RemoteException {
+        // TODO: figure out a way to restore active IME
+        // Currently retrieving active IME requires querying secure settings provider, which is hard
+        // to do without a Context; so the caveat here is that to make the post test device usable,
+        // the active IME needs to be manually switched.
+    }
 }
diff --git a/foo/bar/ComplexDatabase.java b/foo/bar/ComplexDatabase.java
index 7f82dcd..2c47342 100644
--- a/foo/bar/ComplexDatabase.java
+++ b/foo/bar/ComplexDatabase.java
@@ -93,6 +93,7 @@
 
     @Override
     public void clearAllTables() {
+        super.assertNotMainThread();
         final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase();
         try {
             super.beginTransaction();
@@ -101,7 +102,9 @@
         } finally {
             super.endTransaction();
             _db.query("PRAGMA wal_checkpoint(FULL)").close();
-            _db.execSQL("VACUUM");
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM");
+            }
         }
     }
 
diff --git a/java/lang/String.java b/java/lang/String.java
index 7e82347..de54882 100644
--- a/java/lang/String.java
+++ b/java/lang/String.java
@@ -144,7 +144,7 @@
      * Class String is special cased within the Serialization Stream Protocol.
      *
      * A String instance is written into an ObjectOutputStream according to
-     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
+     * <a href="{@docRoot}openjdk-redirect.html?v=8&path=/platform/serialization/spec/output.html">
      * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
      */
     private static final ObjectStreamField[] serialPersistentFields =
diff --git a/java/lang/invoke/CallSite.java b/java/lang/invoke/CallSite.java
index 85b4bb9..1ff1eb8 100644
--- a/java/lang/invoke/CallSite.java
+++ b/java/lang/invoke/CallSite.java
@@ -25,363 +25,15 @@
 
 package java.lang.invoke;
 
-// Android-changed: Not using Empty
-//import sun.invoke.empty.Empty;
-import static java.lang.invoke.MethodHandleStatics.*;
-import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
-
-/**
- * A {@code CallSite} is a holder for a variable {@link MethodHandle},
- * which is called its {@code target}.
- * An {@code invokedynamic} instruction linked to a {@code CallSite} delegates
- * all calls to the site's current target.
- * A {@code CallSite} may be associated with several {@code invokedynamic}
- * instructions, or it may be "free floating", associated with none.
- * In any case, it may be invoked through an associated method handle
- * called its {@linkplain #dynamicInvoker dynamic invoker}.
- * <p>
- * {@code CallSite} is an abstract class which does not allow
- * direct subclassing by users.  It has three immediate,
- * concrete subclasses that may be either instantiated or subclassed.
- * <ul>
- * <li>If a mutable target is not required, an {@code invokedynamic} instruction
- * may be permanently bound by means of a {@linkplain ConstantCallSite constant call site}.
- * <li>If a mutable target is required which has volatile variable semantics,
- * because updates to the target must be immediately and reliably witnessed by other threads,
- * a {@linkplain VolatileCallSite volatile call site} may be used.
- * <li>Otherwise, if a mutable target is required,
- * a {@linkplain MutableCallSite mutable call site} may be used.
- * </ul>
- * <p>
- * A non-constant call site may be <em>relinked</em> by changing its target.
- * The new target must have the same {@linkplain MethodHandle#type() type}
- * as the previous target.
- * Thus, though a call site can be relinked to a series of
- * successive targets, it cannot change its type.
- * <p>
- * Here is a sample use of call sites and bootstrap methods which links every
- * dynamic call site to print its arguments:
-<blockquote><pre>{@code
-static void test() throws Throwable {
-    // THE FOLLOWING LINE IS PSEUDOCODE FOR A JVM INSTRUCTION
-    InvokeDynamic[#bootstrapDynamic].baz("baz arg", 2, 3.14);
-}
-private static void printArgs(Object... args) {
-  System.out.println(java.util.Arrays.deepToString(args));
-}
-private static final MethodHandle printArgs;
-static {
-  MethodHandles.Lookup lookup = MethodHandles.lookup();
-  Class thisClass = lookup.lookupClass();  // (who am I?)
-  printArgs = lookup.findStatic(thisClass,
-      "printArgs", MethodType.methodType(void.class, Object[].class));
-}
-private static CallSite bootstrapDynamic(MethodHandles.Lookup caller, String name, MethodType type) {
-  // ignore caller and name, but match the type:
-  return new ConstantCallSite(printArgs.asType(type));
-}
-}</pre></blockquote>
- * @author John Rose, JSR 292 EG
- */
 abstract
 public class CallSite {
-    // Android-changed: not used.
-    // static { MethodHandleImpl.initStatics(); }
 
-    // The actual payload of this call site:
-    /*package-private*/
-    MethodHandle target;    // Note: This field is known to the JVM.  Do not change.
+    public MethodType type() { return null; }
 
-    /**
-     * Make a blank call site object with the given method type.
-     * An initial target method is supplied which will throw
-     * an {@link IllegalStateException} if called.
-     * <p>
-     * Before this {@code CallSite} object is returned from a bootstrap method,
-     * it is usually provided with a more useful target method,
-     * via a call to {@link CallSite#setTarget(MethodHandle) setTarget}.
-     * @throws NullPointerException if the proposed type is null
-     */
-    /*package-private*/
-    CallSite(MethodType type) {
-        // Android-changed: No cache for these so create uninitializedCallSite target here using
-        // method handle transformations to create a method handle that has the expected method
-        // type but throws an IllegalStateException.
-        // target = makeUninitializedCallSite(type);
-        this.target = MethodHandles.throwException(type.returnType(), IllegalStateException.class);
-        this.target = MethodHandles.insertArguments(
-            this.target, 0, new IllegalStateException("uninitialized call site"));
-        if (type.parameterCount() > 0) {
-            this.target = MethodHandles.dropArguments(this.target, 0, type.ptypes());
-        }
-
-        // Android-changed: Using initializer method for GET_TARGET
-        // rather than complex static initializer.
-        initializeGetTarget();
-    }
-
-    /**
-     * Make a call site object equipped with an initial target method handle.
-     * @param target the method handle which will be the initial target of the call site
-     * @throws NullPointerException if the proposed target is null
-     */
-    /*package-private*/
-    CallSite(MethodHandle target) {
-        target.type();  // null check
-        this.target = target;
-
-        // Android-changed: Using initializer method for GET_TARGET
-        // rather than complex static initializer.
-        initializeGetTarget();
-    }
-
-    /**
-     * Make a call site object equipped with an initial target method handle.
-     * @param targetType the desired type of the call site
-     * @param createTargetHook a hook which will bind the call site to the target method handle
-     * @throws WrongMethodTypeException if the hook cannot be invoked on the required arguments,
-     *         or if the target returned by the hook is not of the given {@code targetType}
-     * @throws NullPointerException if the hook returns a null value
-     * @throws ClassCastException if the hook returns something other than a {@code MethodHandle}
-     * @throws Throwable anything else thrown by the hook function
-     */
-    /*package-private*/
-    CallSite(MethodType targetType, MethodHandle createTargetHook) throws Throwable {
-        this(targetType);
-        ConstantCallSite selfCCS = (ConstantCallSite) this;
-        MethodHandle boundTarget = (MethodHandle) createTargetHook.invokeWithArguments(selfCCS);
-        checkTargetChange(this.target, boundTarget);
-        this.target = boundTarget;
-
-        // Android-changed: Using initializer method for GET_TARGET
-        // rather than complex static initializer.
-        initializeGetTarget();
-    }
-
-    /**
-     * Returns the type of this call site's target.
-     * Although targets may change, any call site's type is permanent, and can never change to an unequal type.
-     * The {@code setTarget} method enforces this invariant by refusing any new target that does
-     * not have the previous target's type.
-     * @return the type of the current target, which is also the type of any future target
-     */
-    public MethodType type() {
-        // warning:  do not call getTarget here, because CCS.getTarget can throw IllegalStateException
-        return target.type();
-    }
-
-    /**
-     * Returns the target method of the call site, according to the
-     * behavior defined by this call site's specific class.
-     * The immediate subclasses of {@code CallSite} document the
-     * class-specific behaviors of this method.
-     *
-     * @return the current linkage state of the call site, its target method handle
-     * @see ConstantCallSite
-     * @see VolatileCallSite
-     * @see #setTarget
-     * @see ConstantCallSite#getTarget
-     * @see MutableCallSite#getTarget
-     * @see VolatileCallSite#getTarget
-     */
     public abstract MethodHandle getTarget();
 
-    /**
-     * Updates the target method of this call site, according to the
-     * behavior defined by this call site's specific class.
-     * The immediate subclasses of {@code CallSite} document the
-     * class-specific behaviors of this method.
-     * <p>
-     * The type of the new target must be {@linkplain MethodType#equals equal to}
-     * the type of the old target.
-     *
-     * @param newTarget the new target
-     * @throws NullPointerException if the proposed new target is null
-     * @throws WrongMethodTypeException if the proposed new target
-     *         has a method type that differs from the previous target
-     * @see CallSite#getTarget
-     * @see ConstantCallSite#setTarget
-     * @see MutableCallSite#setTarget
-     * @see VolatileCallSite#setTarget
-     */
     public abstract void setTarget(MethodHandle newTarget);
 
-    void checkTargetChange(MethodHandle oldTarget, MethodHandle newTarget) {
-        MethodType oldType = oldTarget.type();
-        MethodType newType = newTarget.type();  // null check!
-        if (!newType.equals(oldType))
-            throw wrongTargetType(newTarget, oldType);
-    }
-
-    private static WrongMethodTypeException wrongTargetType(MethodHandle target, MethodType type) {
-        return new WrongMethodTypeException(String.valueOf(target)+" should be of type "+type);
-    }
-
-    /**
-     * Produces a method handle equivalent to an invokedynamic instruction
-     * which has been linked to this call site.
-     * <p>
-     * This method is equivalent to the following code:
-     * <blockquote><pre>{@code
-     * MethodHandle getTarget, invoker, result;
-     * getTarget = MethodHandles.publicLookup().bind(this, "getTarget", MethodType.methodType(MethodHandle.class));
-     * invoker = MethodHandles.exactInvoker(this.type());
-     * result = MethodHandles.foldArguments(invoker, getTarget)
-     * }</pre></blockquote>
-     *
-     * @return a method handle which always invokes this call site's current target
-     */
     public abstract MethodHandle dynamicInvoker();
 
-    /*non-public*/ MethodHandle makeDynamicInvoker() {
-        // Android-changed: Use bindTo() rather than bindArgumentL() (not implemented).
-        MethodHandle getTarget = GET_TARGET.bindTo(this);
-        MethodHandle invoker = MethodHandles.exactInvoker(this.type());
-        return MethodHandles.foldArguments(invoker, getTarget);
-    }
-
-    // Android-changed: no longer final. GET_TARGET assigned in initializeGetTarget().
-    private static MethodHandle GET_TARGET = null;
-
-    private void initializeGetTarget() {
-        // Android-changed: moved from static initializer for
-        // GET_TARGET to avoid issues with running early. Called from
-        // constructors. CallSite creation is not performance critical.
-        synchronized (CallSite.class) {
-            if (GET_TARGET == null) {
-                try {
-                    GET_TARGET = IMPL_LOOKUP.
-                            findVirtual(CallSite.class, "getTarget",
-                                        MethodType.methodType(MethodHandle.class));
-                } catch (ReflectiveOperationException e) {
-                    throw new InternalError(e);
-                }
-            }
-        }
-    }
-
-    // Android-changed: not used.
-    // /** This guy is rolled into the default target if a MethodType is supplied to the constructor. */
-    // /*package-private*/
-    // static Empty uninitializedCallSite() {
-    //     throw new IllegalStateException("uninitialized call site");
-    // }
-
-    // unsafe stuff:
-    private static final long TARGET_OFFSET;
-    static {
-        try {
-            TARGET_OFFSET = UNSAFE.objectFieldOffset(CallSite.class.getDeclaredField("target"));
-        } catch (Exception ex) { throw new Error(ex); }
-    }
-
-    /*package-private*/
-    void setTargetNormal(MethodHandle newTarget) {
-        // Android-changed: Set value directly.
-        // MethodHandleNatives.setCallSiteTargetNormal(this, newTarget);
-        target = newTarget;
-    }
-    /*package-private*/
-    MethodHandle getTargetVolatile() {
-        return (MethodHandle) UNSAFE.getObjectVolatile(this, TARGET_OFFSET);
-    }
-    /*package-private*/
-    void setTargetVolatile(MethodHandle newTarget) {
-        // Android-changed: Set value directly.
-        // MethodHandleNatives.setCallSiteTargetVolatile(this, newTarget);
-        UNSAFE.putObjectVolatile(this, TARGET_OFFSET, newTarget);
-    }
-
-    // Android-changed: not used.
-    // this implements the upcall from the JVM, MethodHandleNatives.makeDynamicCallSite:
-    // static CallSite makeSite(MethodHandle bootstrapMethod,
-    //                          // Callee information:
-    //                          String name, MethodType type,
-    //                          // Extra arguments for BSM, if any:
-    //                          Object info,
-    //                          // Caller information:
-    //                          Class<?> callerClass) {
-    //     MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
-    //     CallSite site;
-    //     try {
-    //         Object binding;
-    //         info = maybeReBox(info);
-    //         if (info == null) {
-    //             binding = bootstrapMethod.invoke(caller, name, type);
-    //         } else if (!info.getClass().isArray()) {
-    //             binding = bootstrapMethod.invoke(caller, name, type, info);
-    //         } else {
-    //             Object[] argv = (Object[]) info;
-    //             maybeReBoxElements(argv);
-    //             switch (argv.length) {
-    //             case 0:
-    //                 binding = bootstrapMethod.invoke(caller, name, type);
-    //                 break;
-    //             case 1:
-    //                 binding = bootstrapMethod.invoke(caller, name, type,
-    //                                                  argv[0]);
-    //                 break;
-    //             case 2:
-    //                 binding = bootstrapMethod.invoke(caller, name, type,
-    //                                                  argv[0], argv[1]);
-    //                 break;
-    //             case 3:
-    //                 binding = bootstrapMethod.invoke(caller, name, type,
-    //                                                  argv[0], argv[1], argv[2]);
-    //                 break;
-    //             case 4:
-    //                 binding = bootstrapMethod.invoke(caller, name, type,
-    //                                                  argv[0], argv[1], argv[2], argv[3]);
-    //                 break;
-    //             case 5:
-    //                 binding = bootstrapMethod.invoke(caller, name, type,
-    //                                                  argv[0], argv[1], argv[2], argv[3], argv[4]);
-    //                 break;
-    //             case 6:
-    //                 binding = bootstrapMethod.invoke(caller, name, type,
-    //                                                  argv[0], argv[1], argv[2], argv[3], argv[4], argv[5]);
-    //                 break;
-    //             default:
-    //                 final int NON_SPREAD_ARG_COUNT = 3;  // (caller, name, type)
-    //                 if (NON_SPREAD_ARG_COUNT + argv.length > MethodType.MAX_MH_ARITY)
-    //                     throw new BootstrapMethodError("too many bootstrap method arguments");
-    //                 MethodType bsmType = bootstrapMethod.type();
-    //                 MethodType invocationType = MethodType.genericMethodType(NON_SPREAD_ARG_COUNT + argv.length);
-    //                 MethodHandle typedBSM = bootstrapMethod.asType(invocationType);
-    //                 MethodHandle spreader = invocationType.invokers().spreadInvoker(NON_SPREAD_ARG_COUNT);
-    //                 binding = spreader.invokeExact(typedBSM, (Object)caller, (Object)name, (Object)type, argv);
-    //             }
-    //         }
-    //         //System.out.println("BSM for "+name+type+" => "+binding);
-    //         if (binding instanceof CallSite) {
-    //             site = (CallSite) binding;
-    //         }  else {
-    //             throw new ClassCastException("bootstrap method failed to produce a CallSite");
-    //         }
-    //         if (!site.getTarget().type().equals(type))
-    //             throw wrongTargetType(site.getTarget(), type);
-    //     } catch (Throwable ex) {
-    //         BootstrapMethodError bex;
-    //         if (ex instanceof BootstrapMethodError)
-    //             bex = (BootstrapMethodError) ex;
-    //         else
-    //             bex = new BootstrapMethodError("call site initialization exception", ex);
-    //         throw bex;
-    //     }
-    //     return site;
-    // }
-
-    // private static Object maybeReBox(Object x) {
-    //     if (x instanceof Integer) {
-    //         int xi = (int) x;
-    //         if (xi == (byte) xi)
-    //             x = xi;  // must rebox; see JLS 5.1.7
-    //     }
-    //     return x;
-    // }
-    // private static void maybeReBoxElements(Object[] xa) {
-    //     for (int i = 0; i < xa.length; i++) {
-    //         xa[i] = maybeReBox(xa[i]);
-    //     }
-    // }
 }
diff --git a/java/lang/invoke/MethodHandle.java b/java/lang/invoke/MethodHandle.java
index cd09322..159f9dd 100644
--- a/java/lang/invoke/MethodHandle.java
+++ b/java/lang/invoke/MethodHandle.java
@@ -25,1355 +25,28 @@
 
 package java.lang.invoke;
 
-
-import dalvik.system.EmulatedStackFrame;
-
-import static java.lang.invoke.MethodHandleStatics.*;
-
-/**
- * A method handle is a typed, directly executable reference to an underlying method,
- * constructor, field, or similar low-level operation, with optional
- * transformations of arguments or return values.
- * These transformations are quite general, and include such patterns as
- * {@linkplain #asType conversion},
- * {@linkplain #bindTo insertion},
- * {@linkplain java.lang.invoke.MethodHandles#dropArguments deletion},
- * and {@linkplain java.lang.invoke.MethodHandles#filterArguments substitution}.
- *
- * <h1>Method handle contents</h1>
- * Method handles are dynamically and strongly typed according to their parameter and return types.
- * They are not distinguished by the name or the defining class of their underlying methods.
- * A method handle must be invoked using a symbolic type descriptor which matches
- * the method handle's own {@linkplain #type type descriptor}.
- * <p>
- * Every method handle reports its type descriptor via the {@link #type type} accessor.
- * This type descriptor is a {@link java.lang.invoke.MethodType MethodType} object,
- * whose structure is a series of classes, one of which is
- * the return type of the method (or {@code void.class} if none).
- * <p>
- * A method handle's type controls the types of invocations it accepts,
- * and the kinds of transformations that apply to it.
- * <p>
- * A method handle contains a pair of special invoker methods
- * called {@link #invokeExact invokeExact} and {@link #invoke invoke}.
- * Both invoker methods provide direct access to the method handle's
- * underlying method, constructor, field, or other operation,
- * as modified by transformations of arguments and return values.
- * Both invokers accept calls which exactly match the method handle's own type.
- * The plain, inexact invoker also accepts a range of other call types.
- * <p>
- * Method handles are immutable and have no visible state.
- * Of course, they can be bound to underlying methods or data which exhibit state.
- * With respect to the Java Memory Model, any method handle will behave
- * as if all of its (internal) fields are final variables.  This means that any method
- * handle made visible to the application will always be fully formed.
- * This is true even if the method handle is published through a shared
- * variable in a data race.
- * <p>
- * Method handles cannot be subclassed by the user.
- * Implementations may (or may not) create internal subclasses of {@code MethodHandle}
- * which may be visible via the {@link java.lang.Object#getClass Object.getClass}
- * operation.  The programmer should not draw conclusions about a method handle
- * from its specific class, as the method handle class hierarchy (if any)
- * may change from time to time or across implementations from different vendors.
- *
- * <h1>Method handle compilation</h1>
- * A Java method call expression naming {@code invokeExact} or {@code invoke}
- * can invoke a method handle from Java source code.
- * From the viewpoint of source code, these methods can take any arguments
- * and their result can be cast to any return type.
- * Formally this is accomplished by giving the invoker methods
- * {@code Object} return types and variable arity {@code Object} arguments,
- * but they have an additional quality called <em>signature polymorphism</em>
- * which connects this freedom of invocation directly to the JVM execution stack.
- * <p>
- * As is usual with virtual methods, source-level calls to {@code invokeExact}
- * and {@code invoke} compile to an {@code invokevirtual} instruction.
- * More unusually, the compiler must record the actual argument types,
- * and may not perform method invocation conversions on the arguments.
- * Instead, it must push them on the stack according to their own unconverted types.
- * The method handle object itself is pushed on the stack before the arguments.
- * The compiler then calls the method handle with a symbolic type descriptor which
- * describes the argument and return types.
- * <p>
- * To issue a complete symbolic type descriptor, the compiler must also determine
- * the return type.  This is based on a cast on the method invocation expression,
- * if there is one, or else {@code Object} if the invocation is an expression
- * or else {@code void} if the invocation is a statement.
- * The cast may be to a primitive type (but not {@code void}).
- * <p>
- * As a corner case, an uncasted {@code null} argument is given
- * a symbolic type descriptor of {@code java.lang.Void}.
- * The ambiguity with the type {@code Void} is harmless, since there are no references of type
- * {@code Void} except the null reference.
- *
- * <h1>Method handle invocation</h1>
- * The first time a {@code invokevirtual} instruction is executed
- * it is linked, by symbolically resolving the names in the instruction
- * and verifying that the method call is statically legal.
- * This is true of calls to {@code invokeExact} and {@code invoke}.
- * In this case, the symbolic type descriptor emitted by the compiler is checked for
- * correct syntax and names it contains are resolved.
- * Thus, an {@code invokevirtual} instruction which invokes
- * a method handle will always link, as long
- * as the symbolic type descriptor is syntactically well-formed
- * and the types exist.
- * <p>
- * When the {@code invokevirtual} is executed after linking,
- * the receiving method handle's type is first checked by the JVM
- * to ensure that it matches the symbolic type descriptor.
- * If the type match fails, it means that the method which the
- * caller is invoking is not present on the individual
- * method handle being invoked.
- * <p>
- * In the case of {@code invokeExact}, the type descriptor of the invocation
- * (after resolving symbolic type names) must exactly match the method type
- * of the receiving method handle.
- * In the case of plain, inexact {@code invoke}, the resolved type descriptor
- * must be a valid argument to the receiver's {@link #asType asType} method.
- * Thus, plain {@code invoke} is more permissive than {@code invokeExact}.
- * <p>
- * After type matching, a call to {@code invokeExact} directly
- * and immediately invoke the method handle's underlying method
- * (or other behavior, as the case may be).
- * <p>
- * A call to plain {@code invoke} works the same as a call to
- * {@code invokeExact}, if the symbolic type descriptor specified by the caller
- * exactly matches the method handle's own type.
- * If there is a type mismatch, {@code invoke} attempts
- * to adjust the type of the receiving method handle,
- * as if by a call to {@link #asType asType},
- * to obtain an exactly invokable method handle {@code M2}.
- * This allows a more powerful negotiation of method type
- * between caller and callee.
- * <p>
- * (<em>Note:</em> The adjusted method handle {@code M2} is not directly observable,
- * and implementations are therefore not required to materialize it.)
- *
- * <h1>Invocation checking</h1>
- * In typical programs, method handle type matching will usually succeed.
- * But if a match fails, the JVM will throw a {@link WrongMethodTypeException},
- * either directly (in the case of {@code invokeExact}) or indirectly as if
- * by a failed call to {@code asType} (in the case of {@code invoke}).
- * <p>
- * Thus, a method type mismatch which might show up as a linkage error
- * in a statically typed program can show up as
- * a dynamic {@code WrongMethodTypeException}
- * in a program which uses method handles.
- * <p>
- * Because method types contain "live" {@code Class} objects,
- * method type matching takes into account both types names and class loaders.
- * Thus, even if a method handle {@code M} is created in one
- * class loader {@code L1} and used in another {@code L2},
- * method handle calls are type-safe, because the caller's symbolic type
- * descriptor, as resolved in {@code L2},
- * is matched against the original callee method's symbolic type descriptor,
- * as resolved in {@code L1}.
- * The resolution in {@code L1} happens when {@code M} is created
- * and its type is assigned, while the resolution in {@code L2} happens
- * when the {@code invokevirtual} instruction is linked.
- * <p>
- * Apart from the checking of type descriptors,
- * a method handle's capability to call its underlying method is unrestricted.
- * If a method handle is formed on a non-public method by a class
- * that has access to that method, the resulting handle can be used
- * in any place by any caller who receives a reference to it.
- * <p>
- * Unlike with the Core Reflection API, where access is checked every time
- * a reflective method is invoked,
- * method handle access checking is performed
- * <a href="MethodHandles.Lookup.html#access">when the method handle is created</a>.
- * In the case of {@code ldc} (see below), access checking is performed as part of linking
- * the constant pool entry underlying the constant method handle.
- * <p>
- * Thus, handles to non-public methods, or to methods in non-public classes,
- * should generally be kept secret.
- * They should not be passed to untrusted code unless their use from
- * the untrusted code would be harmless.
- *
- * <h1>Method handle creation</h1>
- * Java code can create a method handle that directly accesses
- * any method, constructor, or field that is accessible to that code.
- * This is done via a reflective, capability-based API called
- * {@link java.lang.invoke.MethodHandles.Lookup MethodHandles.Lookup}
- * For example, a static method handle can be obtained
- * from {@link java.lang.invoke.MethodHandles.Lookup#findStatic Lookup.findStatic}.
- * There are also conversion methods from Core Reflection API objects,
- * such as {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect}.
- * <p>
- * Like classes and strings, method handles that correspond to accessible
- * fields, methods, and constructors can also be represented directly
- * in a class file's constant pool as constants to be loaded by {@code ldc} bytecodes.
- * A new type of constant pool entry, {@code CONSTANT_MethodHandle},
- * refers directly to an associated {@code CONSTANT_Methodref},
- * {@code CONSTANT_InterfaceMethodref}, or {@code CONSTANT_Fieldref}
- * constant pool entry.
- * (For full details on method handle constants,
- * see sections 4.4.8 and 5.4.3.5 of the Java Virtual Machine Specification.)
- * <p>
- * Method handles produced by lookups or constant loads from methods or
- * constructors with the variable arity modifier bit ({@code 0x0080})
- * have a corresponding variable arity, as if they were defined with
- * the help of {@link #asVarargsCollector asVarargsCollector}.
- * <p>
- * A method reference may refer either to a static or non-static method.
- * In the non-static case, the method handle type includes an explicit
- * receiver argument, prepended before any other arguments.
- * In the method handle's type, the initial receiver argument is typed
- * according to the class under which the method was initially requested.
- * (E.g., if a non-static method handle is obtained via {@code ldc},
- * the type of the receiver is the class named in the constant pool entry.)
- * <p>
- * Method handle constants are subject to the same link-time access checks
- * their corresponding bytecode instructions, and the {@code ldc} instruction
- * will throw corresponding linkage errors if the bytecode behaviors would
- * throw such errors.
- * <p>
- * As a corollary of this, access to protected members is restricted
- * to receivers only of the accessing class, or one of its subclasses,
- * and the accessing class must in turn be a subclass (or package sibling)
- * of the protected member's defining class.
- * If a method reference refers to a protected non-static method or field
- * of a class outside the current package, the receiver argument will
- * be narrowed to the type of the accessing class.
- * <p>
- * When a method handle to a virtual method is invoked, the method is
- * always looked up in the receiver (that is, the first argument).
- * <p>
- * A non-virtual method handle to a specific virtual method implementation
- * can also be created.  These do not perform virtual lookup based on
- * receiver type.  Such a method handle simulates the effect of
- * an {@code invokespecial} instruction to the same method.
- *
- * <h1>Usage examples</h1>
- * Here are some examples of usage:
- * <blockquote><pre>{@code
-Object x, y; String s; int i;
-MethodType mt; MethodHandle mh;
-MethodHandles.Lookup lookup = MethodHandles.lookup();
-// mt is (char,char)String
-mt = MethodType.methodType(String.class, char.class, char.class);
-mh = lookup.findVirtual(String.class, "replace", mt);
-s = (String) mh.invokeExact("daddy",'d','n');
-// invokeExact(Ljava/lang/String;CC)Ljava/lang/String;
-assertEquals(s, "nanny");
-// weakly typed invocation (using MHs.invoke)
-s = (String) mh.invokeWithArguments("sappy", 'p', 'v');
-assertEquals(s, "savvy");
-// mt is (Object[])List
-mt = MethodType.methodType(java.util.List.class, Object[].class);
-mh = lookup.findStatic(java.util.Arrays.class, "asList", mt);
-assert(mh.isVarargsCollector());
-x = mh.invoke("one", "two");
-// invoke(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
-assertEquals(x, java.util.Arrays.asList("one","two"));
-// mt is (Object,Object,Object)Object
-mt = MethodType.genericMethodType(3);
-mh = mh.asType(mt);
-x = mh.invokeExact((Object)1, (Object)2, (Object)3);
-// invokeExact(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
-assertEquals(x, java.util.Arrays.asList(1,2,3));
-// mt is ()int
-mt = MethodType.methodType(int.class);
-mh = lookup.findVirtual(java.util.List.class, "size", mt);
-i = (int) mh.invokeExact(java.util.Arrays.asList(1,2,3));
-// invokeExact(Ljava/util/List;)I
-assert(i == 3);
-mt = MethodType.methodType(void.class, String.class);
-mh = lookup.findVirtual(java.io.PrintStream.class, "println", mt);
-mh.invokeExact(System.out, "Hello, world.");
-// invokeExact(Ljava/io/PrintStream;Ljava/lang/String;)V
- * }</pre></blockquote>
- * Each of the above calls to {@code invokeExact} or plain {@code invoke}
- * generates a single invokevirtual instruction with
- * the symbolic type descriptor indicated in the following comment.
- * In these examples, the helper method {@code assertEquals} is assumed to
- * be a method which calls {@link java.util.Objects#equals(Object,Object) Objects.equals}
- * on its arguments, and asserts that the result is true.
- *
- * <h1>Exceptions</h1>
- * The methods {@code invokeExact} and {@code invoke} are declared
- * to throw {@link java.lang.Throwable Throwable},
- * which is to say that there is no static restriction on what a method handle
- * can throw.  Since the JVM does not distinguish between checked
- * and unchecked exceptions (other than by their class, of course),
- * there is no particular effect on bytecode shape from ascribing
- * checked exceptions to method handle invocations.  But in Java source
- * code, methods which perform method handle calls must either explicitly
- * throw {@code Throwable}, or else must catch all
- * throwables locally, rethrowing only those which are legal in the context,
- * and wrapping ones which are illegal.
- *
- * <h1><a name="sigpoly"></a>Signature polymorphism</h1>
- * The unusual compilation and linkage behavior of
- * {@code invokeExact} and plain {@code invoke}
- * is referenced by the term <em>signature polymorphism</em>.
- * As defined in the Java Language Specification,
- * a signature polymorphic method is one which can operate with
- * any of a wide range of call signatures and return types.
- * <p>
- * In source code, a call to a signature polymorphic method will
- * compile, regardless of the requested symbolic type descriptor.
- * As usual, the Java compiler emits an {@code invokevirtual}
- * instruction with the given symbolic type descriptor against the named method.
- * The unusual part is that the symbolic type descriptor is derived from
- * the actual argument and return types, not from the method declaration.
- * <p>
- * When the JVM processes bytecode containing signature polymorphic calls,
- * it will successfully link any such call, regardless of its symbolic type descriptor.
- * (In order to retain type safety, the JVM will guard such calls with suitable
- * dynamic type checks, as described elsewhere.)
- * <p>
- * Bytecode generators, including the compiler back end, are required to emit
- * untransformed symbolic type descriptors for these methods.
- * Tools which determine symbolic linkage are required to accept such
- * untransformed descriptors, without reporting linkage errors.
- *
- * <h1>Interoperation between method handles and the Core Reflection API</h1>
- * Using factory methods in the {@link java.lang.invoke.MethodHandles.Lookup Lookup} API,
- * any class member represented by a Core Reflection API object
- * can be converted to a behaviorally equivalent method handle.
- * For example, a reflective {@link java.lang.reflect.Method Method} can
- * be converted to a method handle using
- * {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect}.
- * The resulting method handles generally provide more direct and efficient
- * access to the underlying class members.
- * <p>
- * As a special case,
- * when the Core Reflection API is used to view the signature polymorphic
- * methods {@code invokeExact} or plain {@code invoke} in this class,
- * they appear as ordinary non-polymorphic methods.
- * Their reflective appearance, as viewed by
- * {@link java.lang.Class#getDeclaredMethod Class.getDeclaredMethod},
- * is unaffected by their special status in this API.
- * For example, {@link java.lang.reflect.Method#getModifiers Method.getModifiers}
- * will report exactly those modifier bits required for any similarly
- * declared method, including in this case {@code native} and {@code varargs} bits.
- * <p>
- * As with any reflected method, these methods (when reflected) may be
- * invoked via {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}.
- * However, such reflective calls do not result in method handle invocations.
- * Such a call, if passed the required argument
- * (a single one, of type {@code Object[]}), will ignore the argument and
- * will throw an {@code UnsupportedOperationException}.
- * <p>
- * Since {@code invokevirtual} instructions can natively
- * invoke method handles under any symbolic type descriptor, this reflective view conflicts
- * with the normal presentation of these methods via bytecodes.
- * Thus, these two native methods, when reflectively viewed by
- * {@code Class.getDeclaredMethod}, may be regarded as placeholders only.
- * <p>
- * In order to obtain an invoker method for a particular type descriptor,
- * use {@link java.lang.invoke.MethodHandles#exactInvoker MethodHandles.exactInvoker},
- * or {@link java.lang.invoke.MethodHandles#invoker MethodHandles.invoker}.
- * The {@link java.lang.invoke.MethodHandles.Lookup#findVirtual Lookup.findVirtual}
- * API is also able to return a method handle
- * to call {@code invokeExact} or plain {@code invoke},
- * for any specified type descriptor .
- *
- * <h1>Interoperation between method handles and Java generics</h1>
- * A method handle can be obtained on a method, constructor, or field
- * which is declared with Java generic types.
- * As with the Core Reflection API, the type of the method handle
- * will constructed from the erasure of the source-level type.
- * When a method handle is invoked, the types of its arguments
- * or the return value cast type may be generic types or type instances.
- * If this occurs, the compiler will replace those
- * types by their erasures when it constructs the symbolic type descriptor
- * for the {@code invokevirtual} instruction.
- * <p>
- * Method handles do not represent
- * their function-like types in terms of Java parameterized (generic) types,
- * because there are three mismatches between function-like types and parameterized
- * Java types.
- * <ul>
- * <li>Method types range over all possible arities,
- * from no arguments to up to the  <a href="MethodHandle.html#maxarity">maximum number</a> of allowed arguments.
- * Generics are not variadic, and so cannot represent this.</li>
- * <li>Method types can specify arguments of primitive types,
- * which Java generic types cannot range over.</li>
- * <li>Higher order functions over method handles (combinators) are
- * often generic across a wide range of function types, including
- * those of multiple arities.  It is impossible to represent such
- * genericity with a Java type parameter.</li>
- * </ul>
- *
- * <h1><a name="maxarity"></a>Arity limits</h1>
- * The JVM imposes on all methods and constructors of any kind an absolute
- * limit of 255 stacked arguments.  This limit can appear more restrictive
- * in certain cases:
- * <ul>
- * <li>A {@code long} or {@code double} argument counts (for purposes of arity limits) as two argument slots.
- * <li>A non-static method consumes an extra argument for the object on which the method is called.
- * <li>A constructor consumes an extra argument for the object which is being constructed.
- * <li>Since a method handle&rsquo;s {@code invoke} method (or other signature-polymorphic method) is non-virtual,
- *     it consumes an extra argument for the method handle itself, in addition to any non-virtual receiver object.
- * </ul>
- * These limits imply that certain method handles cannot be created, solely because of the JVM limit on stacked arguments.
- * For example, if a static JVM method accepts exactly 255 arguments, a method handle cannot be created for it.
- * Attempts to create method handles with impossible method types lead to an {@link IllegalArgumentException}.
- * In particular, a method handle&rsquo;s type must not have an arity of the exact maximum 255.
- *
- * @see MethodType
- * @see MethodHandles
- * @author John Rose, JSR 292 EG
- */
 public abstract class MethodHandle {
-    // Android-changed:
-    //
-    // static { MethodHandleImpl.initStatics(); }
-    //
-    // LambdaForm and customizationCount are currently unused in our implementation
-    // and will be substituted with appropriate implementation / delegate classes.
-    //
-    // /*private*/ final LambdaForm form;
-    // form is not private so that invokers can easily fetch it
-    // /*non-public*/ byte customizationCount;
-    // customizationCount should be accessible from invokers
 
+    public MethodType type() { return null; }
 
-    /**
-     * Internal marker interface which distinguishes (to the Java compiler)
-     * those methods which are <a href="MethodHandle.html#sigpoly">signature polymorphic</a>.
-     *
-     * @hide
-     */
-    @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
-    @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
-    public @interface PolymorphicSignature { }
+    public final Object invokeExact(Object... args) throws Throwable { return null; }
 
-    /**
-     * The type of this method handle, this corresponds to the exact type of the method
-     * being invoked.
-     */
-    private final MethodType type;
+    public final Object invoke(Object... args) throws Throwable { return null; }
 
-    /**
-     * The nominal type of this method handle, will be non-null if a method handle declares
-     * a different type from its "real" type, which is either the type of the method being invoked
-     * or the type of the emulated stackframe expected by an underyling adapter.
-     */
-    private MethodType nominalType;
+    public Object invokeWithArguments(Object... arguments) throws Throwable { return null; }
 
-    /**
-     * The spread invoker associated with this type with zero trailing arguments.
-     * This is used to speed up invokeWithArguments.
-     */
-    private MethodHandle cachedSpreadInvoker;
+    public Object invokeWithArguments(java.util.List<?> arguments) throws Throwable { return null; }
 
-    /**
-     * The INVOKE* constants and SGET/SPUT and IGET/IPUT constants specify the behaviour of this
-     * method handle with respect to the ArtField* or the ArtMethod* that it operates on. These
-     * behaviours are equivalent to the dex bytecode behaviour on the respective method_id or
-     * field_id in the equivalent instruction.
-     *
-     * INVOKE_TRANSFORM is a special type of handle which doesn't encode any dex bytecode behaviour,
-     * instead it transforms the list of input arguments or performs other higher order operations
-     * before (optionally) delegating to another method handle.
-     *
-     * INVOKE_CALLSITE_TRANSFORM is a variation on INVOKE_TRANSFORM where the method type of
-     * a MethodHandle dynamically varies based on the callsite. This is used by
-     * the VarargsCollector implementation which places any number of trailing arguments
-     * into an array before invoking an arity method. The "any number of trailing arguments" means
-     * it would otherwise generate WrongMethodTypeExceptions as the callsite method type and
-     * VarargsCollector method type appear incompatible.
-     */
+    public MethodHandle asType(MethodType newType) { return null; }
 
-    /** @hide */ public static final int INVOKE_VIRTUAL = 0;
-    /** @hide */ public static final int INVOKE_SUPER = 1;
-    /** @hide */ public static final int INVOKE_DIRECT = 2;
-    /** @hide */ public static final int INVOKE_STATIC = 3;
-    /** @hide */ public static final int INVOKE_INTERFACE = 4;
-    /** @hide */ public static final int INVOKE_TRANSFORM = 5;
-    /** @hide */ public static final int INVOKE_CALLSITE_TRANSFORM = 6;
-    /** @hide */ public static final int INVOKE_VAR_HANDLE = 7;
-    /** @hide */ public static final int INVOKE_VAR_HANDLE_EXACT = 8;
-    /** @hide */ public static final int IGET = 9;
-    /** @hide */ public static final int IPUT = 10;
-    /** @hide */ public static final int SGET = 11;
-    /** @hide */ public static final int SPUT = 12;
+    public MethodHandle asCollector(Class<?> arrayType, int arrayLength) { return null; }
 
-    // The kind of this method handle (used by the runtime). This is one of the INVOKE_*
-    // constants or SGET/SPUT, IGET/IPUT.
-    /** @hide */ protected final int handleKind;
+    public MethodHandle asVarargsCollector(Class<?> arrayType) { return null; }
 
-    // The ArtMethod* or ArtField* associated with this method handle (used by the runtime).
-    /** @hide */ protected final long artFieldOrMethod;
+    public boolean isVarargsCollector() { return false; }
 
-    /** @hide */
-    protected MethodHandle(long artFieldOrMethod, int handleKind, MethodType type) {
-        this.artFieldOrMethod = artFieldOrMethod;
-        this.handleKind = handleKind;
-        this.type = type;
-    }
+    public MethodHandle asFixedArity() { return null; }
 
-    /**
-     * Reports the type of this method handle.
-     * Every invocation of this method handle via {@code invokeExact} must exactly match this type.
-     * @return the method handle type
-     */
-    public MethodType type() {
-        if (nominalType != null) {
-            return nominalType;
-        }
+    public MethodHandle bindTo(Object x) { return null; }
 
-        return type;
-    }
-
-    /**
-     * Invokes the method handle, allowing any caller type descriptor, but requiring an exact type match.
-     * The symbolic type descriptor at the call site of {@code invokeExact} must
-     * exactly match this method handle's {@link #type type}.
-     * No conversions are allowed on arguments or return values.
-     * <p>
-     * When this method is observed via the Core Reflection API,
-     * it will appear as a single native method, taking an object array and returning an object.
-     * If this native method is invoked directly via
-     * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}, via JNI,
-     * or indirectly via {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect},
-     * it will throw an {@code UnsupportedOperationException}.
-     * @param args the signature-polymorphic parameter list, statically represented using varargs
-     * @return the signature-polymorphic result, statically represented using {@code Object}
-     * @throws WrongMethodTypeException if the target's type is not identical with the caller's symbolic type descriptor
-     * @throws Throwable anything thrown by the underlying method propagates unchanged through the method handle call
-     */
-    public final native @PolymorphicSignature Object invokeExact(Object... args) throws Throwable;
-
-    /**
-     * Invokes the method handle, allowing any caller type descriptor,
-     * and optionally performing conversions on arguments and return values.
-     * <p>
-     * If the call site's symbolic type descriptor exactly matches this method handle's {@link #type type},
-     * the call proceeds as if by {@link #invokeExact invokeExact}.
-     * <p>
-     * Otherwise, the call proceeds as if this method handle were first
-     * adjusted by calling {@link #asType asType} to adjust this method handle
-     * to the required type, and then the call proceeds as if by
-     * {@link #invokeExact invokeExact} on the adjusted method handle.
-     * <p>
-     * There is no guarantee that the {@code asType} call is actually made.
-     * If the JVM can predict the results of making the call, it may perform
-     * adaptations directly on the caller's arguments,
-     * and call the target method handle according to its own exact type.
-     * <p>
-     * The resolved type descriptor at the call site of {@code invoke} must
-     * be a valid argument to the receivers {@code asType} method.
-     * In particular, the caller must specify the same argument arity
-     * as the callee's type,
-     * if the callee is not a {@linkplain #asVarargsCollector variable arity collector}.
-     * <p>
-     * When this method is observed via the Core Reflection API,
-     * it will appear as a single native method, taking an object array and returning an object.
-     * If this native method is invoked directly via
-     * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}, via JNI,
-     * or indirectly via {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect},
-     * it will throw an {@code UnsupportedOperationException}.
-     * @param args the signature-polymorphic parameter list, statically represented using varargs
-     * @return the signature-polymorphic result, statically represented using {@code Object}
-     * @throws WrongMethodTypeException if the target's type cannot be adjusted to the caller's symbolic type descriptor
-     * @throws ClassCastException if the target's type can be adjusted to the caller, but a reference cast fails
-     * @throws Throwable anything thrown by the underlying method propagates unchanged through the method handle call
-     */
-    public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;
-
-    // Android-changed: Removed implementation details.
-    //
-    // /*non-public*/ final native @PolymorphicSignature Object invokeBasic(Object... args)
-    // /*non-public*/ static native @PolymorphicSignature Object linkToVirtual(Object... args)
-    // /*non-public*/ static native @PolymorphicSignature Object linkToStatic(Object... args)
-    // /*non-public*/ static native @PolymorphicSignature Object linkToSpecial(Object... args)
-    // /*non-public*/ static native @PolymorphicSignature Object linkToInterface(Object... args)
-
-    /**
-     * Performs a variable arity invocation, passing the arguments in the given list
-     * to the method handle, as if via an inexact {@link #invoke invoke} from a call site
-     * which mentions only the type {@code Object}, and whose arity is the length
-     * of the argument list.
-     * <p>
-     * Specifically, execution proceeds as if by the following steps,
-     * although the methods are not guaranteed to be called if the JVM
-     * can predict their effects.
-     * <ul>
-     * <li>Determine the length of the argument array as {@code N}.
-     *     For a null reference, {@code N=0}. </li>
-     * <li>Determine the general type {@code TN} of {@code N} arguments as
-     *     as {@code TN=MethodType.genericMethodType(N)}.</li>
-     * <li>Force the original target method handle {@code MH0} to the
-     *     required type, as {@code MH1 = MH0.asType(TN)}. </li>
-     * <li>Spread the array into {@code N} separate arguments {@code A0, ...}. </li>
-     * <li>Invoke the type-adjusted method handle on the unpacked arguments:
-     *     MH1.invokeExact(A0, ...). </li>
-     * <li>Take the return value as an {@code Object} reference. </li>
-     * </ul>
-     * <p>
-     * Because of the action of the {@code asType} step, the following argument
-     * conversions are applied as necessary:
-     * <ul>
-     * <li>reference casting
-     * <li>unboxing
-     * <li>widening primitive conversions
-     * </ul>
-     * <p>
-     * The result returned by the call is boxed if it is a primitive,
-     * or forced to null if the return type is void.
-     * <p>
-     * This call is equivalent to the following code:
-     * <blockquote><pre>{@code
-     * MethodHandle invoker = MethodHandles.spreadInvoker(this.type(), 0);
-     * Object result = invoker.invokeExact(this, arguments);
-     * }</pre></blockquote>
-     * <p>
-     * Unlike the signature polymorphic methods {@code invokeExact} and {@code invoke},
-     * {@code invokeWithArguments} can be accessed normally via the Core Reflection API and JNI.
-     * It can therefore be used as a bridge between native or reflective code and method handles.
-     *
-     * @param arguments the arguments to pass to the target
-     * @return the result returned by the target
-     * @throws ClassCastException if an argument cannot be converted by reference casting
-     * @throws WrongMethodTypeException if the target's type cannot be adjusted to take the given number of {@code Object} arguments
-     * @throws Throwable anything thrown by the target method invocation
-     * @see MethodHandles#spreadInvoker
-     */
-    public Object invokeWithArguments(Object... arguments) throws Throwable {
-        MethodHandle invoker = null;
-        synchronized (this) {
-            if (cachedSpreadInvoker == null) {
-                cachedSpreadInvoker = MethodHandles.spreadInvoker(this.type(), 0);
-            }
-
-            invoker = cachedSpreadInvoker;
-        }
-
-        return invoker.invoke(this, arguments);
-    }
-
-    /**
-     * Performs a variable arity invocation, passing the arguments in the given array
-     * to the method handle, as if via an inexact {@link #invoke invoke} from a call site
-     * which mentions only the type {@code Object}, and whose arity is the length
-     * of the argument array.
-     * <p>
-     * This method is also equivalent to the following code:
-     * <blockquote><pre>{@code
-     *   invokeWithArguments(arguments.toArray()
-     * }</pre></blockquote>
-     *
-     * @param arguments the arguments to pass to the target
-     * @return the result returned by the target
-     * @throws NullPointerException if {@code arguments} is a null reference
-     * @throws ClassCastException if an argument cannot be converted by reference casting
-     * @throws WrongMethodTypeException if the target's type cannot be adjusted to take the given number of {@code Object} arguments
-     * @throws Throwable anything thrown by the target method invocation
-     */
-    public Object invokeWithArguments(java.util.List<?> arguments) throws Throwable {
-        return invokeWithArguments(arguments.toArray());
-    }
-
-    /**
-     * Produces an adapter method handle which adapts the type of the
-     * current method handle to a new type.
-     * The resulting method handle is guaranteed to report a type
-     * which is equal to the desired new type.
-     * <p>
-     * If the original type and new type are equal, returns {@code this}.
-     * <p>
-     * The new method handle, when invoked, will perform the following
-     * steps:
-     * <ul>
-     * <li>Convert the incoming argument list to match the original
-     *     method handle's argument list.
-     * <li>Invoke the original method handle on the converted argument list.
-     * <li>Convert any result returned by the original method handle
-     *     to the return type of new method handle.
-     * </ul>
-     * <p>
-     * This method provides the crucial behavioral difference between
-     * {@link #invokeExact invokeExact} and plain, inexact {@link #invoke invoke}.
-     * The two methods
-     * perform the same steps when the caller's type descriptor exactly m atches
-     * the callee's, but when the types differ, plain {@link #invoke invoke}
-     * also calls {@code asType} (or some internal equivalent) in order
-     * to match up the caller's and callee's types.
-     * <p>
-     * If the current method is a variable arity method handle
-     * argument list conversion may involve the conversion and collection
-     * of several arguments into an array, as
-     * {@linkplain #asVarargsCollector described elsewhere}.
-     * In every other case, all conversions are applied <em>pairwise</em>,
-     * which means that each argument or return value is converted to
-     * exactly one argument or return value (or no return value).
-     * The applied conversions are defined by consulting the
-     * the corresponding component types of the old and new
-     * method handle types.
-     * <p>
-     * Let <em>T0</em> and <em>T1</em> be corresponding new and old parameter types,
-     * or old and new return types.  Specifically, for some valid index {@code i}, let
-     * <em>T0</em>{@code =newType.parameterType(i)} and <em>T1</em>{@code =this.type().parameterType(i)}.
-     * Or else, going the other way for return values, let
-     * <em>T0</em>{@code =this.type().returnType()} and <em>T1</em>{@code =newType.returnType()}.
-     * If the types are the same, the new method handle makes no change
-     * to the corresponding argument or return value (if any).
-     * Otherwise, one of the following conversions is applied
-     * if possible:
-     * <ul>
-     * <li>If <em>T0</em> and <em>T1</em> are references, then a cast to <em>T1</em> is applied.
-     *     (The types do not need to be related in any particular way.
-     *     This is because a dynamic value of null can convert to any reference type.)
-     * <li>If <em>T0</em> and <em>T1</em> are primitives, then a Java method invocation
-     *     conversion (JLS 5.3) is applied, if one exists.
-     *     (Specifically, <em>T0</em> must convert to <em>T1</em> by a widening primitive conversion.)
-     * <li>If <em>T0</em> is a primitive and <em>T1</em> a reference,
-     *     a Java casting conversion (JLS 5.5) is applied if one exists.
-     *     (Specifically, the value is boxed from <em>T0</em> to its wrapper class,
-     *     which is then widened as needed to <em>T1</em>.)
-     * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive, an unboxing
-     *     conversion will be applied at runtime, possibly followed
-     *     by a Java method invocation conversion (JLS 5.3)
-     *     on the primitive value.  (These are the primitive widening conversions.)
-     *     <em>T0</em> must be a wrapper class or a supertype of one.
-     *     (In the case where <em>T0</em> is Object, these are the conversions
-     *     allowed by {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}.)
-     *     The unboxing conversion must have a possibility of success, which means that
-     *     if <em>T0</em> is not itself a wrapper class, there must exist at least one
-     *     wrapper class <em>TW</em> which is a subtype of <em>T0</em> and whose unboxed
-     *     primitive value can be widened to <em>T1</em>.
-     * <li>If the return type <em>T1</em> is marked as void, any returned value is discarded
-     * <li>If the return type <em>T0</em> is void and <em>T1</em> a reference, a null value is introduced.
-     * <li>If the return type <em>T0</em> is void and <em>T1</em> a primitive,
-     *     a zero value is introduced.
-     * </ul>
-     * (<em>Note:</em> Both <em>T0</em> and <em>T1</em> may be regarded as static types,
-     * because neither corresponds specifically to the <em>dynamic type</em> of any
-     * actual argument or return value.)
-     * <p>
-     * The method handle conversion cannot be made if any one of the required
-     * pairwise conversions cannot be made.
-     * <p>
-     * At runtime, the conversions applied to reference arguments
-     * or return values may require additional runtime checks which can fail.
-     * An unboxing operation may fail because the original reference is null,
-     * causing a {@link java.lang.NullPointerException NullPointerException}.
-     * An unboxing operation or a reference cast may also fail on a reference
-     * to an object of the wrong type,
-     * causing a {@link java.lang.ClassCastException ClassCastException}.
-     * Although an unboxing operation may accept several kinds of wrappers,
-     * if none are available, a {@code ClassCastException} will be thrown.
-     *
-     * @param newType the expected type of the new method handle
-     * @return a method handle which delegates to {@code this} after performing
-     *           any necessary argument conversions, and arranges for any
-     *           necessary return value conversions
-     * @throws NullPointerException if {@code newType} is a null reference
-     * @throws WrongMethodTypeException if the conversion cannot be made
-     * @see MethodHandles#explicitCastArguments
-     */
-    public MethodHandle asType(MethodType newType) {
-        // Fast path alternative to a heavyweight {@code asType} call.
-        // Return 'this' if the conversion will be a no-op.
-        if (newType == type) {
-            return this;
-        }
-
-        if (!type.isConvertibleTo(newType)) {
-            throw new WrongMethodTypeException("cannot convert " + this + " to " + newType);
-        }
-
-        MethodHandle mh = duplicate();
-        mh.nominalType = newType;
-        return mh;
-    }
-
-    /**
-     * Makes an <em>array-spreading</em> method handle, which accepts a trailing array argument
-     * and spreads its elements as positional arguments.
-     * The new method handle adapts, as its <i>target</i>,
-     * the current method handle.  The type of the adapter will be
-     * the same as the type of the target, except that the final
-     * {@code arrayLength} parameters of the target's type are replaced
-     * by a single array parameter of type {@code arrayType}.
-     * <p>
-     * If the array element type differs from any of the corresponding
-     * argument types on the original target,
-     * the original target is adapted to take the array elements directly,
-     * as if by a call to {@link #asType asType}.
-     * <p>
-     * When called, the adapter replaces a trailing array argument
-     * by the array's elements, each as its own argument to the target.
-     * (The order of the arguments is preserved.)
-     * They are converted pairwise by casting and/or unboxing
-     * to the types of the trailing parameters of the target.
-     * Finally the target is called.
-     * What the target eventually returns is returned unchanged by the adapter.
-     * <p>
-     * Before calling the target, the adapter verifies that the array
-     * contains exactly enough elements to provide a correct argument count
-     * to the target method handle.
-     * (The array may also be null when zero elements are required.)
-     * <p>
-     * If, when the adapter is called, the supplied array argument does
-     * not have the correct number of elements, the adapter will throw
-     * an {@link IllegalArgumentException} instead of invoking the target.
-     * <p>
-     * Here are some simple examples of array-spreading method handles:
-     * <blockquote><pre>{@code
-MethodHandle equals = publicLookup()
-  .findVirtual(String.class, "equals", methodType(boolean.class, Object.class));
-assert( (boolean) equals.invokeExact("me", (Object)"me"));
-assert(!(boolean) equals.invokeExact("me", (Object)"thee"));
-// spread both arguments from a 2-array:
-MethodHandle eq2 = equals.asSpreader(Object[].class, 2);
-assert( (boolean) eq2.invokeExact(new Object[]{ "me", "me" }));
-assert(!(boolean) eq2.invokeExact(new Object[]{ "me", "thee" }));
-// try to spread from anything but a 2-array:
-for (int n = 0; n <= 10; n++) {
-  Object[] badArityArgs = (n == 2 ? null : new Object[n]);
-  try { assert((boolean) eq2.invokeExact(badArityArgs) && false); }
-  catch (IllegalArgumentException ex) { } // OK
-}
-// spread both arguments from a String array:
-MethodHandle eq2s = equals.asSpreader(String[].class, 2);
-assert( (boolean) eq2s.invokeExact(new String[]{ "me", "me" }));
-assert(!(boolean) eq2s.invokeExact(new String[]{ "me", "thee" }));
-// spread second arguments from a 1-array:
-MethodHandle eq1 = equals.asSpreader(Object[].class, 1);
-assert( (boolean) eq1.invokeExact("me", new Object[]{ "me" }));
-assert(!(boolean) eq1.invokeExact("me", new Object[]{ "thee" }));
-// spread no arguments from a 0-array or null:
-MethodHandle eq0 = equals.asSpreader(Object[].class, 0);
-assert( (boolean) eq0.invokeExact("me", (Object)"me", new Object[0]));
-assert(!(boolean) eq0.invokeExact("me", (Object)"thee", (Object[])null));
-// asSpreader and asCollector are approximate inverses:
-for (int n = 0; n <= 2; n++) {
-    for (Class<?> a : new Class<?>[]{Object[].class, String[].class, CharSequence[].class}) {
-        MethodHandle equals2 = equals.asSpreader(a, n).asCollector(a, n);
-        assert( (boolean) equals2.invokeWithArguments("me", "me"));
-        assert(!(boolean) equals2.invokeWithArguments("me", "thee"));
-    }
-}
-MethodHandle caToString = publicLookup()
-  .findStatic(Arrays.class, "toString", methodType(String.class, char[].class));
-assertEquals("[A, B, C]", (String) caToString.invokeExact("ABC".toCharArray()));
-MethodHandle caString3 = caToString.asCollector(char[].class, 3);
-assertEquals("[A, B, C]", (String) caString3.invokeExact('A', 'B', 'C'));
-MethodHandle caToString2 = caString3.asSpreader(char[].class, 2);
-assertEquals("[A, B, C]", (String) caToString2.invokeExact('A', "BC".toCharArray()));
-     * }</pre></blockquote>
-     * @param arrayType usually {@code Object[]}, the type of the array argument from which to extract the spread arguments
-     * @param arrayLength the number of arguments to spread from an incoming array argument
-     * @return a new method handle which spreads its final array argument,
-     *         before calling the original method handle
-     * @throws NullPointerException if {@code arrayType} is a null reference
-     * @throws IllegalArgumentException if {@code arrayType} is not an array type,
-     *         or if target does not have at least
-     *         {@code arrayLength} parameter types,
-     *         or if {@code arrayLength} is negative,
-     *         or if the resulting method handle's type would have
-     *         <a href="MethodHandle.html#maxarity">too many parameters</a>
-     * @throws WrongMethodTypeException if the implied {@code asType} call fails
-     * @see #asCollector
-     */
-    public MethodHandle asSpreader(Class<?> arrayType, int arrayLength) {
-        MethodType postSpreadType = asSpreaderChecks(arrayType, arrayLength);
-
-        final int targetParamCount = postSpreadType.parameterCount();
-        MethodType dropArrayArgs = postSpreadType.dropParameterTypes(
-                (targetParamCount - arrayLength), targetParamCount);
-        MethodType adapterType = dropArrayArgs.appendParameterTypes(arrayType);
-
-        return new Transformers.Spreader(this, adapterType, arrayLength);
-    }
-
-    /**
-     * See if {@code asSpreader} can be validly called with the given arguments.
-     * Return the type of the method handle call after spreading but before conversions.
-     */
-    private MethodType asSpreaderChecks(Class<?> arrayType, int arrayLength) {
-        spreadArrayChecks(arrayType, arrayLength);
-        int nargs = type().parameterCount();
-        if (nargs < arrayLength || arrayLength < 0)
-            throw newIllegalArgumentException("bad spread array length");
-        Class<?> arrayElement = arrayType.getComponentType();
-        MethodType mtype = type();
-        boolean match = true, fail = false;
-        for (int i = nargs - arrayLength; i < nargs; i++) {
-            Class<?> ptype = mtype.parameterType(i);
-            if (ptype != arrayElement) {
-                match = false;
-                if (!MethodType.canConvert(arrayElement, ptype)) {
-                    fail = true;
-                    break;
-                }
-            }
-        }
-        if (match)  return mtype;
-        MethodType needType = mtype.asSpreaderType(arrayType, arrayLength);
-        if (!fail)  return needType;
-        // elicit an error:
-        this.asType(needType);
-        throw newInternalError("should not return", null);
-    }
-
-    private void spreadArrayChecks(Class<?> arrayType, int arrayLength) {
-        Class<?> arrayElement = arrayType.getComponentType();
-        if (arrayElement == null)
-            throw newIllegalArgumentException("not an array type", arrayType);
-        if ((arrayLength & 0x7F) != arrayLength) {
-            if ((arrayLength & 0xFF) != arrayLength)
-                throw newIllegalArgumentException("array length is not legal", arrayLength);
-            assert(arrayLength >= 128);
-            if (arrayElement == long.class ||
-                arrayElement == double.class)
-                throw newIllegalArgumentException("array length is not legal for long[] or double[]", arrayLength);
-        }
-    }
-
-    /**
-     * Makes an <em>array-collecting</em> method handle, which accepts a given number of trailing
-     * positional arguments and collects them into an array argument.
-     * The new method handle adapts, as its <i>target</i>,
-     * the current method handle.  The type of the adapter will be
-     * the same as the type of the target, except that a single trailing
-     * parameter (usually of type {@code arrayType}) is replaced by
-     * {@code arrayLength} parameters whose type is element type of {@code arrayType}.
-     * <p>
-     * If the array type differs from the final argument type on the original target,
-     * the original target is adapted to take the array type directly,
-     * as if by a call to {@link #asType asType}.
-     * <p>
-     * When called, the adapter replaces its trailing {@code arrayLength}
-     * arguments by a single new array of type {@code arrayType}, whose elements
-     * comprise (in order) the replaced arguments.
-     * Finally the target is called.
-     * What the target eventually returns is returned unchanged by the adapter.
-     * <p>
-     * (The array may also be a shared constant when {@code arrayLength} is zero.)
-     * <p>
-     * (<em>Note:</em> The {@code arrayType} is often identical to the last
-     * parameter type of the original target.
-     * It is an explicit argument for symmetry with {@code asSpreader}, and also
-     * to allow the target to use a simple {@code Object} as its last parameter type.)
-     * <p>
-     * In order to create a collecting adapter which is not restricted to a particular
-     * number of collected arguments, use {@link #asVarargsCollector asVarargsCollector} instead.
-     * <p>
-     * Here are some examples of array-collecting method handles:
-     * <blockquote><pre>{@code
-MethodHandle deepToString = publicLookup()
-  .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-assertEquals("[won]",   (String) deepToString.invokeExact(new Object[]{"won"}));
-MethodHandle ts1 = deepToString.asCollector(Object[].class, 1);
-assertEquals(methodType(String.class, Object.class), ts1.type());
-//assertEquals("[won]", (String) ts1.invokeExact(         new Object[]{"won"})); //FAIL
-assertEquals("[[won]]", (String) ts1.invokeExact((Object) new Object[]{"won"}));
-// arrayType can be a subtype of Object[]
-MethodHandle ts2 = deepToString.asCollector(String[].class, 2);
-assertEquals(methodType(String.class, String.class, String.class), ts2.type());
-assertEquals("[two, too]", (String) ts2.invokeExact("two", "too"));
-MethodHandle ts0 = deepToString.asCollector(Object[].class, 0);
-assertEquals("[]", (String) ts0.invokeExact());
-// collectors can be nested, Lisp-style
-MethodHandle ts22 = deepToString.asCollector(Object[].class, 3).asCollector(String[].class, 2);
-assertEquals("[A, B, [C, D]]", ((String) ts22.invokeExact((Object)'A', (Object)"B", "C", "D")));
-// arrayType can be any primitive array type
-MethodHandle bytesToString = publicLookup()
-  .findStatic(Arrays.class, "toString", methodType(String.class, byte[].class))
-  .asCollector(byte[].class, 3);
-assertEquals("[1, 2, 3]", (String) bytesToString.invokeExact((byte)1, (byte)2, (byte)3));
-MethodHandle longsToString = publicLookup()
-  .findStatic(Arrays.class, "toString", methodType(String.class, long[].class))
-  .asCollector(long[].class, 1);
-assertEquals("[123]", (String) longsToString.invokeExact((long)123));
-     * }</pre></blockquote>
-     * @param arrayType often {@code Object[]}, the type of the array argument which will collect the arguments
-     * @param arrayLength the number of arguments to collect into a new array argument
-     * @return a new method handle which collects some trailing argument
-     *         into an array, before calling the original method handle
-     * @throws NullPointerException if {@code arrayType} is a null reference
-     * @throws IllegalArgumentException if {@code arrayType} is not an array type
-     *         or {@code arrayType} is not assignable to this method handle's trailing parameter type,
-     *         or {@code arrayLength} is not a legal array size,
-     *         or the resulting method handle's type would have
-     *         <a href="MethodHandle.html#maxarity">too many parameters</a>
-     * @throws WrongMethodTypeException if the implied {@code asType} call fails
-     * @see #asSpreader
-     * @see #asVarargsCollector
-     */
-    public MethodHandle asCollector(Class<?> arrayType, int arrayLength) {
-        asCollectorChecks(arrayType, arrayLength);
-
-        return new Transformers.Collector(this, arrayType, arrayLength);
-    }
-
-    /**
-     * See if {@code asCollector} can be validly called with the given arguments.
-     * Return false if the last parameter is not an exact match to arrayType.
-     */
-    /*non-public*/ boolean asCollectorChecks(Class<?> arrayType, int arrayLength) {
-        spreadArrayChecks(arrayType, arrayLength);
-        int nargs = type().parameterCount();
-        if (nargs != 0) {
-            Class<?> lastParam = type().parameterType(nargs-1);
-            if (lastParam == arrayType)  return true;
-            if (lastParam.isAssignableFrom(arrayType))  return false;
-        }
-        throw newIllegalArgumentException("array type not assignable to trailing argument", this, arrayType);
-    }
-
-    /**
-     * Makes a <em>variable arity</em> adapter which is able to accept
-     * any number of trailing positional arguments and collect them
-     * into an array argument.
-     * <p>
-     * The type and behavior of the adapter will be the same as
-     * the type and behavior of the target, except that certain
-     * {@code invoke} and {@code asType} requests can lead to
-     * trailing positional arguments being collected into target's
-     * trailing parameter.
-     * Also, the last parameter type of the adapter will be
-     * {@code arrayType}, even if the target has a different
-     * last parameter type.
-     * <p>
-     * This transformation may return {@code this} if the method handle is
-     * already of variable arity and its trailing parameter type
-     * is identical to {@code arrayType}.
-     * <p>
-     * When called with {@link #invokeExact invokeExact}, the adapter invokes
-     * the target with no argument changes.
-     * (<em>Note:</em> This behavior is different from a
-     * {@linkplain #asCollector fixed arity collector},
-     * since it accepts a whole array of indeterminate length,
-     * rather than a fixed number of arguments.)
-     * <p>
-     * When called with plain, inexact {@link #invoke invoke}, if the caller
-     * type is the same as the adapter, the adapter invokes the target as with
-     * {@code invokeExact}.
-     * (This is the normal behavior for {@code invoke} when types match.)
-     * <p>
-     * Otherwise, if the caller and adapter arity are the same, and the
-     * trailing parameter type of the caller is a reference type identical to
-     * or assignable to the trailing parameter type of the adapter,
-     * the arguments and return values are converted pairwise,
-     * as if by {@link #asType asType} on a fixed arity
-     * method handle.
-     * <p>
-     * Otherwise, the arities differ, or the adapter's trailing parameter
-     * type is not assignable from the corresponding caller type.
-     * In this case, the adapter replaces all trailing arguments from
-     * the original trailing argument position onward, by
-     * a new array of type {@code arrayType}, whose elements
-     * comprise (in order) the replaced arguments.
-     * <p>
-     * The caller type must provides as least enough arguments,
-     * and of the correct type, to satisfy the target's requirement for
-     * positional arguments before the trailing array argument.
-     * Thus, the caller must supply, at a minimum, {@code N-1} arguments,
-     * where {@code N} is the arity of the target.
-     * Also, there must exist conversions from the incoming arguments
-     * to the target's arguments.
-     * As with other uses of plain {@code invoke}, if these basic
-     * requirements are not fulfilled, a {@code WrongMethodTypeException}
-     * may be thrown.
-     * <p>
-     * In all cases, what the target eventually returns is returned unchanged by the adapter.
-     * <p>
-     * In the final case, it is exactly as if the target method handle were
-     * temporarily adapted with a {@linkplain #asCollector fixed arity collector}
-     * to the arity required by the caller type.
-     * (As with {@code asCollector}, if the array length is zero,
-     * a shared constant may be used instead of a new array.
-     * If the implied call to {@code asCollector} would throw
-     * an {@code IllegalArgumentException} or {@code WrongMethodTypeException},
-     * the call to the variable arity adapter must throw
-     * {@code WrongMethodTypeException}.)
-     * <p>
-     * The behavior of {@link #asType asType} is also specialized for
-     * variable arity adapters, to maintain the invariant that
-     * plain, inexact {@code invoke} is always equivalent to an {@code asType}
-     * call to adjust the target type, followed by {@code invokeExact}.
-     * Therefore, a variable arity adapter responds
-     * to an {@code asType} request by building a fixed arity collector,
-     * if and only if the adapter and requested type differ either
-     * in arity or trailing argument type.
-     * The resulting fixed arity collector has its type further adjusted
-     * (if necessary) to the requested type by pairwise conversion,
-     * as if by another application of {@code asType}.
-     * <p>
-     * When a method handle is obtained by executing an {@code ldc} instruction
-     * of a {@code CONSTANT_MethodHandle} constant, and the target method is marked
-     * as a variable arity method (with the modifier bit {@code 0x0080}),
-     * the method handle will accept multiple arities, as if the method handle
-     * constant were created by means of a call to {@code asVarargsCollector}.
-     * <p>
-     * In order to create a collecting adapter which collects a predetermined
-     * number of arguments, and whose type reflects this predetermined number,
-     * use {@link #asCollector asCollector} instead.
-     * <p>
-     * No method handle transformations produce new method handles with
-     * variable arity, unless they are documented as doing so.
-     * Therefore, besides {@code asVarargsCollector},
-     * all methods in {@code MethodHandle} and {@code MethodHandles}
-     * will return a method handle with fixed arity,
-     * except in the cases where they are specified to return their original
-     * operand (e.g., {@code asType} of the method handle's own type).
-     * <p>
-     * Calling {@code asVarargsCollector} on a method handle which is already
-     * of variable arity will produce a method handle with the same type and behavior.
-     * It may (or may not) return the original variable arity method handle.
-     * <p>
-     * Here is an example, of a list-making variable arity method handle:
-     * <blockquote><pre>{@code
-MethodHandle deepToString = publicLookup()
-  .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-MethodHandle ts1 = deepToString.asVarargsCollector(Object[].class);
-assertEquals("[won]",   (String) ts1.invokeExact(    new Object[]{"won"}));
-assertEquals("[won]",   (String) ts1.invoke(         new Object[]{"won"}));
-assertEquals("[won]",   (String) ts1.invoke(                      "won" ));
-assertEquals("[[won]]", (String) ts1.invoke((Object) new Object[]{"won"}));
-// findStatic of Arrays.asList(...) produces a variable arity method handle:
-MethodHandle asList = publicLookup()
-  .findStatic(Arrays.class, "asList", methodType(List.class, Object[].class));
-assertEquals(methodType(List.class, Object[].class), asList.type());
-assert(asList.isVarargsCollector());
-assertEquals("[]", asList.invoke().toString());
-assertEquals("[1]", asList.invoke(1).toString());
-assertEquals("[two, too]", asList.invoke("two", "too").toString());
-String[] argv = { "three", "thee", "tee" };
-assertEquals("[three, thee, tee]", asList.invoke(argv).toString());
-assertEquals("[three, thee, tee]", asList.invoke((Object[])argv).toString());
-List ls = (List) asList.invoke((Object)argv);
-assertEquals(1, ls.size());
-assertEquals("[three, thee, tee]", Arrays.toString((Object[])ls.get(0)));
-     * }</pre></blockquote>
-     * <p style="font-size:smaller;">
-     * <em>Discussion:</em>
-     * These rules are designed as a dynamically-typed variation
-     * of the Java rules for variable arity methods.
-     * In both cases, callers to a variable arity method or method handle
-     * can either pass zero or more positional arguments, or else pass
-     * pre-collected arrays of any length.  Users should be aware of the
-     * special role of the final argument, and of the effect of a
-     * type match on that final argument, which determines whether
-     * or not a single trailing argument is interpreted as a whole
-     * array or a single element of an array to be collected.
-     * Note that the dynamic type of the trailing argument has no
-     * effect on this decision, only a comparison between the symbolic
-     * type descriptor of the call site and the type descriptor of the method handle.)
-     *
-     * @param arrayType often {@code Object[]}, the type of the array argument which will collect the arguments
-     * @return a new method handle which can collect any number of trailing arguments
-     *         into an array, before calling the original method handle
-     * @throws NullPointerException if {@code arrayType} is a null reference
-     * @throws IllegalArgumentException if {@code arrayType} is not an array type
-     *         or {@code arrayType} is not assignable to this method handle's trailing parameter type
-     * @see #asCollector
-     * @see #isVarargsCollector
-     * @see #asFixedArity
-     */
-    public MethodHandle asVarargsCollector(Class<?> arrayType) {
-        arrayType.getClass(); // explicit NPE
-        boolean lastMatch = asCollectorChecks(arrayType, 0);
-        if (isVarargsCollector() && lastMatch)
-            return this;
-
-        return new Transformers.VarargsCollector(this);
-    }
-
-    /**
-     * Determines if this method handle
-     * supports {@linkplain #asVarargsCollector variable arity} calls.
-     * Such method handles arise from the following sources:
-     * <ul>
-     * <li>a call to {@linkplain #asVarargsCollector asVarargsCollector}
-     * <li>a call to a {@linkplain java.lang.invoke.MethodHandles.Lookup lookup method}
-     *     which resolves to a variable arity Java method or constructor
-     * <li>an {@code ldc} instruction of a {@code CONSTANT_MethodHandle}
-     *     which resolves to a variable arity Java method or constructor
-     * </ul>
-     * @return true if this method handle accepts more than one arity of plain, inexact {@code invoke} calls
-     * @see #asVarargsCollector
-     * @see #asFixedArity
-     */
-    public boolean isVarargsCollector() {
-        return false;
-    }
-
-    /**
-     * Makes a <em>fixed arity</em> method handle which is otherwise
-     * equivalent to the current method handle.
-     * <p>
-     * If the current method handle is not of
-     * {@linkplain #asVarargsCollector variable arity},
-     * the current method handle is returned.
-     * This is true even if the current method handle
-     * could not be a valid input to {@code asVarargsCollector}.
-     * <p>
-     * Otherwise, the resulting fixed-arity method handle has the same
-     * type and behavior of the current method handle,
-     * except that {@link #isVarargsCollector isVarargsCollector}
-     * will be false.
-     * The fixed-arity method handle may (or may not) be the
-     * a previous argument to {@code asVarargsCollector}.
-     * <p>
-     * Here is an example, of a list-making variable arity method handle:
-     * <blockquote><pre>{@code
-MethodHandle asListVar = publicLookup()
-  .findStatic(Arrays.class, "asList", methodType(List.class, Object[].class))
-  .asVarargsCollector(Object[].class);
-MethodHandle asListFix = asListVar.asFixedArity();
-assertEquals("[1]", asListVar.invoke(1).toString());
-Exception caught = null;
-try { asListFix.invoke((Object)1); }
-catch (Exception ex) { caught = ex; }
-assert(caught instanceof ClassCastException);
-assertEquals("[two, too]", asListVar.invoke("two", "too").toString());
-try { asListFix.invoke("two", "too"); }
-catch (Exception ex) { caught = ex; }
-assert(caught instanceof WrongMethodTypeException);
-Object[] argv = { "three", "thee", "tee" };
-assertEquals("[three, thee, tee]", asListVar.invoke(argv).toString());
-assertEquals("[three, thee, tee]", asListFix.invoke(argv).toString());
-assertEquals(1, ((List) asListVar.invoke((Object)argv)).size());
-assertEquals("[three, thee, tee]", asListFix.invoke((Object)argv).toString());
-     * }</pre></blockquote>
-     *
-     * @return a new method handle which accepts only a fixed number of arguments
-     * @see #asVarargsCollector
-     * @see #isVarargsCollector
-     */
-    public MethodHandle asFixedArity() {
-        // Android-changed: implementation specific.
-        MethodHandle mh = this;
-        if (mh.isVarargsCollector()) {
-            mh = ((Transformers.VarargsCollector) mh).asFixedArity();
-        }
-        assert(!mh.isVarargsCollector());
-        return mh;
-    }
-
-    /**
-     * Binds a value {@code x} to the first argument of a method handle, without invoking it.
-     * The new method handle adapts, as its <i>target</i>,
-     * the current method handle by binding it to the given argument.
-     * The type of the bound handle will be
-     * the same as the type of the target, except that a single leading
-     * reference parameter will be omitted.
-     * <p>
-     * When called, the bound handle inserts the given value {@code x}
-     * as a new leading argument to the target.  The other arguments are
-     * also passed unchanged.
-     * What the target eventually returns is returned unchanged by the bound handle.
-     * <p>
-     * The reference {@code x} must be convertible to the first parameter
-     * type of the target.
-     * <p>
-     * (<em>Note:</em>  Because method handles are immutable, the target method handle
-     * retains its original type and behavior.)
-     * @param x  the value to bind to the first argument of the target
-     * @return a new method handle which prepends the given value to the incoming
-     *         argument list, before calling the original method handle
-     * @throws IllegalArgumentException if the target does not have a
-     *         leading parameter type that is a reference type
-     * @throws ClassCastException if {@code x} cannot be converted
-     *         to the leading parameter type of the target
-     * @see MethodHandles#insertArguments
-     */
-    public MethodHandle bindTo(Object x) {
-        x = type.leadingReferenceParameter().cast(x);  // throw CCE if needed
-
-        return new Transformers.BindTo(this, x);
-    }
-
-    /**
-     * Returns a string representation of the method handle,
-     * starting with the string {@code "MethodHandle"} and
-     * ending with the string representation of the method handle's type.
-     * In other words, this method returns a string equal to the value of:
-     * <blockquote><pre>{@code
-     * "MethodHandle" + type().toString()
-     * }</pre></blockquote>
-     * <p>
-     * (<em>Note:</em>  Future releases of this API may add further information
-     * to the string representation.
-     * Therefore, the present syntax should not be parsed by applications.)
-     *
-     * @return a string representation of the method handle
-     */
-    @Override
-    public String toString() {
-        // Android-changed: Removed debugging support.
-        return "MethodHandle"+type;
-    }
-
-    /** @hide */
-    public int getHandleKind() {
-        return handleKind;
-    }
-
-    /** @hide */
-    protected void transform(EmulatedStackFrame arguments) throws Throwable {
-        throw new AssertionError("MethodHandle.transform should never be called.");
-    }
-
-    /**
-     * Creates a copy of this method handle, copying all relevant data.
-     *
-     * @hide
-     */
-    protected MethodHandle duplicate() {
-        try {
-            return (MethodHandle) this.clone();
-        } catch (CloneNotSupportedException cnse) {
-            throw new AssertionError("Subclass of Transformer is not cloneable");
-        }
-    }
-
-
-    /**
-     * This is the entry point for all transform calls, and dispatches to the protected
-     * transform method. This layer of indirection exists purely for convenience, because
-     * we can invoke-direct on a fixed ArtMethod for all transform variants.
-     *
-     * NOTE: If this extra layer of indirection proves to be a problem, we can get rid
-     * of this layer of indirection at the cost of some additional ugliness.
-     */
-    private void transformInternal(EmulatedStackFrame arguments) throws Throwable {
-        transform(arguments);
-    }
-
-    // Android-changed: Removed implementation details :
-    //
-    // String standardString();
-    // String debugString();
-    //
-    //// Implementation methods.
-    //// Sub-classes can override these default implementations.
-    //// All these methods assume arguments are already validated.
-    //
-    // Other transforms to do:  convert, explicitCast, permute, drop, filter, fold, GWT, catch
-    //
-    // BoundMethodHandle bindArgumentL(int pos, Object value);
-    // /*non-public*/ MethodHandle setVarargs(MemberName member);
-    // /*non-public*/ MethodHandle viewAsType(MethodType newType, boolean strict);
-    // /*non-public*/ boolean viewAsTypeChecks(MethodType newType, boolean strict);
-    //
-    // Decoding
-    //
-    // /*non-public*/ LambdaForm internalForm();
-    // /*non-public*/ MemberName internalMemberName();
-    // /*non-public*/ Class<?> internalCallerClass();
-    // /*non-public*/ MethodHandleImpl.Intrinsic intrinsicName();
-    // /*non-public*/ MethodHandle withInternalMemberName(MemberName member, boolean isInvokeSpecial);
-    // /*non-public*/ boolean isInvokeSpecial();
-    // /*non-public*/ Object internalValues();
-    // /*non-public*/ Object internalProperties();
-    //
-    //// Method handle implementation methods.
-    //// Sub-classes can override these default implementations.
-    //// All these methods assume arguments are already validated.
-    //
-    // /*non-public*/ abstract MethodHandle copyWith(MethodType mt, LambdaForm lf);
-    // abstract BoundMethodHandle rebind();
-    // /*non-public*/ void updateForm(LambdaForm newForm);
-    // /*non-public*/ void customize();
-    // private static final long FORM_OFFSET;
 }
diff --git a/java/lang/invoke/MethodHandles.java b/java/lang/invoke/MethodHandles.java
index bc1e944..f27ad98 100644
--- a/java/lang/invoke/MethodHandles.java
+++ b/java/lang/invoke/MethodHandles.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2008, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,3479 +25,129 @@
 
 package java.lang.invoke;
 
-import java.lang.reflect.*;
-import java.nio.ByteOrder;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
 import java.util.List;
-import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.NoSuchElementException;
 
-import dalvik.system.VMStack;
-import sun.invoke.util.VerifyAccess;
-import sun.invoke.util.Wrapper;
-import static java.lang.invoke.MethodHandleStatics.*;
-
-/**
- * This class consists exclusively of static methods that operate on or return
- * method handles. They fall into several categories:
- * <ul>
- * <li>Lookup methods which help create method handles for methods and fields.
- * <li>Combinator methods, which combine or transform pre-existing method handles into new ones.
- * <li>Other factory methods to create method handles that emulate other common JVM operations or control flow patterns.
- * </ul>
- * <p>
- * @author John Rose, JSR 292 EG
- * @since 1.7
- */
 public class MethodHandles {
 
-    private MethodHandles() { }  // do not instantiate
+    public static Lookup lookup() { return null; }
 
-    // Android-changed: We do not use MemberName / MethodHandleImpl.
-    //
-    // private static final MemberName.Factory IMPL_NAMES = MemberName.getFactory();
-    // static { MethodHandleImpl.initStatics(); }
-    // See IMPL_LOOKUP below.
+    public static Lookup publicLookup() { return null; }
 
-    //// Method handle creation from ordinary methods.
-
-    /**
-     * Returns a {@link Lookup lookup object} with
-     * full capabilities to emulate all supported bytecode behaviors of the caller.
-     * These capabilities include <a href="MethodHandles.Lookup.html#privacc">private access</a> to the caller.
-     * Factory methods on the lookup object can create
-     * <a href="MethodHandleInfo.html#directmh">direct method handles</a>
-     * for any member that the caller has access to via bytecodes,
-     * including protected and private fields and methods.
-     * This lookup object is a <em>capability</em> which may be delegated to trusted agents.
-     * Do not store it in place where untrusted code can access it.
-     * <p>
-     * This method is caller sensitive, which means that it may return different
-     * values to different callers.
-     * <p>
-     * For any given caller class {@code C}, the lookup object returned by this call
-     * has equivalent capabilities to any lookup object
-     * supplied by the JVM to the bootstrap method of an
-     * <a href="package-summary.html#indyinsn">invokedynamic instruction</a>
-     * executing in the same caller class {@code C}.
-     * @return a lookup object for the caller of this method, with private access
-     */
-    // Android-changed: Remove caller sensitive.
-    // @CallerSensitive
-    public static Lookup lookup() {
-        // Android-changed: Do not use Reflection.getCallerClass().
-        return new Lookup(VMStack.getStackClass1());
-    }
-
-    /**
-     * Returns a {@link Lookup lookup object} which is trusted minimally.
-     * It can only be used to create method handles to
-     * publicly accessible fields and methods.
-     * <p>
-     * As a matter of pure convention, the {@linkplain Lookup#lookupClass lookup class}
-     * of this lookup object will be {@link java.lang.Object}.
-     *
-     * <p style="font-size:smaller;">
-     * <em>Discussion:</em>
-     * The lookup class can be changed to any other class {@code C} using an expression of the form
-     * {@link Lookup#in publicLookup().in(C.class)}.
-     * Since all classes have equal access to public names,
-     * such a change would confer no new access rights.
-     * A public lookup object is always subject to
-     * <a href="MethodHandles.Lookup.html#secmgr">security manager checks</a>.
-     * Also, it cannot access
-     * <a href="MethodHandles.Lookup.html#callsens">caller sensitive methods</a>.
-     * @return a lookup object which is trusted minimally
-     */
-    public static Lookup publicLookup() {
-        return Lookup.PUBLIC_LOOKUP;
-    }
-
-    /**
-     * Performs an unchecked "crack" of a
-     * <a href="MethodHandleInfo.html#directmh">direct method handle</a>.
-     * The result is as if the user had obtained a lookup object capable enough
-     * to crack the target method handle, called
-     * {@link java.lang.invoke.MethodHandles.Lookup#revealDirect Lookup.revealDirect}
-     * on the target to obtain its symbolic reference, and then called
-     * {@link java.lang.invoke.MethodHandleInfo#reflectAs MethodHandleInfo.reflectAs}
-     * to resolve the symbolic reference to a member.
-     * <p>
-     * If there is a security manager, its {@code checkPermission} method
-     * is called with a {@code ReflectPermission("suppressAccessChecks")} permission.
-     * @param <T> the desired type of the result, either {@link Member} or a subtype
-     * @param target a direct method handle to crack into symbolic reference components
-     * @param expected a class object representing the desired result type {@code T}
-     * @return a reference to the method, constructor, or field object
-     * @exception SecurityException if the caller is not privileged to call {@code setAccessible}
-     * @exception NullPointerException if either argument is {@code null}
-     * @exception IllegalArgumentException if the target is not a direct method handle
-     * @exception ClassCastException if the member is not of the expected type
-     * @since 1.8
-     */
     public static <T extends Member> T
-    reflectAs(Class<T> expected, MethodHandle target) {
-        MethodHandleImpl directTarget = getMethodHandleImpl(target);
-        // Given that this is specified to be an "unchecked" crack, we can directly allocate
-        // a member from the underlying ArtField / Method and bypass all associated access checks.
-        return expected.cast(directTarget.getMemberInternal());
-    }
+    reflectAs(Class<T> expected, MethodHandle target) { return null; }
 
-    /**
-     * A <em>lookup object</em> is a factory for creating method handles,
-     * when the creation requires access checking.
-     * Method handles do not perform
-     * access checks when they are called, but rather when they are created.
-     * Therefore, method handle access
-     * restrictions must be enforced when a method handle is created.
-     * The caller class against which those restrictions are enforced
-     * is known as the {@linkplain #lookupClass lookup class}.
-     * <p>
-     * A lookup class which needs to create method handles will call
-     * {@link #lookup MethodHandles.lookup} to create a factory for itself.
-     * When the {@code Lookup} factory object is created, the identity of the lookup class is
-     * determined, and securely stored in the {@code Lookup} object.
-     * The lookup class (or its delegates) may then use factory methods
-     * on the {@code Lookup} object to create method handles for access-checked members.
-     * This includes all methods, constructors, and fields which are allowed to the lookup class,
-     * even private ones.
-     *
-     * <h1><a name="lookups"></a>Lookup Factory Methods</h1>
-     * The factory methods on a {@code Lookup} object correspond to all major
-     * use cases for methods, constructors, and fields.
-     * Each method handle created by a factory method is the functional
-     * equivalent of a particular <em>bytecode behavior</em>.
-     * (Bytecode behaviors are described in section 5.4.3.5 of the Java Virtual Machine Specification.)
-     * Here is a summary of the correspondence between these factory methods and
-     * the behavior the resulting method handles:
-     * <table border=1 cellpadding=5 summary="lookup method behaviors">
-     * <tr>
-     *     <th><a name="equiv"></a>lookup expression</th>
-     *     <th>member</th>
-     *     <th>bytecode behavior</th>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findGetter lookup.findGetter(C.class,"f",FT.class)}</td>
-     *     <td>{@code FT f;}</td><td>{@code (T) this.f;}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findStaticGetter lookup.findStaticGetter(C.class,"f",FT.class)}</td>
-     *     <td>{@code static}<br>{@code FT f;}</td><td>{@code (T) C.f;}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findSetter lookup.findSetter(C.class,"f",FT.class)}</td>
-     *     <td>{@code FT f;}</td><td>{@code this.f = x;}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findStaticSetter lookup.findStaticSetter(C.class,"f",FT.class)}</td>
-     *     <td>{@code static}<br>{@code FT f;}</td><td>{@code C.f = arg;}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findVirtual lookup.findVirtual(C.class,"m",MT)}</td>
-     *     <td>{@code T m(A*);}</td><td>{@code (T) this.m(arg*);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findStatic lookup.findStatic(C.class,"m",MT)}</td>
-     *     <td>{@code static}<br>{@code T m(A*);}</td><td>{@code (T) C.m(arg*);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findSpecial lookup.findSpecial(C.class,"m",MT,this.class)}</td>
-     *     <td>{@code T m(A*);}</td><td>{@code (T) super.m(arg*);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#findConstructor lookup.findConstructor(C.class,MT)}</td>
-     *     <td>{@code C(A*);}</td><td>{@code new C(arg*);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectGetter lookup.unreflectGetter(aField)}</td>
-     *     <td>({@code static})?<br>{@code FT f;}</td><td>{@code (FT) aField.get(thisOrNull);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectSetter lookup.unreflectSetter(aField)}</td>
-     *     <td>({@code static})?<br>{@code FT f;}</td><td>{@code aField.set(thisOrNull, arg);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflect lookup.unreflect(aMethod)}</td>
-     *     <td>({@code static})?<br>{@code T m(A*);}</td><td>{@code (T) aMethod.invoke(thisOrNull, arg*);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectConstructor lookup.unreflectConstructor(aConstructor)}</td>
-     *     <td>{@code C(A*);}</td><td>{@code (C) aConstructor.newInstance(arg*);}</td>
-     * </tr>
-     * <tr>
-     *     <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflect lookup.unreflect(aMethod)}</td>
-     *     <td>({@code static})?<br>{@code T m(A*);}</td><td>{@code (T) aMethod.invoke(thisOrNull, arg*);}</td>
-     * </tr>
-     * </table>
-     *
-     * Here, the type {@code C} is the class or interface being searched for a member,
-     * documented as a parameter named {@code refc} in the lookup methods.
-     * The method type {@code MT} is composed from the return type {@code T}
-     * and the sequence of argument types {@code A*}.
-     * The constructor also has a sequence of argument types {@code A*} and
-     * is deemed to return the newly-created object of type {@code C}.
-     * Both {@code MT} and the field type {@code FT} are documented as a parameter named {@code type}.
-     * The formal parameter {@code this} stands for the self-reference of type {@code C};
-     * if it is present, it is always the leading argument to the method handle invocation.
-     * (In the case of some {@code protected} members, {@code this} may be
-     * restricted in type to the lookup class; see below.)
-     * The name {@code arg} stands for all the other method handle arguments.
-     * In the code examples for the Core Reflection API, the name {@code thisOrNull}
-     * stands for a null reference if the accessed method or field is static,
-     * and {@code this} otherwise.
-     * The names {@code aMethod}, {@code aField}, and {@code aConstructor} stand
-     * for reflective objects corresponding to the given members.
-     * <p>
-     * In cases where the given member is of variable arity (i.e., a method or constructor)
-     * the returned method handle will also be of {@linkplain MethodHandle#asVarargsCollector variable arity}.
-     * In all other cases, the returned method handle will be of fixed arity.
-     * <p style="font-size:smaller;">
-     * <em>Discussion:</em>
-     * The equivalence between looked-up method handles and underlying
-     * class members and bytecode behaviors
-     * can break down in a few ways:
-     * <ul style="font-size:smaller;">
-     * <li>If {@code C} is not symbolically accessible from the lookup class's loader,
-     * the lookup can still succeed, even when there is no equivalent
-     * Java expression or bytecoded constant.
-     * <li>Likewise, if {@code T} or {@code MT}
-     * is not symbolically accessible from the lookup class's loader,
-     * the lookup can still succeed.
-     * For example, lookups for {@code MethodHandle.invokeExact} and
-     * {@code MethodHandle.invoke} will always succeed, regardless of requested type.
-     * <li>If there is a security manager installed, it can forbid the lookup
-     * on various grounds (<a href="MethodHandles.Lookup.html#secmgr">see below</a>).
-     * By contrast, the {@code ldc} instruction on a {@code CONSTANT_MethodHandle}
-     * constant is not subject to security manager checks.
-     * <li>If the looked-up method has a
-     * <a href="MethodHandle.html#maxarity">very large arity</a>,
-     * the method handle creation may fail, due to the method handle
-     * type having too many parameters.
-     * </ul>
-     *
-     * <h1><a name="access"></a>Access checking</h1>
-     * Access checks are applied in the factory methods of {@code Lookup},
-     * when a method handle is created.
-     * This is a key difference from the Core Reflection API, since
-     * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
-     * performs access checking against every caller, on every call.
-     * <p>
-     * All access checks start from a {@code Lookup} object, which
-     * compares its recorded lookup class against all requests to
-     * create method handles.
-     * A single {@code Lookup} object can be used to create any number
-     * of access-checked method handles, all checked against a single
-     * lookup class.
-     * <p>
-     * A {@code Lookup} object can be shared with other trusted code,
-     * such as a metaobject protocol.
-     * A shared {@code Lookup} object delegates the capability
-     * to create method handles on private members of the lookup class.
-     * Even if privileged code uses the {@code Lookup} object,
-     * the access checking is confined to the privileges of the
-     * original lookup class.
-     * <p>
-     * A lookup can fail, because
-     * the containing class is not accessible to the lookup class, or
-     * because the desired class member is missing, or because the
-     * desired class member is not accessible to the lookup class, or
-     * because the lookup object is not trusted enough to access the member.
-     * In any of these cases, a {@code ReflectiveOperationException} will be
-     * thrown from the attempted lookup.  The exact class will be one of
-     * the following:
-     * <ul>
-     * <li>NoSuchMethodException &mdash; if a method is requested but does not exist
-     * <li>NoSuchFieldException &mdash; if a field is requested but does not exist
-     * <li>IllegalAccessException &mdash; if the member exists but an access check fails
-     * </ul>
-     * <p>
-     * In general, the conditions under which a method handle may be
-     * looked up for a method {@code M} are no more restrictive than the conditions
-     * under which the lookup class could have compiled, verified, and resolved a call to {@code M}.
-     * Where the JVM would raise exceptions like {@code NoSuchMethodError},
-     * a method handle lookup will generally raise a corresponding
-     * checked exception, such as {@code NoSuchMethodException}.
-     * And the effect of invoking the method handle resulting from the lookup
-     * is <a href="MethodHandles.Lookup.html#equiv">exactly equivalent</a>
-     * to executing the compiled, verified, and resolved call to {@code M}.
-     * The same point is true of fields and constructors.
-     * <p style="font-size:smaller;">
-     * <em>Discussion:</em>
-     * Access checks only apply to named and reflected methods,
-     * constructors, and fields.
-     * Other method handle creation methods, such as
-     * {@link MethodHandle#asType MethodHandle.asType},
-     * do not require any access checks, and are used
-     * independently of any {@code Lookup} object.
-     * <p>
-     * If the desired member is {@code protected}, the usual JVM rules apply,
-     * including the requirement that the lookup class must be either be in the
-     * same package as the desired member, or must inherit that member.
-     * (See the Java Virtual Machine Specification, sections 4.9.2, 5.4.3.5, and 6.4.)
-     * In addition, if the desired member is a non-static field or method
-     * in a different package, the resulting method handle may only be applied
-     * to objects of the lookup class or one of its subclasses.
-     * This requirement is enforced by narrowing the type of the leading
-     * {@code this} parameter from {@code C}
-     * (which will necessarily be a superclass of the lookup class)
-     * to the lookup class itself.
-     * <p>
-     * The JVM imposes a similar requirement on {@code invokespecial} instruction,
-     * that the receiver argument must match both the resolved method <em>and</em>
-     * the current class.  Again, this requirement is enforced by narrowing the
-     * type of the leading parameter to the resulting method handle.
-     * (See the Java Virtual Machine Specification, section 4.10.1.9.)
-     * <p>
-     * The JVM represents constructors and static initializer blocks as internal methods
-     * with special names ({@code "<init>"} and {@code "<clinit>"}).
-     * The internal syntax of invocation instructions allows them to refer to such internal
-     * methods as if they were normal methods, but the JVM bytecode verifier rejects them.
-     * A lookup of such an internal method will produce a {@code NoSuchMethodException}.
-     * <p>
-     * In some cases, access between nested classes is obtained by the Java compiler by creating
-     * an wrapper method to access a private method of another class
-     * in the same top-level declaration.
-     * For example, a nested class {@code C.D}
-     * can access private members within other related classes such as
-     * {@code C}, {@code C.D.E}, or {@code C.B},
-     * but the Java compiler may need to generate wrapper methods in
-     * those related classes.  In such cases, a {@code Lookup} object on
-     * {@code C.E} would be unable to those private members.
-     * A workaround for this limitation is the {@link Lookup#in Lookup.in} method,
-     * which can transform a lookup on {@code C.E} into one on any of those other
-     * classes, without special elevation of privilege.
-     * <p>
-     * The accesses permitted to a given lookup object may be limited,
-     * according to its set of {@link #lookupModes lookupModes},
-     * to a subset of members normally accessible to the lookup class.
-     * For example, the {@link #publicLookup publicLookup}
-     * method produces a lookup object which is only allowed to access
-     * public members in public classes.
-     * The caller sensitive method {@link #lookup lookup}
-     * produces a lookup object with full capabilities relative to
-     * its caller class, to emulate all supported bytecode behaviors.
-     * Also, the {@link Lookup#in Lookup.in} method may produce a lookup object
-     * with fewer access modes than the original lookup object.
-     *
-     * <p style="font-size:smaller;">
-     * <a name="privacc"></a>
-     * <em>Discussion of private access:</em>
-     * We say that a lookup has <em>private access</em>
-     * if its {@linkplain #lookupModes lookup modes}
-     * include the possibility of accessing {@code private} members.
-     * As documented in the relevant methods elsewhere,
-     * only lookups with private access possess the following capabilities:
-     * <ul style="font-size:smaller;">
-     * <li>access private fields, methods, and constructors of the lookup class
-     * <li>create method handles which invoke <a href="MethodHandles.Lookup.html#callsens">caller sensitive</a> methods,
-     *     such as {@code Class.forName}
-     * <li>create method handles which {@link Lookup#findSpecial emulate invokespecial} instructions
-     * <li>avoid <a href="MethodHandles.Lookup.html#secmgr">package access checks</a>
-     *     for classes accessible to the lookup class
-     * <li>create {@link Lookup#in delegated lookup objects} which have private access to other classes
-     *     within the same package member
-     * </ul>
-     * <p style="font-size:smaller;">
-     * Each of these permissions is a consequence of the fact that a lookup object
-     * with private access can be securely traced back to an originating class,
-     * whose <a href="MethodHandles.Lookup.html#equiv">bytecode behaviors</a> and Java language access permissions
-     * can be reliably determined and emulated by method handles.
-     *
-     * <h1><a name="secmgr"></a>Security manager interactions</h1>
-     * Although bytecode instructions can only refer to classes in
-     * a related class loader, this API can search for methods in any
-     * class, as long as a reference to its {@code Class} object is
-     * available.  Such cross-loader references are also possible with the
-     * Core Reflection API, and are impossible to bytecode instructions
-     * such as {@code invokestatic} or {@code getfield}.
-     * There is a {@linkplain java.lang.SecurityManager security manager API}
-     * to allow applications to check such cross-loader references.
-     * These checks apply to both the {@code MethodHandles.Lookup} API
-     * and the Core Reflection API
-     * (as found on {@link java.lang.Class Class}).
-     * <p>
-     * If a security manager is present, member lookups are subject to
-     * additional checks.
-     * From one to three calls are made to the security manager.
-     * Any of these calls can refuse access by throwing a
-     * {@link java.lang.SecurityException SecurityException}.
-     * Define {@code smgr} as the security manager,
-     * {@code lookc} as the lookup class of the current lookup object,
-     * {@code refc} as the containing class in which the member
-     * is being sought, and {@code defc} as the class in which the
-     * member is actually defined.
-     * The value {@code lookc} is defined as <em>not present</em>
-     * if the current lookup object does not have
-     * <a href="MethodHandles.Lookup.html#privacc">private access</a>.
-     * The calls are made according to the following rules:
-     * <ul>
-     * <li><b>Step 1:</b>
-     *     If {@code lookc} is not present, or if its class loader is not
-     *     the same as or an ancestor of the class loader of {@code refc},
-     *     then {@link SecurityManager#checkPackageAccess
-     *     smgr.checkPackageAccess(refcPkg)} is called,
-     *     where {@code refcPkg} is the package of {@code refc}.
-     * <li><b>Step 2:</b>
-     *     If the retrieved member is not public and
-     *     {@code lookc} is not present, then
-     *     {@link SecurityManager#checkPermission smgr.checkPermission}
-     *     with {@code RuntimePermission("accessDeclaredMembers")} is called.
-     * <li><b>Step 3:</b>
-     *     If the retrieved member is not public,
-     *     and if {@code lookc} is not present,
-     *     and if {@code defc} and {@code refc} are different,
-     *     then {@link SecurityManager#checkPackageAccess
-     *     smgr.checkPackageAccess(defcPkg)} is called,
-     *     where {@code defcPkg} is the package of {@code defc}.
-     * </ul>
-     * Security checks are performed after other access checks have passed.
-     * Therefore, the above rules presuppose a member that is public,
-     * or else that is being accessed from a lookup class that has
-     * rights to access the member.
-     *
-     * <h1><a name="callsens"></a>Caller sensitive methods</h1>
-     * A small number of Java methods have a special property called caller sensitivity.
-     * A <em>caller-sensitive</em> method can behave differently depending on the
-     * identity of its immediate caller.
-     * <p>
-     * If a method handle for a caller-sensitive method is requested,
-     * the general rules for <a href="MethodHandles.Lookup.html#equiv">bytecode behaviors</a> apply,
-     * but they take account of the lookup class in a special way.
-     * The resulting method handle behaves as if it were called
-     * from an instruction contained in the lookup class,
-     * so that the caller-sensitive method detects the lookup class.
-     * (By contrast, the invoker of the method handle is disregarded.)
-     * Thus, in the case of caller-sensitive methods,
-     * different lookup classes may give rise to
-     * differently behaving method handles.
-     * <p>
-     * In cases where the lookup object is
-     * {@link #publicLookup publicLookup()},
-     * or some other lookup object without
-     * <a href="MethodHandles.Lookup.html#privacc">private access</a>,
-     * the lookup class is disregarded.
-     * In such cases, no caller-sensitive method handle can be created,
-     * access is forbidden, and the lookup fails with an
-     * {@code IllegalAccessException}.
-     * <p style="font-size:smaller;">
-     * <em>Discussion:</em>
-     * For example, the caller-sensitive method
-     * {@link java.lang.Class#forName(String) Class.forName(x)}
-     * can return varying classes or throw varying exceptions,
-     * depending on the class loader of the class that calls it.
-     * A public lookup of {@code Class.forName} will fail, because
-     * there is no reasonable way to determine its bytecode behavior.
-     * <p style="font-size:smaller;">
-     * If an application caches method handles for broad sharing,
-     * it should use {@code publicLookup()} to create them.
-     * If there is a lookup of {@code Class.forName}, it will fail,
-     * and the application must take appropriate action in that case.
-     * It may be that a later lookup, perhaps during the invocation of a
-     * bootstrap method, can incorporate the specific identity
-     * of the caller, making the method accessible.
-     * <p style="font-size:smaller;">
-     * The function {@code MethodHandles.lookup} is caller sensitive
-     * so that there can be a secure foundation for lookups.
-     * Nearly all other methods in the JSR 292 API rely on lookup
-     * objects to check access requests.
-     */
-    // Android-changed: Change link targets from MethodHandles#[public]Lookup to
-    // #[public]Lookup to work around complaints from javadoc.
     public static final
     class Lookup {
-        /** The class on behalf of whom the lookup is being performed. */
-        /* @NonNull */ private final Class<?> lookupClass;
+        public static final int PUBLIC = 0;
 
-        /** The allowed sorts of members which may be looked up (PUBLIC, etc.). */
-        private final int allowedModes;
+        public static final int PRIVATE = 0;
 
-        /** A single-bit mask representing {@code public} access,
-         *  which may contribute to the result of {@link #lookupModes lookupModes}.
-         *  The value, {@code 0x01}, happens to be the same as the value of the
-         *  {@code public} {@linkplain java.lang.reflect.Modifier#PUBLIC modifier bit}.
-         */
-        public static final int PUBLIC = Modifier.PUBLIC;
+        public static final int PROTECTED = 0;
 
-        /** A single-bit mask representing {@code private} access,
-         *  which may contribute to the result of {@link #lookupModes lookupModes}.
-         *  The value, {@code 0x02}, happens to be the same as the value of the
-         *  {@code private} {@linkplain java.lang.reflect.Modifier#PRIVATE modifier bit}.
-         */
-        public static final int PRIVATE = Modifier.PRIVATE;
+        public static final int PACKAGE =  0;
 
-        /** A single-bit mask representing {@code protected} access,
-         *  which may contribute to the result of {@link #lookupModes lookupModes}.
-         *  The value, {@code 0x04}, happens to be the same as the value of the
-         *  {@code protected} {@linkplain java.lang.reflect.Modifier#PROTECTED modifier bit}.
-         */
-        public static final int PROTECTED = Modifier.PROTECTED;
+        public Class<?> lookupClass() { return null; }
 
-        /** A single-bit mask representing {@code package} access (default access),
-         *  which may contribute to the result of {@link #lookupModes lookupModes}.
-         *  The value is {@code 0x08}, which does not correspond meaningfully to
-         *  any particular {@linkplain java.lang.reflect.Modifier modifier bit}.
-         */
-        public static final int PACKAGE = Modifier.STATIC;
+        public int lookupModes() { return 0; }
 
-        private static final int ALL_MODES = (PUBLIC | PRIVATE | PROTECTED | PACKAGE);
+        public Lookup in(Class<?> requestedLookupClass) { return null; }
 
-        // Android-note: Android has no notion of a trusted lookup. If required, such lookups
-        // are performed by the runtime. As a result, we always use lookupClass, which will always
-        // be non-null in our implementation.
-        //
-        // private static final int TRUSTED   = -1;
-
-        private static int fixmods(int mods) {
-            mods &= (ALL_MODES - PACKAGE);
-            return (mods != 0) ? mods : PACKAGE;
-        }
-
-        /** Tells which class is performing the lookup.  It is this class against
-         *  which checks are performed for visibility and access permissions.
-         *  <p>
-         *  The class implies a maximum level of access permission,
-         *  but the permissions may be additionally limited by the bitmask
-         *  {@link #lookupModes lookupModes}, which controls whether non-public members
-         *  can be accessed.
-         *  @return the lookup class, on behalf of which this lookup object finds members
-         */
-        public Class<?> lookupClass() {
-            return lookupClass;
-        }
-
-        /** Tells which access-protection classes of members this lookup object can produce.
-         *  The result is a bit-mask of the bits
-         *  {@linkplain #PUBLIC PUBLIC (0x01)},
-         *  {@linkplain #PRIVATE PRIVATE (0x02)},
-         *  {@linkplain #PROTECTED PROTECTED (0x04)},
-         *  and {@linkplain #PACKAGE PACKAGE (0x08)}.
-         *  <p>
-         *  A freshly-created lookup object
-         *  on the {@linkplain java.lang.invoke.MethodHandles#lookup() caller's class}
-         *  has all possible bits set, since the caller class can access all its own members.
-         *  A lookup object on a new lookup class
-         *  {@linkplain java.lang.invoke.MethodHandles.Lookup#in created from a previous lookup object}
-         *  may have some mode bits set to zero.
-         *  The purpose of this is to restrict access via the new lookup object,
-         *  so that it can access only names which can be reached by the original
-         *  lookup object, and also by the new lookup class.
-         *  @return the lookup modes, which limit the kinds of access performed by this lookup object
-         */
-        public int lookupModes() {
-            return allowedModes & ALL_MODES;
-        }
-
-        /** Embody the current class (the lookupClass) as a lookup class
-         * for method handle creation.
-         * Must be called by from a method in this package,
-         * which in turn is called by a method not in this package.
-         */
-        Lookup(Class<?> lookupClass) {
-            this(lookupClass, ALL_MODES);
-            // make sure we haven't accidentally picked up a privileged class:
-            checkUnprivilegedlookupClass(lookupClass, ALL_MODES);
-        }
-
-        private Lookup(Class<?> lookupClass, int allowedModes) {
-            this.lookupClass = lookupClass;
-            this.allowedModes = allowedModes;
-        }
-
-        /**
-         * Creates a lookup on the specified new lookup class.
-         * The resulting object will report the specified
-         * class as its own {@link #lookupClass lookupClass}.
-         * <p>
-         * However, the resulting {@code Lookup} object is guaranteed
-         * to have no more access capabilities than the original.
-         * In particular, access capabilities can be lost as follows:<ul>
-         * <li>If the new lookup class differs from the old one,
-         * protected members will not be accessible by virtue of inheritance.
-         * (Protected members may continue to be accessible because of package sharing.)
-         * <li>If the new lookup class is in a different package
-         * than the old one, protected and default (package) members will not be accessible.
-         * <li>If the new lookup class is not within the same package member
-         * as the old one, private members will not be accessible.
-         * <li>If the new lookup class is not accessible to the old lookup class,
-         * then no members, not even public members, will be accessible.
-         * (In all other cases, public members will continue to be accessible.)
-         * </ul>
-         *
-         * @param requestedLookupClass the desired lookup class for the new lookup object
-         * @return a lookup object which reports the desired lookup class
-         * @throws NullPointerException if the argument is null
-         */
-        public Lookup in(Class<?> requestedLookupClass) {
-            requestedLookupClass.getClass();  // null check
-            // Android-changed: There's no notion of a trusted lookup.
-            // if (allowedModes == TRUSTED)  // IMPL_LOOKUP can make any lookup at all
-            //    return new Lookup(requestedLookupClass, ALL_MODES);
-
-            if (requestedLookupClass == this.lookupClass)
-                return this;  // keep same capabilities
-            int newModes = (allowedModes & (ALL_MODES & ~PROTECTED));
-            if ((newModes & PACKAGE) != 0
-                && !VerifyAccess.isSamePackage(this.lookupClass, requestedLookupClass)) {
-                newModes &= ~(PACKAGE|PRIVATE);
-            }
-            // Allow nestmate lookups to be created without special privilege:
-            if ((newModes & PRIVATE) != 0
-                && !VerifyAccess.isSamePackageMember(this.lookupClass, requestedLookupClass)) {
-                newModes &= ~PRIVATE;
-            }
-            if ((newModes & PUBLIC) != 0
-                && !VerifyAccess.isClassAccessible(requestedLookupClass, this.lookupClass, allowedModes)) {
-                // The requested class it not accessible from the lookup class.
-                // No permissions.
-                newModes = 0;
-            }
-            checkUnprivilegedlookupClass(requestedLookupClass, newModes);
-            return new Lookup(requestedLookupClass, newModes);
-        }
-
-        // Make sure outer class is initialized first.
-        //
-        // Android-changed: Removed unnecessary reference to IMPL_NAMES.
-        // static { IMPL_NAMES.getClass(); }
-
-        /** Version of lookup which is trusted minimally.
-         *  It can only be used to create method handles to
-         *  publicly accessible members.
-         */
-        static final Lookup PUBLIC_LOOKUP = new Lookup(Object.class, PUBLIC);
-
-        /** Package-private version of lookup which is trusted. */
-        static final Lookup IMPL_LOOKUP = new Lookup(Object.class, ALL_MODES);
-
-        private static void checkUnprivilegedlookupClass(Class<?> lookupClass, int allowedModes) {
-            String name = lookupClass.getName();
-            if (name.startsWith("java.lang.invoke."))
-                throw newIllegalArgumentException("illegal lookupClass: "+lookupClass);
-
-            // For caller-sensitive MethodHandles.lookup()
-            // disallow lookup more restricted packages
-            //
-            // Android-changed: The bootstrap classloader isn't null.
-            if (allowedModes == ALL_MODES &&
-                    lookupClass.getClassLoader() == Object.class.getClassLoader()) {
-                if (name.startsWith("java.") ||
-                        (name.startsWith("sun.")
-                                && !name.startsWith("sun.invoke.")
-                                && !name.equals("sun.reflect.ReflectionFactory"))) {
-                    throw newIllegalArgumentException("illegal lookupClass: " + lookupClass);
-                }
-            }
-        }
-
-        /**
-         * Displays the name of the class from which lookups are to be made.
-         * (The name is the one reported by {@link java.lang.Class#getName() Class.getName}.)
-         * If there are restrictions on the access permitted to this lookup,
-         * this is indicated by adding a suffix to the class name, consisting
-         * of a slash and a keyword.  The keyword represents the strongest
-         * allowed access, and is chosen as follows:
-         * <ul>
-         * <li>If no access is allowed, the suffix is "/noaccess".
-         * <li>If only public access is allowed, the suffix is "/public".
-         * <li>If only public and package access are allowed, the suffix is "/package".
-         * <li>If only public, package, and private access are allowed, the suffix is "/private".
-         * </ul>
-         * If none of the above cases apply, it is the case that full
-         * access (public, package, private, and protected) is allowed.
-         * In this case, no suffix is added.
-         * This is true only of an object obtained originally from
-         * {@link java.lang.invoke.MethodHandles#lookup MethodHandles.lookup}.
-         * Objects created by {@link java.lang.invoke.MethodHandles.Lookup#in Lookup.in}
-         * always have restricted access, and will display a suffix.
-         * <p>
-         * (It may seem strange that protected access should be
-         * stronger than private access.  Viewed independently from
-         * package access, protected access is the first to be lost,
-         * because it requires a direct subclass relationship between
-         * caller and callee.)
-         * @see #in
-         */
-        @Override
-        public String toString() {
-            String cname = lookupClass.getName();
-            switch (allowedModes) {
-            case 0:  // no privileges
-                return cname + "/noaccess";
-            case PUBLIC:
-                return cname + "/public";
-            case PUBLIC|PACKAGE:
-                return cname + "/package";
-            case ALL_MODES & ~PROTECTED:
-                return cname + "/private";
-            case ALL_MODES:
-                return cname;
-            // Android-changed: No support for TRUSTED callers.
-            // case TRUSTED:
-            //    return "/trusted";  // internal only; not exported
-            default:  // Should not happen, but it's a bitfield...
-                cname = cname + "/" + Integer.toHexString(allowedModes);
-                assert(false) : cname;
-                return cname;
-            }
-        }
-
-        /**
-         * Produces a method handle for a static method.
-         * The type of the method handle will be that of the method.
-         * (Since static methods do not take receivers, there is no
-         * additional receiver argument inserted into the method handle type,
-         * as there would be with {@link #findVirtual findVirtual} or {@link #findSpecial findSpecial}.)
-         * The method and all its argument types must be accessible to the lookup object.
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the method's variable arity modifier bit ({@code 0x0080}) is set.
-         * <p>
-         * If the returned method handle is invoked, the method's class will
-         * be initialized, if it has not already been initialized.
-         * <p><b>Example:</b>
-         * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_asList = publicLookup().findStatic(Arrays.class,
-  "asList", methodType(List.class, Object[].class));
-assertEquals("[x, y]", MH_asList.invoke("x", "y").toString());
-         * }</pre></blockquote>
-         * @param refc the class from which the method is accessed
-         * @param name the name of the method
-         * @param type the type of the method
-         * @return the desired method handle
-         * @throws NoSuchMethodException if the method does not exist
-         * @throws IllegalAccessException if access checking fails,
-         *                                or if the method is not {@code static},
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
         public
-        MethodHandle findStatic(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
-            Method method = refc.getDeclaredMethod(name, type.ptypes());
-            final int modifiers = method.getModifiers();
-            if (!Modifier.isStatic(modifiers)) {
-                throw new IllegalAccessException("Method" + method + " is not static");
-            }
-            checkReturnType(method, type);
-            checkAccess(refc, method.getDeclaringClass(), modifiers, method.getName());
-            return createMethodHandle(method, MethodHandle.INVOKE_STATIC, type);
-        }
+        MethodHandle findStatic(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
 
-        private MethodHandle findVirtualForMH(String name, MethodType type) {
-            // these names require special lookups because of the implicit MethodType argument
-            if ("invoke".equals(name))
-                return invoker(type);
-            if ("invokeExact".equals(name))
-                return exactInvoker(type);
-            return null;
-        }
+        public MethodHandle findVirtual(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
 
-        private MethodHandle findVirtualForVH(String name, MethodType type) {
-            VarHandle.AccessMode accessMode;
-            try {
-                accessMode = VarHandle.AccessMode.valueFromMethodName(name);
-            } catch (IllegalArgumentException e) {
-                return null;
-            }
-            return varHandleInvoker(accessMode, type);
-        }
+        public MethodHandle findConstructor(Class<?> refc, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
 
-        private static MethodHandle createMethodHandle(Method method, int handleKind,
-                                                       MethodType methodType) {
-            MethodHandle mh = new MethodHandleImpl(method.getArtMethod(), handleKind, methodType);
-            if (method.isVarArgs()) {
-                return new Transformers.VarargsCollector(mh);
-            } else {
-                return mh;
-            }
-        }
-
-        /**
-         * Produces a method handle for a virtual method.
-         * The type of the method handle will be that of the method,
-         * with the receiver type (usually {@code refc}) prepended.
-         * The method and all its argument types must be accessible to the lookup object.
-         * <p>
-         * When called, the handle will treat the first argument as a receiver
-         * and dispatch on the receiver's type to determine which method
-         * implementation to enter.
-         * (The dispatching action is identical with that performed by an
-         * {@code invokevirtual} or {@code invokeinterface} instruction.)
-         * <p>
-         * The first argument will be of type {@code refc} if the lookup
-         * class has full privileges to access the member.  Otherwise
-         * the member must be {@code protected} and the first argument
-         * will be restricted in type to the lookup class.
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the method's variable arity modifier bit ({@code 0x0080}) is set.
-         * <p>
-         * Because of the general <a href="MethodHandles.Lookup.html#equiv">equivalence</a> between {@code invokevirtual}
-         * instructions and method handles produced by {@code findVirtual},
-         * if the class is {@code MethodHandle} and the name string is
-         * {@code invokeExact} or {@code invoke}, the resulting
-         * method handle is equivalent to one produced by
-         * {@link java.lang.invoke.MethodHandles#exactInvoker MethodHandles.exactInvoker} or
-         * {@link java.lang.invoke.MethodHandles#invoker MethodHandles.invoker}
-         * with the same {@code type} argument.
-         *
-         * <b>Example:</b>
-         * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_concat = publicLookup().findVirtual(String.class,
-  "concat", methodType(String.class, String.class));
-MethodHandle MH_hashCode = publicLookup().findVirtual(Object.class,
-  "hashCode", methodType(int.class));
-MethodHandle MH_hashCode_String = publicLookup().findVirtual(String.class,
-  "hashCode", methodType(int.class));
-assertEquals("xy", (String) MH_concat.invokeExact("x", "y"));
-assertEquals("xy".hashCode(), (int) MH_hashCode.invokeExact((Object)"xy"));
-assertEquals("xy".hashCode(), (int) MH_hashCode_String.invokeExact("xy"));
-// interface method:
-MethodHandle MH_subSequence = publicLookup().findVirtual(CharSequence.class,
-  "subSequence", methodType(CharSequence.class, int.class, int.class));
-assertEquals("def", MH_subSequence.invoke("abcdefghi", 3, 6).toString());
-// constructor "internal method" must be accessed differently:
-MethodType MT_newString = methodType(void.class); //()V for new String()
-try { assertEquals("impossible", lookup()
-        .findVirtual(String.class, "<init>", MT_newString));
- } catch (NoSuchMethodException ex) { } // OK
-MethodHandle MH_newString = publicLookup()
-  .findConstructor(String.class, MT_newString);
-assertEquals("", (String) MH_newString.invokeExact());
-         * }</pre></blockquote>
-         *
-         * @param refc the class or interface from which the method is accessed
-         * @param name the name of the method
-         * @param type the type of the method, with the receiver argument omitted
-         * @return the desired method handle
-         * @throws NoSuchMethodException if the method does not exist
-         * @throws IllegalAccessException if access checking fails,
-         *                                or if the method is {@code static}
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
-        public MethodHandle findVirtual(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
-            // Special case : when we're looking up a virtual method on the MethodHandles class
-            // itself, we can return one of our specialized invokers.
-            if (refc == MethodHandle.class) {
-                MethodHandle mh = findVirtualForMH(name, type);
-                if (mh != null) {
-                    return mh;
-                }
-            } else if (refc == VarHandle.class) {
-                // Returns an non-exact invoker.
-                MethodHandle mh = findVirtualForVH(name, type);
-                if (mh != null) {
-                    return mh;
-                }
-            }
-
-            Method method = refc.getInstanceMethod(name, type.ptypes());
-            if (method == null) {
-                // This is pretty ugly and a consequence of the MethodHandles API. We have to throw
-                // an IAE and not an NSME if the method exists but is static (even though the RI's
-                // IAE has a message that says "no such method"). We confine the ugliness and
-                // slowness to the failure case, and allow getInstanceMethod to remain fairly
-                // general.
-                try {
-                    Method m = refc.getDeclaredMethod(name, type.ptypes());
-                    if (Modifier.isStatic(m.getModifiers())) {
-                        throw new IllegalAccessException("Method" + m + " is static");
-                    }
-                } catch (NoSuchMethodException ignored) {
-                }
-
-                throw new NoSuchMethodException(name + " "  + Arrays.toString(type.ptypes()));
-            }
-            checkReturnType(method, type);
-
-            // We have a valid method, perform access checks.
-            checkAccess(refc, method.getDeclaringClass(), method.getModifiers(), method.getName());
-
-            // Insert the leading reference parameter.
-            MethodType handleType = type.insertParameterTypes(0, refc);
-            return createMethodHandle(method, MethodHandle.INVOKE_VIRTUAL, handleType);
-        }
-
-        /**
-         * Produces a method handle which creates an object and initializes it, using
-         * the constructor of the specified type.
-         * The parameter types of the method handle will be those of the constructor,
-         * while the return type will be a reference to the constructor's class.
-         * The constructor and all its argument types must be accessible to the lookup object.
-         * <p>
-         * The requested type must have a return type of {@code void}.
-         * (This is consistent with the JVM's treatment of constructor type descriptors.)
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the constructor's variable arity modifier bit ({@code 0x0080}) is set.
-         * <p>
-         * If the returned method handle is invoked, the constructor's class will
-         * be initialized, if it has not already been initialized.
-         * <p><b>Example:</b>
-         * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_newArrayList = publicLookup().findConstructor(
-  ArrayList.class, methodType(void.class, Collection.class));
-Collection orig = Arrays.asList("x", "y");
-Collection copy = (ArrayList) MH_newArrayList.invokeExact(orig);
-assert(orig != copy);
-assertEquals(orig, copy);
-// a variable-arity constructor:
-MethodHandle MH_newProcessBuilder = publicLookup().findConstructor(
-  ProcessBuilder.class, methodType(void.class, String[].class));
-ProcessBuilder pb = (ProcessBuilder)
-  MH_newProcessBuilder.invoke("x", "y", "z");
-assertEquals("[x, y, z]", pb.command().toString());
-         * }</pre></blockquote>
-         * @param refc the class or interface from which the method is accessed
-         * @param type the type of the method, with the receiver argument omitted, and a void return type
-         * @return the desired method handle
-         * @throws NoSuchMethodException if the constructor does not exist
-         * @throws IllegalAccessException if access checking fails
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
-        public MethodHandle findConstructor(Class<?> refc, MethodType type) throws NoSuchMethodException, IllegalAccessException {
-            if (refc.isArray()) {
-                throw new NoSuchMethodException("no constructor for array class: " + refc.getName());
-            }
-            // The queried |type| is (PT1,PT2,..)V
-            Constructor constructor = refc.getDeclaredConstructor(type.ptypes());
-            if (constructor == null) {
-                throw new NoSuchMethodException(
-                    "No constructor for " + constructor.getDeclaringClass() + " matching " + type);
-            }
-            checkAccess(refc, constructor.getDeclaringClass(), constructor.getModifiers(),
-                    constructor.getName());
-
-            return createMethodHandleForConstructor(constructor);
-        }
-
-        private MethodHandle createMethodHandleForConstructor(Constructor constructor) {
-            Class<?> refc = constructor.getDeclaringClass();
-            MethodType constructorType =
-                    MethodType.methodType(refc, constructor.getParameterTypes());
-            MethodHandle mh;
-            if (refc == String.class) {
-                // String constructors have optimized StringFactory methods
-                // that matches returned type. These factory methods combine the
-                // memory allocation and initialization calls for String objects.
-                mh = new MethodHandleImpl(constructor.getArtMethod(), MethodHandle.INVOKE_DIRECT,
-                                          constructorType);
-            } else {
-                // Constructors for all other classes use a Construct transformer to perform
-                // their memory allocation and call to <init>.
-                MethodType initType = initMethodType(constructorType);
-                MethodHandle initHandle = new MethodHandleImpl(
-                    constructor.getArtMethod(), MethodHandle.INVOKE_DIRECT, initType);
-                mh = new Transformers.Construct(initHandle, constructorType);
-            }
-
-            if (constructor.isVarArgs()) {
-                mh = new Transformers.VarargsCollector(mh);
-            }
-            return mh;
-        }
-
-        private static MethodType initMethodType(MethodType constructorType) {
-            // Returns a MethodType appropriate for class <init>
-            // methods. Constructor MethodTypes have the form
-            // (PT1,PT2,...)C and class <init> MethodTypes have the
-            // form (C,PT1,PT2,...)V.
-            assert constructorType.rtype() != void.class;
-
-            // Insert constructorType C as the first parameter type in
-            // the MethodType for <init>.
-            Class<?> [] initPtypes = new Class<?> [constructorType.ptypes().length + 1];
-            initPtypes[0] = constructorType.rtype();
-            System.arraycopy(constructorType.ptypes(), 0, initPtypes, 1,
-                             constructorType.ptypes().length);
-
-            // Set the return type for the <init> MethodType to be void.
-            return MethodType.methodType(void.class, initPtypes);
-        }
-
-        /**
-         * Produces an early-bound method handle for a virtual method.
-         * It will bypass checks for overriding methods on the receiver,
-         * <a href="MethodHandles.Lookup.html#equiv">as if called</a> from an {@code invokespecial}
-         * instruction from within the explicitly specified {@code specialCaller}.
-         * The type of the method handle will be that of the method,
-         * with a suitably restricted receiver type prepended.
-         * (The receiver type will be {@code specialCaller} or a subtype.)
-         * The method and all its argument types must be accessible
-         * to the lookup object.
-         * <p>
-         * Before method resolution,
-         * if the explicitly specified caller class is not identical with the
-         * lookup class, or if this lookup object does not have
-         * <a href="MethodHandles.Lookup.html#privacc">private access</a>
-         * privileges, the access fails.
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the method's variable arity modifier bit ({@code 0x0080}) is set.
-         * <p style="font-size:smaller;">
-         * <em>(Note:  JVM internal methods named {@code "<init>"} are not visible to this API,
-         * even though the {@code invokespecial} instruction can refer to them
-         * in special circumstances.  Use {@link #findConstructor findConstructor}
-         * to access instance initialization methods in a safe manner.)</em>
-         * <p><b>Example:</b>
-         * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-static class Listie extends ArrayList {
-  public String toString() { return "[wee Listie]"; }
-  static Lookup lookup() { return MethodHandles.lookup(); }
-}
-...
-// no access to constructor via invokeSpecial:
-MethodHandle MH_newListie = Listie.lookup()
-  .findConstructor(Listie.class, methodType(void.class));
-Listie l = (Listie) MH_newListie.invokeExact();
-try { assertEquals("impossible", Listie.lookup().findSpecial(
-        Listie.class, "<init>", methodType(void.class), Listie.class));
- } catch (NoSuchMethodException ex) { } // OK
-// access to super and self methods via invokeSpecial:
-MethodHandle MH_super = Listie.lookup().findSpecial(
-  ArrayList.class, "toString" , methodType(String.class), Listie.class);
-MethodHandle MH_this = Listie.lookup().findSpecial(
-  Listie.class, "toString" , methodType(String.class), Listie.class);
-MethodHandle MH_duper = Listie.lookup().findSpecial(
-  Object.class, "toString" , methodType(String.class), Listie.class);
-assertEquals("[]", (String) MH_super.invokeExact(l));
-assertEquals(""+l, (String) MH_this.invokeExact(l));
-assertEquals("[]", (String) MH_duper.invokeExact(l)); // ArrayList method
-try { assertEquals("inaccessible", Listie.lookup().findSpecial(
-        String.class, "toString", methodType(String.class), Listie.class));
- } catch (IllegalAccessException ex) { } // OK
-Listie subl = new Listie() { public String toString() { return "[subclass]"; } };
-assertEquals(""+l, (String) MH_this.invokeExact(subl)); // Listie method
-         * }</pre></blockquote>
-         *
-         * @param refc the class or interface from which the method is accessed
-         * @param name the name of the method (which must not be "&lt;init&gt;")
-         * @param type the type of the method, with the receiver argument omitted
-         * @param specialCaller the proposed calling class to perform the {@code invokespecial}
-         * @return the desired method handle
-         * @throws NoSuchMethodException if the method does not exist
-         * @throws IllegalAccessException if access checking fails
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
         public MethodHandle findSpecial(Class<?> refc, String name, MethodType type,
-                                        Class<?> specialCaller) throws NoSuchMethodException, IllegalAccessException {
-            if (specialCaller == null) {
-                throw new NullPointerException("specialCaller == null");
-            }
+                                        Class<?> specialCaller) throws NoSuchMethodException, IllegalAccessException { return null; }
 
-            if (type == null) {
-                throw new NullPointerException("type == null");
-            }
+        public MethodHandle findGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
 
-            if (name == null) {
-                throw new NullPointerException("name == null");
-            }
+        public MethodHandle findSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
 
-            if (refc == null) {
-                throw new NullPointerException("ref == null");
-            }
+        public MethodHandle findStaticGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
 
-            // Make sure that the special caller is identical to the lookup class or that we have
-            // private access.
-            checkSpecialCaller(specialCaller);
+        public MethodHandle findStaticSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
 
-            // Even though constructors are invoked using a "special" invoke, handles to them can't
-            // be created using findSpecial. Callers must use findConstructor instead. Similarly,
-            // there is no path for calling static class initializers.
-            if (name.startsWith("<")) {
-                throw new NoSuchMethodException(name + " is not a valid method name.");
-            }
+        public MethodHandle bind(Object receiver, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
 
-            Method method = refc.getDeclaredMethod(name, type.ptypes());
-            checkReturnType(method, type);
-            return findSpecial(method, type, refc, specialCaller);
-        }
+        public MethodHandle unreflect(Method m) throws IllegalAccessException { return null; }
 
-        private MethodHandle findSpecial(Method method, MethodType type,
-                                         Class<?> refc, Class<?> specialCaller)
-                throws IllegalAccessException {
-            if (Modifier.isStatic(method.getModifiers())) {
-                throw new IllegalAccessException("expected a non-static method:" + method);
-            }
+        public MethodHandle unreflectSpecial(Method m, Class<?> specialCaller) throws IllegalAccessException { return null; }
 
-            if (Modifier.isPrivate(method.getModifiers())) {
-                // Since this is a private method, we'll need to also make sure that the
-                // lookup class is the same as the refering class. We've already checked that
-                // the specialCaller is the same as the special lookup class, both of these must
-                // be the same as the declaring class(*) in order to access the private method.
-                //
-                // (*) Well, this isn't true for nested classes but OpenJDK doesn't support those
-                // either.
-                if (refc != lookupClass()) {
-                    throw new IllegalAccessException("no private access for invokespecial : "
-                            + refc + ", from" + this);
-                }
+        public MethodHandle unreflectConstructor(Constructor<?> c) throws IllegalAccessException { return null; }
 
-                // This is a private method, so there's nothing special to do.
-                MethodType handleType = type.insertParameterTypes(0, refc);
-                return createMethodHandle(method, MethodHandle.INVOKE_DIRECT, handleType);
-            }
+        public MethodHandle unreflectGetter(Field f) throws IllegalAccessException { return null; }
 
-            // This is a public, protected or package-private method, which means we're expecting
-            // invoke-super semantics. We'll have to restrict the receiver type appropriately on the
-            // handle once we check that there really is a "super" relationship between them.
-            if (!method.getDeclaringClass().isAssignableFrom(specialCaller)) {
-                throw new IllegalAccessException(refc + "is not assignable from " + specialCaller);
-            }
+        public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { return null; }
 
-            // Note that we restrict the receiver to "specialCaller" instances.
-            MethodType handleType = type.insertParameterTypes(0, specialCaller);
-            return createMethodHandle(method, MethodHandle.INVOKE_SUPER, handleType);
-        }
+        public MethodHandleInfo revealDirect(MethodHandle target) { return null; }
 
-        /**
-         * Produces a method handle giving read access to a non-static field.
-         * The type of the method handle will have a return type of the field's
-         * value type.
-         * The method handle's single argument will be the instance containing
-         * the field.
-         * Access checking is performed immediately on behalf of the lookup class.
-         * @param refc the class or interface from which the method is accessed
-         * @param name the field's name
-         * @param type the field's type
-         * @return a method handle which can load values from the field
-         * @throws NoSuchFieldException if the field does not exist
-         * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
-        public MethodHandle findGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
-            return findAccessor(refc, name, type, MethodHandle.IGET);
-        }
-
-        private MethodHandle findAccessor(Class<?> refc, String name, Class<?> type, int kind)
-            throws NoSuchFieldException, IllegalAccessException {
-            final Field field = findFieldOfType(refc, name, type);
-            return findAccessor(field, refc, type, kind, true /* performAccessChecks */);
-        }
-
-        private MethodHandle findAccessor(Field field, Class<?> refc, Class<?> type, int kind,
-                                          boolean performAccessChecks)
-                throws IllegalAccessException {
-            final boolean isSetterKind = kind == MethodHandle.IPUT || kind == MethodHandle.SPUT;
-            final boolean isStaticKind = kind == MethodHandle.SGET || kind == MethodHandle.SPUT;
-            commonFieldChecks(field, refc, type, isStaticKind, performAccessChecks);
-            if (performAccessChecks) {
-                final int modifiers = field.getModifiers();
-                if (isSetterKind && Modifier.isFinal(modifiers)) {
-                    throw new IllegalAccessException("Field " + field + " is final");
-                }
-            }
-
-            final MethodType methodType;
-            switch (kind) {
-                case MethodHandle.SGET:
-                    methodType = MethodType.methodType(type);
-                    break;
-                case MethodHandle.SPUT:
-                    methodType = MethodType.methodType(void.class, type);
-                    break;
-                case MethodHandle.IGET:
-                    methodType = MethodType.methodType(type, refc);
-                    break;
-                case MethodHandle.IPUT:
-                    methodType = MethodType.methodType(void.class, refc, type);
-                    break;
-                default:
-                    throw new IllegalArgumentException("Invalid kind " + kind);
-            }
-            return new MethodHandleImpl(field.getArtField(), kind, methodType);
-        }
-
-        /**
-         * Produces a method handle giving write access to a non-static field.
-         * The type of the method handle will have a void return type.
-         * The method handle will take two arguments, the instance containing
-         * the field, and the value to be stored.
-         * The second argument will be of the field's value type.
-         * Access checking is performed immediately on behalf of the lookup class.
-         * @param refc the class or interface from which the method is accessed
-         * @param name the field's name
-         * @param type the field's type
-         * @return a method handle which can store values into the field
-         * @throws NoSuchFieldException if the field does not exist
-         * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
-        public MethodHandle findSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
-            return findAccessor(refc, name, type, MethodHandle.IPUT);
-        }
-
-        // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
-        /**
-         * Produces a VarHandle giving access to a non-static field {@code name}
-         * of type {@code type} declared in a class of type {@code recv}.
-         * The VarHandle's variable type is {@code type} and it has one
-         * coordinate type, {@code recv}.
-         * <p>
-         * Access checking is performed immediately on behalf of the lookup
-         * class.
-         * <p>
-         * Certain access modes of the returned VarHandle are unsupported under
-         * the following conditions:
-         * <ul>
-         * <li>if the field is declared {@code final}, then the write, atomic
-         *     update, numeric atomic update, and bitwise atomic update access
-         *     modes are unsupported.
-         * <li>if the field type is anything other than {@code byte},
-         *     {@code short}, {@code char}, {@code int}, {@code long},
-         *     {@code float}, or {@code double} then numeric atomic update
-         *     access modes are unsupported.
-         * <li>if the field type is anything other than {@code boolean},
-         *     {@code byte}, {@code short}, {@code char}, {@code int} or
-         *     {@code long} then bitwise atomic update access modes are
-         *     unsupported.
-         * </ul>
-         * <p>
-         * If the field is declared {@code volatile} then the returned VarHandle
-         * will override access to the field (effectively ignore the
-         * {@code volatile} declaration) in accordance to its specified
-         * access modes.
-         * <p>
-         * If the field type is {@code float} or {@code double} then numeric
-         * and atomic update access modes compare values using their bitwise
-         * representation (see {@link Float#floatToRawIntBits} and
-         * {@link Double#doubleToRawLongBits}, respectively).
-         * @apiNote
-         * Bitwise comparison of {@code float} values or {@code double} values,
-         * as performed by the numeric and atomic update access modes, differ
-         * from the primitive {@code ==} operator and the {@link Float#equals}
-         * and {@link Double#equals} methods, specifically with respect to
-         * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
-         * Care should be taken when performing a compare and set or a compare
-         * and exchange operation with such values since the operation may
-         * unexpectedly fail.
-         * There are many possible NaN values that are considered to be
-         * {@code NaN} in Java, although no IEEE 754 floating-point operation
-         * provided by Java can distinguish between them.  Operation failure can
-         * occur if the expected or witness value is a NaN value and it is
-         * transformed (perhaps in a platform specific manner) into another NaN
-         * value, and thus has a different bitwise representation (see
-         * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
-         * details).
-         * The values {@code -0.0} and {@code +0.0} have different bitwise
-         * representations but are considered equal when using the primitive
-         * {@code ==} operator.  Operation failure can occur if, for example, a
-         * numeric algorithm computes an expected value to be say {@code -0.0}
-         * and previously computed the witness value to be say {@code +0.0}.
-         * @param recv the receiver class, of type {@code R}, that declares the
-         * non-static field
-         * @param name the field's name
-         * @param type the field's type, of type {@code T}
-         * @return a VarHandle giving access to non-static fields.
-         * @throws NoSuchFieldException if the field does not exist
-         * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         * @since 9
-         * @hide
-         */
-        public VarHandle findVarHandle(Class<?> recv, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
-            final Field field = findFieldOfType(recv, name, type);
-            final boolean isStatic = false;
-            final boolean performAccessChecks = true;
-            commonFieldChecks(field, recv, type, isStatic, performAccessChecks);
-            return FieldVarHandle.create(field);
-        }
-        // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
-        // BEGIN Android-added: Common field resolution and access check methods.
-        private Field findFieldOfType(final Class<?> refc, String name, Class<?> type)
-                throws NoSuchFieldException {
-            Field field = null;
-
-            // Search refc and super classes for the field.
-            for (Class<?> cls = refc; cls != null; cls = cls.getSuperclass()) {
-                try {
-                    field = cls.getDeclaredField(name);
-                    break;
-                } catch (NoSuchFieldException e) {
-                }
-            }
-
-            if (field == null) {
-                // Force failure citing refc.
-                field = refc.getDeclaredField(name);
-            }
-
-            final Class<?> fieldType = field.getType();
-            if (fieldType != type) {
-                throw new NoSuchFieldException(name);
-            }
-            return field;
-        }
-
-        private void commonFieldChecks(Field field, Class<?> refc, Class<?> type,
-                                       boolean isStatic, boolean performAccessChecks)
-                throws IllegalAccessException {
-            final int modifiers = field.getModifiers();
-            if (performAccessChecks) {
-                checkAccess(refc, field.getDeclaringClass(), modifiers, field.getName());
-            }
-            if (Modifier.isStatic(modifiers) != isStatic) {
-                String reason = "Field " + field + " is " +
-                        (isStatic ? "not " : "") + "static";
-                throw new IllegalAccessException(reason);
-            }
-        }
-        // END Android-added: Common field resolution and access check methods.
-
-        /**
-         * Produces a method handle giving read access to a static field.
-         * The type of the method handle will have a return type of the field's
-         * value type.
-         * The method handle will take no arguments.
-         * Access checking is performed immediately on behalf of the lookup class.
-         * <p>
-         * If the returned method handle is invoked, the field's class will
-         * be initialized, if it has not already been initialized.
-         * @param refc the class or interface from which the method is accessed
-         * @param name the field's name
-         * @param type the field's type
-         * @return a method handle which can load values from the field
-         * @throws NoSuchFieldException if the field does not exist
-         * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
-        public MethodHandle findStaticGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
-            return findAccessor(refc, name, type, MethodHandle.SGET);
-        }
-
-        /**
-         * Produces a method handle giving write access to a static field.
-         * The type of the method handle will have a void return type.
-         * The method handle will take a single
-         * argument, of the field's value type, the value to be stored.
-         * Access checking is performed immediately on behalf of the lookup class.
-         * <p>
-         * If the returned method handle is invoked, the field's class will
-         * be initialized, if it has not already been initialized.
-         * @param refc the class or interface from which the method is accessed
-         * @param name the field's name
-         * @param type the field's type
-         * @return a method handle which can store values into the field
-         * @throws NoSuchFieldException if the field does not exist
-         * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         */
-        public MethodHandle findStaticSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
-            return findAccessor(refc, name, type, MethodHandle.SPUT);
-        }
-
-        // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
-        /**
-         * Produces a VarHandle giving access to a static field {@code name} of
-         * type {@code type} declared in a class of type {@code decl}.
-         * The VarHandle's variable type is {@code type} and it has no
-         * coordinate types.
-         * <p>
-         * Access checking is performed immediately on behalf of the lookup
-         * class.
-         * <p>
-         * If the returned VarHandle is operated on, the declaring class will be
-         * initialized, if it has not already been initialized.
-         * <p>
-         * Certain access modes of the returned VarHandle are unsupported under
-         * the following conditions:
-         * <ul>
-         * <li>if the field is declared {@code final}, then the write, atomic
-         *     update, numeric atomic update, and bitwise atomic update access
-         *     modes are unsupported.
-         * <li>if the field type is anything other than {@code byte},
-         *     {@code short}, {@code char}, {@code int}, {@code long},
-         *     {@code float}, or {@code double}, then numeric atomic update
-         *     access modes are unsupported.
-         * <li>if the field type is anything other than {@code boolean},
-         *     {@code byte}, {@code short}, {@code char}, {@code int} or
-         *     {@code long} then bitwise atomic update access modes are
-         *     unsupported.
-         * </ul>
-         * <p>
-         * If the field is declared {@code volatile} then the returned VarHandle
-         * will override access to the field (effectively ignore the
-         * {@code volatile} declaration) in accordance to its specified
-         * access modes.
-         * <p>
-         * If the field type is {@code float} or {@code double} then numeric
-         * and atomic update access modes compare values using their bitwise
-         * representation (see {@link Float#floatToRawIntBits} and
-         * {@link Double#doubleToRawLongBits}, respectively).
-         * @apiNote
-         * Bitwise comparison of {@code float} values or {@code double} values,
-         * as performed by the numeric and atomic update access modes, differ
-         * from the primitive {@code ==} operator and the {@link Float#equals}
-         * and {@link Double#equals} methods, specifically with respect to
-         * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
-         * Care should be taken when performing a compare and set or a compare
-         * and exchange operation with such values since the operation may
-         * unexpectedly fail.
-         * There are many possible NaN values that are considered to be
-         * {@code NaN} in Java, although no IEEE 754 floating-point operation
-         * provided by Java can distinguish between them.  Operation failure can
-         * occur if the expected or witness value is a NaN value and it is
-         * transformed (perhaps in a platform specific manner) into another NaN
-         * value, and thus has a different bitwise representation (see
-         * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
-         * details).
-         * The values {@code -0.0} and {@code +0.0} have different bitwise
-         * representations but are considered equal when using the primitive
-         * {@code ==} operator.  Operation failure can occur if, for example, a
-         * numeric algorithm computes an expected value to be say {@code -0.0}
-         * and previously computed the witness value to be say {@code +0.0}.
-         * @param decl the class that declares the static field
-         * @param name the field's name
-         * @param type the field's type, of type {@code T}
-         * @return a VarHandle giving access to a static field
-         * @throws NoSuchFieldException if the field does not exist
-         * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         * @since 9
-         * @hide
-         */
-        public VarHandle findStaticVarHandle(Class<?> decl, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
-            final Field field = findFieldOfType(decl, name, type);
-            final boolean isStatic = true;
-            final boolean performAccessChecks = true;
-            commonFieldChecks(field, decl, type, isStatic, performAccessChecks);
-            return FieldVarHandle.create(field);
-        }
-        // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
-        /**
-         * Produces an early-bound method handle for a non-static method.
-         * The receiver must have a supertype {@code defc} in which a method
-         * of the given name and type is accessible to the lookup class.
-         * The method and all its argument types must be accessible to the lookup object.
-         * The type of the method handle will be that of the method,
-         * without any insertion of an additional receiver parameter.
-         * The given receiver will be bound into the method handle,
-         * so that every call to the method handle will invoke the
-         * requested method on the given receiver.
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the method's variable arity modifier bit ({@code 0x0080}) is set
-         * <em>and</em> the trailing array argument is not the only argument.
-         * (If the trailing array argument is the only argument,
-         * the given receiver value will be bound to it.)
-         * <p>
-         * This is equivalent to the following code:
-         * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle mh0 = lookup().findVirtual(defc, name, type);
-MethodHandle mh1 = mh0.bindTo(receiver);
-MethodType mt1 = mh1.type();
-if (mh0.isVarargsCollector())
-  mh1 = mh1.asVarargsCollector(mt1.parameterType(mt1.parameterCount()-1));
-return mh1;
-         * }</pre></blockquote>
-         * where {@code defc} is either {@code receiver.getClass()} or a super
-         * type of that class, in which the requested method is accessible
-         * to the lookup class.
-         * (Note that {@code bindTo} does not preserve variable arity.)
-         * @param receiver the object from which the method is accessed
-         * @param name the name of the method
-         * @param type the type of the method, with the receiver argument omitted
-         * @return the desired method handle
-         * @throws NoSuchMethodException if the method does not exist
-         * @throws IllegalAccessException if access checking fails
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws NullPointerException if any argument is null
-         * @see MethodHandle#bindTo
-         * @see #findVirtual
-         */
-        public MethodHandle bind(Object receiver, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
-            MethodHandle handle = findVirtual(receiver.getClass(), name, type);
-            MethodHandle adapter = handle.bindTo(receiver);
-            MethodType adapterType = adapter.type();
-            if (handle.isVarargsCollector()) {
-                adapter = adapter.asVarargsCollector(
-                        adapterType.parameterType(adapterType.parameterCount() - 1));
-            }
-
-            return adapter;
-        }
-
-        /**
-         * Makes a <a href="MethodHandleInfo.html#directmh">direct method handle</a>
-         * to <i>m</i>, if the lookup class has permission.
-         * If <i>m</i> is non-static, the receiver argument is treated as an initial argument.
-         * If <i>m</i> is virtual, overriding is respected on every call.
-         * Unlike the Core Reflection API, exceptions are <em>not</em> wrapped.
-         * The type of the method handle will be that of the method,
-         * with the receiver type prepended (but only if it is non-static).
-         * If the method's {@code accessible} flag is not set,
-         * access checking is performed immediately on behalf of the lookup class.
-         * If <i>m</i> is not public, do not share the resulting handle with untrusted parties.
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the method's variable arity modifier bit ({@code 0x0080}) is set.
-         * <p>
-         * If <i>m</i> is static, and
-         * if the returned method handle is invoked, the method's class will
-         * be initialized, if it has not already been initialized.
-         * @param m the reflected method
-         * @return a method handle which can invoke the reflected method
-         * @throws IllegalAccessException if access checking fails
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @throws NullPointerException if the argument is null
-         */
-        public MethodHandle unreflect(Method m) throws IllegalAccessException {
-            if (m == null) {
-                throw new NullPointerException("m == null");
-            }
-
-            MethodType methodType = MethodType.methodType(m.getReturnType(),
-                    m.getParameterTypes());
-
-            // We should only perform access checks if setAccessible hasn't been called yet.
-            if (!m.isAccessible()) {
-                checkAccess(m.getDeclaringClass(), m.getDeclaringClass(), m.getModifiers(),
-                        m.getName());
-            }
-
-            if (Modifier.isStatic(m.getModifiers())) {
-                return createMethodHandle(m, MethodHandle.INVOKE_STATIC, methodType);
-            } else {
-                methodType = methodType.insertParameterTypes(0, m.getDeclaringClass());
-                return createMethodHandle(m, MethodHandle.INVOKE_VIRTUAL, methodType);
-            }
-        }
-
-        /**
-         * Produces a method handle for a reflected method.
-         * It will bypass checks for overriding methods on the receiver,
-         * <a href="MethodHandles.Lookup.html#equiv">as if called</a> from an {@code invokespecial}
-         * instruction from within the explicitly specified {@code specialCaller}.
-         * The type of the method handle will be that of the method,
-         * with a suitably restricted receiver type prepended.
-         * (The receiver type will be {@code specialCaller} or a subtype.)
-         * If the method's {@code accessible} flag is not set,
-         * access checking is performed immediately on behalf of the lookup class,
-         * as if {@code invokespecial} instruction were being linked.
-         * <p>
-         * Before method resolution,
-         * if the explicitly specified caller class is not identical with the
-         * lookup class, or if this lookup object does not have
-         * <a href="MethodHandles.Lookup.html#privacc">private access</a>
-         * privileges, the access fails.
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the method's variable arity modifier bit ({@code 0x0080}) is set.
-         * @param m the reflected method
-         * @param specialCaller the class nominally calling the method
-         * @return a method handle which can invoke the reflected method
-         * @throws IllegalAccessException if access checking fails
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @throws NullPointerException if any argument is null
-         */
-        public MethodHandle unreflectSpecial(Method m, Class<?> specialCaller) throws IllegalAccessException {
-            if (m == null) {
-                throw new NullPointerException("m == null");
-            }
-
-            if (specialCaller == null) {
-                throw new NullPointerException("specialCaller == null");
-            }
-
-            if (!m.isAccessible()) {
-                checkSpecialCaller(specialCaller);
-            }
-
-            final MethodType methodType = MethodType.methodType(m.getReturnType(),
-                    m.getParameterTypes());
-            return findSpecial(m, methodType, m.getDeclaringClass() /* refc */, specialCaller);
-        }
-
-        /**
-         * Produces a method handle for a reflected constructor.
-         * The type of the method handle will be that of the constructor,
-         * with the return type changed to the declaring class.
-         * The method handle will perform a {@code newInstance} operation,
-         * creating a new instance of the constructor's class on the
-         * arguments passed to the method handle.
-         * <p>
-         * If the constructor's {@code accessible} flag is not set,
-         * access checking is performed immediately on behalf of the lookup class.
-         * <p>
-         * The returned method handle will have
-         * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
-         * the constructor's variable arity modifier bit ({@code 0x0080}) is set.
-         * <p>
-         * If the returned method handle is invoked, the constructor's class will
-         * be initialized, if it has not already been initialized.
-         * @param c the reflected constructor
-         * @return a method handle which can invoke the reflected constructor
-         * @throws IllegalAccessException if access checking fails
-         *                                or if the method's variable arity modifier bit
-         *                                is set and {@code asVarargsCollector} fails
-         * @throws NullPointerException if the argument is null
-         */
-        public MethodHandle unreflectConstructor(Constructor<?> c) throws IllegalAccessException {
-            if (c == null) {
-                throw new NullPointerException("c == null");
-            }
-
-            if (!c.isAccessible()) {
-                checkAccess(c.getDeclaringClass(), c.getDeclaringClass(), c.getModifiers(),
-                        c.getName());
-            }
-
-            return createMethodHandleForConstructor(c);
-        }
-
-        /**
-         * Produces a method handle giving read access to a reflected field.
-         * The type of the method handle will have a return type of the field's
-         * value type.
-         * If the field is static, the method handle will take no arguments.
-         * Otherwise, its single argument will be the instance containing
-         * the field.
-         * If the field's {@code accessible} flag is not set,
-         * access checking is performed immediately on behalf of the lookup class.
-         * <p>
-         * If the field is static, and
-         * if the returned method handle is invoked, the field's class will
-         * be initialized, if it has not already been initialized.
-         * @param f the reflected field
-         * @return a method handle which can load values from the reflected field
-         * @throws IllegalAccessException if access checking fails
-         * @throws NullPointerException if the argument is null
-         */
-        public MethodHandle unreflectGetter(Field f) throws IllegalAccessException {
-            return findAccessor(f, f.getDeclaringClass(), f.getType(),
-                    Modifier.isStatic(f.getModifiers()) ? MethodHandle.SGET : MethodHandle.IGET,
-                    !f.isAccessible() /* performAccessChecks */);
-        }
-
-        /**
-         * Produces a method handle giving write access to a reflected field.
-         * The type of the method handle will have a void return type.
-         * If the field is static, the method handle will take a single
-         * argument, of the field's value type, the value to be stored.
-         * Otherwise, the two arguments will be the instance containing
-         * the field, and the value to be stored.
-         * If the field's {@code accessible} flag is not set,
-         * access checking is performed immediately on behalf of the lookup class.
-         * <p>
-         * If the field is static, and
-         * if the returned method handle is invoked, the field's class will
-         * be initialized, if it has not already been initialized.
-         * @param f the reflected field
-         * @return a method handle which can store values into the reflected field
-         * @throws IllegalAccessException if access checking fails
-         * @throws NullPointerException if the argument is null
-         */
-        public MethodHandle unreflectSetter(Field f) throws IllegalAccessException {
-            return findAccessor(f, f.getDeclaringClass(), f.getType(),
-                    Modifier.isStatic(f.getModifiers()) ? MethodHandle.SPUT : MethodHandle.IPUT,
-                    !f.isAccessible() /* performAccessChecks */);
-        }
-
-        // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
-        /**
-         * Produces a VarHandle giving access to a reflected field {@code f}
-         * of type {@code T} declared in a class of type {@code R}.
-         * The VarHandle's variable type is {@code T}.
-         * If the field is non-static the VarHandle has one coordinate type,
-         * {@code R}.  Otherwise, the field is static, and the VarHandle has no
-         * coordinate types.
-         * <p>
-         * Access checking is performed immediately on behalf of the lookup
-         * class, regardless of the value of the field's {@code accessible}
-         * flag.
-         * <p>
-         * If the field is static, and if the returned VarHandle is operated
-         * on, the field's declaring class will be initialized, if it has not
-         * already been initialized.
-         * <p>
-         * Certain access modes of the returned VarHandle are unsupported under
-         * the following conditions:
-         * <ul>
-         * <li>if the field is declared {@code final}, then the write, atomic
-         *     update, numeric atomic update, and bitwise atomic update access
-         *     modes are unsupported.
-         * <li>if the field type is anything other than {@code byte},
-         *     {@code short}, {@code char}, {@code int}, {@code long},
-         *     {@code float}, or {@code double} then numeric atomic update
-         *     access modes are unsupported.
-         * <li>if the field type is anything other than {@code boolean},
-         *     {@code byte}, {@code short}, {@code char}, {@code int} or
-         *     {@code long} then bitwise atomic update access modes are
-         *     unsupported.
-         * </ul>
-         * <p>
-         * If the field is declared {@code volatile} then the returned VarHandle
-         * will override access to the field (effectively ignore the
-         * {@code volatile} declaration) in accordance to its specified
-         * access modes.
-         * <p>
-         * If the field type is {@code float} or {@code double} then numeric
-         * and atomic update access modes compare values using their bitwise
-         * representation (see {@link Float#floatToRawIntBits} and
-         * {@link Double#doubleToRawLongBits}, respectively).
-         * @apiNote
-         * Bitwise comparison of {@code float} values or {@code double} values,
-         * as performed by the numeric and atomic update access modes, differ
-         * from the primitive {@code ==} operator and the {@link Float#equals}
-         * and {@link Double#equals} methods, specifically with respect to
-         * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
-         * Care should be taken when performing a compare and set or a compare
-         * and exchange operation with such values since the operation may
-         * unexpectedly fail.
-         * There are many possible NaN values that are considered to be
-         * {@code NaN} in Java, although no IEEE 754 floating-point operation
-         * provided by Java can distinguish between them.  Operation failure can
-         * occur if the expected or witness value is a NaN value and it is
-         * transformed (perhaps in a platform specific manner) into another NaN
-         * value, and thus has a different bitwise representation (see
-         * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
-         * details).
-         * The values {@code -0.0} and {@code +0.0} have different bitwise
-         * representations but are considered equal when using the primitive
-         * {@code ==} operator.  Operation failure can occur if, for example, a
-         * numeric algorithm computes an expected value to be say {@code -0.0}
-         * and previously computed the witness value to be say {@code +0.0}.
-         * @param f the reflected field, with a field of type {@code T}, and
-         * a declaring class of type {@code R}
-         * @return a VarHandle giving access to non-static fields or a static
-         * field
-         * @throws IllegalAccessException if access checking fails
-         * @throws NullPointerException if the argument is null
-         * @since 9
-         * @hide
-         */
-        public VarHandle unreflectVarHandle(Field f) throws IllegalAccessException {
-            final boolean isStatic = Modifier.isStatic(f.getModifiers());
-            final boolean performAccessChecks = true;
-            commonFieldChecks(f, f.getDeclaringClass(), f.getType(), isStatic, performAccessChecks);
-            return FieldVarHandle.create(f);
-        }
-        // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
-        /**
-         * Cracks a <a href="MethodHandleInfo.html#directmh">direct method handle</a>
-         * created by this lookup object or a similar one.
-         * Security and access checks are performed to ensure that this lookup object
-         * is capable of reproducing the target method handle.
-         * This means that the cracking may fail if target is a direct method handle
-         * but was created by an unrelated lookup object.
-         * This can happen if the method handle is <a href="MethodHandles.Lookup.html#callsens">caller sensitive</a>
-         * and was created by a lookup object for a different class.
-         * @param target a direct method handle to crack into symbolic reference components
-         * @return a symbolic reference which can be used to reconstruct this method handle from this lookup object
-         * @exception SecurityException if a security manager is present and it
-         *                              <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
-         * @throws IllegalArgumentException if the target is not a direct method handle or if access checking fails
-         * @exception NullPointerException if the target is {@code null}
-         * @see MethodHandleInfo
-         * @since 1.8
-         */
-        public MethodHandleInfo revealDirect(MethodHandle target) {
-            MethodHandleImpl directTarget = getMethodHandleImpl(target);
-            MethodHandleInfo info = directTarget.reveal();
-
-            try {
-                checkAccess(lookupClass(), info.getDeclaringClass(), info.getModifiers(),
-                        info.getName());
-            } catch (IllegalAccessException exception) {
-                throw new IllegalArgumentException("Unable to access memeber.", exception);
-            }
-
-            return info;
-        }
-
-        private boolean hasPrivateAccess() {
-            return (allowedModes & PRIVATE) != 0;
-        }
-
-        /** Check public/protected/private bits on the symbolic reference class and its member. */
-        void checkAccess(Class<?> refc, Class<?> defc, int mods, String methName)
-                throws IllegalAccessException {
-            int allowedModes = this.allowedModes;
-
-            if (Modifier.isProtected(mods) &&
-                    defc == Object.class &&
-                    "clone".equals(methName) &&
-                    refc.isArray()) {
-                // The JVM does this hack also.
-                // (See ClassVerifier::verify_invoke_instructions
-                // and LinkResolver::check_method_accessability.)
-                // Because the JVM does not allow separate methods on array types,
-                // there is no separate method for int[].clone.
-                // All arrays simply inherit Object.clone.
-                // But for access checking logic, we make Object.clone
-                // (normally protected) appear to be public.
-                // Later on, when the DirectMethodHandle is created,
-                // its leading argument will be restricted to the
-                // requested array type.
-                // N.B. The return type is not adjusted, because
-                // that is *not* the bytecode behavior.
-                mods ^= Modifier.PROTECTED | Modifier.PUBLIC;
-            }
-
-            if (Modifier.isProtected(mods) && Modifier.isConstructor(mods)) {
-                // cannot "new" a protected ctor in a different package
-                mods ^= Modifier.PROTECTED;
-            }
-
-            if (Modifier.isPublic(mods) && Modifier.isPublic(refc.getModifiers()) && allowedModes != 0)
-                return;  // common case
-            int requestedModes = fixmods(mods);  // adjust 0 => PACKAGE
-            if ((requestedModes & allowedModes) != 0) {
-                if (VerifyAccess.isMemberAccessible(refc, defc, mods, lookupClass(), allowedModes))
-                    return;
-            } else {
-                // Protected members can also be checked as if they were package-private.
-                if ((requestedModes & PROTECTED) != 0 && (allowedModes & PACKAGE) != 0
-                        && VerifyAccess.isSamePackage(defc, lookupClass()))
-                    return;
-            }
-
-            throwMakeAccessException(accessFailedMessage(refc, defc, mods), this);
-        }
-
-        String accessFailedMessage(Class<?> refc, Class<?> defc, int mods) {
-            // check the class first:
-            boolean classOK = (Modifier.isPublic(defc.getModifiers()) &&
-                    (defc == refc ||
-                            Modifier.isPublic(refc.getModifiers())));
-            if (!classOK && (allowedModes & PACKAGE) != 0) {
-                classOK = (VerifyAccess.isClassAccessible(defc, lookupClass(), ALL_MODES) &&
-                        (defc == refc ||
-                                VerifyAccess.isClassAccessible(refc, lookupClass(), ALL_MODES)));
-            }
-            if (!classOK)
-                return "class is not public";
-            if (Modifier.isPublic(mods))
-                return "access to public member failed";  // (how?)
-            if (Modifier.isPrivate(mods))
-                return "member is private";
-            if (Modifier.isProtected(mods))
-                return "member is protected";
-            return "member is private to package";
-        }
-
-        // Android-changed: checkSpecialCaller assumes that ALLOW_NESTMATE_ACCESS = false,
-        // as in upstream OpenJDK.
-        //
-        // private static final boolean ALLOW_NESTMATE_ACCESS = false;
-
-        private void checkSpecialCaller(Class<?> specialCaller) throws IllegalAccessException {
-            // Android-changed: No support for TRUSTED lookups. Also construct the
-            // IllegalAccessException by hand because the upstream code implicitly assumes
-            // that the lookupClass == specialCaller.
-            //
-            // if (allowedModes == TRUSTED)  return;
-            if (!hasPrivateAccess() || (specialCaller != lookupClass())) {
-                throw new IllegalAccessException("no private access for invokespecial : "
-                        + specialCaller + ", from" + this);
-            }
-        }
-
-        private void throwMakeAccessException(String message, Object from) throws
-                IllegalAccessException{
-            message = message + ": "+ toString();
-            if (from != null)  message += ", from " + from;
-            throw new IllegalAccessException(message);
-        }
-
-        private void checkReturnType(Method method, MethodType methodType)
-                throws NoSuchMethodException {
-            if (method.getReturnType() != methodType.rtype()) {
-                throw new NoSuchMethodException(method.getName() + methodType);
-            }
-        }
     }
 
-    /**
-     * "Cracks" {@code target} to reveal the underlying {@code MethodHandleImpl}.
-     */
-    private static MethodHandleImpl getMethodHandleImpl(MethodHandle target) {
-        // Special case : We implement handles to constructors as transformers,
-        // so we must extract the underlying handle from the transformer.
-        if (target instanceof Transformers.Construct) {
-            target = ((Transformers.Construct) target).getConstructorHandle();
-        }
-
-        // Special case: Var-args methods are also implemented as Transformers,
-        // so we should get the underlying handle in that case as well.
-        if (target instanceof Transformers.VarargsCollector) {
-            target = target.asFixedArity();
-        }
-
-        if (target instanceof MethodHandleImpl) {
-            return (MethodHandleImpl) target;
-        }
-
-        throw new IllegalArgumentException(target + " is not a direct handle");
-    }
-
-    // BEGIN Android-added: method to check if a class is an array.
-    private static void checkClassIsArray(Class<?> c) {
-        if (!c.isArray()) {
-            throw new IllegalArgumentException("Not an array type: " + c);
-        }
-    }
-
-    private static void checkTypeIsViewable(Class<?> componentType) {
-        if (componentType == short.class ||
-            componentType == char.class ||
-            componentType == int.class ||
-            componentType == long.class ||
-            componentType == float.class ||
-            componentType == double.class) {
-            return;
-        }
-        throw new UnsupportedOperationException("Component type not supported: " + componentType);
-    }
-    // END Android-added: method to check if a class is an array.
-
-    /**
-     * Produces a method handle giving read access to elements of an array.
-     * The type of the method handle will have a return type of the array's
-     * element type.  Its first argument will be the array type,
-     * and the second will be {@code int}.
-     * @param arrayClass an array type
-     * @return a method handle which can load values from the given array type
-     * @throws NullPointerException if the argument is null
-     * @throws  IllegalArgumentException if arrayClass is not an array type
-     */
     public static
-    MethodHandle arrayElementGetter(Class<?> arrayClass) throws IllegalArgumentException {
-        checkClassIsArray(arrayClass);
-        final Class<?> componentType = arrayClass.getComponentType();
-        if (componentType.isPrimitive()) {
-            try {
-                return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class,
-                        "arrayElementGetter",
-                        MethodType.methodType(componentType, arrayClass, int.class));
-            } catch (NoSuchMethodException | IllegalAccessException exception) {
-                throw new AssertionError(exception);
-            }
-        }
+    MethodHandle arrayElementGetter(Class<?> arrayClass) throws IllegalArgumentException { return null; }
 
-        return new Transformers.ReferenceArrayElementGetter(arrayClass);
-    }
-
-    /** @hide */ public static byte arrayElementGetter(byte[] array, int i) { return array[i]; }
-    /** @hide */ public static boolean arrayElementGetter(boolean[] array, int i) { return array[i]; }
-    /** @hide */ public static char arrayElementGetter(char[] array, int i) { return array[i]; }
-    /** @hide */ public static short arrayElementGetter(short[] array, int i) { return array[i]; }
-    /** @hide */ public static int arrayElementGetter(int[] array, int i) { return array[i]; }
-    /** @hide */ public static long arrayElementGetter(long[] array, int i) { return array[i]; }
-    /** @hide */ public static float arrayElementGetter(float[] array, int i) { return array[i]; }
-    /** @hide */ public static double arrayElementGetter(double[] array, int i) { return array[i]; }
-
-    /**
-     * Produces a method handle giving write access to elements of an array.
-     * The type of the method handle will have a void return type.
-     * Its last argument will be the array's element type.
-     * The first and second arguments will be the array type and int.
-     * @param arrayClass the class of an array
-     * @return a method handle which can store values into the array type
-     * @throws NullPointerException if the argument is null
-     * @throws IllegalArgumentException if arrayClass is not an array type
-     */
     public static
-    MethodHandle arrayElementSetter(Class<?> arrayClass) throws IllegalArgumentException {
-        checkClassIsArray(arrayClass);
-        final Class<?> componentType = arrayClass.getComponentType();
-        if (componentType.isPrimitive()) {
-            try {
-                return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class,
-                        "arrayElementSetter",
-                        MethodType.methodType(void.class, arrayClass, int.class, componentType));
-            } catch (NoSuchMethodException | IllegalAccessException exception) {
-                throw new AssertionError(exception);
-            }
-        }
+    MethodHandle arrayElementSetter(Class<?> arrayClass) throws IllegalArgumentException { return null; }
 
-        return new Transformers.ReferenceArrayElementSetter(arrayClass);
-    }
-
-    /** @hide */
-    public static void arrayElementSetter(byte[] array, int i, byte val) { array[i] = val; }
-    /** @hide */
-    public static void arrayElementSetter(boolean[] array, int i, boolean val) { array[i] = val; }
-    /** @hide */
-    public static void arrayElementSetter(char[] array, int i, char val) { array[i] = val; }
-    /** @hide */
-    public static void arrayElementSetter(short[] array, int i, short val) { array[i] = val; }
-    /** @hide */
-    public static void arrayElementSetter(int[] array, int i, int val) { array[i] = val; }
-    /** @hide */
-    public static void arrayElementSetter(long[] array, int i, long val) { array[i] = val; }
-    /** @hide */
-    public static void arrayElementSetter(float[] array, int i, float val) { array[i] = val; }
-    /** @hide */
-    public static void arrayElementSetter(double[] array, int i, double val) { array[i] = val; }
-
-    // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory methods.
-    /**
-     * Produces a VarHandle giving access to elements of an array of type
-     * {@code arrayClass}.  The VarHandle's variable type is the component type
-     * of {@code arrayClass} and the list of coordinate types is
-     * {@code (arrayClass, int)}, where the {@code int} coordinate type
-     * corresponds to an argument that is an index into an array.
-     * <p>
-     * Certain access modes of the returned VarHandle are unsupported under
-     * the following conditions:
-     * <ul>
-     * <li>if the component type is anything other than {@code byte},
-     *     {@code short}, {@code char}, {@code int}, {@code long},
-     *     {@code float}, or {@code double} then numeric atomic update access
-     *     modes are unsupported.
-     * <li>if the field type is anything other than {@code boolean},
-     *     {@code byte}, {@code short}, {@code char}, {@code int} or
-     *     {@code long} then bitwise atomic update access modes are
-     *     unsupported.
-     * </ul>
-     * <p>
-     * If the component type is {@code float} or {@code double} then numeric
-     * and atomic update access modes compare values using their bitwise
-     * representation (see {@link Float#floatToRawIntBits} and
-     * {@link Double#doubleToRawLongBits}, respectively).
-     * @apiNote
-     * Bitwise comparison of {@code float} values or {@code double} values,
-     * as performed by the numeric and atomic update access modes, differ
-     * from the primitive {@code ==} operator and the {@link Float#equals}
-     * and {@link Double#equals} methods, specifically with respect to
-     * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
-     * Care should be taken when performing a compare and set or a compare
-     * and exchange operation with such values since the operation may
-     * unexpectedly fail.
-     * There are many possible NaN values that are considered to be
-     * {@code NaN} in Java, although no IEEE 754 floating-point operation
-     * provided by Java can distinguish between them.  Operation failure can
-     * occur if the expected or witness value is a NaN value and it is
-     * transformed (perhaps in a platform specific manner) into another NaN
-     * value, and thus has a different bitwise representation (see
-     * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
-     * details).
-     * The values {@code -0.0} and {@code +0.0} have different bitwise
-     * representations but are considered equal when using the primitive
-     * {@code ==} operator.  Operation failure can occur if, for example, a
-     * numeric algorithm computes an expected value to be say {@code -0.0}
-     * and previously computed the witness value to be say {@code +0.0}.
-     * @param arrayClass the class of an array, of type {@code T[]}
-     * @return a VarHandle giving access to elements of an array
-     * @throws NullPointerException if the arrayClass is null
-     * @throws IllegalArgumentException if arrayClass is not an array type
-     * @since 9
-     * @hide
-     */
-    public static
-    VarHandle arrayElementVarHandle(Class<?> arrayClass) throws IllegalArgumentException {
-        checkClassIsArray(arrayClass);
-        return ArrayElementVarHandle.create(arrayClass);
-    }
-
-    /**
-     * Produces a VarHandle giving access to elements of a {@code byte[]} array
-     * viewed as if it were a different primitive array type, such as
-     * {@code int[]} or {@code long[]}.
-     * The VarHandle's variable type is the component type of
-     * {@code viewArrayClass} and the list of coordinate types is
-     * {@code (byte[], int)}, where the {@code int} coordinate type
-     * corresponds to an argument that is an index into a {@code byte[]} array.
-     * The returned VarHandle accesses bytes at an index in a {@code byte[]}
-     * array, composing bytes to or from a value of the component type of
-     * {@code viewArrayClass} according to the given endianness.
-     * <p>
-     * The supported component types (variables types) are {@code short},
-     * {@code char}, {@code int}, {@code long}, {@code float} and
-     * {@code double}.
-     * <p>
-     * Access of bytes at a given index will result in an
-     * {@code IndexOutOfBoundsException} if the index is less than {@code 0}
-     * or greater than the {@code byte[]} array length minus the size (in bytes)
-     * of {@code T}.
-     * <p>
-     * Access of bytes at an index may be aligned or misaligned for {@code T},
-     * with respect to the underlying memory address, {@code A} say, associated
-     * with the array and index.
-     * If access is misaligned then access for anything other than the
-     * {@code get} and {@code set} access modes will result in an
-     * {@code IllegalStateException}.  In such cases atomic access is only
-     * guaranteed with respect to the largest power of two that divides the GCD
-     * of {@code A} and the size (in bytes) of {@code T}.
-     * If access is aligned then following access modes are supported and are
-     * guaranteed to support atomic access:
-     * <ul>
-     * <li>read write access modes for all {@code T}, with the exception of
-     *     access modes {@code get} and {@code set} for {@code long} and
-     *     {@code double} on 32-bit platforms.
-     * <li>atomic update access modes for {@code int}, {@code long},
-     *     {@code float} or {@code double}.
-     *     (Future major platform releases of the JDK may support additional
-     *     types for certain currently unsupported access modes.)
-     * <li>numeric atomic update access modes for {@code int} and {@code long}.
-     *     (Future major platform releases of the JDK may support additional
-     *     numeric types for certain currently unsupported access modes.)
-     * <li>bitwise atomic update access modes for {@code int} and {@code long}.
-     *     (Future major platform releases of the JDK may support additional
-     *     numeric types for certain currently unsupported access modes.)
-     * </ul>
-     * <p>
-     * Misaligned access, and therefore atomicity guarantees, may be determined
-     * for {@code byte[]} arrays without operating on a specific array.  Given
-     * an {@code index}, {@code T} and it's corresponding boxed type,
-     * {@code T_BOX}, misalignment may be determined as follows:
-     * <pre>{@code
-     * int sizeOfT = T_BOX.BYTES;  // size in bytes of T
-     * int misalignedAtZeroIndex = ByteBuffer.wrap(new byte[0]).
-     *     alignmentOffset(0, sizeOfT);
-     * int misalignedAtIndex = (misalignedAtZeroIndex + index) % sizeOfT;
-     * boolean isMisaligned = misalignedAtIndex != 0;
-     * }</pre>
-     * <p>
-     * If the variable type is {@code float} or {@code double} then atomic
-     * update access modes compare values using their bitwise representation
-     * (see {@link Float#floatToRawIntBits} and
-     * {@link Double#doubleToRawLongBits}, respectively).
-     * @param viewArrayClass the view array class, with a component type of
-     * type {@code T}
-     * @param byteOrder the endianness of the view array elements, as
-     * stored in the underlying {@code byte} array
-     * @return a VarHandle giving access to elements of a {@code byte[]} array
-     * viewed as if elements corresponding to the components type of the view
-     * array class
-     * @throws NullPointerException if viewArrayClass or byteOrder is null
-     * @throws IllegalArgumentException if viewArrayClass is not an array type
-     * @throws UnsupportedOperationException if the component type of
-     * viewArrayClass is not supported as a variable type
-     * @since 9
-     * @hide
-     */
-    public static
-    VarHandle byteArrayViewVarHandle(Class<?> viewArrayClass,
-                                     ByteOrder byteOrder) throws IllegalArgumentException {
-        checkClassIsArray(viewArrayClass);
-        checkTypeIsViewable(viewArrayClass.getComponentType());
-        return ByteArrayViewVarHandle.create(viewArrayClass, byteOrder);
-    }
-
-    /**
-     * Produces a VarHandle giving access to elements of a {@code ByteBuffer}
-     * viewed as if it were an array of elements of a different primitive
-     * component type to that of {@code byte}, such as {@code int[]} or
-     * {@code long[]}.
-     * The VarHandle's variable type is the component type of
-     * {@code viewArrayClass} and the list of coordinate types is
-     * {@code (ByteBuffer, int)}, where the {@code int} coordinate type
-     * corresponds to an argument that is an index into a {@code byte[]} array.
-     * The returned VarHandle accesses bytes at an index in a
-     * {@code ByteBuffer}, composing bytes to or from a value of the component
-     * type of {@code viewArrayClass} according to the given endianness.
-     * <p>
-     * The supported component types (variables types) are {@code short},
-     * {@code char}, {@code int}, {@code long}, {@code float} and
-     * {@code double}.
-     * <p>
-     * Access will result in a {@code ReadOnlyBufferException} for anything
-     * other than the read access modes if the {@code ByteBuffer} is read-only.
-     * <p>
-     * Access of bytes at a given index will result in an
-     * {@code IndexOutOfBoundsException} if the index is less than {@code 0}
-     * or greater than the {@code ByteBuffer} limit minus the size (in bytes) of
-     * {@code T}.
-     * <p>
-     * Access of bytes at an index may be aligned or misaligned for {@code T},
-     * with respect to the underlying memory address, {@code A} say, associated
-     * with the {@code ByteBuffer} and index.
-     * If access is misaligned then access for anything other than the
-     * {@code get} and {@code set} access modes will result in an
-     * {@code IllegalStateException}.  In such cases atomic access is only
-     * guaranteed with respect to the largest power of two that divides the GCD
-     * of {@code A} and the size (in bytes) of {@code T}.
-     * If access is aligned then following access modes are supported and are
-     * guaranteed to support atomic access:
-     * <ul>
-     * <li>read write access modes for all {@code T}, with the exception of
-     *     access modes {@code get} and {@code set} for {@code long} and
-     *     {@code double} on 32-bit platforms.
-     * <li>atomic update access modes for {@code int}, {@code long},
-     *     {@code float} or {@code double}.
-     *     (Future major platform releases of the JDK may support additional
-     *     types for certain currently unsupported access modes.)
-     * <li>numeric atomic update access modes for {@code int} and {@code long}.
-     *     (Future major platform releases of the JDK may support additional
-     *     numeric types for certain currently unsupported access modes.)
-     * <li>bitwise atomic update access modes for {@code int} and {@code long}.
-     *     (Future major platform releases of the JDK may support additional
-     *     numeric types for certain currently unsupported access modes.)
-     * </ul>
-     * <p>
-     * Misaligned access, and therefore atomicity guarantees, may be determined
-     * for a {@code ByteBuffer}, {@code bb} (direct or otherwise), an
-     * {@code index}, {@code T} and it's corresponding boxed type,
-     * {@code T_BOX}, as follows:
-     * <pre>{@code
-     * int sizeOfT = T_BOX.BYTES;  // size in bytes of T
-     * ByteBuffer bb = ...
-     * int misalignedAtIndex = bb.alignmentOffset(index, sizeOfT);
-     * boolean isMisaligned = misalignedAtIndex != 0;
-     * }</pre>
-     * <p>
-     * If the variable type is {@code float} or {@code double} then atomic
-     * update access modes compare values using their bitwise representation
-     * (see {@link Float#floatToRawIntBits} and
-     * {@link Double#doubleToRawLongBits}, respectively).
-     * @param viewArrayClass the view array class, with a component type of
-     * type {@code T}
-     * @param byteOrder the endianness of the view array elements, as
-     * stored in the underlying {@code ByteBuffer} (Note this overrides the
-     * endianness of a {@code ByteBuffer})
-     * @return a VarHandle giving access to elements of a {@code ByteBuffer}
-     * viewed as if elements corresponding to the components type of the view
-     * array class
-     * @throws NullPointerException if viewArrayClass or byteOrder is null
-     * @throws IllegalArgumentException if viewArrayClass is not an array type
-     * @throws UnsupportedOperationException if the component type of
-     * viewArrayClass is not supported as a variable type
-     * @since 9
-     * @hide
-     */
-    public static
-    VarHandle byteBufferViewVarHandle(Class<?> viewArrayClass,
-                                      ByteOrder byteOrder) throws IllegalArgumentException {
-        checkClassIsArray(viewArrayClass);
-        checkTypeIsViewable(viewArrayClass.getComponentType());
-        return ByteBufferViewVarHandle.create(viewArrayClass, byteOrder);
-    }
-    // END Android-changed: OpenJDK 9+181 VarHandle API factory methods.
-
-    /// method handle invocation (reflective style)
-
-    /**
-     * Produces a method handle which will invoke any method handle of the
-     * given {@code type}, with a given number of trailing arguments replaced by
-     * a single trailing {@code Object[]} array.
-     * The resulting invoker will be a method handle with the following
-     * arguments:
-     * <ul>
-     * <li>a single {@code MethodHandle} target
-     * <li>zero or more leading values (counted by {@code leadingArgCount})
-     * <li>an {@code Object[]} array containing trailing arguments
-     * </ul>
-     * <p>
-     * The invoker will invoke its target like a call to {@link MethodHandle#invoke invoke} with
-     * the indicated {@code type}.
-     * That is, if the target is exactly of the given {@code type}, it will behave
-     * like {@code invokeExact}; otherwise it behave as if {@link MethodHandle#asType asType}
-     * is used to convert the target to the required {@code type}.
-     * <p>
-     * The type of the returned invoker will not be the given {@code type}, but rather
-     * will have all parameters except the first {@code leadingArgCount}
-     * replaced by a single array of type {@code Object[]}, which will be
-     * the final parameter.
-     * <p>
-     * Before invoking its target, the invoker will spread the final array, apply
-     * reference casts as necessary, and unbox and widen primitive arguments.
-     * If, when the invoker is called, the supplied array argument does
-     * not have the correct number of elements, the invoker will throw
-     * an {@link IllegalArgumentException} instead of invoking the target.
-     * <p>
-     * This method is equivalent to the following code (though it may be more efficient):
-     * <blockquote><pre>{@code
-MethodHandle invoker = MethodHandles.invoker(type);
-int spreadArgCount = type.parameterCount() - leadingArgCount;
-invoker = invoker.asSpreader(Object[].class, spreadArgCount);
-return invoker;
-     * }</pre></blockquote>
-     * This method throws no reflective or security exceptions.
-     * @param type the desired target type
-     * @param leadingArgCount number of fixed arguments, to be passed unchanged to the target
-     * @return a method handle suitable for invoking any method handle of the given type
-     * @throws NullPointerException if {@code type} is null
-     * @throws IllegalArgumentException if {@code leadingArgCount} is not in
-     *                  the range from 0 to {@code type.parameterCount()} inclusive,
-     *                  or if the resulting method handle's type would have
-     *          <a href="MethodHandle.html#maxarity">too many parameters</a>
-     */
     static public
-    MethodHandle spreadInvoker(MethodType type, int leadingArgCount) {
-        if (leadingArgCount < 0 || leadingArgCount > type.parameterCount())
-            throw newIllegalArgumentException("bad argument count", leadingArgCount);
+    MethodHandle spreadInvoker(MethodType type, int leadingArgCount) { return null; }
 
-        MethodHandle invoker = MethodHandles.invoker(type);
-        int spreadArgCount = type.parameterCount() - leadingArgCount;
-        invoker = invoker.asSpreader(Object[].class, spreadArgCount);
-        return invoker;
-    }
-
-    /**
-     * Produces a special <em>invoker method handle</em> which can be used to
-     * invoke any method handle of the given type, as if by {@link MethodHandle#invokeExact invokeExact}.
-     * The resulting invoker will have a type which is
-     * exactly equal to the desired type, except that it will accept
-     * an additional leading argument of type {@code MethodHandle}.
-     * <p>
-     * This method is equivalent to the following code (though it may be more efficient):
-     * {@code publicLookup().findVirtual(MethodHandle.class, "invokeExact", type)}
-     *
-     * <p style="font-size:smaller;">
-     * <em>Discussion:</em>
-     * Invoker method handles can be useful when working with variable method handles
-     * of unknown types.
-     * For example, to emulate an {@code invokeExact} call to a variable method
-     * handle {@code M}, extract its type {@code T},
-     * look up the invoker method {@code X} for {@code T},
-     * and call the invoker method, as {@code X.invoke(T, A...)}.
-     * (It would not work to call {@code X.invokeExact}, since the type {@code T}
-     * is unknown.)
-     * If spreading, collecting, or other argument transformations are required,
-     * they can be applied once to the invoker {@code X} and reused on many {@code M}
-     * method handle values, as long as they are compatible with the type of {@code X}.
-     * <p style="font-size:smaller;">
-     * <em>(Note:  The invoker method is not available via the Core Reflection API.
-     * An attempt to call {@linkplain java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
-     * on the declared {@code invokeExact} or {@code invoke} method will raise an
-     * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}.)</em>
-     * <p>
-     * This method throws no reflective or security exceptions.
-     * @param type the desired target type
-     * @return a method handle suitable for invoking any method handle of the given type
-     * @throws IllegalArgumentException if the resulting method handle's type would have
-     *          <a href="MethodHandle.html#maxarity">too many parameters</a>
-     */
     static public
-    MethodHandle exactInvoker(MethodType type) {
-        return new Transformers.Invoker(type, true /* isExactInvoker */);
-    }
+    MethodHandle exactInvoker(MethodType type) { return null; }
 
-    /**
-     * Produces a special <em>invoker method handle</em> which can be used to
-     * invoke any method handle compatible with the given type, as if by {@link MethodHandle#invoke invoke}.
-     * The resulting invoker will have a type which is
-     * exactly equal to the desired type, except that it will accept
-     * an additional leading argument of type {@code MethodHandle}.
-     * <p>
-     * Before invoking its target, if the target differs from the expected type,
-     * the invoker will apply reference casts as
-     * necessary and box, unbox, or widen primitive values, as if by {@link MethodHandle#asType asType}.
-     * Similarly, the return value will be converted as necessary.
-     * If the target is a {@linkplain MethodHandle#asVarargsCollector variable arity method handle},
-     * the required arity conversion will be made, again as if by {@link MethodHandle#asType asType}.
-     * <p>
-     * This method is equivalent to the following code (though it may be more efficient):
-     * {@code publicLookup().findVirtual(MethodHandle.class, "invoke", type)}
-     * <p style="font-size:smaller;">
-     * <em>Discussion:</em>
-     * A {@linkplain MethodType#genericMethodType general method type} is one which
-     * mentions only {@code Object} arguments and return values.
-     * An invoker for such a type is capable of calling any method handle
-     * of the same arity as the general type.
-     * <p style="font-size:smaller;">
-     * <em>(Note:  The invoker method is not available via the Core Reflection API.
-     * An attempt to call {@linkplain java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
-     * on the declared {@code invokeExact} or {@code invoke} method will raise an
-     * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}.)</em>
-     * <p>
-     * This method throws no reflective or security exceptions.
-     * @param type the desired target type
-     * @return a method handle suitable for invoking any method handle convertible to the given type
-     * @throws IllegalArgumentException if the resulting method handle's type would have
-     *          <a href="MethodHandle.html#maxarity">too many parameters</a>
-     */
     static public
-    MethodHandle invoker(MethodType type) {
-        return new Transformers.Invoker(type, false /* isExactInvoker */);
-    }
+    MethodHandle invoker(MethodType type) { return null; }
 
-    // BEGIN Android-added: resolver for VarHandle accessor methods.
-    static private MethodHandle methodHandleForVarHandleAccessor(VarHandle.AccessMode accessMode,
-                                                                 MethodType type,
-                                                                 boolean isExactInvoker) {
-        Class<?> refc = VarHandle.class;
-        Method method;
-        try {
-            method = refc.getDeclaredMethod(accessMode.methodName(), Object[].class);
-        } catch (NoSuchMethodException e) {
-            throw new InternalError("No method for AccessMode " + accessMode, e);
-        }
-        MethodType methodType = type.insertParameterTypes(0, VarHandle.class);
-        int kind = isExactInvoker ? MethodHandle.INVOKE_VAR_HANDLE_EXACT
-                                  : MethodHandle.INVOKE_VAR_HANDLE;
-        return new MethodHandleImpl(method.getArtMethod(), kind, methodType);
-    }
-    // END Android-added: resolver for VarHandle accessor methods.
-
-    /**
-     * Produces a special <em>invoker method handle</em> which can be used to
-     * invoke a signature-polymorphic access mode method on any VarHandle whose
-     * associated access mode type is compatible with the given type.
-     * The resulting invoker will have a type which is exactly equal to the
-     * desired given type, except that it will accept an additional leading
-     * argument of type {@code VarHandle}.
-     *
-     * @param accessMode the VarHandle access mode
-     * @param type the desired target type
-     * @return a method handle suitable for invoking an access mode method of
-     *         any VarHandle whose access mode type is of the given type.
-     * @since 9
-     * @hide
-     */
-    static public
-    MethodHandle varHandleExactInvoker(VarHandle.AccessMode accessMode, MethodType type) {
-        return methodHandleForVarHandleAccessor(accessMode, type, true /* isExactInvoker */);
-    }
-
-    /**
-     * Produces a special <em>invoker method handle</em> which can be used to
-     * invoke a signature-polymorphic access mode method on any VarHandle whose
-     * associated access mode type is compatible with the given type.
-     * The resulting invoker will have a type which is exactly equal to the
-     * desired given type, except that it will accept an additional leading
-     * argument of type {@code VarHandle}.
-     * <p>
-     * Before invoking its target, if the access mode type differs from the
-     * desired given type, the invoker will apply reference casts as necessary
-     * and box, unbox, or widen primitive values, as if by
-     * {@link MethodHandle#asType asType}.  Similarly, the return value will be
-     * converted as necessary.
-     * <p>
-     * This method is equivalent to the following code (though it may be more
-     * efficient): {@code publicLookup().findVirtual(VarHandle.class, accessMode.name(), type)}
-     *
-     * @param accessMode the VarHandle access mode
-     * @param type the desired target type
-     * @return a method handle suitable for invoking an access mode method of
-     *         any VarHandle whose access mode type is convertible to the given
-     *         type.
-     * @since 9
-     * @hide
-     */
-    static public
-    MethodHandle varHandleInvoker(VarHandle.AccessMode accessMode, MethodType type) {
-        return methodHandleForVarHandleAccessor(accessMode, type, false /* isExactInvoker */);
-    }
-
-    // Android-changed: Basic invokers are not supported.
-    //
-    // static /*non-public*/
-    // MethodHandle basicInvoker(MethodType type) {
-    //     return type.invokers().basicInvoker();
-    // }
-
-     /// method handle modification (creation from other method handles)
-
-    /**
-     * Produces a method handle which adapts the type of the
-     * given method handle to a new type by pairwise argument and return type conversion.
-     * The original type and new type must have the same number of arguments.
-     * The resulting method handle is guaranteed to report a type
-     * which is equal to the desired new type.
-     * <p>
-     * If the original type and new type are equal, returns target.
-     * <p>
-     * The same conversions are allowed as for {@link MethodHandle#asType MethodHandle.asType},
-     * and some additional conversions are also applied if those conversions fail.
-     * Given types <em>T0</em>, <em>T1</em>, one of the following conversions is applied
-     * if possible, before or instead of any conversions done by {@code asType}:
-     * <ul>
-     * <li>If <em>T0</em> and <em>T1</em> are references, and <em>T1</em> is an interface type,
-     *     then the value of type <em>T0</em> is passed as a <em>T1</em> without a cast.
-     *     (This treatment of interfaces follows the usage of the bytecode verifier.)
-     * <li>If <em>T0</em> is boolean and <em>T1</em> is another primitive,
-     *     the boolean is converted to a byte value, 1 for true, 0 for false.
-     *     (This treatment follows the usage of the bytecode verifier.)
-     * <li>If <em>T1</em> is boolean and <em>T0</em> is another primitive,
-     *     <em>T0</em> is converted to byte via Java casting conversion (JLS 5.5),
-     *     and the low order bit of the result is tested, as if by {@code (x & 1) != 0}.
-     * <li>If <em>T0</em> and <em>T1</em> are primitives other than boolean,
-     *     then a Java casting conversion (JLS 5.5) is applied.
-     *     (Specifically, <em>T0</em> will convert to <em>T1</em> by
-     *     widening and/or narrowing.)
-     * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive, an unboxing
-     *     conversion will be applied at runtime, possibly followed
-     *     by a Java casting conversion (JLS 5.5) on the primitive value,
-     *     possibly followed by a conversion from byte to boolean by testing
-     *     the low-order bit.
-     * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive,
-     *     and if the reference is null at runtime, a zero value is introduced.
-     * </ul>
-     * @param target the method handle to invoke after arguments are retyped
-     * @param newType the expected type of the new method handle
-     * @return a method handle which delegates to the target after performing
-     *           any necessary argument conversions, and arranges for any
-     *           necessary return value conversions
-     * @throws NullPointerException if either argument is null
-     * @throws WrongMethodTypeException if the conversion cannot be made
-     * @see MethodHandle#asType
-     */
     public static
-    MethodHandle explicitCastArguments(MethodHandle target, MethodType newType) {
-        explicitCastArgumentsChecks(target, newType);
-        // use the asTypeCache when possible:
-        MethodType oldType = target.type();
-        if (oldType == newType) return target;
-        if (oldType.explicitCastEquivalentToAsType(newType)) {
-            return target.asFixedArity().asType(newType);
-        }
+    MethodHandle explicitCastArguments(MethodHandle target, MethodType newType) { return null; }
 
-        return new Transformers.ExplicitCastArguments(target, newType);
-    }
-
-    private static void explicitCastArgumentsChecks(MethodHandle target, MethodType newType) {
-        if (target.type().parameterCount() != newType.parameterCount()) {
-            throw new WrongMethodTypeException("cannot explicitly cast " + target + " to " + newType);
-        }
-    }
-
-    /**
-     * Produces a method handle which adapts the calling sequence of the
-     * given method handle to a new type, by reordering the arguments.
-     * The resulting method handle is guaranteed to report a type
-     * which is equal to the desired new type.
-     * <p>
-     * The given array controls the reordering.
-     * Call {@code #I} the number of incoming parameters (the value
-     * {@code newType.parameterCount()}, and call {@code #O} the number
-     * of outgoing parameters (the value {@code target.type().parameterCount()}).
-     * Then the length of the reordering array must be {@code #O},
-     * and each element must be a non-negative number less than {@code #I}.
-     * For every {@code N} less than {@code #O}, the {@code N}-th
-     * outgoing argument will be taken from the {@code I}-th incoming
-     * argument, where {@code I} is {@code reorder[N]}.
-     * <p>
-     * No argument or return value conversions are applied.
-     * The type of each incoming argument, as determined by {@code newType},
-     * must be identical to the type of the corresponding outgoing parameter
-     * or parameters in the target method handle.
-     * The return type of {@code newType} must be identical to the return
-     * type of the original target.
-     * <p>
-     * The reordering array need not specify an actual permutation.
-     * An incoming argument will be duplicated if its index appears
-     * more than once in the array, and an incoming argument will be dropped
-     * if its index does not appear in the array.
-     * As in the case of {@link #dropArguments(MethodHandle,int,List) dropArguments},
-     * incoming arguments which are not mentioned in the reordering array
-     * are may be any type, as determined only by {@code newType}.
-     * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodType intfn1 = methodType(int.class, int.class);
-MethodType intfn2 = methodType(int.class, int.class, int.class);
-MethodHandle sub = ... (int x, int y) -> (x-y) ...;
-assert(sub.type().equals(intfn2));
-MethodHandle sub1 = permuteArguments(sub, intfn2, 0, 1);
-MethodHandle rsub = permuteArguments(sub, intfn2, 1, 0);
-assert((int)rsub.invokeExact(1, 100) == 99);
-MethodHandle add = ... (int x, int y) -> (x+y) ...;
-assert(add.type().equals(intfn2));
-MethodHandle twice = permuteArguments(add, intfn1, 0, 0);
-assert(twice.type().equals(intfn1));
-assert((int)twice.invokeExact(21) == 42);
-     * }</pre></blockquote>
-     * @param target the method handle to invoke after arguments are reordered
-     * @param newType the expected type of the new method handle
-     * @param reorder an index array which controls the reordering
-     * @return a method handle which delegates to the target after it
-     *           drops unused arguments and moves and/or duplicates the other arguments
-     * @throws NullPointerException if any argument is null
-     * @throws IllegalArgumentException if the index array length is not equal to
-     *                  the arity of the target, or if any index array element
-     *                  not a valid index for a parameter of {@code newType},
-     *                  or if two corresponding parameter types in
-     *                  {@code target.type()} and {@code newType} are not identical,
-     */
     public static
-    MethodHandle permuteArguments(MethodHandle target, MethodType newType, int... reorder) {
-        reorder = reorder.clone();  // get a private copy
-        MethodType oldType = target.type();
-        permuteArgumentChecks(reorder, newType, oldType);
+    MethodHandle permuteArguments(MethodHandle target, MethodType newType, int... reorder) { return null; }
 
-        return new Transformers.PermuteArguments(newType, target, reorder);
-    }
-
-    // Android-changed: findFirstDupOrDrop is unused and removed.
-    // private static int findFirstDupOrDrop(int[] reorder, int newArity);
-
-    private static boolean permuteArgumentChecks(int[] reorder, MethodType newType, MethodType oldType) {
-        if (newType.returnType() != oldType.returnType())
-            throw newIllegalArgumentException("return types do not match",
-                    oldType, newType);
-        if (reorder.length == oldType.parameterCount()) {
-            int limit = newType.parameterCount();
-            boolean bad = false;
-            for (int j = 0; j < reorder.length; j++) {
-                int i = reorder[j];
-                if (i < 0 || i >= limit) {
-                    bad = true; break;
-                }
-                Class<?> src = newType.parameterType(i);
-                Class<?> dst = oldType.parameterType(j);
-                if (src != dst)
-                    throw newIllegalArgumentException("parameter types do not match after reorder",
-                            oldType, newType);
-            }
-            if (!bad)  return true;
-        }
-        throw newIllegalArgumentException("bad reorder array: "+Arrays.toString(reorder));
-    }
-
-    /**
-     * Produces a method handle of the requested return type which returns the given
-     * constant value every time it is invoked.
-     * <p>
-     * Before the method handle is returned, the passed-in value is converted to the requested type.
-     * If the requested type is primitive, widening primitive conversions are attempted,
-     * else reference conversions are attempted.
-     * <p>The returned method handle is equivalent to {@code identity(type).bindTo(value)}.
-     * @param type the return type of the desired method handle
-     * @param value the value to return
-     * @return a method handle of the given return type and no arguments, which always returns the given value
-     * @throws NullPointerException if the {@code type} argument is null
-     * @throws ClassCastException if the value cannot be converted to the required return type
-     * @throws IllegalArgumentException if the given type is {@code void.class}
-     */
     public static
-    MethodHandle constant(Class<?> type, Object value) {
-        if (type.isPrimitive()) {
-            if (type == void.class)
-                throw newIllegalArgumentException("void type");
-            Wrapper w = Wrapper.forPrimitiveType(type);
-            value = w.convert(value, type);
-        }
+    MethodHandle constant(Class<?> type, Object value) { return null; }
 
-        return new Transformers.Constant(type, value);
-    }
-
-    /**
-     * Produces a method handle which returns its sole argument when invoked.
-     * @param type the type of the sole parameter and return value of the desired method handle
-     * @return a unary method handle which accepts and returns the given type
-     * @throws NullPointerException if the argument is null
-     * @throws IllegalArgumentException if the given type is {@code void.class}
-     */
     public static
-    MethodHandle identity(Class<?> type) {
-        if (type == null) {
-            throw new NullPointerException("type == null");
-        }
+    MethodHandle identity(Class<?> type) { return null; }
 
-        if (type.isPrimitive()) {
-            try {
-                return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class, "identity",
-                        MethodType.methodType(type, type));
-            } catch (NoSuchMethodException | IllegalAccessException e) {
-                throw new AssertionError(e);
-            }
-        }
-
-        return new Transformers.ReferenceIdentity(type);
-    }
-
-    /** @hide */ public static byte identity(byte val) { return val; }
-    /** @hide */ public static boolean identity(boolean val) { return val; }
-    /** @hide */ public static char identity(char val) { return val; }
-    /** @hide */ public static short identity(short val) { return val; }
-    /** @hide */ public static int identity(int val) { return val; }
-    /** @hide */ public static long identity(long val) { return val; }
-    /** @hide */ public static float identity(float val) { return val; }
-    /** @hide */ public static double identity(double val) { return val; }
-
-    /**
-     * Provides a target method handle with one or more <em>bound arguments</em>
-     * in advance of the method handle's invocation.
-     * The formal parameters to the target corresponding to the bound
-     * arguments are called <em>bound parameters</em>.
-     * Returns a new method handle which saves away the bound arguments.
-     * When it is invoked, it receives arguments for any non-bound parameters,
-     * binds the saved arguments to their corresponding parameters,
-     * and calls the original target.
-     * <p>
-     * The type of the new method handle will drop the types for the bound
-     * parameters from the original target type, since the new method handle
-     * will no longer require those arguments to be supplied by its callers.
-     * <p>
-     * Each given argument object must match the corresponding bound parameter type.
-     * If a bound parameter type is a primitive, the argument object
-     * must be a wrapper, and will be unboxed to produce the primitive value.
-     * <p>
-     * The {@code pos} argument selects which parameters are to be bound.
-     * It may range between zero and <i>N-L</i> (inclusively),
-     * where <i>N</i> is the arity of the target method handle
-     * and <i>L</i> is the length of the values array.
-     * @param target the method handle to invoke after the argument is inserted
-     * @param pos where to insert the argument (zero for the first)
-     * @param values the series of arguments to insert
-     * @return a method handle which inserts an additional argument,
-     *         before calling the original method handle
-     * @throws NullPointerException if the target or the {@code values} array is null
-     * @see MethodHandle#bindTo
-     */
     public static
-    MethodHandle insertArguments(MethodHandle target, int pos, Object... values) {
-        int insCount = values.length;
-        Class<?>[] ptypes = insertArgumentsChecks(target, insCount, pos);
-        if (insCount == 0)  {
-            return target;
-        }
+    MethodHandle insertArguments(MethodHandle target, int pos, Object... values) { return null; }
 
-        // Throw ClassCastExceptions early if we can't cast any of the provided values
-        // to the required type.
-        for (int i = 0; i < insCount; i++) {
-            final Class<?> ptype = ptypes[pos + i];
-            if (!ptype.isPrimitive()) {
-                ptypes[pos + i].cast(values[i]);
-            } else {
-                // Will throw a ClassCastException if something terrible happens.
-                values[i] = Wrapper.forPrimitiveType(ptype).convert(values[i], ptype);
-            }
-        }
-
-        return new Transformers.InsertArguments(target, pos, values);
-    }
-
-    // Android-changed: insertArgumentPrimitive is unused.
-    //
-    // private static BoundMethodHandle insertArgumentPrimitive(BoundMethodHandle result, int pos,
-    //                                                          Class<?> ptype, Object value) {
-    //     Wrapper w = Wrapper.forPrimitiveType(ptype);
-    //     // perform unboxing and/or primitive conversion
-    //     value = w.convert(value, ptype);
-    //     switch (w) {
-    //     case INT:     return result.bindArgumentI(pos, (int)value);
-    //     case LONG:    return result.bindArgumentJ(pos, (long)value);
-    //     case FLOAT:   return result.bindArgumentF(pos, (float)value);
-    //     case DOUBLE:  return result.bindArgumentD(pos, (double)value);
-    //     default:      return result.bindArgumentI(pos, ValueConversions.widenSubword(value));
-    //     }
-    // }
-
-    private static Class<?>[] insertArgumentsChecks(MethodHandle target, int insCount, int pos) throws RuntimeException {
-        MethodType oldType = target.type();
-        int outargs = oldType.parameterCount();
-        int inargs  = outargs - insCount;
-        if (inargs < 0)
-            throw newIllegalArgumentException("too many values to insert");
-        if (pos < 0 || pos > inargs)
-            throw newIllegalArgumentException("no argument type to append");
-        return oldType.ptypes();
-    }
-
-    /**
-     * Produces a method handle which will discard some dummy arguments
-     * before calling some other specified <i>target</i> method handle.
-     * The type of the new method handle will be the same as the target's type,
-     * except it will also include the dummy argument types,
-     * at some given position.
-     * <p>
-     * The {@code pos} argument may range between zero and <i>N</i>,
-     * where <i>N</i> is the arity of the target.
-     * If {@code pos} is zero, the dummy arguments will precede
-     * the target's real arguments; if {@code pos} is <i>N</i>
-     * they will come after.
-     * <p>
-     * <b>Example:</b>
-     * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
-  "concat", methodType(String.class, String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodType bigType = cat.type().insertParameterTypes(0, int.class, String.class);
-MethodHandle d0 = dropArguments(cat, 0, bigType.parameterList().subList(0,2));
-assertEquals(bigType, d0.type());
-assertEquals("yz", (String) d0.invokeExact(123, "x", "y", "z"));
-     * }</pre></blockquote>
-     * <p>
-     * This method is also equivalent to the following code:
-     * <blockquote><pre>
-     * {@link #dropArguments(MethodHandle,int,Class...) dropArguments}{@code (target, pos, valueTypes.toArray(new Class[0]))}
-     * </pre></blockquote>
-     * @param target the method handle to invoke after the arguments are dropped
-     * @param valueTypes the type(s) of the argument(s) to drop
-     * @param pos position of first argument to drop (zero for the leftmost)
-     * @return a method handle which drops arguments of the given types,
-     *         before calling the original method handle
-     * @throws NullPointerException if the target is null,
-     *                              or if the {@code valueTypes} list or any of its elements is null
-     * @throws IllegalArgumentException if any element of {@code valueTypes} is {@code void.class},
-     *                  or if {@code pos} is negative or greater than the arity of the target,
-     *                  or if the new method handle's type would have too many parameters
-     */
     public static
-    MethodHandle dropArguments(MethodHandle target, int pos, List<Class<?>> valueTypes) {
-        valueTypes = copyTypes(valueTypes);
-        MethodType oldType = target.type();  // get NPE
-        int dropped = dropArgumentChecks(oldType, pos, valueTypes);
+    MethodHandle dropArguments(MethodHandle target, int pos, List<Class<?>> valueTypes) { return null; }
 
-        MethodType newType = oldType.insertParameterTypes(pos, valueTypes);
-        if (dropped == 0) {
-            return target;
-        }
-
-        return new Transformers.DropArguments(newType, target, pos, valueTypes.size());
-    }
-
-    private static List<Class<?>> copyTypes(List<Class<?>> types) {
-        Object[] a = types.toArray();
-        return Arrays.asList(Arrays.copyOf(a, a.length, Class[].class));
-    }
-
-    private static int dropArgumentChecks(MethodType oldType, int pos, List<Class<?>> valueTypes) {
-        int dropped = valueTypes.size();
-        MethodType.checkSlotCount(dropped);
-        int outargs = oldType.parameterCount();
-        int inargs  = outargs + dropped;
-        if (pos < 0 || pos > outargs)
-            throw newIllegalArgumentException("no argument type to remove"
-                    + Arrays.asList(oldType, pos, valueTypes, inargs, outargs)
-                    );
-        return dropped;
-    }
-
-    /**
-     * Produces a method handle which will discard some dummy arguments
-     * before calling some other specified <i>target</i> method handle.
-     * The type of the new method handle will be the same as the target's type,
-     * except it will also include the dummy argument types,
-     * at some given position.
-     * <p>
-     * The {@code pos} argument may range between zero and <i>N</i>,
-     * where <i>N</i> is the arity of the target.
-     * If {@code pos} is zero, the dummy arguments will precede
-     * the target's real arguments; if {@code pos} is <i>N</i>
-     * they will come after.
-     * <p>
-     * <b>Example:</b>
-     * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
-  "concat", methodType(String.class, String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodHandle d0 = dropArguments(cat, 0, String.class);
-assertEquals("yz", (String) d0.invokeExact("x", "y", "z"));
-MethodHandle d1 = dropArguments(cat, 1, String.class);
-assertEquals("xz", (String) d1.invokeExact("x", "y", "z"));
-MethodHandle d2 = dropArguments(cat, 2, String.class);
-assertEquals("xy", (String) d2.invokeExact("x", "y", "z"));
-MethodHandle d12 = dropArguments(cat, 1, int.class, boolean.class);
-assertEquals("xz", (String) d12.invokeExact("x", 12, true, "z"));
-     * }</pre></blockquote>
-     * <p>
-     * This method is also equivalent to the following code:
-     * <blockquote><pre>
-     * {@link #dropArguments(MethodHandle,int,List) dropArguments}{@code (target, pos, Arrays.asList(valueTypes))}
-     * </pre></blockquote>
-     * @param target the method handle to invoke after the arguments are dropped
-     * @param valueTypes the type(s) of the argument(s) to drop
-     * @param pos position of first argument to drop (zero for the leftmost)
-     * @return a method handle which drops arguments of the given types,
-     *         before calling the original method handle
-     * @throws NullPointerException if the target is null,
-     *                              or if the {@code valueTypes} array or any of its elements is null
-     * @throws IllegalArgumentException if any element of {@code valueTypes} is {@code void.class},
-     *                  or if {@code pos} is negative or greater than the arity of the target,
-     *                  or if the new method handle's type would have
-     *                  <a href="MethodHandle.html#maxarity">too many parameters</a>
-     */
     public static
-    MethodHandle dropArguments(MethodHandle target, int pos, Class<?>... valueTypes) {
-        return dropArguments(target, pos, Arrays.asList(valueTypes));
-    }
+    MethodHandle dropArguments(MethodHandle target, int pos, Class<?>... valueTypes) { return null; }
 
-    /**
-     * Adapts a target method handle by pre-processing
-     * one or more of its arguments, each with its own unary filter function,
-     * and then calling the target with each pre-processed argument
-     * replaced by the result of its corresponding filter function.
-     * <p>
-     * The pre-processing is performed by one or more method handles,
-     * specified in the elements of the {@code filters} array.
-     * The first element of the filter array corresponds to the {@code pos}
-     * argument of the target, and so on in sequence.
-     * <p>
-     * Null arguments in the array are treated as identity functions,
-     * and the corresponding arguments left unchanged.
-     * (If there are no non-null elements in the array, the original target is returned.)
-     * Each filter is applied to the corresponding argument of the adapter.
-     * <p>
-     * If a filter {@code F} applies to the {@code N}th argument of
-     * the target, then {@code F} must be a method handle which
-     * takes exactly one argument.  The type of {@code F}'s sole argument
-     * replaces the corresponding argument type of the target
-     * in the resulting adapted method handle.
-     * The return type of {@code F} must be identical to the corresponding
-     * parameter type of the target.
-     * <p>
-     * It is an error if there are elements of {@code filters}
-     * (null or not)
-     * which do not correspond to argument positions in the target.
-     * <p><b>Example:</b>
-     * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
-  "concat", methodType(String.class, String.class));
-MethodHandle upcase = lookup().findVirtual(String.class,
-  "toUpperCase", methodType(String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodHandle f0 = filterArguments(cat, 0, upcase);
-assertEquals("Xy", (String) f0.invokeExact("x", "y")); // Xy
-MethodHandle f1 = filterArguments(cat, 1, upcase);
-assertEquals("xY", (String) f1.invokeExact("x", "y")); // xY
-MethodHandle f2 = filterArguments(cat, 0, upcase, upcase);
-assertEquals("XY", (String) f2.invokeExact("x", "y")); // XY
-     * }</pre></blockquote>
-     * <p> Here is pseudocode for the resulting adapter:
-     * <blockquote><pre>{@code
-     * V target(P... p, A[i]... a[i], B... b);
-     * A[i] filter[i](V[i]);
-     * T adapter(P... p, V[i]... v[i], B... b) {
-     *   return target(p..., f[i](v[i])..., b...);
-     * }
-     * }</pre></blockquote>
-     *
-     * @param target the method handle to invoke after arguments are filtered
-     * @param pos the position of the first argument to filter
-     * @param filters method handles to call initially on filtered arguments
-     * @return method handle which incorporates the specified argument filtering logic
-     * @throws NullPointerException if the target is null
-     *                              or if the {@code filters} array is null
-     * @throws IllegalArgumentException if a non-null element of {@code filters}
-     *          does not match a corresponding argument type of target as described above,
-     *          or if the {@code pos+filters.length} is greater than {@code target.type().parameterCount()},
-     *          or if the resulting method handle's type would have
-     *          <a href="MethodHandle.html#maxarity">too many parameters</a>
-     */
     public static
-    MethodHandle filterArguments(MethodHandle target, int pos, MethodHandle... filters) {
-        filterArgumentsCheckArity(target, pos, filters);
+    MethodHandle filterArguments(MethodHandle target, int pos, MethodHandle... filters) { return null; }
 
-        for (int i = 0; i < filters.length; ++i) {
-            filterArgumentChecks(target, i + pos, filters[i]);
-        }
-
-        return new Transformers.FilterArguments(target, pos, filters);
-    }
-
-    private static void filterArgumentsCheckArity(MethodHandle target, int pos, MethodHandle[] filters) {
-        MethodType targetType = target.type();
-        int maxPos = targetType.parameterCount();
-        if (pos + filters.length > maxPos)
-            throw newIllegalArgumentException("too many filters");
-    }
-
-    private static void filterArgumentChecks(MethodHandle target, int pos, MethodHandle filter) throws RuntimeException {
-        MethodType targetType = target.type();
-        MethodType filterType = filter.type();
-        if (filterType.parameterCount() != 1
-            || filterType.returnType() != targetType.parameterType(pos))
-            throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
-    }
-
-    /**
-     * Adapts a target method handle by pre-processing
-     * a sub-sequence of its arguments with a filter (another method handle).
-     * The pre-processed arguments are replaced by the result (if any) of the
-     * filter function.
-     * The target is then called on the modified (usually shortened) argument list.
-     * <p>
-     * If the filter returns a value, the target must accept that value as
-     * its argument in position {@code pos}, preceded and/or followed by
-     * any arguments not passed to the filter.
-     * If the filter returns void, the target must accept all arguments
-     * not passed to the filter.
-     * No arguments are reordered, and a result returned from the filter
-     * replaces (in order) the whole subsequence of arguments originally
-     * passed to the adapter.
-     * <p>
-     * The argument types (if any) of the filter
-     * replace zero or one argument types of the target, at position {@code pos},
-     * in the resulting adapted method handle.
-     * The return type of the filter (if any) must be identical to the
-     * argument type of the target at position {@code pos}, and that target argument
-     * is supplied by the return value of the filter.
-     * <p>
-     * In all cases, {@code pos} must be greater than or equal to zero, and
-     * {@code pos} must also be less than or equal to the target's arity.
-     * <p><b>Example:</b>
-     * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle deepToString = publicLookup()
-  .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-
-MethodHandle ts1 = deepToString.asCollector(String[].class, 1);
-assertEquals("[strange]", (String) ts1.invokeExact("strange"));
-
-MethodHandle ts2 = deepToString.asCollector(String[].class, 2);
-assertEquals("[up, down]", (String) ts2.invokeExact("up", "down"));
-
-MethodHandle ts3 = deepToString.asCollector(String[].class, 3);
-MethodHandle ts3_ts2 = collectArguments(ts3, 1, ts2);
-assertEquals("[top, [up, down], strange]",
-             (String) ts3_ts2.invokeExact("top", "up", "down", "strange"));
-
-MethodHandle ts3_ts2_ts1 = collectArguments(ts3_ts2, 3, ts1);
-assertEquals("[top, [up, down], [strange]]",
-             (String) ts3_ts2_ts1.invokeExact("top", "up", "down", "strange"));
-
-MethodHandle ts3_ts2_ts3 = collectArguments(ts3_ts2, 1, ts3);
-assertEquals("[top, [[up, down, strange], charm], bottom]",
-             (String) ts3_ts2_ts3.invokeExact("top", "up", "down", "strange", "charm", "bottom"));
-     * }</pre></blockquote>
-     * <p> Here is pseudocode for the resulting adapter:
-     * <blockquote><pre>{@code
-     * T target(A...,V,C...);
-     * V filter(B...);
-     * T adapter(A... a,B... b,C... c) {
-     *   V v = filter(b...);
-     *   return target(a...,v,c...);
-     * }
-     * // and if the filter has no arguments:
-     * T target2(A...,V,C...);
-     * V filter2();
-     * T adapter2(A... a,C... c) {
-     *   V v = filter2();
-     *   return target2(a...,v,c...);
-     * }
-     * // and if the filter has a void return:
-     * T target3(A...,C...);
-     * void filter3(B...);
-     * void adapter3(A... a,B... b,C... c) {
-     *   filter3(b...);
-     *   return target3(a...,c...);
-     * }
-     * }</pre></blockquote>
-     * <p>
-     * A collection adapter {@code collectArguments(mh, 0, coll)} is equivalent to
-     * one which first "folds" the affected arguments, and then drops them, in separate
-     * steps as follows:
-     * <blockquote><pre>{@code
-     * mh = MethodHandles.dropArguments(mh, 1, coll.type().parameterList()); //step 2
-     * mh = MethodHandles.foldArguments(mh, coll); //step 1
-     * }</pre></blockquote>
-     * If the target method handle consumes no arguments besides than the result
-     * (if any) of the filter {@code coll}, then {@code collectArguments(mh, 0, coll)}
-     * is equivalent to {@code filterReturnValue(coll, mh)}.
-     * If the filter method handle {@code coll} consumes one argument and produces
-     * a non-void result, then {@code collectArguments(mh, N, coll)}
-     * is equivalent to {@code filterArguments(mh, N, coll)}.
-     * Other equivalences are possible but would require argument permutation.
-     *
-     * @param target the method handle to invoke after filtering the subsequence of arguments
-     * @param pos the position of the first adapter argument to pass to the filter,
-     *            and/or the target argument which receives the result of the filter
-     * @param filter method handle to call on the subsequence of arguments
-     * @return method handle which incorporates the specified argument subsequence filtering logic
-     * @throws NullPointerException if either argument is null
-     * @throws IllegalArgumentException if the return type of {@code filter}
-     *          is non-void and is not the same as the {@code pos} argument of the target,
-     *          or if {@code pos} is not between 0 and the target's arity, inclusive,
-     *          or if the resulting method handle's type would have
-     *          <a href="MethodHandle.html#maxarity">too many parameters</a>
-     * @see MethodHandles#foldArguments
-     * @see MethodHandles#filterArguments
-     * @see MethodHandles#filterReturnValue
-     */
     public static
-    MethodHandle collectArguments(MethodHandle target, int pos, MethodHandle filter) {
-        MethodType newType = collectArgumentsChecks(target, pos, filter);
-        return new Transformers.CollectArguments(target, filter, pos, newType);
-    }
+    MethodHandle collectArguments(MethodHandle target, int pos, MethodHandle filter) { return null; }
 
-    private static MethodType collectArgumentsChecks(MethodHandle target, int pos, MethodHandle filter) throws RuntimeException {
-        MethodType targetType = target.type();
-        MethodType filterType = filter.type();
-        Class<?> rtype = filterType.returnType();
-        List<Class<?>> filterArgs = filterType.parameterList();
-        if (rtype == void.class) {
-            return targetType.insertParameterTypes(pos, filterArgs);
-        }
-        if (rtype != targetType.parameterType(pos)) {
-            throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
-        }
-        return targetType.dropParameterTypes(pos, pos+1).insertParameterTypes(pos, filterArgs);
-    }
-
-    /**
-     * Adapts a target method handle by post-processing
-     * its return value (if any) with a filter (another method handle).
-     * The result of the filter is returned from the adapter.
-     * <p>
-     * If the target returns a value, the filter must accept that value as
-     * its only argument.
-     * If the target returns void, the filter must accept no arguments.
-     * <p>
-     * The return type of the filter
-     * replaces the return type of the target
-     * in the resulting adapted method handle.
-     * The argument type of the filter (if any) must be identical to the
-     * return type of the target.
-     * <p><b>Example:</b>
-     * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
-  "concat", methodType(String.class, String.class));
-MethodHandle length = lookup().findVirtual(String.class,
-  "length", methodType(int.class));
-System.out.println((String) cat.invokeExact("x", "y")); // xy
-MethodHandle f0 = filterReturnValue(cat, length);
-System.out.println((int) f0.invokeExact("x", "y")); // 2
-     * }</pre></blockquote>
-     * <p> Here is pseudocode for the resulting adapter:
-     * <blockquote><pre>{@code
-     * V target(A...);
-     * T filter(V);
-     * T adapter(A... a) {
-     *   V v = target(a...);
-     *   return filter(v);
-     * }
-     * // and if the target has a void return:
-     * void target2(A...);
-     * T filter2();
-     * T adapter2(A... a) {
-     *   target2(a...);
-     *   return filter2();
-     * }
-     * // and if the filter has a void return:
-     * V target3(A...);
-     * void filter3(V);
-     * void adapter3(A... a) {
-     *   V v = target3(a...);
-     *   filter3(v);
-     * }
-     * }</pre></blockquote>
-     * @param target the method handle to invoke before filtering the return value
-     * @param filter method handle to call on the return value
-     * @return method handle which incorporates the specified return value filtering logic
-     * @throws NullPointerException if either argument is null
-     * @throws IllegalArgumentException if the argument list of {@code filter}
-     *          does not match the return type of target as described above
-     */
     public static
-    MethodHandle filterReturnValue(MethodHandle target, MethodHandle filter) {
-        MethodType targetType = target.type();
-        MethodType filterType = filter.type();
-        filterReturnValueChecks(targetType, filterType);
+    MethodHandle filterReturnValue(MethodHandle target, MethodHandle filter) { return null; }
 
-        return new Transformers.FilterReturnValue(target, filter);
-    }
-
-    private static void filterReturnValueChecks(MethodType targetType, MethodType filterType) throws RuntimeException {
-        Class<?> rtype = targetType.returnType();
-        int filterValues = filterType.parameterCount();
-        if (filterValues == 0
-                ? (rtype != void.class)
-                : (rtype != filterType.parameterType(0) || filterValues != 1))
-            throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
-    }
-
-    /**
-     * Adapts a target method handle by pre-processing
-     * some of its arguments, and then calling the target with
-     * the result of the pre-processing, inserted into the original
-     * sequence of arguments.
-     * <p>
-     * The pre-processing is performed by {@code combiner}, a second method handle.
-     * Of the arguments passed to the adapter, the first {@code N} arguments
-     * are copied to the combiner, which is then called.
-     * (Here, {@code N} is defined as the parameter count of the combiner.)
-     * After this, control passes to the target, with any result
-     * from the combiner inserted before the original {@code N} incoming
-     * arguments.
-     * <p>
-     * If the combiner returns a value, the first parameter type of the target
-     * must be identical with the return type of the combiner, and the next
-     * {@code N} parameter types of the target must exactly match the parameters
-     * of the combiner.
-     * <p>
-     * If the combiner has a void return, no result will be inserted,
-     * and the first {@code N} parameter types of the target
-     * must exactly match the parameters of the combiner.
-     * <p>
-     * The resulting adapter is the same type as the target, except that the
-     * first parameter type is dropped,
-     * if it corresponds to the result of the combiner.
-     * <p>
-     * (Note that {@link #dropArguments(MethodHandle,int,List) dropArguments} can be used to remove any arguments
-     * that either the combiner or the target does not wish to receive.
-     * If some of the incoming arguments are destined only for the combiner,
-     * consider using {@link MethodHandle#asCollector asCollector} instead, since those
-     * arguments will not need to be live on the stack on entry to the
-     * target.)
-     * <p><b>Example:</b>
-     * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle trace = publicLookup().findVirtual(java.io.PrintStream.class,
-  "println", methodType(void.class, String.class))
-    .bindTo(System.out);
-MethodHandle cat = lookup().findVirtual(String.class,
-  "concat", methodType(String.class, String.class));
-assertEquals("boojum", (String) cat.invokeExact("boo", "jum"));
-MethodHandle catTrace = foldArguments(cat, trace);
-// also prints "boo":
-assertEquals("boojum", (String) catTrace.invokeExact("boo", "jum"));
-     * }</pre></blockquote>
-     * <p> Here is pseudocode for the resulting adapter:
-     * <blockquote><pre>{@code
-     * // there are N arguments in A...
-     * T target(V, A[N]..., B...);
-     * V combiner(A...);
-     * T adapter(A... a, B... b) {
-     *   V v = combiner(a...);
-     *   return target(v, a..., b...);
-     * }
-     * // and if the combiner has a void return:
-     * T target2(A[N]..., B...);
-     * void combiner2(A...);
-     * T adapter2(A... a, B... b) {
-     *   combiner2(a...);
-     *   return target2(a..., b...);
-     * }
-     * }</pre></blockquote>
-     * @param target the method handle to invoke after arguments are combined
-     * @param combiner method handle to call initially on the incoming arguments
-     * @return method handle which incorporates the specified argument folding logic
-     * @throws NullPointerException if either argument is null
-     * @throws IllegalArgumentException if {@code combiner}'s return type
-     *          is non-void and not the same as the first argument type of
-     *          the target, or if the initial {@code N} argument types
-     *          of the target
-     *          (skipping one matching the {@code combiner}'s return type)
-     *          are not identical with the argument types of {@code combiner}
-     */
     public static
-    MethodHandle foldArguments(MethodHandle target, MethodHandle combiner) {
-        int foldPos = 0;
-        MethodType targetType = target.type();
-        MethodType combinerType = combiner.type();
-        Class<?> rtype = foldArgumentChecks(foldPos, targetType, combinerType);
+    MethodHandle foldArguments(MethodHandle target, MethodHandle combiner) { return null; }
 
-        return new Transformers.FoldArguments(target, combiner);
-    }
-
-    private static Class<?> foldArgumentChecks(int foldPos, MethodType targetType, MethodType combinerType) {
-        int foldArgs   = combinerType.parameterCount();
-        Class<?> rtype = combinerType.returnType();
-        int foldVals = rtype == void.class ? 0 : 1;
-        int afterInsertPos = foldPos + foldVals;
-        boolean ok = (targetType.parameterCount() >= afterInsertPos + foldArgs);
-        if (ok && !(combinerType.parameterList()
-                    .equals(targetType.parameterList().subList(afterInsertPos,
-                                                               afterInsertPos + foldArgs))))
-            ok = false;
-        if (ok && foldVals != 0 && combinerType.returnType() != targetType.parameterType(0))
-            ok = false;
-        if (!ok)
-            throw misMatchedTypes("target and combiner types", targetType, combinerType);
-        return rtype;
-    }
-
-    /**
-     * Makes a method handle which adapts a target method handle,
-     * by guarding it with a test, a boolean-valued method handle.
-     * If the guard fails, a fallback handle is called instead.
-     * All three method handles must have the same corresponding
-     * argument and return types, except that the return type
-     * of the test must be boolean, and the test is allowed
-     * to have fewer arguments than the other two method handles.
-     * <p> Here is pseudocode for the resulting adapter:
-     * <blockquote><pre>{@code
-     * boolean test(A...);
-     * T target(A...,B...);
-     * T fallback(A...,B...);
-     * T adapter(A... a,B... b) {
-     *   if (test(a...))
-     *     return target(a..., b...);
-     *   else
-     *     return fallback(a..., b...);
-     * }
-     * }</pre></blockquote>
-     * Note that the test arguments ({@code a...} in the pseudocode) cannot
-     * be modified by execution of the test, and so are passed unchanged
-     * from the caller to the target or fallback as appropriate.
-     * @param test method handle used for test, must return boolean
-     * @param target method handle to call if test passes
-     * @param fallback method handle to call if test fails
-     * @return method handle which incorporates the specified if/then/else logic
-     * @throws NullPointerException if any argument is null
-     * @throws IllegalArgumentException if {@code test} does not return boolean,
-     *          or if all three method types do not match (with the return
-     *          type of {@code test} changed to match that of the target).
-     */
     public static
     MethodHandle guardWithTest(MethodHandle test,
                                MethodHandle target,
-                               MethodHandle fallback) {
-        MethodType gtype = test.type();
-        MethodType ttype = target.type();
-        MethodType ftype = fallback.type();
-        if (!ttype.equals(ftype))
-            throw misMatchedTypes("target and fallback types", ttype, ftype);
-        if (gtype.returnType() != boolean.class)
-            throw newIllegalArgumentException("guard type is not a predicate "+gtype);
-        List<Class<?>> targs = ttype.parameterList();
-        List<Class<?>> gargs = gtype.parameterList();
-        if (!targs.equals(gargs)) {
-            int gpc = gargs.size(), tpc = targs.size();
-            if (gpc >= tpc || !targs.subList(0, gpc).equals(gargs))
-                throw misMatchedTypes("target and test types", ttype, gtype);
-            test = dropArguments(test, gpc, targs.subList(gpc, tpc));
-            gtype = test.type();
-        }
+                               MethodHandle fallback) { return null; }
 
-        return new Transformers.GuardWithTest(test, target, fallback);
-    }
-
-    static RuntimeException misMatchedTypes(String what, MethodType t1, MethodType t2) {
-        return newIllegalArgumentException(what + " must match: " + t1 + " != " + t2);
-    }
-
-    /**
-     * Makes a method handle which adapts a target method handle,
-     * by running it inside an exception handler.
-     * If the target returns normally, the adapter returns that value.
-     * If an exception matching the specified type is thrown, the fallback
-     * handle is called instead on the exception, plus the original arguments.
-     * <p>
-     * The target and handler must have the same corresponding
-     * argument and return types, except that handler may omit trailing arguments
-     * (similarly to the predicate in {@link #guardWithTest guardWithTest}).
-     * Also, the handler must have an extra leading parameter of {@code exType} or a supertype.
-     * <p> Here is pseudocode for the resulting adapter:
-     * <blockquote><pre>{@code
-     * T target(A..., B...);
-     * T handler(ExType, A...);
-     * T adapter(A... a, B... b) {
-     *   try {
-     *     return target(a..., b...);
-     *   } catch (ExType ex) {
-     *     return handler(ex, a...);
-     *   }
-     * }
-     * }</pre></blockquote>
-     * Note that the saved arguments ({@code a...} in the pseudocode) cannot
-     * be modified by execution of the target, and so are passed unchanged
-     * from the caller to the handler, if the handler is invoked.
-     * <p>
-     * The target and handler must return the same type, even if the handler
-     * always throws.  (This might happen, for instance, because the handler
-     * is simulating a {@code finally} clause).
-     * To create such a throwing handler, compose the handler creation logic
-     * with {@link #throwException throwException},
-     * in order to create a method handle of the correct return type.
-     * @param target method handle to call
-     * @param exType the type of exception which the handler will catch
-     * @param handler method handle to call if a matching exception is thrown
-     * @return method handle which incorporates the specified try/catch logic
-     * @throws NullPointerException if any argument is null
-     * @throws IllegalArgumentException if {@code handler} does not accept
-     *          the given exception type, or if the method handle types do
-     *          not match in their return types and their
-     *          corresponding parameters
-     */
     public static
     MethodHandle catchException(MethodHandle target,
                                 Class<? extends Throwable> exType,
-                                MethodHandle handler) {
-        MethodType ttype = target.type();
-        MethodType htype = handler.type();
-        if (htype.parameterCount() < 1 ||
-            !htype.parameterType(0).isAssignableFrom(exType))
-            throw newIllegalArgumentException("handler does not accept exception type "+exType);
-        if (htype.returnType() != ttype.returnType())
-            throw misMatchedTypes("target and handler return types", ttype, htype);
-        List<Class<?>> targs = ttype.parameterList();
-        List<Class<?>> hargs = htype.parameterList();
-        hargs = hargs.subList(1, hargs.size());  // omit leading parameter from handler
-        if (!targs.equals(hargs)) {
-            int hpc = hargs.size(), tpc = targs.size();
-            if (hpc >= tpc || !targs.subList(0, hpc).equals(hargs))
-                throw misMatchedTypes("target and handler types", ttype, htype);
-        }
+                                MethodHandle handler) { return null; }
 
-        return new Transformers.CatchException(target, handler, exType);
-    }
-
-    /**
-     * Produces a method handle which will throw exceptions of the given {@code exType}.
-     * The method handle will accept a single argument of {@code exType},
-     * and immediately throw it as an exception.
-     * The method type will nominally specify a return of {@code returnType}.
-     * The return type may be anything convenient:  It doesn't matter to the
-     * method handle's behavior, since it will never return normally.
-     * @param returnType the return type of the desired method handle
-     * @param exType the parameter type of the desired method handle
-     * @return method handle which can throw the given exceptions
-     * @throws NullPointerException if either argument is null
-     */
     public static
-    MethodHandle throwException(Class<?> returnType, Class<? extends Throwable> exType) {
-        if (!Throwable.class.isAssignableFrom(exType))
-            throw new ClassCastException(exType.getName());
-
-        return new Transformers.AlwaysThrow(returnType, exType);
-    }
+    MethodHandle throwException(Class<?> returnType, Class<? extends Throwable> exType) { return null; }
 }
diff --git a/java/lang/invoke/MethodType.java b/java/lang/invoke/MethodType.java
index bfa7ccd..4cb5c22 100644
--- a/java/lang/invoke/MethodType.java
+++ b/java/lang/invoke/MethodType.java
@@ -25,1227 +25,78 @@
 
 package java.lang.invoke;
 
-import sun.invoke.util.Wrapper;
-import java.lang.ref.WeakReference;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentHashMap;
-import sun.invoke.util.BytecodeDescriptor;
-import static java.lang.invoke.MethodHandleStatics.*;
 
-/**
- * A method type represents the arguments and return type accepted and
- * returned by a method handle, or the arguments and return type passed
- * and expected  by a method handle caller.  Method types must be properly
- * matched between a method handle and all its callers,
- * and the JVM's operations enforce this matching at, specifically
- * during calls to {@link MethodHandle#invokeExact MethodHandle.invokeExact}
- * and {@link MethodHandle#invoke MethodHandle.invoke}, and during execution
- * of {@code invokedynamic} instructions.
- * <p>
- * The structure is a return type accompanied by any number of parameter types.
- * The types (primitive, {@code void}, and reference) are represented by {@link Class} objects.
- * (For ease of exposition, we treat {@code void} as if it were a type.
- * In fact, it denotes the absence of a return type.)
- * <p>
- * All instances of {@code MethodType} are immutable.
- * Two instances are completely interchangeable if they compare equal.
- * Equality depends on pairwise correspondence of the return and parameter types and on nothing else.
- * <p>
- * This type can be created only by factory methods.
- * All factory methods may cache values, though caching is not guaranteed.
- * Some factory methods are static, while others are virtual methods which
- * modify precursor method types, e.g., by changing a selected parameter.
- * <p>
- * Factory methods which operate on groups of parameter types
- * are systematically presented in two versions, so that both Java arrays and
- * Java lists can be used to work with groups of parameter types.
- * The query methods {@code parameterArray} and {@code parameterList}
- * also provide a choice between arrays and lists.
- * <p>
- * {@code MethodType} objects are sometimes derived from bytecode instructions
- * such as {@code invokedynamic}, specifically from the type descriptor strings associated
- * with the instructions in a class file's constant pool.
- * <p>
- * Like classes and strings, method types can also be represented directly
- * in a class file's constant pool as constants.
- * A method type may be loaded by an {@code ldc} instruction which refers
- * to a suitable {@code CONSTANT_MethodType} constant pool entry.
- * The entry refers to a {@code CONSTANT_Utf8} spelling for the descriptor string.
- * (For full details on method type constants,
- * see sections 4.4.8 and 5.4.3.5 of the Java Virtual Machine Specification.)
- * <p>
- * When the JVM materializes a {@code MethodType} from a descriptor string,
- * all classes named in the descriptor must be accessible, and will be loaded.
- * (But the classes need not be initialized, as is the case with a {@code CONSTANT_Class}.)
- * This loading may occur at any time before the {@code MethodType} object is first derived.
- * @author John Rose, JSR 292 EG
- */
 public final
 class MethodType implements java.io.Serializable {
-    private static final long serialVersionUID = 292L;  // {rtype, {ptype...}}
 
-    // The rtype and ptypes fields define the structural identity of the method type:
-    private final Class<?>   rtype;
-    private final Class<?>[] ptypes;
-
-    // The remaining fields are caches of various sorts:
-    private @Stable MethodTypeForm form; // erased form, plus cached data about primitives
-    private @Stable MethodType wrapAlt;  // alternative wrapped/unwrapped version
-    // Android-changed: Remove adapter cache. We're not dynamically generating any
-    // adapters at this point.
-    // private @Stable Invokers invokers;   // cache of handy higher-order adapters
-    private @Stable String methodDescriptor;  // cache for toMethodDescriptorString
-
-    /**
-     * Check the given parameters for validity and store them into the final fields.
-     */
-    private MethodType(Class<?> rtype, Class<?>[] ptypes, boolean trusted) {
-        checkRtype(rtype);
-        checkPtypes(ptypes);
-        this.rtype = rtype;
-        // defensively copy the array passed in by the user
-        this.ptypes = trusted ? ptypes : Arrays.copyOf(ptypes, ptypes.length);
-    }
-
-    /**
-     * Construct a temporary unchecked instance of MethodType for use only as a key to the intern table.
-     * Does not check the given parameters for validity, and must be discarded after it is used as a searching key.
-     * The parameters are reversed for this constructor, so that is is not accidentally used.
-     */
-    private MethodType(Class<?>[] ptypes, Class<?> rtype) {
-        this.rtype = rtype;
-        this.ptypes = ptypes;
-    }
-
-    /*trusted*/ MethodTypeForm form() { return form; }
-    /*trusted*/ /** @hide */ public Class<?> rtype() { return rtype; }
-    /*trusted*/ /** @hide */ public Class<?>[] ptypes() { return ptypes; }
-
-    // Android-changed: Removed method setForm. It's unused in the JDK and there's no
-    // good reason to allow the form to be set externally.
-    //
-    // void setForm(MethodTypeForm f) { form = f; }
-
-    /** This number, mandated by the JVM spec as 255,
-     *  is the maximum number of <em>slots</em>
-     *  that any Java method can receive in its argument list.
-     *  It limits both JVM signatures and method type objects.
-     *  The longest possible invocation will look like
-     *  {@code staticMethod(arg1, arg2, ..., arg255)} or
-     *  {@code x.virtualMethod(arg1, arg2, ..., arg254)}.
-     */
-    /*non-public*/ static final int MAX_JVM_ARITY = 255;  // this is mandated by the JVM spec.
-
-    /** This number is the maximum arity of a method handle, 254.
-     *  It is derived from the absolute JVM-imposed arity by subtracting one,
-     *  which is the slot occupied by the method handle itself at the
-     *  beginning of the argument list used to invoke the method handle.
-     *  The longest possible invocation will look like
-     *  {@code mh.invoke(arg1, arg2, ..., arg254)}.
-     */
-    // Issue:  Should we allow MH.invokeWithArguments to go to the full 255?
-    /*non-public*/ static final int MAX_MH_ARITY = MAX_JVM_ARITY-1;  // deduct one for mh receiver
-
-    /** This number is the maximum arity of a method handle invoker, 253.
-     *  It is derived from the absolute JVM-imposed arity by subtracting two,
-     *  which are the slots occupied by invoke method handle, and the
-     *  target method handle, which are both at the beginning of the argument
-     *  list used to invoke the target method handle.
-     *  The longest possible invocation will look like
-     *  {@code invokermh.invoke(targetmh, arg1, arg2, ..., arg253)}.
-     */
-    /*non-public*/ static final int MAX_MH_INVOKER_ARITY = MAX_MH_ARITY-1;  // deduct one more for invoker
-
-    private static void checkRtype(Class<?> rtype) {
-        Objects.requireNonNull(rtype);
-    }
-    private static void checkPtype(Class<?> ptype) {
-        Objects.requireNonNull(ptype);
-        if (ptype == void.class)
-            throw newIllegalArgumentException("parameter type cannot be void");
-    }
-    /** Return number of extra slots (count of long/double args). */
-    private static int checkPtypes(Class<?>[] ptypes) {
-        int slots = 0;
-        for (Class<?> ptype : ptypes) {
-            checkPtype(ptype);
-            if (ptype == double.class || ptype == long.class) {
-                slots++;
-            }
-        }
-        checkSlotCount(ptypes.length + slots);
-        return slots;
-    }
-    static void checkSlotCount(int count) {
-        assert((MAX_JVM_ARITY & (MAX_JVM_ARITY+1)) == 0);
-        // MAX_JVM_ARITY must be power of 2 minus 1 for following code trick to work:
-        if ((count & MAX_JVM_ARITY) != count)
-            throw newIllegalArgumentException("bad parameter count "+count);
-    }
-    private static IndexOutOfBoundsException newIndexOutOfBoundsException(Object num) {
-        if (num instanceof Integer)  num = "bad index: "+num;
-        return new IndexOutOfBoundsException(num.toString());
-    }
-
-    static final ConcurrentWeakInternSet<MethodType> internTable = new ConcurrentWeakInternSet<>();
-
-    static final Class<?>[] NO_PTYPES = {};
-
-    /**
-     * Finds or creates an instance of the given method type.
-     * @param rtype  the return type
-     * @param ptypes the parameter types
-     * @return a method type with the given components
-     * @throws NullPointerException if {@code rtype} or {@code ptypes} or any element of {@code ptypes} is null
-     * @throws IllegalArgumentException if any element of {@code ptypes} is {@code void.class}
-     */
     public static
     MethodType methodType(Class<?> rtype, Class<?>[] ptypes) {
-        return makeImpl(rtype, ptypes, false);
+        return null;
     }
 
-    /**
-     * Finds or creates a method type with the given components.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param rtype  the return type
-     * @param ptypes the parameter types
-     * @return a method type with the given components
-     * @throws NullPointerException if {@code rtype} or {@code ptypes} or any element of {@code ptypes} is null
-     * @throws IllegalArgumentException if any element of {@code ptypes} is {@code void.class}
-     */
     public static
     MethodType methodType(Class<?> rtype, List<Class<?>> ptypes) {
-        boolean notrust = false;  // random List impl. could return evil ptypes array
-        return makeImpl(rtype, listToArray(ptypes), notrust);
+        return null;
     }
 
-    private static Class<?>[] listToArray(List<Class<?>> ptypes) {
-        // sanity check the size before the toArray call, since size might be huge
-        checkSlotCount(ptypes.size());
-        return ptypes.toArray(NO_PTYPES);
-    }
-
-    /**
-     * Finds or creates a method type with the given components.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * The leading parameter type is prepended to the remaining array.
-     * @param rtype  the return type
-     * @param ptype0 the first parameter type
-     * @param ptypes the remaining parameter types
-     * @return a method type with the given components
-     * @throws NullPointerException if {@code rtype} or {@code ptype0} or {@code ptypes} or any element of {@code ptypes} is null
-     * @throws IllegalArgumentException if {@code ptype0} or {@code ptypes} or any element of {@code ptypes} is {@code void.class}
-     */
     public static
-    MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) {
-        Class<?>[] ptypes1 = new Class<?>[1+ptypes.length];
-        ptypes1[0] = ptype0;
-        System.arraycopy(ptypes, 0, ptypes1, 1, ptypes.length);
-        return makeImpl(rtype, ptypes1, true);
-    }
+    MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) { return null; }
 
-    /**
-     * Finds or creates a method type with the given components.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * The resulting method has no parameter types.
-     * @param rtype  the return type
-     * @return a method type with the given return value
-     * @throws NullPointerException if {@code rtype} is null
-     */
     public static
-    MethodType methodType(Class<?> rtype) {
-        return makeImpl(rtype, NO_PTYPES, true);
-    }
+    MethodType methodType(Class<?> rtype) { return null; }
 
-    /**
-     * Finds or creates a method type with the given components.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * The resulting method has the single given parameter type.
-     * @param rtype  the return type
-     * @param ptype0 the parameter type
-     * @return a method type with the given return value and parameter type
-     * @throws NullPointerException if {@code rtype} or {@code ptype0} is null
-     * @throws IllegalArgumentException if {@code ptype0} is {@code void.class}
-     */
     public static
-    MethodType methodType(Class<?> rtype, Class<?> ptype0) {
-        return makeImpl(rtype, new Class<?>[]{ ptype0 }, true);
-    }
+    MethodType methodType(Class<?> rtype, Class<?> ptype0) { return null; }
 
-    /**
-     * Finds or creates a method type with the given components.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * The resulting method has the same parameter types as {@code ptypes},
-     * and the specified return type.
-     * @param rtype  the return type
-     * @param ptypes the method type which supplies the parameter types
-     * @return a method type with the given components
-     * @throws NullPointerException if {@code rtype} or {@code ptypes} is null
-     */
     public static
-    MethodType methodType(Class<?> rtype, MethodType ptypes) {
-        return makeImpl(rtype, ptypes.ptypes, true);
-    }
+    MethodType methodType(Class<?> rtype, MethodType ptypes) { return null; }
 
-    /**
-     * Sole factory method to find or create an interned method type.
-     * @param rtype desired return type
-     * @param ptypes desired parameter types
-     * @param trusted whether the ptypes can be used without cloning
-     * @return the unique method type of the desired structure
-     */
-    /*trusted*/ static
-    MethodType makeImpl(Class<?> rtype, Class<?>[] ptypes, boolean trusted) {
-        MethodType mt = internTable.get(new MethodType(ptypes, rtype));
-        if (mt != null)
-            return mt;
-        if (ptypes.length == 0) {
-            ptypes = NO_PTYPES; trusted = true;
-        }
-        mt = new MethodType(rtype, ptypes, trusted);
-        // promote the object to the Real Thing, and reprobe
-        mt.form = MethodTypeForm.findForm(mt);
-        return internTable.add(mt);
-    }
-    private static final MethodType[] objectOnlyTypes = new MethodType[20];
-
-    /**
-     * Finds or creates a method type whose components are {@code Object} with an optional trailing {@code Object[]} array.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * All parameters and the return type will be {@code Object},
-     * except the final array parameter if any, which will be {@code Object[]}.
-     * @param objectArgCount number of parameters (excluding the final array parameter if any)
-     * @param finalArray whether there will be a trailing array parameter, of type {@code Object[]}
-     * @return a generally applicable method type, for all calls of the given fixed argument count and a collected array of further arguments
-     * @throws IllegalArgumentException if {@code objectArgCount} is negative or greater than 255 (or 254, if {@code finalArray} is true)
-     * @see #genericMethodType(int)
-     */
     public static
-    MethodType genericMethodType(int objectArgCount, boolean finalArray) {
-        MethodType mt;
-        checkSlotCount(objectArgCount);
-        int ivarargs = (!finalArray ? 0 : 1);
-        int ootIndex = objectArgCount*2 + ivarargs;
-        if (ootIndex < objectOnlyTypes.length) {
-            mt = objectOnlyTypes[ootIndex];
-            if (mt != null)  return mt;
-        }
-        Class<?>[] ptypes = new Class<?>[objectArgCount + ivarargs];
-        Arrays.fill(ptypes, Object.class);
-        if (ivarargs != 0)  ptypes[objectArgCount] = Object[].class;
-        mt = makeImpl(Object.class, ptypes, true);
-        if (ootIndex < objectOnlyTypes.length) {
-            objectOnlyTypes[ootIndex] = mt;     // cache it here also!
-        }
-        return mt;
-    }
+    MethodType genericMethodType(int objectArgCount, boolean finalArray) { return null; }
 
-    /**
-     * Finds or creates a method type whose components are all {@code Object}.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * All parameters and the return type will be Object.
-     * @param objectArgCount number of parameters
-     * @return a generally applicable method type, for all calls of the given argument count
-     * @throws IllegalArgumentException if {@code objectArgCount} is negative or greater than 255
-     * @see #genericMethodType(int, boolean)
-     */
     public static
-    MethodType genericMethodType(int objectArgCount) {
-        return genericMethodType(objectArgCount, false);
-    }
+    MethodType genericMethodType(int objectArgCount) { return null; }
 
-    /**
-     * Finds or creates a method type with a single different parameter type.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param num    the index (zero-based) of the parameter type to change
-     * @param nptype a new parameter type to replace the old one with
-     * @return the same type, except with the selected parameter changed
-     * @throws IndexOutOfBoundsException if {@code num} is not a valid index into {@code parameterArray()}
-     * @throws IllegalArgumentException if {@code nptype} is {@code void.class}
-     * @throws NullPointerException if {@code nptype} is null
-     */
-    public MethodType changeParameterType(int num, Class<?> nptype) {
-        if (parameterType(num) == nptype)  return this;
-        checkPtype(nptype);
-        Class<?>[] nptypes = ptypes.clone();
-        nptypes[num] = nptype;
-        return makeImpl(rtype, nptypes, true);
-    }
+    public MethodType changeParameterType(int num, Class<?> nptype) { return null; }
 
-    /**
-     * Finds or creates a method type with additional parameter types.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param num    the position (zero-based) of the inserted parameter type(s)
-     * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
-     * @return the same type, except with the selected parameter(s) inserted
-     * @throws IndexOutOfBoundsException if {@code num} is negative or greater than {@code parameterCount()}
-     * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
-     *                                  or if the resulting method type would have more than 255 parameter slots
-     * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
-     */
-    public MethodType insertParameterTypes(int num, Class<?>... ptypesToInsert) {
-        int len = ptypes.length;
-        if (num < 0 || num > len)
-            throw newIndexOutOfBoundsException(num);
-        int ins = checkPtypes(ptypesToInsert);
-        checkSlotCount(parameterSlotCount() + ptypesToInsert.length + ins);
-        int ilen = ptypesToInsert.length;
-        if (ilen == 0)  return this;
-        Class<?>[] nptypes = Arrays.copyOfRange(ptypes, 0, len+ilen);
-        System.arraycopy(nptypes, num, nptypes, num+ilen, len-num);
-        System.arraycopy(ptypesToInsert, 0, nptypes, num, ilen);
-        return makeImpl(rtype, nptypes, true);
-    }
+    public MethodType insertParameterTypes(int num, Class<?>... ptypesToInsert) { return null; }
 
-    /**
-     * Finds or creates a method type with additional parameter types.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param ptypesToInsert zero or more new parameter types to insert after the end of the parameter list
-     * @return the same type, except with the selected parameter(s) appended
-     * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
-     *                                  or if the resulting method type would have more than 255 parameter slots
-     * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
-     */
-    public MethodType appendParameterTypes(Class<?>... ptypesToInsert) {
-        return insertParameterTypes(parameterCount(), ptypesToInsert);
-    }
+    public MethodType appendParameterTypes(Class<?>... ptypesToInsert) { return null; }
 
-    /**
-     * Finds or creates a method type with additional parameter types.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param num    the position (zero-based) of the inserted parameter type(s)
-     * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
-     * @return the same type, except with the selected parameter(s) inserted
-     * @throws IndexOutOfBoundsException if {@code num} is negative or greater than {@code parameterCount()}
-     * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
-     *                                  or if the resulting method type would have more than 255 parameter slots
-     * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
-     */
-    public MethodType insertParameterTypes(int num, List<Class<?>> ptypesToInsert) {
-        return insertParameterTypes(num, listToArray(ptypesToInsert));
-    }
+    public MethodType insertParameterTypes(int num, List<Class<?>> ptypesToInsert) { return null; }
 
-    /**
-     * Finds or creates a method type with additional parameter types.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param ptypesToInsert zero or more new parameter types to insert after the end of the parameter list
-     * @return the same type, except with the selected parameter(s) appended
-     * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
-     *                                  or if the resulting method type would have more than 255 parameter slots
-     * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
-     */
-    public MethodType appendParameterTypes(List<Class<?>> ptypesToInsert) {
-        return insertParameterTypes(parameterCount(), ptypesToInsert);
-    }
+    public MethodType appendParameterTypes(List<Class<?>> ptypesToInsert) { return null; }
 
-     /**
-     * Finds or creates a method type with modified parameter types.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param start  the position (zero-based) of the first replaced parameter type(s)
-     * @param end    the position (zero-based) after the last replaced parameter type(s)
-     * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
-     * @return the same type, except with the selected parameter(s) replaced
-     * @throws IndexOutOfBoundsException if {@code start} is negative or greater than {@code parameterCount()}
-     *                                  or if {@code end} is negative or greater than {@code parameterCount()}
-     *                                  or if {@code start} is greater than {@code end}
-     * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
-     *                                  or if the resulting method type would have more than 255 parameter slots
-     * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
-     */
-    /*non-public*/ MethodType replaceParameterTypes(int start, int end, Class<?>... ptypesToInsert) {
-        if (start == end)
-            return insertParameterTypes(start, ptypesToInsert);
-        int len = ptypes.length;
-        if (!(0 <= start && start <= end && end <= len))
-            throw newIndexOutOfBoundsException("start="+start+" end="+end);
-        int ilen = ptypesToInsert.length;
-        if (ilen == 0)
-            return dropParameterTypes(start, end);
-        return dropParameterTypes(start, end).insertParameterTypes(start, ptypesToInsert);
-    }
+    public MethodType dropParameterTypes(int start, int end) { return null; }
 
-    /** Replace the last arrayLength parameter types with the component type of arrayType.
-     * @param arrayType any array type
-     * @param arrayLength the number of parameter types to change
-     * @return the resulting type
-     */
-    /*non-public*/ MethodType asSpreaderType(Class<?> arrayType, int arrayLength) {
-        assert(parameterCount() >= arrayLength);
-        int spreadPos = ptypes.length - arrayLength;
-        if (arrayLength == 0)  return this;  // nothing to change
-        if (arrayType == Object[].class) {
-            if (isGeneric())  return this;  // nothing to change
-            if (spreadPos == 0) {
-                // no leading arguments to preserve; go generic
-                MethodType res = genericMethodType(arrayLength);
-                if (rtype != Object.class) {
-                    res = res.changeReturnType(rtype);
-                }
-                return res;
-            }
-        }
-        Class<?> elemType = arrayType.getComponentType();
-        assert(elemType != null);
-        for (int i = spreadPos; i < ptypes.length; i++) {
-            if (ptypes[i] != elemType) {
-                Class<?>[] fixedPtypes = ptypes.clone();
-                Arrays.fill(fixedPtypes, i, ptypes.length, elemType);
-                return methodType(rtype, fixedPtypes);
-            }
-        }
-        return this;  // arguments check out; no change
-    }
+    public MethodType changeReturnType(Class<?> nrtype) { return null; }
 
-    /** Return the leading parameter type, which must exist and be a reference.
-     *  @return the leading parameter type, after error checks
-     */
-    /*non-public*/ Class<?> leadingReferenceParameter() {
-        Class<?> ptype;
-        if (ptypes.length == 0 ||
-            (ptype = ptypes[0]).isPrimitive())
-            throw newIllegalArgumentException("no leading reference parameter");
-        return ptype;
-    }
+    public boolean hasPrimitives() { return false; }
 
-    /** Delete the last parameter type and replace it with arrayLength copies of the component type of arrayType.
-     * @param arrayType any array type
-     * @param arrayLength the number of parameter types to insert
-     * @return the resulting type
-     */
-    /*non-public*/ MethodType asCollectorType(Class<?> arrayType, int arrayLength) {
-        assert(parameterCount() >= 1);
-        assert(lastParameterType().isAssignableFrom(arrayType));
-        MethodType res;
-        if (arrayType == Object[].class) {
-            res = genericMethodType(arrayLength);
-            if (rtype != Object.class) {
-                res = res.changeReturnType(rtype);
-            }
-        } else {
-            Class<?> elemType = arrayType.getComponentType();
-            assert(elemType != null);
-            res = methodType(rtype, Collections.nCopies(arrayLength, elemType));
-        }
-        if (ptypes.length == 1) {
-            return res;
-        } else {
-            return res.insertParameterTypes(0, parameterList().subList(0, ptypes.length-1));
-        }
-    }
+    public boolean hasWrappers() { return false; }
 
-    /**
-     * Finds or creates a method type with some parameter types omitted.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param start  the index (zero-based) of the first parameter type to remove
-     * @param end    the index (greater than {@code start}) of the first parameter type after not to remove
-     * @return the same type, except with the selected parameter(s) removed
-     * @throws IndexOutOfBoundsException if {@code start} is negative or greater than {@code parameterCount()}
-     *                                  or if {@code end} is negative or greater than {@code parameterCount()}
-     *                                  or if {@code start} is greater than {@code end}
-     */
-    public MethodType dropParameterTypes(int start, int end) {
-        int len = ptypes.length;
-        if (!(0 <= start && start <= end && end <= len))
-            throw newIndexOutOfBoundsException("start="+start+" end="+end);
-        if (start == end)  return this;
-        Class<?>[] nptypes;
-        if (start == 0) {
-            if (end == len) {
-                // drop all parameters
-                nptypes = NO_PTYPES;
-            } else {
-                // drop initial parameter(s)
-                nptypes = Arrays.copyOfRange(ptypes, end, len);
-            }
-        } else {
-            if (end == len) {
-                // drop trailing parameter(s)
-                nptypes = Arrays.copyOfRange(ptypes, 0, start);
-            } else {
-                int tail = len - end;
-                nptypes = Arrays.copyOfRange(ptypes, 0, start + tail);
-                System.arraycopy(ptypes, end, nptypes, start, tail);
-            }
-        }
-        return makeImpl(rtype, nptypes, true);
-    }
+    public MethodType erase() { return null; }
 
-    /**
-     * Finds or creates a method type with a different return type.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * @param nrtype a return parameter type to replace the old one with
-     * @return the same type, except with the return type change
-     * @throws NullPointerException if {@code nrtype} is null
-     */
-    public MethodType changeReturnType(Class<?> nrtype) {
-        if (returnType() == nrtype)  return this;
-        return makeImpl(nrtype, ptypes, true);
-    }
+    public MethodType generic() { return null; }
 
-    /**
-     * Reports if this type contains a primitive argument or return value.
-     * The return type {@code void} counts as a primitive.
-     * @return true if any of the types are primitives
-     */
-    public boolean hasPrimitives() {
-        return form.hasPrimitives();
-    }
+    public MethodType wrap() { return null; }
 
-    /**
-     * Reports if this type contains a wrapper argument or return value.
-     * Wrappers are types which box primitive values, such as {@link Integer}.
-     * The reference type {@code java.lang.Void} counts as a wrapper,
-     * if it occurs as a return type.
-     * @return true if any of the types are wrappers
-     */
-    public boolean hasWrappers() {
-        return unwrap() != this;
-    }
+    public MethodType unwrap() { return null; }
 
-    /**
-     * Erases all reference types to {@code Object}.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * All primitive types (including {@code void}) will remain unchanged.
-     * @return a version of the original type with all reference types replaced
-     */
-    public MethodType erase() {
-        return form.erasedType();
-    }
+    public Class<?> parameterType(int num) { return null; }
 
-    /**
-     * Erases all reference types to {@code Object}, and all subword types to {@code int}.
-     * This is the reduced type polymorphism used by private methods
-     * such as {@link MethodHandle#invokeBasic invokeBasic}.
-     * @return a version of the original type with all reference and subword types replaced
-     */
-    /*non-public*/ MethodType basicType() {
-        return form.basicType();
-    }
+    public int parameterCount() { return 0; }
 
-    /**
-     * @return a version of the original type with MethodHandle prepended as the first argument
-     */
-    /*non-public*/ MethodType invokerType() {
-        return insertParameterTypes(0, MethodHandle.class);
-    }
+    public Class<?> returnType() { return null; }
 
-    /**
-     * Converts all types, both reference and primitive, to {@code Object}.
-     * Convenience method for {@link #genericMethodType(int) genericMethodType}.
-     * The expression {@code type.wrap().erase()} produces the same value
-     * as {@code type.generic()}.
-     * @return a version of the original type with all types replaced
-     */
-    public MethodType generic() {
-        return genericMethodType(parameterCount());
-    }
+    public List<Class<?>> parameterList() { return null; }
 
-    /*non-public*/ boolean isGeneric() {
-        return this == erase() && !hasPrimitives();
-    }
+    public Class<?>[] parameterArray() { return null; }
 
-    /**
-     * Converts all primitive types to their corresponding wrapper types.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * All reference types (including wrapper types) will remain unchanged.
-     * A {@code void} return type is changed to the type {@code java.lang.Void}.
-     * The expression {@code type.wrap().erase()} produces the same value
-     * as {@code type.generic()}.
-     * @return a version of the original type with all primitive types replaced
-     */
-    public MethodType wrap() {
-        return hasPrimitives() ? wrapWithPrims(this) : this;
-    }
-
-    /**
-     * Converts all wrapper types to their corresponding primitive types.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * All primitive types (including {@code void}) will remain unchanged.
-     * A return type of {@code java.lang.Void} is changed to {@code void}.
-     * @return a version of the original type with all wrapper types replaced
-     */
-    public MethodType unwrap() {
-        MethodType noprims = !hasPrimitives() ? this : wrapWithPrims(this);
-        return unwrapWithNoPrims(noprims);
-    }
-
-    private static MethodType wrapWithPrims(MethodType pt) {
-        assert(pt.hasPrimitives());
-        MethodType wt = pt.wrapAlt;
-        if (wt == null) {
-            // fill in lazily
-            wt = MethodTypeForm.canonicalize(pt, MethodTypeForm.WRAP, MethodTypeForm.WRAP);
-            assert(wt != null);
-            pt.wrapAlt = wt;
-        }
-        return wt;
-    }
-
-    private static MethodType unwrapWithNoPrims(MethodType wt) {
-        assert(!wt.hasPrimitives());
-        MethodType uwt = wt.wrapAlt;
-        if (uwt == null) {
-            // fill in lazily
-            uwt = MethodTypeForm.canonicalize(wt, MethodTypeForm.UNWRAP, MethodTypeForm.UNWRAP);
-            if (uwt == null)
-                uwt = wt;    // type has no wrappers or prims at all
-            wt.wrapAlt = uwt;
-        }
-        return uwt;
-    }
-
-    /**
-     * Returns the parameter type at the specified index, within this method type.
-     * @param num the index (zero-based) of the desired parameter type
-     * @return the selected parameter type
-     * @throws IndexOutOfBoundsException if {@code num} is not a valid index into {@code parameterArray()}
-     */
-    public Class<?> parameterType(int num) {
-        return ptypes[num];
-    }
-    /**
-     * Returns the number of parameter types in this method type.
-     * @return the number of parameter types
-     */
-    public int parameterCount() {
-        return ptypes.length;
-    }
-    /**
-     * Returns the return type of this method type.
-     * @return the return type
-     */
-    public Class<?> returnType() {
-        return rtype;
-    }
-
-    /**
-     * Presents the parameter types as a list (a convenience method).
-     * The list will be immutable.
-     * @return the parameter types (as an immutable list)
-     */
-    public List<Class<?>> parameterList() {
-        return Collections.unmodifiableList(Arrays.asList(ptypes.clone()));
-    }
-
-    /*non-public*/ Class<?> lastParameterType() {
-        int len = ptypes.length;
-        return len == 0 ? void.class : ptypes[len-1];
-    }
-
-    /**
-     * Presents the parameter types as an array (a convenience method).
-     * Changes to the array will not result in changes to the type.
-     * @return the parameter types (as a fresh copy if necessary)
-     */
-    public Class<?>[] parameterArray() {
-        return ptypes.clone();
-    }
-
-    /**
-     * Compares the specified object with this type for equality.
-     * That is, it returns <tt>true</tt> if and only if the specified object
-     * is also a method type with exactly the same parameters and return type.
-     * @param x object to compare
-     * @see Object#equals(Object)
-     */
-    @Override
-    public boolean equals(Object x) {
-        return this == x || x instanceof MethodType && equals((MethodType)x);
-    }
-
-    private boolean equals(MethodType that) {
-        return this.rtype == that.rtype
-            && Arrays.equals(this.ptypes, that.ptypes);
-    }
-
-    /**
-     * Returns the hash code value for this method type.
-     * It is defined to be the same as the hashcode of a List
-     * whose elements are the return type followed by the
-     * parameter types.
-     * @return the hash code value for this method type
-     * @see Object#hashCode()
-     * @see #equals(Object)
-     * @see List#hashCode()
-     */
-    @Override
-    public int hashCode() {
-      int hashCode = 31 + rtype.hashCode();
-      for (Class<?> ptype : ptypes)
-          hashCode = 31*hashCode + ptype.hashCode();
-      return hashCode;
-    }
-
-    /**
-     * Returns a string representation of the method type,
-     * of the form {@code "(PT0,PT1...)RT"}.
-     * The string representation of a method type is a
-     * parenthesis enclosed, comma separated list of type names,
-     * followed immediately by the return type.
-     * <p>
-     * Each type is represented by its
-     * {@link java.lang.Class#getSimpleName simple name}.
-     */
-    @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder();
-        sb.append("(");
-        for (int i = 0; i < ptypes.length; i++) {
-            if (i > 0)  sb.append(",");
-            sb.append(ptypes[i].getSimpleName());
-        }
-        sb.append(")");
-        sb.append(rtype.getSimpleName());
-        return sb.toString();
-    }
-
-    /** True if the old return type can always be viewed (w/o casting) under new return type,
-     *  and the new parameters can be viewed (w/o casting) under the old parameter types.
-     */
-    // Android-changed: Removed implementation details.
-    // boolean isViewableAs(MethodType newType, boolean keepInterfaces);
-    // boolean parametersAreViewableAs(MethodType newType, boolean keepInterfaces);
-    /*non-public*/
-    boolean isConvertibleTo(MethodType newType) {
-        MethodTypeForm oldForm = this.form();
-        MethodTypeForm newForm = newType.form();
-        if (oldForm == newForm)
-            // same parameter count, same primitive/object mix
-            return true;
-        if (!canConvert(returnType(), newType.returnType()))
-            return false;
-        Class<?>[] srcTypes = newType.ptypes;
-        Class<?>[] dstTypes = ptypes;
-        if (srcTypes == dstTypes)
-            return true;
-        int argc;
-        if ((argc = srcTypes.length) != dstTypes.length)
-            return false;
-        if (argc <= 1) {
-            if (argc == 1 && !canConvert(srcTypes[0], dstTypes[0]))
-                return false;
-            return true;
-        }
-        if ((oldForm.primitiveParameterCount() == 0 && oldForm.erasedType == this) ||
-            (newForm.primitiveParameterCount() == 0 && newForm.erasedType == newType)) {
-            // Somewhat complicated test to avoid a loop of 2 or more trips.
-            // If either type has only Object parameters, we know we can convert.
-            assert(canConvertParameters(srcTypes, dstTypes));
-            return true;
-        }
-        return canConvertParameters(srcTypes, dstTypes);
-    }
-
-    /** Returns true if MHs.explicitCastArguments produces the same result as MH.asType.
-     *  If the type conversion is impossible for either, the result should be false.
-     */
-    /*non-public*/
-    boolean explicitCastEquivalentToAsType(MethodType newType) {
-        if (this == newType)  return true;
-        if (!explicitCastEquivalentToAsType(rtype, newType.rtype)) {
-            return false;
-        }
-        Class<?>[] srcTypes = newType.ptypes;
-        Class<?>[] dstTypes = ptypes;
-        if (dstTypes == srcTypes) {
-            return true;
-        }
-        assert(dstTypes.length == srcTypes.length);
-        for (int i = 0; i < dstTypes.length; i++) {
-            if (!explicitCastEquivalentToAsType(srcTypes[i], dstTypes[i])) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /** Reports true if the src can be converted to the dst, by both asType and MHs.eCE,
-     *  and with the same effect.
-     *  MHs.eCA has the following "upgrades" to MH.asType:
-     *  1. interfaces are unchecked (that is, treated as if aliased to Object)
-     *     Therefore, {@code Object->CharSequence} is possible in both cases but has different semantics
-     *  2a. the full matrix of primitive-to-primitive conversions is supported
-     *      Narrowing like {@code long->byte} and basic-typing like {@code boolean->int}
-     *      are not supported by asType, but anything supported by asType is equivalent
-     *      with MHs.eCE.
-     *  2b. conversion of void->primitive means explicit cast has to insert zero/false/null.
-     *  3a. unboxing conversions can be followed by the full matrix of primitive conversions
-     *  3b. unboxing of null is permitted (creates a zero primitive value)
-     * Other than interfaces, reference-to-reference conversions are the same.
-     * Boxing primitives to references is the same for both operators.
-     */
-    private static boolean explicitCastEquivalentToAsType(Class<?> src, Class<?> dst) {
-        if (src == dst || dst == Object.class || dst == void.class) {
-            return true;
-        } else if (src.isPrimitive() && src != void.class) {
-            // Could be a prim/prim conversion, where casting is a strict superset.
-            // Or a boxing conversion, which is always to an exact wrapper class.
-            return canConvert(src, dst);
-        } else if (dst.isPrimitive()) {
-            // Unboxing behavior is different between MHs.eCA & MH.asType (see 3b).
-            return false;
-        } else {
-            // R->R always works, but we have to avoid a check-cast to an interface.
-            return !dst.isInterface() || dst.isAssignableFrom(src);
-        }
-    }
-
-    private boolean canConvertParameters(Class<?>[] srcTypes, Class<?>[] dstTypes) {
-        for (int i = 0; i < srcTypes.length; i++) {
-            if (!canConvert(srcTypes[i], dstTypes[i])) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /*non-public*/
-    static boolean canConvert(Class<?> src, Class<?> dst) {
-        // short-circuit a few cases:
-        if (src == dst || src == Object.class || dst == Object.class)  return true;
-        // the remainder of this logic is documented in MethodHandle.asType
-        if (src.isPrimitive()) {
-            // can force void to an explicit null, a la reflect.Method.invoke
-            // can also force void to a primitive zero, by analogy
-            if (src == void.class)  return true;  //or !dst.isPrimitive()?
-            Wrapper sw = Wrapper.forPrimitiveType(src);
-            if (dst.isPrimitive()) {
-                // P->P must widen
-                return Wrapper.forPrimitiveType(dst).isConvertibleFrom(sw);
-            } else {
-                // P->R must box and widen
-                return dst.isAssignableFrom(sw.wrapperType());
-            }
-        } else if (dst.isPrimitive()) {
-            // any value can be dropped
-            if (dst == void.class)  return true;
-            Wrapper dw = Wrapper.forPrimitiveType(dst);
-            // R->P must be able to unbox (from a dynamically chosen type) and widen
-            // For example:
-            //   Byte/Number/Comparable/Object -> dw:Byte -> byte.
-            //   Character/Comparable/Object -> dw:Character -> char
-            //   Boolean/Comparable/Object -> dw:Boolean -> boolean
-            // This means that dw must be cast-compatible with src.
-            if (src.isAssignableFrom(dw.wrapperType())) {
-                return true;
-            }
-            // The above does not work if the source reference is strongly typed
-            // to a wrapper whose primitive must be widened.  For example:
-            //   Byte -> unbox:byte -> short/int/long/float/double
-            //   Character -> unbox:char -> int/long/float/double
-            if (Wrapper.isWrapperType(src) &&
-                dw.isConvertibleFrom(Wrapper.forWrapperType(src))) {
-                // can unbox from src and then widen to dst
-                return true;
-            }
-            // We have already covered cases which arise due to runtime unboxing
-            // of a reference type which covers several wrapper types:
-            //   Object -> cast:Integer -> unbox:int -> long/float/double
-            //   Serializable -> cast:Byte -> unbox:byte -> byte/short/int/long/float/double
-            // An marginal case is Number -> dw:Character -> char, which would be OK if there were a
-            // subclass of Number which wraps a value that can convert to char.
-            // Since there is none, we don't need an extra check here to cover char or boolean.
-            return false;
-        } else {
-            // R->R always works, since null is always valid dynamically
-            return true;
-        }
-    }
-
-    /** Reports the number of JVM stack slots required to invoke a method
-     * of this type.  Note that (for historical reasons) the JVM requires
-     * a second stack slot to pass long and double arguments.
-     * So this method returns {@link #parameterCount() parameterCount} plus the
-     * number of long and double parameters (if any).
-     * <p>
-     * This method is included for the benefit of applications that must
-     * generate bytecodes that process method handles and invokedynamic.
-     * @return the number of JVM stack slots for this type's parameters
-     */
-    /*non-public*/ int parameterSlotCount() {
-        return form.parameterSlotCount();
-    }
-
-    /// Queries which have to do with the bytecode architecture
-
-    // Android-changed: These methods aren't needed on Android and are unused within the JDK.
-    //
-    // int parameterSlotDepth(int num);
-    // int returnSlotCount();
-    //
-    // Android-changed: Removed cache of higher order adapters.
-    //
-    // Invokers invokers();
-
-    /**
-     * Finds or creates an instance of a method type, given the spelling of its bytecode descriptor.
-     * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
-     * Any class or interface name embedded in the descriptor string
-     * will be resolved by calling {@link ClassLoader#loadClass(java.lang.String)}
-     * on the given loader (or if it is null, on the system class loader).
-     * <p>
-     * Note that it is possible to encounter method types which cannot be
-     * constructed by this method, because their component types are
-     * not all reachable from a common class loader.
-     * <p>
-     * This method is included for the benefit of applications that must
-     * generate bytecodes that process method handles and {@code invokedynamic}.
-     * @param descriptor a bytecode-level type descriptor string "(T...)T"
-     * @param loader the class loader in which to look up the types
-     * @return a method type matching the bytecode-level type descriptor
-     * @throws NullPointerException if the string is null
-     * @throws IllegalArgumentException if the string is not well-formed
-     * @throws TypeNotPresentException if a named type cannot be found
-     */
     public static MethodType fromMethodDescriptorString(String descriptor, ClassLoader loader)
-        throws IllegalArgumentException, TypeNotPresentException
-    {
-        if (!descriptor.startsWith("(") ||  // also generates NPE if needed
-            descriptor.indexOf(')') < 0 ||
-            descriptor.indexOf('.') >= 0)
-            throw newIllegalArgumentException("not a method descriptor: "+descriptor);
-        List<Class<?>> types = BytecodeDescriptor.parseMethod(descriptor, loader);
-        Class<?> rtype = types.remove(types.size() - 1);
-        checkSlotCount(types.size());
-        Class<?>[] ptypes = listToArray(types);
-        return makeImpl(rtype, ptypes, true);
-    }
+        throws IllegalArgumentException, TypeNotPresentException { return null; }
 
-    /**
-     * Produces a bytecode descriptor representation of the method type.
-     * <p>
-     * Note that this is not a strict inverse of {@link #fromMethodDescriptorString fromMethodDescriptorString}.
-     * Two distinct classes which share a common name but have different class loaders
-     * will appear identical when viewed within descriptor strings.
-     * <p>
-     * This method is included for the benefit of applications that must
-     * generate bytecodes that process method handles and {@code invokedynamic}.
-     * {@link #fromMethodDescriptorString(java.lang.String, java.lang.ClassLoader) fromMethodDescriptorString},
-     * because the latter requires a suitable class loader argument.
-     * @return the bytecode type descriptor representation
-     */
-    public String toMethodDescriptorString() {
-        String desc = methodDescriptor;
-        if (desc == null) {
-            desc = BytecodeDescriptor.unparse(this);
-            methodDescriptor = desc;
-        }
-        return desc;
-    }
-
-    /*non-public*/ static String toFieldDescriptorString(Class<?> cls) {
-        return BytecodeDescriptor.unparse(cls);
-    }
-
-    /// Serialization.
-
-    /**
-     * There are no serializable fields for {@code MethodType}.
-     */
-    private static final java.io.ObjectStreamField[] serialPersistentFields = { };
-
-    /**
-     * Save the {@code MethodType} instance to a stream.
-     *
-     * @serialData
-     * For portability, the serialized format does not refer to named fields.
-     * Instead, the return type and parameter type arrays are written directly
-     * from the {@code writeObject} method, using two calls to {@code s.writeObject}
-     * as follows:
-     * <blockquote><pre>{@code
-s.writeObject(this.returnType());
-s.writeObject(this.parameterArray());
-     * }</pre></blockquote>
-     * <p>
-     * The deserialized field values are checked as if they were
-     * provided to the factory method {@link #methodType(Class,Class[]) methodType}.
-     * For example, null values, or {@code void} parameter types,
-     * will lead to exceptions during deserialization.
-     * @param s the stream to write the object to
-     * @throws java.io.IOException if there is a problem writing the object
-     */
-    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
-        s.defaultWriteObject();  // requires serialPersistentFields to be an empty array
-        s.writeObject(returnType());
-        s.writeObject(parameterArray());
-    }
-
-    /**
-     * Reconstitute the {@code MethodType} instance from a stream (that is,
-     * deserialize it).
-     * This instance is a scratch object with bogus final fields.
-     * It provides the parameters to the factory method called by
-     * {@link #readResolve readResolve}.
-     * After that call it is discarded.
-     * @param s the stream to read the object from
-     * @throws java.io.IOException if there is a problem reading the object
-     * @throws ClassNotFoundException if one of the component classes cannot be resolved
-     * @see #MethodType()
-     * @see #readResolve
-     * @see #writeObject
-     */
-    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
-        s.defaultReadObject();  // requires serialPersistentFields to be an empty array
-
-        Class<?>   returnType     = (Class<?>)   s.readObject();
-        Class<?>[] parameterArray = (Class<?>[]) s.readObject();
-
-        // Probably this object will never escape, but let's check
-        // the field values now, just to be sure.
-        checkRtype(returnType);
-        checkPtypes(parameterArray);
-
-        parameterArray = parameterArray.clone();  // make sure it is unshared
-        MethodType_init(returnType, parameterArray);
-    }
-
-    /**
-     * For serialization only.
-     * Sets the final fields to null, pending {@code Unsafe.putObject}.
-     */
-    private MethodType() {
-        this.rtype = null;
-        this.ptypes = null;
-    }
-    private void MethodType_init(Class<?> rtype, Class<?>[] ptypes) {
-        // In order to communicate these values to readResolve, we must
-        // store them into the implementation-specific final fields.
-        checkRtype(rtype);
-        checkPtypes(ptypes);
-        UNSAFE.putObject(this, rtypeOffset, rtype);
-        UNSAFE.putObject(this, ptypesOffset, ptypes);
-    }
-
-    // Support for resetting final fields while deserializing
-    private static final long rtypeOffset, ptypesOffset;
-    static {
-        try {
-            rtypeOffset = UNSAFE.objectFieldOffset
-                (MethodType.class.getDeclaredField("rtype"));
-            ptypesOffset = UNSAFE.objectFieldOffset
-                (MethodType.class.getDeclaredField("ptypes"));
-        } catch (Exception ex) {
-            throw new Error(ex);
-        }
-    }
-
-    /**
-     * Resolves and initializes a {@code MethodType} object
-     * after serialization.
-     * @return the fully initialized {@code MethodType} object
-     */
-    private Object readResolve() {
-        // Do not use a trusted path for deserialization:
-        //return makeImpl(rtype, ptypes, true);
-        // Verify all operands, and make sure ptypes is unshared:
-        return methodType(rtype, ptypes);
-    }
-
-    /**
-     * Simple implementation of weak concurrent intern set.
-     *
-     * @param <T> interned type
-     */
-    private static class ConcurrentWeakInternSet<T> {
-
-        private final ConcurrentMap<WeakEntry<T>, WeakEntry<T>> map;
-        private final ReferenceQueue<T> stale;
-
-        public ConcurrentWeakInternSet() {
-            this.map = new ConcurrentHashMap<>();
-            this.stale = new ReferenceQueue<>();
-        }
-
-        /**
-         * Get the existing interned element.
-         * This method returns null if no element is interned.
-         *
-         * @param elem element to look up
-         * @return the interned element
-         */
-        public T get(T elem) {
-            if (elem == null) throw new NullPointerException();
-            expungeStaleElements();
-
-            WeakEntry<T> value = map.get(new WeakEntry<>(elem));
-            if (value != null) {
-                T res = value.get();
-                if (res != null) {
-                    return res;
-                }
-            }
-            return null;
-        }
-
-        /**
-         * Interns the element.
-         * Always returns non-null element, matching the one in the intern set.
-         * Under the race against another add(), it can return <i>different</i>
-         * element, if another thread beats us to interning it.
-         *
-         * @param elem element to add
-         * @return element that was actually added
-         */
-        public T add(T elem) {
-            if (elem == null) throw new NullPointerException();
-
-            // Playing double race here, and so spinloop is required.
-            // First race is with two concurrent updaters.
-            // Second race is with GC purging weak ref under our feet.
-            // Hopefully, we almost always end up with a single pass.
-            T interned;
-            WeakEntry<T> e = new WeakEntry<>(elem, stale);
-            do {
-                expungeStaleElements();
-                WeakEntry<T> exist = map.putIfAbsent(e, e);
-                interned = (exist == null) ? elem : exist.get();
-            } while (interned == null);
-            return interned;
-        }
-
-        private void expungeStaleElements() {
-            Reference<? extends T> reference;
-            while ((reference = stale.poll()) != null) {
-                map.remove(reference);
-            }
-        }
-
-        private static class WeakEntry<T> extends WeakReference<T> {
-
-            public final int hashcode;
-
-            public WeakEntry(T key, ReferenceQueue<T> queue) {
-                super(key, queue);
-                hashcode = key.hashCode();
-            }
-
-            public WeakEntry(T key) {
-                super(key);
-                hashcode = key.hashCode();
-            }
-
-            @Override
-            public boolean equals(Object obj) {
-                if (obj instanceof WeakEntry) {
-                    Object that = ((WeakEntry) obj).get();
-                    Object mine = get();
-                    return (that == null || mine == null) ? (this == obj) : mine.equals(that);
-                }
-                return false;
-            }
-
-            @Override
-            public int hashCode() {
-                return hashcode;
-            }
-
-        }
-    }
+    public String toMethodDescriptorString() { return null; }
 
 }
diff --git a/java/lang/ref/Reference.java b/java/lang/ref/Reference.java
index 06b517e..4d51b18 100644
--- a/java/lang/ref/Reference.java
+++ b/java/lang/ref/Reference.java
@@ -282,16 +282,26 @@
 
     // Android-changed: reachabilityFence implementation differs from OpenJDK9.
     public static void reachabilityFence(Object ref) {
+        // This code is usually replaced by much faster intrinsic implementations.
+        // It will be executed for tests run with the access checks interpreter in
+        // ART, e.g. with --verify-soft-fail.  Since this is a volatile store, it
+        // cannot easily be moved up past prior accesses, even if this method is
+        // inlined.
         SinkHolder.sink = ref;
-        // TODO: This is a horrible implementation. Fix it. Remove SinkHolder.
-        // b/72698200 .
+        // Leaving SinkHolder set to ref is unpleasant, since it keeps ref live
+        // until the next reachabilityFence call. This causes e.g. 036-finalizer
+        // to fail. Clear it again in a way that's unlikely to be optimizable.
+        // The fact that finalize_count is volatile makes it hard to move the test up.
+        if (SinkHolder.finalize_count == 0) {
+            SinkHolder.sink = null;
+        }
     }
 
     private static class SinkHolder {
         static volatile Object sink;
 
         // Ensure that sink looks live to even a reasonably clever compiler.
-        private static int finalize_count = 0;
+        private static volatile int finalize_count = 0;
 
         private static Object sinkUser = new Object() {
             protected void finalize() {
diff --git a/java/net/Inet6AddressImpl.java b/java/net/Inet6AddressImpl.java
index cfc2d13..1edfe34 100644
--- a/java/net/Inet6AddressImpl.java
+++ b/java/net/Inet6AddressImpl.java
@@ -290,9 +290,11 @@
         } catch (IOException e) {
             // Silently ignore and fall back.
         } finally {
-            try {
-                Libcore.os.close(fd);
-            } catch (ErrnoException e) { }
+            if (fd != null) {
+                try {
+                    Libcore.os.close(fd);
+                } catch (ErrnoException e) { }
+            }
         }
 
         return false;
diff --git a/java/security/KeyStore.java b/java/security/KeyStore.java
index d091781..8fe46b8 100644
--- a/java/security/KeyStore.java
+++ b/java/security/KeyStore.java
@@ -311,7 +311,7 @@
          * @param protectionAlgorithm the encryption algorithm name, for
          *     example, {@code PBEWithHmacSHA256AndAES_256}.
          *     See the Cipher section in the <a href=
-         * "{@docRoot}/../technotes/guides/security/StandardNames.html#Cipher">
+         * "{@docRoot}/openjdk-redirect.html?v=8&path=/technotes/guides/security/StandardNames.html#Cipher">
          * Java Cryptography Architecture Standard Algorithm Name
          * Documentation</a>
          *     for information about standard encryption algorithm names.
diff --git a/java/security/cert/package-info.java b/java/security/cert/package-info.java
index 0ef896b..0c1cb1f 100644
--- a/java/security/cert/package-info.java
+++ b/java/security/cert/package-info.java
@@ -52,7 +52,7 @@
  *   <li><a href="http://www.ietf.org/rfc/rfc5280.txt">
  *     http://www.ietf.org/rfc/rfc5280.txt</a>
  *   <li><a href=
- *     "{@docRoot}/../technotes/guides/security/certpath/CertPathProgGuide.html">
+ *     "{@docRoot}/openjdk-redirect.html?v=8&path=/technotes/guides/security/certpath/CertPathProgGuide.html">
  *     <b>Java&trade;
  *     PKI Programmer's Guide</b></a>
  *   <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/cert3.html">
diff --git a/java/security/package-info.java b/java/security/package-info.java
index 376aa9d..50a1527 100644
--- a/java/security/package-info.java
+++ b/java/security/package-info.java
@@ -46,14 +46,14 @@
  * <h2>Package Specification</h2>
  *
  * <ul>
- *   <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/crypto/CryptoSpec.html">
+ *   <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/crypto/CryptoSpec.html">
  *     <b>Java&trade;
  *     Cryptography Architecture (JCA) Reference Guide</b></a></li>
  *
  *   <li>PKCS #8: Private-Key Information Syntax Standard, Version 1.2,
  *     November 1993</li>
  *
- *   <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/StandardNames.html">
+ *   <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/StandardNames.html">
  *     <b>Java&trade;
  *     Cryptography Architecture Standard Algorithm Name
  *     Documentation</b></a></li>
@@ -64,44 +64,44 @@
  * For further documentation, please see:
  * <ul>
  *   <li><a href=
- *     "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/spec/security-spec.doc.html">
+ *     "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/spec/security-spec.doc.html">
  *     <b>Java&trade;
  *     SE Platform Security Architecture</b></a></li>
  *
  *   <li><a href=
- *     "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/crypto/HowToImplAProvider.html">
+ *     "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/crypto/HowToImplAProvider.html">
  *     <b>How to Implement a Provider in the
  *     Java&trade; Cryptography Architecture
  *     </b></a></li>
  *
  *   <li><a href=
- *     "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/PolicyFiles.html"><b>
+ *     "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/PolicyFiles.html"><b>
  *     Default Policy Implementation and Policy File Syntax
  *     </b></a></li>
  *
  *   <li><a href=
- *     "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/permissions.html"><b>
+ *     "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/permissions.html"><b>
  *     Permissions in the
  *     Java&trade; SE Development Kit (JDK)
  *     </b></a></li>
  *
  *   <li><a href=
- *     "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/SecurityToolsSummary.html"><b>
+ *     "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/SecurityToolsSummary.html"><b>
  *     Summary of Tools for
  *     Java&trade; Platform Security
  *     </b></a></li>
  *
  *   <li><b>keytool</b>
- *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/unix/keytool.html">
+ *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/unix/keytool.html">
  *       for Solaris/Linux</a>)
- *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/windows/keytool.html">
+ *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/windows/keytool.html">
  *       for Windows</a>)
  *     </li>
  *
  *   <li><b>jarsigner</b>
- *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/unix/jarsigner.html">
+ *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/unix/jarsigner.html">
  *       for Solaris/Linux</a>)
- *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/windows/jarsigner.html">
+ *     (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/windows/jarsigner.html">
  *       for Windows</a>)
  *     </li>
  *
diff --git a/java/util/LinkedHashMap.java b/java/util/LinkedHashMap.java
index aec40bc..7c8cad0 100644
--- a/java/util/LinkedHashMap.java
+++ b/java/util/LinkedHashMap.java
@@ -158,7 +158,7 @@
  * {@code LinkedHashMap}.
  *
  * <p>This class is a member of the
- * <a href="{@docRoot}/../technotes/guides/collections/index.html">
+ * <a href="{@docRoot}/openjdk-redirect.html?v=8&path=/technotes/guides/collections/index.html">
  * Java Collections Framework</a>.
  *
  * @implNote
diff --git a/java/util/Locale.java b/java/util/Locale.java
index 9d9f3e4..c96bb13 100644
--- a/java/util/Locale.java
+++ b/java/util/Locale.java
@@ -520,6 +520,10 @@
  *     <td><a href="http://site.icu-project.org/download/58">ICU 58.2</a></td>
  *     <td><a href="http://cldr.unicode.org/index/downloads/cldr-30">CLDR 30.0.3</a></td>
  *     <td><a href="http://www.unicode.org/versions/Unicode9.0.0/">Unicode 9.0</a></td></tr>
+ * <tr><td>Android 9.0 (TBD)</td>
+ *     <td><a href="http://site.icu-project.org/download/60">ICU 60.2</a></td>
+ *     <td><a href="http://cldr.unicode.org/index/downloads/cldr-32">CLDR 32.0.1</a></td>
+ *     <td><a href="http://www.unicode.org/versions/Unicode10.0.0/">Unicode 10.0</a></td></tr>
  * </table>
  *
  * <a name="default_locale"></a><h4>Be wary of the default locale</h3>
diff --git a/javax/security/auth/login/package-info.java b/javax/security/auth/login/package-info.java
index a0f7f68..301ac21 100644
--- a/javax/security/auth/login/package-info.java
+++ b/javax/security/auth/login/package-info.java
@@ -28,7 +28,7 @@
  * <h2>Package Specification</h2>
  *
  * <ul>
- *   <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/StandardNames.html">
+ *   <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/StandardNames.html">
  *     <b>Java&trade;
  *     Cryptography Architecture Standard Algorithm Name
  *     Documentation</b></a></li>