Merge "Add media output group panel index"
diff --git a/api/current.txt b/api/current.txt
index 7a5753a..24478f4 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -56486,6 +56486,7 @@
     method public int describeContents();
     method @NonNull public android.util.Size getMaxSize();
     method @NonNull public android.util.Size getMinSize();
+    method @Nullable public String getStyle();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.view.inline.InlinePresentationSpec> CREATOR;
   }
@@ -56690,9 +56691,11 @@
 
   public final class InlineSuggestionsRequest implements android.os.Parcelable {
     method public int describeContents();
+    method @Nullable public android.os.Bundle getExtras();
     method @NonNull public String getHostPackageName();
     method public int getMaxSuggestionCount();
     method @NonNull public java.util.List<android.view.inline.InlinePresentationSpec> getPresentationSpecs();
+    method @NonNull public android.os.LocaleList getSupportedLocales();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InlineSuggestionsRequest> CREATOR;
     field public static final int SUGGESTION_COUNT_UNLIMITED = 2147483647; // 0x7fffffff
@@ -56702,7 +56705,9 @@
     ctor public InlineSuggestionsRequest.Builder(@NonNull java.util.List<android.view.inline.InlinePresentationSpec>);
     method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder addPresentationSpecs(@NonNull android.view.inline.InlinePresentationSpec);
     method @NonNull public android.view.inputmethod.InlineSuggestionsRequest build();
+    method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setExtras(@Nullable android.os.Bundle);
     method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setMaxSuggestionCount(int);
+    method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setSupportedLocales(@NonNull android.os.LocaleList);
   }
 
   public final class InlineSuggestionsResponse implements android.os.Parcelable {
diff --git a/api/system-current.txt b/api/system-current.txt
index 773cd4d..5b6bd21 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -14081,14 +14081,6 @@
 
 }
 
-package android.view.inline {
-
-  public final class InlinePresentationSpec implements android.os.Parcelable {
-    method @Nullable public String getStyle();
-  }
-
-}
-
 package android.webkit {
 
   public abstract class CookieManager {
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index 6e7f081..97e0a2e 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -248,6 +248,9 @@
 #endif
     event->updateValue(8 /*user id field id*/, userId, INT);
 
+    // If this event is a rollback event, then the following bits in the event
+    // are invalid and we will need to update them with the values we pulled
+    // from disk.
     if (is_rollback) {
         int bit = trainInfo.requiresStaging ? 1 : 0;
         event->updateValue(3 /*requires staging field id*/, bit, INT);
@@ -294,19 +297,28 @@
 
     if (!trainInfo->experimentIds.empty()) {
         int64_t firstId = trainInfo->experimentIds.at(0);
+        auto& ids = trainInfo->experimentIds;
         switch (trainInfo->status) {
             case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALL_SUCCESS:
-                trainInfo->experimentIds.push_back(firstId + 1);
+                if (find(ids.begin(), ids.end(), firstId + 1) == ids.end()) {
+                    ids.push_back(firstId + 1);
+                }
                 break;
             case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALLER_ROLLBACK_INITIATED:
-                trainInfo->experimentIds.push_back(firstId + 2);
+                if (find(ids.begin(), ids.end(), firstId + 2) == ids.end()) {
+                    ids.push_back(firstId + 2);
+                }
                 break;
             case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALLER_ROLLBACK_SUCCESS:
-                trainInfo->experimentIds.push_back(firstId + 3);
+                if (find(ids.begin(), ids.end(), firstId + 3) == ids.end()) {
+                    ids.push_back(firstId + 3);
+                }
                 break;
         }
     }
 
+    // If this event is a rollback event, the following fields are invalid and
+    // need to be replaced by the fields stored to disk.
     if (is_rollback) {
         trainInfo->requiresStaging = trainInfoOnDisk.requiresStaging;
         trainInfo->rollbackEnabled = trainInfoOnDisk.rollbackEnabled;
@@ -356,6 +368,7 @@
     }
     bool readTrainInfoSuccess = false;
     InstallTrainInfo trainInfoOnDisk;
+    // We use the package name of the event as the train name.
     readTrainInfoSuccess = StorageManager::readTrainInfo(packageNameIn, trainInfoOnDisk);
 
     if (!readTrainInfoSuccess) {
@@ -365,13 +378,20 @@
     if (trainInfoOnDisk.experimentIds.empty()) {
         return vector<int64_t>();
     }
+
+    int64_t firstId = trainInfoOnDisk.experimentIds[0];
+    auto& ids = trainInfoOnDisk.experimentIds;
     switch (rollbackTypeIn) {
         case android::util::WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE:
-            trainInfoOnDisk.experimentIds.push_back(trainInfoOnDisk.experimentIds[0] + 4);
+            if (find(ids.begin(), ids.end(), firstId + 4) == ids.end()) {
+                ids.push_back(firstId + 4);
+            }
             StorageManager::writeTrainInfo(trainInfoOnDisk);
             break;
         case android::util::WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS:
-            trainInfoOnDisk.experimentIds.push_back(trainInfoOnDisk.experimentIds[0] + 5);
+            if (find(ids.begin(), ids.end(), firstId + 5) == ids.end()) {
+                ids.push_back(firstId + 5);
+            }
             StorageManager::writeTrainInfo(trainInfoOnDisk);
             break;
     }
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index 7087c68..7ab6c71 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -280,7 +280,7 @@
 status_t StatsService::handleShellCommand(int in, int out, int err, const char** argv,
                                           uint32_t argc) {
     uid_t uid = AIBinder_getCallingUid();
-    if (uid != AID_ROOT || uid != AID_SHELL) {
+    if (uid != AID_ROOT && uid != AID_SHELL) {
         return PERMISSION_DENIED;
     }
 
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index a305948..ea26ede 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -4720,19 +4720,19 @@
     /**
      * Returns the {@code uid} of the owner of a network connection.
      *
-     * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and
-     * {@code IPPROTO_UDP} currently supported.
+     * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and {@code
+     *     IPPROTO_UDP} currently supported.
      * @param local The local {@link InetSocketAddress} of a connection.
      * @param remote The remote {@link InetSocketAddress} of a connection.
-     *
      * @return {@code uid} if the connection is found and the app has permission to observe it
-     * (e.g., if it is associated with the calling VPN app's tunnel) or
-     * {@link android.os.Process#INVALID_UID} if the connection is not found.
-     * Throws {@link SecurityException} if the caller is not the active VPN for the current user.
-     * Throws {@link IllegalArgumentException} if an unsupported protocol is requested.
+     *     (e.g., if it is associated with the calling VPN app's VpnService tunnel) or {@link
+     *     android.os.Process#INVALID_UID} if the connection is not found.
+     * @throws {@link SecurityException} if the caller is not the active VpnService for the current
+     *     user.
+     * @throws {@link IllegalArgumentException} if an unsupported protocol is requested.
      */
-    public int getConnectionOwnerUid(int protocol, @NonNull InetSocketAddress local,
-            @NonNull InetSocketAddress remote) {
+    public int getConnectionOwnerUid(
+            int protocol, @NonNull InetSocketAddress local, @NonNull InetSocketAddress remote) {
         ConnectionInfo connectionInfo = new ConnectionInfo(protocol, local, remote);
         try {
             return mService.getConnectionOwnerUid(connectionInfo);
diff --git a/core/java/android/net/NetworkScore.java b/core/java/android/net/NetworkScore.java
index ae17378..d2e59eb 100644
--- a/core/java/android/net/NetworkScore.java
+++ b/core/java/android/net/NetworkScore.java
@@ -21,7 +21,6 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
-import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -87,7 +86,7 @@
         /** toString */
         public String toString() {
             return "latency = " + latencyMs + " downlinkBandwidth = " + downlinkBandwidthKBps
-                    + "uplinkBandwidth = " + uplinkBandwidthKBps;
+                    + " uplinkBandwidth = " + uplinkBandwidthKBps;
         }
 
         @NonNull
@@ -354,17 +353,27 @@
         private Metrics mLinkLayerMetrics = new Metrics(Metrics.LATENCY_UNKNOWN,
                 Metrics.BANDWIDTH_UNKNOWN, Metrics.BANDWIDTH_UNKNOWN);
         @NonNull
-        private Metrics mEndToMetrics = new Metrics(Metrics.LATENCY_UNKNOWN,
+        private Metrics mEndToEndMetrics = new Metrics(Metrics.LATENCY_UNKNOWN,
                 Metrics.BANDWIDTH_UNKNOWN, Metrics.BANDWIDTH_UNKNOWN);
         private int mSignalStrength = UNKNOWN_SIGNAL_STRENGTH;
         private int mRange = RANGE_UNKNOWN;
         private boolean mExiting = false;
         private int mLegacyScore = 0;
-        @NonNull private Bundle mExtensions = new Bundle();
 
         /** Create a new builder. */
         public Builder() { }
 
+        /** @hide */
+        public Builder(@NonNull final NetworkScore source) {
+            mPolicy = source.mPolicy;
+            mLinkLayerMetrics = source.mLinkLayerMetrics;
+            mEndToEndMetrics = source.mEndToEndMetrics;
+            mSignalStrength = source.mSignalStrength;
+            mRange = source.mRange;
+            mExiting = source.mExiting;
+            mLegacyScore = source.mLegacyScore;
+        }
+
         /** Add a policy flag. */
         @NonNull public Builder addPolicy(@Policy final int policy) {
             mPolicy |= policy;
@@ -385,7 +394,7 @@
 
         /** Set the end-to-end metrics. */
         @NonNull public Builder setEndToEndMetrics(@NonNull final Metrics endToEndMetrics) {
-            mEndToMetrics = endToEndMetrics;
+            mEndToEndMetrics = endToEndMetrics;
             return this;
         }
 
@@ -417,7 +426,7 @@
 
         /** Build the NetworkScore object represented by this builder. */
         @NonNull public NetworkScore build() {
-            return new NetworkScore(mPolicy, mLinkLayerMetrics, mEndToMetrics,
+            return new NetworkScore(mPolicy, mLinkLayerMetrics, mEndToEndMetrics,
                     mSignalStrength, mRange, mExiting, mLegacyScore);
         }
     }
diff --git a/core/java/android/view/inline/InlinePresentationSpec.java b/core/java/android/view/inline/InlinePresentationSpec.java
index 406e599..8bda339 100644
--- a/core/java/android/view/inline/InlinePresentationSpec.java
+++ b/core/java/android/view/inline/InlinePresentationSpec.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SystemApi;
 import android.os.Parcelable;
 import android.util.Size;
 
@@ -53,15 +52,6 @@
         return null;
     }
 
-    /**
-     * @hide
-     */
-    @SystemApi
-    public @Nullable String getStyle() {
-        return mStyle;
-    }
-
-
     /** @hide */
     @DataClass.Suppress({"setMaxSize", "setMinSize"})
     abstract static class BaseBuilder {
@@ -114,6 +104,17 @@
         return mMaxSize;
     }
 
+    /**
+     * The fully qualified resource name of the UI style resource identifier, defaults to {@code
+     * null}.
+     *
+     * <p> The value can be obtained by calling {@code Resources#getResourceName(int)}.
+     */
+    @DataClass.Generated.Member
+    public @Nullable String getStyle() {
+        return mStyle;
+    }
+
     @Override
     @DataClass.Generated.Member
     public String toString() {
@@ -283,10 +284,10 @@
     }
 
     @DataClass.Generated(
-            time = 1581117017522L,
+            time = 1581736227796L,
             codegenVersion = "1.0.14",
             sourceFile = "frameworks/base/core/java/android/view/inline/InlinePresentationSpec.java",
-            inputSignatures = "private final @android.annotation.NonNull android.util.Size mMinSize\nprivate final @android.annotation.NonNull android.util.Size mMaxSize\nprivate final @android.annotation.Nullable java.lang.String mStyle\nprivate static  java.lang.String defaultStyle()\npublic @android.annotation.SystemApi @android.annotation.Nullable java.lang.String getStyle()\nclass InlinePresentationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nclass BaseBuilder extends java.lang.Object implements []")
+            inputSignatures = "private final @android.annotation.NonNull android.util.Size mMinSize\nprivate final @android.annotation.NonNull android.util.Size mMaxSize\nprivate final @android.annotation.Nullable java.lang.String mStyle\nprivate static  java.lang.String defaultStyle()\nclass InlinePresentationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nclass BaseBuilder extends java.lang.Object implements []")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
index be9370a..5700dda 100644
--- a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
+++ b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
@@ -19,7 +19,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.LocaleList;
 import android.os.Parcelable;
 import android.view.inline.InlinePresentationSpec;
 
@@ -60,6 +62,13 @@
     private @NonNull String mHostPackageName;
 
     /**
+     * The IME provided locales for the request. If non-empty, the inline suggestions should
+     * return languages from the supported locales. If not provided, it'll default to system locale.
+     */
+    private @NonNull LocaleList mSupportedLocales;
+
+    // TODO(b/149609075): the generated code needs to be manually fixed due to the bug.
+    /**
      * The host input token of the IME that made the request. This will be set by the system for
      * safety reasons.
      *
@@ -68,12 +77,9 @@
     private @Nullable IBinder mHostInputToken;
 
     /**
-     * @hide
-     * @see {@link #mHostPackageName}.
+     * The extras state propagated from the IME to pass extra data.
      */
-    public void setHostPackageName(@NonNull String hostPackageName) {
-        mHostPackageName = hostPackageName;
-    }
+    private @Nullable Bundle mExtras;
 
     /**
      * @hide
@@ -95,10 +101,20 @@
         return ActivityThread.currentPackageName();
     }
 
+    private static LocaleList defaultSupportedLocales() {
+        return LocaleList.getDefault();
+    }
+
+    @Nullable
     private static IBinder defaultHostInputToken() {
         return null;
     }
 
+    @Nullable
+    private static Bundle defaultExtras() {
+        return null;
+    }
+
     /** @hide */
     abstract static class BaseBuilder {
         abstract Builder setPresentationSpecs(@NonNull List<InlinePresentationSpec> value);
@@ -128,7 +144,9 @@
             int maxSuggestionCount,
             @NonNull List<InlinePresentationSpec> presentationSpecs,
             @NonNull String hostPackageName,
-            @Nullable IBinder hostInputToken) {
+            @NonNull LocaleList supportedLocales,
+            @Nullable IBinder hostInputToken,
+            @Nullable Bundle extras) {
         this.mMaxSuggestionCount = maxSuggestionCount;
         this.mPresentationSpecs = presentationSpecs;
         com.android.internal.util.AnnotationValidations.validate(
@@ -136,7 +154,11 @@
         this.mHostPackageName = hostPackageName;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mHostPackageName);
+        this.mSupportedLocales = supportedLocales;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mSupportedLocales);
         this.mHostInputToken = hostInputToken;
+        this.mExtras = extras;
 
         onConstructed();
     }
@@ -171,6 +193,15 @@
     }
 
     /**
+     * The IME provided locales for the request. If non-empty, the inline suggestions should
+     * return languages from the supported locales. If not provided, it'll default to system locale.
+     */
+    @DataClass.Generated.Member
+    public @NonNull LocaleList getSupportedLocales() {
+        return mSupportedLocales;
+    }
+
+    /**
      * The host input token of the IME that made the request. This will be set by the system for
      * safety reasons.
      *
@@ -181,6 +212,14 @@
         return mHostInputToken;
     }
 
+    /**
+     * The extras state propagated from the IME to pass extra data.
+     */
+    @DataClass.Generated.Member
+    public @Nullable Bundle getExtras() {
+        return mExtras;
+    }
+
     @Override
     @DataClass.Generated.Member
     public String toString() {
@@ -191,7 +230,9 @@
                 "maxSuggestionCount = " + mMaxSuggestionCount + ", " +
                 "presentationSpecs = " + mPresentationSpecs + ", " +
                 "hostPackageName = " + mHostPackageName + ", " +
-                "hostInputToken = " + mHostInputToken +
+                "supportedLocales = " + mSupportedLocales + ", " +
+                "hostInputToken = " + mHostInputToken + ", " +
+                "extras = " + mExtras +
         " }";
     }
 
@@ -211,7 +252,9 @@
                 && mMaxSuggestionCount == that.mMaxSuggestionCount
                 && java.util.Objects.equals(mPresentationSpecs, that.mPresentationSpecs)
                 && java.util.Objects.equals(mHostPackageName, that.mHostPackageName)
-                && java.util.Objects.equals(mHostInputToken, that.mHostInputToken);
+                && java.util.Objects.equals(mSupportedLocales, that.mSupportedLocales)
+                && java.util.Objects.equals(mHostInputToken, that.mHostInputToken)
+                && java.util.Objects.equals(mExtras, that.mExtras);
     }
 
     @Override
@@ -224,7 +267,9 @@
         _hash = 31 * _hash + mMaxSuggestionCount;
         _hash = 31 * _hash + java.util.Objects.hashCode(mPresentationSpecs);
         _hash = 31 * _hash + java.util.Objects.hashCode(mHostPackageName);
+        _hash = 31 * _hash + java.util.Objects.hashCode(mSupportedLocales);
         _hash = 31 * _hash + java.util.Objects.hashCode(mHostInputToken);
+        _hash = 31 * _hash + java.util.Objects.hashCode(mExtras);
         return _hash;
     }
 
@@ -235,12 +280,15 @@
         // void parcelFieldName(Parcel dest, int flags) { ... }
 
         byte flg = 0;
-        if (mHostInputToken != null) flg |= 0x8;
+        if (mHostInputToken != null) flg |= 0x10;
+        if (mExtras != null) flg |= 0x20;
         dest.writeByte(flg);
         dest.writeInt(mMaxSuggestionCount);
         dest.writeParcelableList(mPresentationSpecs, flags);
         dest.writeString(mHostPackageName);
+        dest.writeTypedObject(mSupportedLocales, flags);
         if (mHostInputToken != null) dest.writeStrongBinder(mHostInputToken);
+        if (mExtras != null) dest.writeBundle(mExtras);
     }
 
     @Override
@@ -259,7 +307,9 @@
         List<InlinePresentationSpec> presentationSpecs = new ArrayList<>();
         in.readParcelableList(presentationSpecs, InlinePresentationSpec.class.getClassLoader());
         String hostPackageName = in.readString();
-        IBinder hostInputToken = (flg & 0x8) == 0 ? null : in.readStrongBinder();
+        LocaleList supportedLocales = (LocaleList) in.readTypedObject(LocaleList.CREATOR);
+        IBinder hostInputToken = (flg & 0x10) == 0 ? null : in.readStrongBinder();
+        Bundle extras = (flg & 0x20) == 0 ? null : in.readBundle();
 
         this.mMaxSuggestionCount = maxSuggestionCount;
         this.mPresentationSpecs = presentationSpecs;
@@ -268,7 +318,11 @@
         this.mHostPackageName = hostPackageName;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mHostPackageName);
+        this.mSupportedLocales = supportedLocales;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mSupportedLocales);
         this.mHostInputToken = hostInputToken;
+        this.mExtras = extras;
 
         onConstructed();
     }
@@ -297,7 +351,9 @@
         private int mMaxSuggestionCount;
         private @NonNull List<InlinePresentationSpec> mPresentationSpecs;
         private @NonNull String mHostPackageName;
+        private @NonNull LocaleList mSupportedLocales;
         private @Nullable IBinder mHostInputToken;
+        private @Nullable Bundle mExtras;
 
         private long mBuilderFieldsSet = 0L;
 
@@ -368,6 +424,18 @@
         }
 
         /**
+         * The IME provided locales for the request. If non-empty, the inline suggestions should
+         * return languages from the supported locales. If not provided, it'll default to system locale.
+         */
+        @DataClass.Generated.Member
+        public @NonNull Builder setSupportedLocales(@NonNull LocaleList value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x8;
+            mSupportedLocales = value;
+            return this;
+        }
+
+        /**
          * The host input token of the IME that made the request. This will be set by the system for
          * safety reasons.
          *
@@ -377,15 +445,26 @@
         @Override
         @NonNull Builder setHostInputToken(@Nullable IBinder value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x8;
+            mBuilderFieldsSet |= 0x10;
             mHostInputToken = value;
             return this;
         }
 
+        /**
+         * The extras state propagated from the IME to pass extra data.
+         */
+        @DataClass.Generated.Member
+        public @NonNull Builder setExtras(@Nullable Bundle value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x20;
+            mExtras = value;
+            return this;
+        }
+
         /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull InlineSuggestionsRequest build() {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x10; // Mark builder used
+            mBuilderFieldsSet |= 0x40; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x1) == 0) {
                 mMaxSuggestionCount = defaultMaxSuggestionCount();
@@ -394,18 +473,26 @@
                 mHostPackageName = defaultHostPackageName();
             }
             if ((mBuilderFieldsSet & 0x8) == 0) {
+                mSupportedLocales = defaultSupportedLocales();
+            }
+            if ((mBuilderFieldsSet & 0x10) == 0) {
                 mHostInputToken = defaultHostInputToken();
             }
+            if ((mBuilderFieldsSet & 0x20) == 0) {
+                mExtras = defaultExtras();
+            }
             InlineSuggestionsRequest o = new InlineSuggestionsRequest(
                     mMaxSuggestionCount,
                     mPresentationSpecs,
                     mHostPackageName,
-                    mHostInputToken);
+                    mSupportedLocales,
+                    mHostInputToken,
+                    mExtras);
             return o;
         }
 
         private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x10) != 0) {
+            if ((mBuilderFieldsSet & 0x40) != 0) {
                 throw new IllegalStateException(
                         "This Builder should not be reused. Use a new Builder instance instead");
             }
@@ -413,10 +500,10 @@
     }
 
     @DataClass.Generated(
-            time = 1581555687721L,
+            time = 1581747892762L,
             codegenVersion = "1.0.14",
             sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java",
-            inputSignatures = "public static final  int SUGGESTION_COUNT_UNLIMITED\nprivate final  int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.view.inline.InlinePresentationSpec> mPresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\npublic  void setHostPackageName(java.lang.String)\npublic  void setHostInputToken(android.os.IBinder)\nprivate  void onConstructed()\nprivate static  int defaultMaxSuggestionCount()\nprivate static  java.lang.String defaultHostPackageName()\nprivate static  android.os.IBinder defaultHostInputToken()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setPresentationSpecs(java.util.List<android.view.inline.InlinePresentationSpec>)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nclass BaseBuilder extends java.lang.Object implements []")
+            inputSignatures = "public static final  int SUGGESTION_COUNT_UNLIMITED\nprivate final  int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.view.inline.InlinePresentationSpec> mPresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate @android.annotation.Nullable android.os.Bundle mExtras\npublic  void setHostInputToken(android.os.IBinder)\nprivate  void onConstructed()\nprivate static  int defaultMaxSuggestionCount()\nprivate static  java.lang.String defaultHostPackageName()\nprivate static  android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable android.os.Bundle defaultExtras()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setPresentationSpecs(java.util.List<android.view.inline.InlinePresentationSpec>)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nclass BaseBuilder extends java.lang.Object implements []")
     @Deprecated
     private void __metadata() {}
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 5ff88ac..f69e4f5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -193,6 +193,34 @@
     }
 
     /**
+     * Get the MediaDevice list that has been selected to current media.
+     *
+     * @return list of MediaDevice
+     */
+    List<MediaDevice> getSelectedMediaDevice() {
+        final List<MediaDevice> deviceList = new ArrayList<>();
+        if (TextUtils.isEmpty(mPackageName)) {
+            Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!");
+            return deviceList;
+        }
+
+        final RoutingSessionInfo info = getRoutingSessionInfo();
+        if (info != null) {
+            for (MediaRoute2Info route : mRouterManager.getControllerForSession(info)
+                    .getSelectedRoutes()) {
+                deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
+                        route, mPackageName));
+            }
+            return deviceList;
+        }
+
+        Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : "
+                + mPackageName);
+
+        return deviceList;
+    }
+
+    /**
      * Adjust the volume of {@link android.media.RoutingSessionInfo}.
      *
      * @param volume the value of volume
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index fc373a5..617da6e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -282,6 +282,15 @@
     }
 
     /**
+     * Get the MediaDevice list that has been selected to current media.
+     *
+     * @return list of MediaDevice
+     */
+    public List<MediaDevice> getSelectedMediaDevice() {
+        return mInfoMediaManager.getSelectedMediaDevice();
+    }
+
+    /**
      * Adjust the volume of session.
      *
      * @param volume the value of volume
diff --git a/packages/Tethering/Android.bp b/packages/Tethering/Android.bp
index fa3926c..3111ab7 100644
--- a/packages/Tethering/Android.bp
+++ b/packages/Tethering/Android.bp
@@ -17,6 +17,7 @@
 java_defaults {
     name: "TetheringAndroidLibraryDefaults",
     // TODO (b/146757305): change to module API once available
+    // TODO (b/148190005): change to module-libs-api-stubs-current once it is ready.
     sdk_version: "core_platform",
     srcs: [
         "src/**/*.java",
@@ -34,7 +35,12 @@
         "net-utils-framework-common",
     ],
     libs: [
+        // Order matters: framework-tethering needs to be before the system stubs, otherwise
+        // hidden fields in the framework-tethering classes (which are also used to generate stubs)
+        // will not be found.
         "framework-tethering",
+        "android_system_stubs_current",
+        "framework-res",
         "unsupportedappusage",
         "android_system_stubs_current",
         "framework-res",
@@ -86,6 +92,7 @@
 java_defaults {
     name: "TetheringAppDefaults",
     // TODO (b/146757305): change to module API once available
+    // TODO (b/148190005): change to module-libs-api-stubs-current once it is ready.
     sdk_version: "core_platform",
     privileged: true,
     // Build system doesn't track transitive dependeicies for jni_libs, list all the dependencies
@@ -99,6 +106,9 @@
         "res",
     ],
     libs: [
+        // Order matters: framework-tethering needs to be before the system stubs, otherwise
+        // hidden fields in the framework-tethering classes (which are also used to generate stubs)
+        // will not be found.
         "framework-tethering",
         "android_system_stubs_current",
         "framework-res",
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 5febe73..8be7964 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -1,16 +1,16 @@
-/**
- * Copyright (c) 2019, The Android Open Source Project
+/*
+ * Copyright (C) 2020 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
+ *      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 perNmissions and
+ * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 package android.net;
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
index 28a810d..a554193 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
+++ b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
@@ -17,6 +17,7 @@
 package android.net;
 
 import android.net.Network;
+import android.net.TetheredClient;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetherStatesParcel;
@@ -33,4 +34,5 @@
     void onUpstreamChanged(in Network network);
     void onConfigurationChanged(in TetheringConfigurationParcel config);
     void onTetherStatesChanged(in TetherStatesParcel states);
+    void onTetherClientsChanged(in List<TetheredClient> clients);
 }
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
index 779aa3b..8b8b9e5 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
+++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
@@ -191,6 +191,15 @@
                 return new AddressInfo[size];
             }
         };
+
+        @NonNull
+        @Override
+        public String toString() {
+            return "AddressInfo {"
+                    + mAddress
+                    + (mHostname != null ? ", hostname " + mHostname : "")
+                    + "}";
+        }
     }
 
     @Override
@@ -212,4 +221,13 @@
             return new TetheredClient[size];
         }
     };
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "TetheredClient {hwAddr " + mMacAddress
+                + ", addresses " + mAddresses
+                + ", tetheringType " + mTetheringType
+                + "}";
+    }
 }
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl b/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
index 14ee2d3..c064aa4 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
+++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
@@ -17,6 +17,7 @@
 package android.net;
 
 import android.net.Network;
+import android.net.TetheredClient;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetherStatesParcel;
 
@@ -29,4 +30,5 @@
     Network upstreamNetwork;
     TetheringConfigurationParcel config;
     TetherStatesParcel states;
+    List<TetheredClient> tetheredClients;
 }
\ No newline at end of file
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 6a9f010..bfa962a 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -375,6 +375,9 @@
             mTetherStatesParcel = states;
         }
 
+        @Override
+        public void onTetherClientsChanged(List<TetheredClient> clients) { }
+
         public void waitForStarted() {
             mWaitForCallback.block(DEFAULT_TIMEOUT_MS);
             throwIfPermissionFailure(mError);
@@ -921,6 +924,7 @@
                         sendRegexpsChanged(parcel.config);
                         maybeSendTetherableIfacesChangedCallback(parcel.states);
                         maybeSendTetheredIfacesChangedCallback(parcel.states);
+                        callback.onClientsChanged(parcel.tetheredClients);
                     });
                 }
 
@@ -951,6 +955,11 @@
                         maybeSendTetheredIfacesChangedCallback(states);
                     });
                 }
+
+                @Override
+                public void onTetherClientsChanged(final List<TetheredClient> clients) {
+                    executor.execute(() -> callback.onClientsChanged(clients));
+                }
             };
             getConnector(c -> c.registerTetheringEventCallback(remoteCallback, callerPkg));
             mTetheringEventCallbacks.put(callback, remoteCallback);
diff --git a/packages/Tethering/src/android/net/ip/IpServer.java b/packages/Tethering/src/android/net/ip/IpServer.java
index 2653b6d..b4d49c0 100644
--- a/packages/Tethering/src/android/net/ip/IpServer.java
+++ b/packages/Tethering/src/android/net/ip/IpServer.java
@@ -19,6 +19,7 @@
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH;
 import static android.net.util.NetworkConstants.FF;
 import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
 import static android.net.util.NetworkConstants.asByte;
@@ -29,11 +30,15 @@
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
+import android.net.MacAddress;
 import android.net.RouteInfo;
+import android.net.TetheredClient;
 import android.net.TetheringManager;
+import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.DhcpServingParamsParcelExt;
+import android.net.dhcp.IDhcpLeaseCallbacks;
 import android.net.dhcp.IDhcpServer;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
 import android.net.shared.NetdUtils;
@@ -48,6 +53,8 @@
 import android.util.Log;
 import android.util.SparseArray;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.util.MessageUtils;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
@@ -57,7 +64,10 @@
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
@@ -130,6 +140,11 @@
          * @param newLp the new LinkProperties to report
          */
         public void updateLinkProperties(IpServer who, LinkProperties newLp) { }
+
+        /**
+         * Notify that the DHCP leases changed in one of the IpServers.
+         */
+        public void dhcpLeasesChanged() { }
     }
 
     /** Capture IpServer dependencies, for injection. */
@@ -205,6 +220,8 @@
     private IDhcpServer mDhcpServer;
     private RaParams mLastRaParams;
     private LinkAddress mIpv4Address;
+    @NonNull
+    private List<TetheredClient> mDhcpLeases = Collections.emptyList();
 
     public IpServer(
             String ifaceName, Looper looper, int interfaceType, SharedLog log,
@@ -262,6 +279,14 @@
         return new LinkProperties(mLinkProperties);
     }
 
+    /**
+     * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
+     * thread.
+     */
+    public List<TetheredClient> getAllLeases() {
+        return Collections.unmodifiableList(mDhcpLeases);
+    }
+
     /** Stop this IpServer. After this is called this IpServer should not be used any more. */
     public void stop() {
         sendMessage(CMD_INTERFACE_DOWN);
@@ -334,7 +359,7 @@
 
                 mDhcpServer = server;
                 try {
-                    mDhcpServer.start(new OnHandlerStatusCallback() {
+                    mDhcpServer.startWithCallbacks(new OnHandlerStatusCallback() {
                         @Override
                         public void callback(int startStatusCode) {
                             if (startStatusCode != STATUS_SUCCESS) {
@@ -342,7 +367,7 @@
                                 handleError();
                             }
                         }
-                    });
+                    }, new DhcpLeaseCallback());
                 } catch (RemoteException e) {
                     throw new IllegalStateException(e);
                 }
@@ -355,6 +380,48 @@
         }
     }
 
+    private class DhcpLeaseCallback extends IDhcpLeaseCallbacks.Stub {
+        @Override
+        public void onLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables) {
+            final ArrayList<TetheredClient> leases = new ArrayList<>();
+            for (DhcpLeaseParcelable lease : leaseParcelables) {
+                final LinkAddress address = new LinkAddress(
+                        intToInet4AddressHTH(lease.netAddr), lease.prefixLength);
+
+                final MacAddress macAddress;
+                try {
+                    macAddress = MacAddress.fromBytes(lease.hwAddr);
+                } catch (IllegalArgumentException e) {
+                    Log.wtf(TAG, "Invalid address received from DhcpServer: "
+                            + Arrays.toString(lease.hwAddr));
+                    return;
+                }
+
+                final TetheredClient.AddressInfo addressInfo = new TetheredClient.AddressInfo(
+                        address, lease.hostname, lease.expTime);
+                leases.add(new TetheredClient(
+                        macAddress,
+                        Collections.singletonList(addressInfo),
+                        mInterfaceType));
+            }
+
+            getHandler().post(() -> {
+                mDhcpLeases = leases;
+                mCallback.dhcpLeasesChanged();
+            });
+        }
+
+        @Override
+        public int getInterfaceVersion() {
+            return this.VERSION;
+        }
+
+        @Override
+        public String getInterfaceHash() throws RemoteException {
+            return this.HASH;
+        }
+    }
+
     private boolean startDhcp(Inet4Address addr, int prefixLen) {
         if (mUsingLegacyDhcp) {
             return true;
@@ -388,6 +455,8 @@
                             mLastError = TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
                             // Not much more we can do here
                         }
+                        mDhcpLeases.clear();
+                        getHandler().post(mCallback::dhcpLeasesChanged);
                     }
                 });
                 mDhcpServer = null;
diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java b/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java
new file mode 100644
index 0000000..cdd1a5d
--- /dev/null
+++ b/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2020 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.connectivity.tethering;
+
+import static android.net.TetheringManager.TETHERING_WIFI;
+
+import android.net.MacAddress;
+import android.net.TetheredClient;
+import android.net.TetheredClient.AddressInfo;
+import android.net.ip.IpServer;
+import android.net.wifi.WifiClient;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tracker for clients connected to downstreams.
+ *
+ * <p>This class is not thread safe, it is intended to be used only from the tethering handler
+ * thread.
+ */
+public class ConnectedClientsTracker {
+    private final Clock mClock;
+
+    @NonNull
+    private List<WifiClient> mLastWifiClients = Collections.emptyList();
+    @NonNull
+    private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
+
+    @VisibleForTesting
+    static class Clock {
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+
+    public ConnectedClientsTracker() {
+        this(new Clock());
+    }
+
+    @VisibleForTesting
+    ConnectedClientsTracker(Clock clock) {
+        mClock = clock;
+    }
+
+    /**
+     * Update the tracker with new connected clients.
+     *
+     * <p>The new list can be obtained through {@link #getLastTetheredClients()}.
+     * @param ipServers The IpServers used to assign addresses to clients.
+     * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last
+     *                    update.
+     * @return True if the list of clients changed since the last calculation.
+     */
+    public boolean updateConnectedClients(
+            Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) {
+        final long now = mClock.elapsedRealtime();
+
+        if (wifiClients != null) {
+            mLastWifiClients = wifiClients;
+        }
+        final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
+
+        // Build the list of non-expired leases from all IpServers, grouped by mac address
+        final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
+        for (IpServer server : ipServers) {
+            for (TetheredClient client : server.getAllLeases()) {
+                if (client.getTetheringType() == TETHERING_WIFI
+                        && !wifiClientMacs.contains(client.getMacAddress())) {
+                    // Skip leases of WiFi clients that are not (or no longer) L2-connected
+                    continue;
+                }
+                final TetheredClient prunedClient = pruneExpired(client, now);
+                if (prunedClient == null) continue; // All addresses expired
+
+                addLease(clientsMap, prunedClient);
+            }
+        }
+
+        // TODO: add IPv6 addresses from netlink
+
+        // Add connected WiFi clients that do not have any known address
+        for (MacAddress client : wifiClientMacs) {
+            if (clientsMap.containsKey(client)) continue;
+            clientsMap.put(client, new TetheredClient(
+                    client, Collections.emptyList() /* addresses */, TETHERING_WIFI));
+        }
+
+        final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
+        final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
+                || !clients.containsAll(mLastTetheredClients);
+        mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients));
+        return clientsChanged;
+    }
+
+    private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
+        final TetheredClient aggregateClient = clientsMap.getOrDefault(
+                lease.getMacAddress(), lease);
+        if (aggregateClient == lease) {
+            // This is the first lease with this mac address
+            clientsMap.put(lease.getMacAddress(), lease);
+            return;
+        }
+
+        // Only add the address info; this assumes that the tethering type is the same when the mac
+        // address is the same. If a client is connected through different tethering types with the
+        // same mac address, connected clients callbacks will report all of its addresses under only
+        // one of these tethering types. This keeps the API simple considering that such a scenario
+        // would really be a rare edge case.
+        clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease));
+    }
+
+    /**
+     * Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}.
+     *
+     * <p>The returned list is immutable.
+     */
+    @NonNull
+    public List<TetheredClient> getLastTetheredClients() {
+        return mLastTetheredClients;
+    }
+
+    private static boolean hasExpiredAddress(List<AddressInfo> addresses, long now) {
+        for (AddressInfo info : addresses) {
+            if (info.getExpirationTime() <= now) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Nullable
+    private static TetheredClient pruneExpired(TetheredClient client, long now) {
+        final List<AddressInfo> addresses = client.getAddresses();
+        if (addresses.size() == 0) return null;
+        if (!hasExpiredAddress(addresses, now)) return client;
+
+        final ArrayList<AddressInfo> newAddrs = new ArrayList<>(addresses.size() - 1);
+        for (AddressInfo info : addresses) {
+            if (info.getExpirationTime() > now) {
+                newAddrs.add(info);
+            }
+        }
+
+        if (newAddrs.size() == 0) {
+            return null;
+        }
+        return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType());
+    }
+
+    @NonNull
+    private static Set<MacAddress> getClientMacs(@NonNull List<WifiClient> clients) {
+        final Set<MacAddress> macs = new HashSet<>(clients.size());
+        for (WifiClient c : clients) {
+            macs.add(c.getMacAddress());
+        }
+        return macs;
+    }
+}
diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
index 64c16e4..6261def 100644
--- a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
+++ b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
@@ -24,6 +24,7 @@
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
 import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
 import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
@@ -79,6 +80,7 @@
 import android.net.Network;
 import android.net.NetworkInfo;
 import android.net.TetherStatesParcel;
+import android.net.TetheredClient;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringRequestParcel;
@@ -89,6 +91,7 @@
 import android.net.util.PrefixUtils;
 import android.net.util.SharedLog;
 import android.net.util.VersionedBroadcastListener;
+import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
 import android.net.wifi.p2p.WifiP2pGroup;
 import android.net.wifi.p2p.WifiP2pInfo;
@@ -128,8 +131,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
@@ -145,6 +150,10 @@
     private static final boolean DBG = false;
     private static final boolean VDBG = false;
 
+    // TODO: add the below permissions to @SystemApi
+    private static final String PERMISSION_NETWORK_SETTINGS = "android.permission.NETWORK_SETTINGS";
+    private static final String PERMISSION_NETWORK_STACK = "android.permission.NETWORK_STACK";
+
     private static final Class[] sMessageClasses = {
             Tethering.class, TetherMasterSM.class, IpServer.class
     };
@@ -176,6 +185,17 @@
         }
     }
 
+    /**
+     * Cookie added when registering {@link android.net.TetheringManager.TetheringEventCallback}.
+     */
+    private static class CallbackCookie {
+        public final boolean hasListClientsPermission;
+
+        private CallbackCookie(boolean hasListClientsPermission) {
+            this.hasListClientsPermission = hasListClientsPermission;
+        }
+    }
+
     private final SharedLog mLog = new SharedLog(TAG);
     private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks =
             new RemoteCallbackList<>();
@@ -191,7 +211,8 @@
     private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
     // TODO: Figure out how to merge this and other downstream-tracking objects
     // into a single coherent structure.
-    private final HashSet<IpServer> mForwardedDownstreams;
+    // Use LinkedHashSet for predictable ordering order for ConnectedClientsTracker.
+    private final LinkedHashSet<IpServer> mForwardedDownstreams;
     private final VersionedBroadcastListener mCarrierConfigChange;
     private final TetheringDependencies mDeps;
     private final EntitlementManager mEntitlementMgr;
@@ -200,6 +221,7 @@
     private final NetdCallback mNetdCallback;
     private final UserRestrictionActionListener mTetheringRestriction;
     private final ActiveDataSubIdListener mActiveDataSubIdListener;
+    private final ConnectedClientsTracker mConnectedClientsTracker;
     private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
     // All the usage of mTetheringEventCallback should run in the same thread.
     private ITetheringEventCallback mTetheringEventCallback = null;
@@ -234,6 +256,7 @@
         mPublicSync = new Object();
 
         mTetherStates = new ArrayMap<>();
+        mConnectedClientsTracker = new ConnectedClientsTracker();
 
         mTetherMasterSM = new TetherMasterSM("TetherMaster", mLooper, deps);
         mTetherMasterSM.start();
@@ -246,7 +269,7 @@
                 statsManager, mLog);
         mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog,
                 TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
-        mForwardedDownstreams = new HashSet<>();
+        mForwardedDownstreams = new LinkedHashSet<>();
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
@@ -291,6 +314,9 @@
 
         startStateMachineUpdaters(mHandler);
         startTrackDefaultNetwork();
+        getWifiManager().registerSoftApCallback(
+                mHandler::post /* executor */,
+                new TetheringSoftApCallback());
     }
 
     private void startStateMachineUpdaters(Handler handler) {
@@ -385,6 +411,24 @@
         }
     }
 
+    private class TetheringSoftApCallback implements WifiManager.SoftApCallback {
+        // TODO: Remove onStateChanged override when this method has default on
+        // WifiManager#SoftApCallback interface.
+        // Wifi listener for state change of the soft AP
+        @Override
+        public void onStateChanged(final int state, final int failureReason) {
+            // Nothing
+        }
+
+        // Called by wifi when the number of soft AP clients changed.
+        @Override
+        public void onConnectedClientsChanged(final List<WifiClient> clients) {
+            if (mConnectedClientsTracker.updateConnectedClients(mForwardedDownstreams, clients)) {
+                reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
+            }
+        }
+    }
+
     void interfaceStatusChanged(String iface, boolean up) {
         // Never called directly: only called from interfaceLinkStateChanged.
         // See NetlinkHandler.cpp: notifyInterfaceChanged.
@@ -1938,14 +1982,21 @@
 
     /** Register tethering event callback */
     void registerTetheringEventCallback(ITetheringEventCallback callback) {
+        final boolean hasListPermission =
+                hasCallingPermission(PERMISSION_NETWORK_SETTINGS)
+                        || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
+                        || hasCallingPermission(PERMISSION_NETWORK_STACK);
         mHandler.post(() -> {
-            mTetheringEventCallbacks.register(callback);
+            mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
             final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
             parcel.tetheringSupported = mDeps.isTetheringSupported();
             parcel.upstreamNetwork = mTetherUpstream;
             parcel.config = mConfig.toStableParcelable();
             parcel.states =
                     mTetherStatesParcel != null ? mTetherStatesParcel : emptyTetherStatesParcel();
+            parcel.tetheredClients = hasListPermission
+                    ? mConnectedClientsTracker.getLastTetheredClients()
+                    : Collections.emptyList();
             try {
                 callback.onCallbackStarted(parcel);
             } catch (RemoteException e) {
@@ -1965,6 +2016,10 @@
         return parcel;
     }
 
+    private boolean hasCallingPermission(@NonNull String permission) {
+        return mContext.checkCallingPermission(permission) == PERMISSION_GRANTED;
+    }
+
     /** Unregister tethering event callback */
     void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
         mHandler.post(() -> {
@@ -2018,6 +2073,24 @@
         }
     }
 
+    private void reportTetherClientsChanged(List<TetheredClient> clients) {
+        final int length = mTetheringEventCallbacks.beginBroadcast();
+        try {
+            for (int i = 0; i < length; i++) {
+                try {
+                    final CallbackCookie cookie =
+                            (CallbackCookie) mTetheringEventCallbacks.getBroadcastCookie(i);
+                    if (!cookie.hasListClientsPermission) continue;
+                    mTetheringEventCallbacks.getBroadcastItem(i).onTetherClientsChanged(clients);
+                } catch (RemoteException e) {
+                    // Not really very much to do here.
+                }
+            }
+        } finally {
+            mTetheringEventCallbacks.finishBroadcast();
+        }
+    }
+
     void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
         // Binder.java closes the resource for us.
         @SuppressWarnings("resource")
@@ -2109,6 +2182,14 @@
             public void updateLinkProperties(IpServer who, LinkProperties newLp) {
                 notifyLinkPropertiesChanged(who, newLp);
             }
+
+            @Override
+            public void dhcpLeasesChanged() {
+                if (mConnectedClientsTracker.updateConnectedClients(
+                        mForwardedDownstreams, null /* wifiClients */)) {
+                    reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
+                }
+            }
         };
     }
 
diff --git a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index f29ad78..acedfab 100644
--- a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -469,7 +469,8 @@
 
     private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception {
         verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any());
-        verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).start(any());
+        verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
         final DhcpServingParamsParcel params = mDhcpParamsCaptor.getValue();
         // Last address byte is random
         assertTrue(expectedPrefix.contains(intToInet4AddressHTH(params.serverAddr)));
diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt
new file mode 100644
index 0000000..56f3e21
--- /dev/null
+++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 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.connectivity.tethering
+
+import android.net.LinkAddress
+import android.net.MacAddress
+import android.net.TetheredClient
+import android.net.TetheredClient.AddressInfo
+import android.net.TetheringManager.TETHERING_USB
+import android.net.TetheringManager.TETHERING_WIFI
+import android.net.ip.IpServer
+import android.net.wifi.WifiClient
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ConnectedClientsTrackerTest {
+
+    private val server1 = mock(IpServer::class.java)
+    private val server2 = mock(IpServer::class.java)
+    private val servers = listOf(server1, server2)
+
+    private val clock = TestClock(1324L)
+
+    private val client1Addr = MacAddress.fromString("01:23:45:67:89:0A")
+    private val client1 = TetheredClient(client1Addr, listOf(
+            AddressInfo(LinkAddress("192.168.43.44/32"), null /* hostname */, clock.time + 20)),
+            TETHERING_WIFI)
+    private val wifiClient1 = makeWifiClient(client1Addr)
+    private val client2Addr = MacAddress.fromString("02:34:56:78:90:AB")
+    private val client2Exp30AddrInfo = AddressInfo(
+            LinkAddress("192.168.43.45/32"), "my_hostname", clock.time + 30)
+    private val client2 = TetheredClient(client2Addr, listOf(
+            client2Exp30AddrInfo,
+            AddressInfo(LinkAddress("2001:db8:12::34/72"), "other_hostname", clock.time + 10)),
+            TETHERING_WIFI)
+    private val wifiClient2 = makeWifiClient(client2Addr)
+    private val client3Addr = MacAddress.fromString("03:45:67:89:0A:BC")
+    private val client3 = TetheredClient(client3Addr,
+            listOf(AddressInfo(LinkAddress("2001:db8:34::34/72"), "other_other_hostname",
+                    clock.time + 10)),
+            TETHERING_USB)
+
+    @Test
+    fun testUpdateConnectedClients() {
+        doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+        doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+
+        val tracker = ConnectedClientsTracker(clock)
+        assertFalse(tracker.updateConnectedClients(servers, null))
+
+        // Obtain a lease for client 1
+        doReturn(listOf(client1)).`when`(server1).allLeases
+        assertSameClients(listOf(client1), assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+        // Client 2 L2-connected, no lease yet
+        val client2WithoutAddr = TetheredClient(client2Addr, emptyList(), TETHERING_WIFI)
+        assertSameClients(listOf(client1, client2WithoutAddr),
+                assertNewClients(tracker, servers, listOf(wifiClient1, wifiClient2)))
+
+        // Client 2 lease obtained
+        doReturn(listOf(client1, client2)).`when`(server1).allLeases
+        assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers, null))
+
+        // Client 3 lease obtained
+        doReturn(listOf(client3)).`when`(server2).allLeases
+        assertSameClients(listOf(client1, client2, client3),
+                assertNewClients(tracker, servers, null))
+
+        // Client 2 L2-disconnected
+        assertSameClients(listOf(client1, client3),
+                assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+        // Client 1 L2-disconnected
+        assertSameClients(listOf(client3), assertNewClients(tracker, servers, emptyList()))
+
+        // Client 1 comes back
+        assertSameClients(listOf(client1, client3),
+                assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+        // Leases lost, client 1 still L2-connected
+        doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+        doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+        assertSameClients(listOf(TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)),
+                assertNewClients(tracker, servers, null))
+    }
+
+    @Test
+    fun testUpdateConnectedClients_LeaseExpiration() {
+        val tracker = ConnectedClientsTracker(clock)
+        doReturn(listOf(client1, client2)).`when`(server1).allLeases
+        doReturn(listOf(client3)).`when`(server2).allLeases
+        assertSameClients(listOf(client1, client2, client3), assertNewClients(
+                tracker, servers, listOf(wifiClient1, wifiClient2)))
+
+        clock.time += 20
+        // Client 3 has no remaining lease: removed
+        val expectedClients = listOf(
+                // Client 1 has no remaining lease but is L2-connected
+                TetheredClient(client1Addr, emptyList(), TETHERING_WIFI),
+                // Client 2 has some expired leases
+                TetheredClient(
+                        client2Addr,
+                        // Only the "t + 30" address is left, the "t + 10" address expired
+                        listOf(client2Exp30AddrInfo),
+                        TETHERING_WIFI))
+        assertSameClients(expectedClients, assertNewClients(tracker, servers, null))
+    }
+
+    private fun assertNewClients(
+        tracker: ConnectedClientsTracker,
+        ipServers: Iterable<IpServer>,
+        wifiClients: List<WifiClient>?
+    ): List<TetheredClient> {
+        assertTrue(tracker.updateConnectedClients(ipServers, wifiClients))
+        return tracker.lastTetheredClients
+    }
+
+    private fun assertSameClients(expected: List<TetheredClient>, actual: List<TetheredClient>) {
+        val expectedSet = HashSet(expected)
+        assertEquals(expected.size, expectedSet.size)
+        assertEquals(expectedSet, HashSet(actual))
+    }
+
+    private fun makeWifiClient(macAddr: MacAddress): WifiClient {
+        // Use a mock WifiClient as the constructor is not part of the WiFi module exported API.
+        return mock(WifiClient::class.java).apply { doReturn(macAddr).`when`(this).macAddress }
+    }
+
+    private class TestClock(var time: Long) : ConnectedClientsTracker.Clock() {
+        override fun elapsedRealtime(): Long {
+            return time
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
index 6d49e20..8e5aaf2 100644
--- a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
+++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
@@ -88,6 +88,7 @@
 import android.net.NetworkRequest;
 import android.net.RouteInfo;
 import android.net.TetherStatesParcel;
+import android.net.TetheredClient;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringRequestParcel;
@@ -142,6 +143,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Vector;
 
 @RunWith(AndroidJUnit4.class)
@@ -470,6 +472,7 @@
                 ArgumentCaptor.forClass(PhoneStateListener.class);
         verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
                 eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE));
+        verify(mWifiManager).registerSoftApCallback(any(), any());
         mPhoneStateListener = phoneListenerCaptor.getValue();
     }
 
@@ -728,7 +731,8 @@
 
         sendIPv6TetherUpdates(upstreamState);
         verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull());
-        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
     }
 
     @Test
@@ -764,7 +768,8 @@
         verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
         verify(mRouterAdvertisementDaemon, times(1)).start();
-        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
 
         sendIPv6TetherUpdates(upstreamState);
         verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
@@ -778,7 +783,8 @@
 
         verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
         verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
 
@@ -794,7 +800,8 @@
         runUsbTethering(upstreamState);
 
         verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
 
         // Then 464xlat comes up
@@ -817,7 +824,8 @@
         verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
         // DHCP not restarted on downstream (still times(1))
-        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
     }
 
     @Test
@@ -847,7 +855,8 @@
     public void workingNcmTethering() throws Exception {
         runNcmTethering();
 
-        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
     }
 
     @Test
@@ -1171,6 +1180,11 @@
         }
 
         @Override
+        public void onTetherClientsChanged(List<TetheredClient> clients) {
+            // TODO: check this
+        }
+
+        @Override
         public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
             mActualUpstreams.add(parcel.upstreamNetwork);
             mTetheringConfigs.add(parcel.config);
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 60a30d3..0f36260 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -3817,8 +3817,9 @@
         return avoidBadWifi();
     }
 
-
     private void rematchForAvoidBadWifiUpdate() {
+        ensureRunningOnConnectivityServiceThread();
+        mixInAllNetworkScores();
         rematchAllNetworksAndRequests();
         for (NetworkAgentInfo nai: mNetworkAgentInfos.values()) {
             if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
@@ -7035,9 +7036,45 @@
         }
     }
 
+    /**
+     * Re-mixin all network scores.
+     * This is called when some global setting like avoidBadWifi has changed.
+     * TODO : remove this when all usages have been removed.
+     */
+    private void mixInAllNetworkScores() {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+            nai.setNetworkScore(mixInNetworkScore(nai, nai.getNetworkScore()));
+        }
+    }
+
+    /**
+     * Mix in the Connectivity-managed parts of the NetworkScore.
+     * @param nai The NAI this score applies to.
+     * @param sourceScore the score sent by the network agent, or the previous score of this NAI.
+     * @return A new score with the Connectivity-managed parts mixed in.
+     */
+    @NonNull
+    private NetworkScore mixInNetworkScore(@NonNull final NetworkAgentInfo nai,
+            @NonNull final NetworkScore sourceScore) {
+        final NetworkScore.Builder score = new NetworkScore.Builder(sourceScore);
+
+        // TODO : this should be done in Telephony. It should be handled per-network because
+        // it's a carrier-dependent config.
+        if (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+            if (mMultinetworkPolicyTracker.getAvoidBadWifi()) {
+                score.clearPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI);
+            } else {
+                score.addPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI);
+            }
+        }
+
+        return score.build();
+    }
+
     private void updateNetworkScore(NetworkAgentInfo nai, NetworkScore ns) {
         if (VDBG || DDBG) log("updateNetworkScore for " + nai.toShortString() + " to " + ns);
-        nai.setNetworkScore(ns);
+        nai.setNetworkScore(mixInNetworkScore(nai, ns));
         rematchAllNetworksAndRequests();
         sendUpdatedScoreToFactories(nai);
     }
@@ -7519,6 +7556,13 @@
      */
     public int getConnectionOwnerUid(ConnectionInfo connectionInfo) {
         final Vpn vpn = enforceActiveVpnOrNetworkStackPermission();
+
+        // Only VpnService based VPNs should be able to get this information.
+        if (vpn != null && vpn.getActiveAppVpnType() != VpnManager.TYPE_VPN_SERVICE) {
+            throw new SecurityException(
+                    "getConnectionOwnerUid() not allowed for non-VpnService VPNs");
+        }
+
         if (connectionInfo.protocol != IPPROTO_TCP && connectionInfo.protocol != IPPROTO_UDP) {
             throw new IllegalArgumentException("Unsupported protocol " + connectionInfo.protocol);
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1da07ef..5df3e1f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4866,7 +4866,7 @@
     }
 
     @GuardedBy("this")
-    private final boolean attachApplicationLocked(IApplicationThread thread,
+    private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
             int pid, int callingUid, long startSeq) {
 
         // Find the application record that is being attached...  either via
@@ -5287,6 +5287,9 @@
 
     @Override
     public final void attachApplication(IApplicationThread thread, long startSeq) {
+        if (thread == null) {
+            throw new SecurityException("Invalid application interface");
+        }
         synchronized (this) {
             int callingPid = Binder.getCallingPid();
             final int callingUid = Binder.getCallingUid();
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index af47430..821653a 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -235,8 +235,8 @@
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        checkCompatChangeReadAndLogPermission();
         if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, "platform_compat", pw)) return;
+        checkCompatChangeReadAndLogPermission();
         mCompatConfig.dumpConfig(pw);
     }
 
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 1a68f1b..77f4093 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -794,10 +794,10 @@
                     // ignore
                 }
                 mContext.unbindService(mConnection);
-                mConnection = null;
+                cleanupVpnStateLocked();
             } else if (mVpnRunner != null) {
+                // cleanupVpnStateLocked() is called from mVpnRunner.exit()
                 mVpnRunner.exit();
-                mVpnRunner = null;
             }
 
             try {
@@ -1108,7 +1108,6 @@
      */
     public synchronized ParcelFileDescriptor establish(VpnConfig config) {
         // Check if the caller is already prepared.
-        UserManager mgr = UserManager.get(mContext);
         if (Binder.getCallingUid() != mOwnerUID) {
             return null;
         }
@@ -1122,10 +1121,7 @@
         long token = Binder.clearCallingIdentity();
         try {
             // Restricted users are not allowed to create VPNs, they are tied to Owner
-            UserInfo user = mgr.getUserInfo(mUserHandle);
-            if (user.isRestricted()) {
-                throw new SecurityException("Restricted users cannot establish VPNs");
-            }
+            enforceNotRestrictedUser();
 
             ResolveInfo info = AppGlobals.getPackageManager().resolveService(intent,
                     null, 0, mUserHandle);
@@ -1547,24 +1543,30 @@
         public void interfaceRemoved(String interfaze) {
             synchronized (Vpn.this) {
                 if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
-                    mStatusIntent = null;
-                    mNetworkCapabilities.setUids(null);
-                    mConfig = null;
-                    mInterface = null;
                     if (mConnection != null) {
                         mContext.unbindService(mConnection);
-                        mConnection = null;
-                        agentDisconnect();
+                        cleanupVpnStateLocked();
                     } else if (mVpnRunner != null) {
-                        // agentDisconnect must be called from mVpnRunner.exit()
+                        // cleanupVpnStateLocked() is called from mVpnRunner.exit()
                         mVpnRunner.exit();
-                        mVpnRunner = null;
                     }
                 }
             }
         }
     };
 
+    private void cleanupVpnStateLocked() {
+        mStatusIntent = null;
+        mNetworkCapabilities.setUids(null);
+        mConfig = null;
+        mInterface = null;
+
+        // Unconditionally clear both VpnService and VpnRunner fields.
+        mVpnRunner = null;
+        mConnection = null;
+        agentDisconnect();
+    }
+
     private void enforceControlPermission() {
         mContext.enforceCallingPermission(Manifest.permission.CONTROL_VPN, "Unauthorized Caller");
     }
@@ -1677,6 +1679,25 @@
     }
 
     /**
+     * Gets the currently running App-based VPN type
+     *
+     * @return the {@link VpnManager.VpnType}. {@link VpnManager.TYPE_VPN_NONE} if not running an
+     *     app-based VPN. While VpnService-based VPNs are always app VPNs and LegacyVpn is always
+     *     Settings-based, the Platform VPNs can be initiated by both apps and Settings.
+     */
+    public synchronized int getActiveAppVpnType() {
+        if (VpnConfig.LEGACY_VPN.equals(mPackage)) {
+            return VpnManager.TYPE_VPN_NONE;
+        }
+
+        if (mVpnRunner != null && mVpnRunner instanceof IkeV2VpnRunner) {
+            return VpnManager.TYPE_VPN_PLATFORM;
+        } else {
+            return VpnManager.TYPE_VPN_SERVICE;
+        }
+    }
+
+    /**
      * @param uid The target uid.
      *
      * @return {@code true} if {@code uid} is included in one of the mBlockedUidsAsToldToNetd
@@ -1804,6 +1825,17 @@
         throw new IllegalStateException("Unable to find IPv4 default gateway");
     }
 
+    private void enforceNotRestrictedUser() {
+        Binder.withCleanCallingIdentity(() -> {
+            final UserManager mgr = UserManager.get(mContext);
+            final UserInfo user = mgr.getUserInfo(mUserHandle);
+
+            if (user.isRestricted()) {
+                throw new SecurityException("Restricted users cannot configure VPNs");
+            }
+        });
+    }
+
     /**
      * Start legacy VPN, controlling native daemons as needed. Creates a
      * secondary thread to perform connection work, returning quickly.
@@ -2024,7 +2056,25 @@
 
         public abstract void run();
 
-        protected abstract void exit();
+        /**
+         * Disconnects the NetworkAgent and cleans up all state related to the VpnRunner.
+         *
+         * <p>All outer Vpn instance state is cleaned up in cleanupVpnStateLocked()
+         */
+        protected abstract void exitVpnRunner();
+
+        /**
+         * Triggers the cleanup of the VpnRunner, and additionally cleans up Vpn instance-wide state
+         *
+         * <p>This method ensures that simple calls to exit() will always clean up global state
+         * properly.
+         */
+        protected final void exit() {
+            synchronized (Vpn.this) {
+                exitVpnRunner();
+                cleanupVpnStateLocked();
+            }
+        }
     }
 
     interface IkeV2VpnRunnerCallback {
@@ -2354,17 +2404,6 @@
         }
 
         /**
-         * Triggers cleanup of outer class' state
-         *
-         * <p>Can be called from any thread, as it does not mutate state in the Ikev2VpnRunner.
-         */
-        private void cleanupVpnState() {
-            synchronized (Vpn.this) {
-                agentDisconnect();
-            }
-        }
-
-        /**
          * Cleans up all Ikev2VpnRunner internal state
          *
          * <p>This method MUST always be called on the mExecutor thread in order to ensure
@@ -2383,10 +2422,7 @@
         }
 
         @Override
-        public void exit() {
-            // Cleanup outer class' state immediately, otherwise race conditions may ensue.
-            cleanupVpnState();
-
+        public void exitVpnRunner() {
             mExecutor.execute(() -> {
                 shutdownVpnRunner();
             });
@@ -2485,10 +2521,9 @@
 
         /** Tears down this LegacyVpn connection */
         @Override
-        public void exit() {
+        public void exitVpnRunner() {
             // We assume that everything is reset after stopping the daemons.
             interrupt();
-            agentDisconnect();
             try {
                 mContext.unregisterReceiver(mBroadcastReceiver);
             } catch (IllegalArgumentException e) {}
@@ -2761,6 +2796,7 @@
         checkNotNull(keyStore, "KeyStore missing");
 
         verifyCallingUidAndPackage(packageName);
+        enforceNotRestrictedUser();
 
         final byte[] encodedProfile = profile.encode();
         if (encodedProfile.length > MAX_VPN_PROFILE_SIZE_BYTES) {
@@ -2796,6 +2832,7 @@
         checkNotNull(keyStore, "KeyStore missing");
 
         verifyCallingUidAndPackage(packageName);
+        enforceNotRestrictedUser();
 
         Binder.withCleanCallingIdentity(
                 () -> {
@@ -2838,6 +2875,8 @@
         checkNotNull(packageName, "No package name provided");
         checkNotNull(keyStore, "KeyStore missing");
 
+        enforceNotRestrictedUser();
+
         // Prepare VPN for startup
         if (!prepare(packageName, null /* newPackage */, VpnManager.TYPE_VPN_PLATFORM)) {
             throw new SecurityException("User consent not granted for package " + packageName);
@@ -2903,6 +2942,8 @@
     public synchronized void stopVpnProfile(@NonNull String packageName) {
         checkNotNull(packageName, "No package name provided");
 
+        enforceNotRestrictedUser();
+
         // To stop the VPN profile, the caller must be the current prepared package and must be
         // running an Ikev2VpnProfile.
         if (!isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner) {
diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
index 8379614..33e01bd 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
@@ -166,6 +166,20 @@
         }
     }
 
+    @Nullable
+    MediaRoute2Info getSelectedRoute() {
+        return (mSelectedRoute == null) ? null : mSelectedRoute.route;
+    }
+
+    @NonNull
+    List<MediaRoute2Info> getTransferableRoutes() {
+        List<MediaRoute2Info> routes = getAllBluetoothRoutes();
+        if (mSelectedRoute != null) {
+            routes.remove(mSelectedRoute.route);
+        }
+        return routes;
+    }
+
     @NonNull
     List<MediaRoute2Info> getAllBluetoothRoutes() {
         ArrayList<MediaRoute2Info> routes = new ArrayList<>();
@@ -175,9 +189,12 @@
         return routes;
     }
 
-    @Nullable
-    String getSelectedRouteId() {
-        return mSelectedRoute == null ? null : mSelectedRoute.route.getId();
+    boolean setSelectedRouteVolume(int volume) {
+        if (mSelectedRoute == null) return false;
+        mSelectedRoute.route = new MediaRoute2Info.Builder(mSelectedRoute.route)
+                .setVolume(volume)
+                .build();
+        return true;
     }
 
     private void notifyBluetoothRoutesUpdated() {
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
index efb9515..fe118e5 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
@@ -16,6 +16,7 @@
 
 package com.android.server.media;
 
+import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -171,7 +172,7 @@
     };
 
     public interface Callback {
-        void onAddProviderService(MediaRoute2ProviderServiceProxy proxy);
-        void onRemoveProviderService(MediaRoute2ProviderServiceProxy proxy);
+        void onAddProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy);
+        void onRemoveProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy);
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index a268720..3588916 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -87,6 +87,11 @@
         mContext = context;
     }
 
+    ////////////////////////////////////////////////////////////////
+    ////  Calls from MediaRouter2
+    ////   - Should not have @NonNull/@Nullable on any arguments
+    ////////////////////////////////////////////////////////////////
+
     @NonNull
     public List<MediaRoute2Info> getSystemRoutes() {
         final int uid = Binder.getCallingUid();
@@ -135,9 +140,11 @@
         }
     }
 
-    public void registerRouter2(@NonNull IMediaRouter2 router,
-            @NonNull String packageName) {
+    public void registerRouter2(IMediaRouter2 router, String packageName) {
         Objects.requireNonNull(router, "router must not be null");
+        if (TextUtils.isEmpty(packageName)) {
+            throw new IllegalArgumentException("packageName must not be empty");
+        }
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -155,7 +162,7 @@
         }
     }
 
-    public void unregisterRouter2(@NonNull IMediaRouter2 router) {
+    public void unregisterRouter2(IMediaRouter2 router) {
         Objects.requireNonNull(router, "router must not be null");
 
         final long token = Binder.clearCallingIdentity();
@@ -168,33 +175,35 @@
         }
     }
 
-    public void registerManager(@NonNull IMediaRouter2Manager manager,
-            @NonNull String packageName) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        //TODO: should check permission
-        final boolean trusted = true;
-
-        final int uid = Binder.getCallingUid();
-        final int pid = Binder.getCallingPid();
-        final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+    public void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
+            RouteDiscoveryPreference preference) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(preference, "preference must not be null");
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                registerManagerLocked(manager, uid, pid, packageName, userId, trusted);
+                RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder());
+                if (routerRecord == null) {
+                    Slog.w(TAG, "Ignoring updating discoveryRequest of null routerRecord.");
+                    return;
+                }
+                setDiscoveryRequestWithRouter2Locked(routerRecord, preference);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
     }
 
-    public void unregisterManager(@NonNull IMediaRouter2Manager manager) {
-        Objects.requireNonNull(manager, "manager must not be null");
+    public void setRouteVolumeWithRouter2(IMediaRouter2 router,
+            MediaRoute2Info route, int volume) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(route, "route must not be null");
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                unregisterManagerLocked(manager, false);
+                setRouteVolumeWithRouter2Locked(router, route, volume);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -209,7 +218,7 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                requestCreateSessionLocked(router, route, requestId, sessionHints);
+                requestCreateSessionWithRouter2Locked(router, route, requestId, sessionHints);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -227,7 +236,7 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                selectRouteLocked(router, uniqueSessionId, route);
+                selectRouteWithRouter2Locked(router, uniqueSessionId, route);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -246,7 +255,7 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                deselectRouteLocked(router, uniqueSessionId, route);
+                deselectRouteWithRouter2Locked(router, uniqueSessionId, route);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -271,6 +280,21 @@
         }
     }
 
+    public void setSessionVolumeWithRouter2(IMediaRouter2 router, String uniqueSessionId,
+            int volume) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(uniqueSessionId, "uniqueSessionId must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                setSessionVolumeWithRouter2Locked(router, uniqueSessionId, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     public void releaseSessionWithRouter2(IMediaRouter2 router, String uniqueSessionId) {
         Objects.requireNonNull(router, "router must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
@@ -280,64 +304,60 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                releaseSessionLocked(router, uniqueSessionId);
+                releaseSessionWithRouter2Locked(router, uniqueSessionId);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
     }
 
-    public void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
-            RouteDiscoveryPreference preference) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(preference, "preference must not be null");
+    ////////////////////////////////////////////////////////////////
+    ////  Calls from MediaRouter2Manager
+    ////   - Should not have @NonNull/@Nullable on any arguments
+    ////////////////////////////////////////////////////////////////
 
+    @NonNull
+    public List<RoutingSessionInfo> getActiveSessions(IMediaRouter2Manager manager) {
+        Objects.requireNonNull(manager, "manager must not be null");
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder());
-                setDiscoveryRequestLocked(routerRecord, preference);
+                return getActiveSessionsLocked(manager);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
     }
 
-    public void setRouteVolumeWithRouter2(IMediaRouter2 router,
-            MediaRoute2Info route, int volume) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(route, "route must not be null");
+    public void registerManager(IMediaRouter2Manager manager, String packageName) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(packageName)) {
+            throw new IllegalArgumentException("packageName must not be empty");
+        }
+
+        final boolean trusted = true;
+
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                setRouteVolumeLocked(router, route, volume);
+                registerManagerLocked(manager, uid, pid, packageName, userId, trusted);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
     }
 
-    public void setSessionVolumeWithRouter2(IMediaRouter2 router, String sessionId, int volume) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(sessionId, "sessionId must not be null");
+    public void unregisterManager(IMediaRouter2Manager manager) {
+        Objects.requireNonNull(manager, "manager must not be null");
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                setSessionVolumeLocked(router, sessionId, volume);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void requestCreateSessionWithManager(IMediaRouter2Manager manager, String packageName,
-            MediaRoute2Info route, int requestId) {
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                requestClientCreateSessionLocked(manager, packageName, route, requestId);
+                unregisterManagerLocked(manager, false);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -352,7 +372,78 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                setRouteVolumeLocked(manager, route, volume);
+                setRouteVolumeWithManagerLocked(manager, route, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void requestCreateSessionWithManager(IMediaRouter2Manager manager, String packageName,
+            MediaRoute2Info route, int requestId) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(packageName)) {
+            throw new IllegalArgumentException("packageName must not be empty");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                requestCreateSessionWithManagerLocked(manager, packageName, route, requestId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void selectRouteWithManager(IMediaRouter2Manager manager, String uniqueSessionId,
+            MediaRoute2Info route) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                selectRouteWithManagerLocked(manager, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void deselectRouteWithManager(IMediaRouter2Manager manager, String uniqueSessionId,
+            MediaRoute2Info route) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                deselectRouteWithManagerLocked(manager, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void transferToRouteWithManager(IMediaRouter2Manager manager, String uniqueSessionId,
+            MediaRoute2Info route) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                transferToRouteWithManagerLocked(manager, uniqueSessionId, route);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -360,73 +451,32 @@
     }
 
     public void setSessionVolumeWithManager(IMediaRouter2Manager manager,
-            String sessionId, int volume) {
+            String uniqueSessionId, int volume) {
         Objects.requireNonNull(manager, "manager must not be null");
-        Objects.requireNonNull(sessionId, "sessionId must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                setSessionVolumeLocked(manager, sessionId, volume);
+                setSessionVolumeWithManagerLocked(manager, uniqueSessionId, volume);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
     }
 
-    @NonNull
-    public List<RoutingSessionInfo> getActiveSessions(IMediaRouter2Manager manager) {
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                return getActiveSessionsLocked(manager);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
+    public void releaseSessionWithManager(IMediaRouter2Manager manager, String uniqueSessionId) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
         }
-    }
 
-    public void selectRouteWithManager(IMediaRouter2Manager manager, String sessionId,
-            MediaRoute2Info route) {
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                selectRouteWithManagerLocked(manager, sessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void deselectRouteWithManager(IMediaRouter2Manager manager, String sessionId,
-            MediaRoute2Info route) {
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                deselectRouteWithManagerLocked(manager, sessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void transferToRouteWithManager(IMediaRouter2Manager manager, String sessionId,
-            MediaRoute2Info route) {
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                transferToRouteWithManagerLocked(manager, sessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void releaseSessionWithManager(IMediaRouter2Manager manager, String sessionId) {
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                releaseSessionWithManagerLocked(manager, sessionId);
+                releaseSessionWithManagerLocked(manager, uniqueSessionId);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -457,247 +507,180 @@
         }
     }
 
-    void routerDied(RouterRecord routerRecord) {
+    void routerDied(@NonNull RouterRecord routerRecord) {
         synchronized (mLock) {
             unregisterRouter2Locked(routerRecord.mRouter, true);
         }
     }
 
-    void managerDied(ManagerRecord managerRecord) {
+    void managerDied(@NonNull ManagerRecord managerRecord) {
         synchronized (mLock) {
             unregisterManagerLocked(managerRecord.mManager, true);
         }
     }
 
-    private void registerRouter2Locked(IMediaRouter2 router,
-            int uid, int pid, String packageName, int userId, boolean trusted) {
+    ////////////////////////////////////////////////////////////////
+    ////  ***Locked methods related to MediaRouter2
+    ////   - Should have @NonNull/@Nullable on all arguments
+    ////////////////////////////////////////////////////////////////
+
+    private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid,
+            @NonNull String packageName, int userId, boolean trusted) {
         final IBinder binder = router.asBinder();
-        if (mAllRouterRecords.get(binder) == null) {
-
-            UserRecord userRecord = getOrCreateUserRecordLocked(userId);
-            RouterRecord routerRecord = new RouterRecord(userRecord, router, uid, pid,
-                    packageName, trusted);
-            try {
-                binder.linkToDeath(routerRecord, 0);
-            } catch (RemoteException ex) {
-                throw new RuntimeException("NediaRouter2 died prematurely.", ex);
-            }
-
-            userRecord.mRouterRecords.add(routerRecord);
-            mAllRouterRecords.put(binder, routerRecord);
-
-            userRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::notifyRoutesToRouter, userRecord.mHandler, router));
+        if (mAllRouterRecords.get(binder) != null) {
+            Slog.w(TAG, "Same router already exists. packageName=" + packageName);
+            return;
         }
+
+        UserRecord userRecord = getOrCreateUserRecordLocked(userId);
+        RouterRecord routerRecord = new RouterRecord(
+                userRecord, router, uid, pid, packageName, trusted);
+        try {
+            binder.linkToDeath(routerRecord, 0);
+        } catch (RemoteException ex) {
+            throw new RuntimeException("MediaRouter2 died prematurely.", ex);
+        }
+
+        userRecord.mRouterRecords.add(routerRecord);
+        mAllRouterRecords.put(binder, routerRecord);
+
+        userRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::notifyRoutesToRouter, userRecord.mHandler, router));
     }
 
-    private void unregisterRouter2Locked(IMediaRouter2 router, boolean died) {
+    private void unregisterRouter2Locked(@NonNull IMediaRouter2 router, boolean died) {
         RouterRecord routerRecord = mAllRouterRecords.remove(router.asBinder());
-        if (routerRecord != null) {
-            UserRecord userRecord = routerRecord.mUserRecord;
-            userRecord.mRouterRecords.remove(routerRecord);
-            //TODO: update discovery request
-            routerRecord.dispose();
-            disposeUserIfNeededLocked(userRecord); // since router removed from user
+        if (routerRecord == null) {
+            Slog.w(TAG, "Ignoring unregistering unknown router2");
+            return;
         }
+
+        UserRecord userRecord = routerRecord.mUserRecord;
+        userRecord.mRouterRecords.remove(routerRecord);
+        //TODO: update discovery request
+        routerRecord.dispose();
+        disposeUserIfNeededLocked(userRecord); // since router removed from user
     }
 
-    private void requestCreateSessionLocked(@NonNull IMediaRouter2 router,
-            @NonNull MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
-        final IBinder binder = router.asBinder();
-        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
-        // router id is not assigned yet
-        if (toRouterOrManagerId(requestId) == 0) {
-            requestId = toUniqueRequestId(routerRecord.mRouterId, toOriginalRequestId(requestId));
+    private void setDiscoveryRequestWithRouter2Locked(@NonNull RouterRecord routerRecord,
+            @NonNull RouteDiscoveryPreference discoveryRequest) {
+        if (routerRecord.mDiscoveryPreference.equals(discoveryRequest)) {
+            return;
         }
-
-        if (routerRecord != null) {
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::requestCreateSessionOnHandler,
-                            routerRecord.mUserRecord.mHandler,
-                            routerRecord, route, requestId, sessionHints));
-        }
+        routerRecord.mDiscoveryPreference = discoveryRequest;
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
+                        routerRecord.mUserRecord.mHandler, routerRecord));
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::updateDiscoveryPreference,
+                        routerRecord.mUserRecord.mHandler));
     }
 
-    private void selectRouteLocked(@NonNull IMediaRouter2 router, String uniqueSessionId,
-            @NonNull MediaRoute2Info route) {
-        final IBinder binder = router.asBinder();
-        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
-        if (routerRecord != null) {
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::selectRouteOnHandler,
-                            routerRecord.mUserRecord.mHandler,
-                            routerRecord, uniqueSessionId, route));
-        }
-    }
-
-    private void deselectRouteLocked(@NonNull IMediaRouter2 router, String uniqueSessionId,
-            @NonNull MediaRoute2Info route) {
-        final IBinder binder = router.asBinder();
-        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
-        if (routerRecord != null) {
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::deselectRouteOnHandler,
-                            routerRecord.mUserRecord.mHandler,
-                            routerRecord, uniqueSessionId, route));
-        }
-    }
-
-    private void transferToRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
-            String uniqueSessionId, @NonNull MediaRoute2Info route) {
-        final IBinder binder = router.asBinder();
-        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
-        if (routerRecord != null) {
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::transferToRouteOnHandler,
-                            routerRecord.mUserRecord.mHandler,
-                            routerRecord, uniqueSessionId, route));
-        }
-    }
-
-    private void releaseSessionLocked(@NonNull IMediaRouter2 router, String uniqueSessionId) {
-        final IBinder binder = router.asBinder();
-        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
-        if (routerRecord != null) {
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::releaseSessionOnHandler,
-                            routerRecord.mUserRecord.mHandler,
-                            routerRecord, uniqueSessionId));
-        }
-    }
-
-    private void setDiscoveryRequestLocked(RouterRecord routerRecord,
-            RouteDiscoveryPreference discoveryRequest) {
-        if (routerRecord != null) {
-            if (routerRecord.mDiscoveryPreference.equals(discoveryRequest)) {
-                return;
-            }
-
-            routerRecord.mDiscoveryPreference = discoveryRequest;
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
-                            routerRecord.mUserRecord.mHandler, routerRecord));
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::updateDiscoveryPreference,
-                            routerRecord.mUserRecord.mHandler));
-        }
-    }
-
-    private void setRouteVolumeLocked(IMediaRouter2 router, MediaRoute2Info route,
-            int volume) {
+    private void setRouteVolumeWithRouter2Locked(@NonNull IMediaRouter2 router,
+            @NonNull MediaRoute2Info route, int volume) {
         final IBinder binder = router.asBinder();
         RouterRecord routerRecord = mAllRouterRecords.get(binder);
 
         if (routerRecord != null) {
             routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::setRouteVolume,
+                    obtainMessage(UserHandler::setRouteVolumeOnHandler,
                             routerRecord.mUserRecord.mHandler, route, volume));
         }
     }
 
-    private void setSessionVolumeLocked(IMediaRouter2 router, String sessionId,
-            int volume) {
+    private void requestCreateSessionWithRouter2Locked(@NonNull IMediaRouter2 router,
+            @NonNull MediaRoute2Info route, int requestId, @Nullable Bundle sessionHints) {
+        final IBinder binder = router.asBinder();
+        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+        if (routerRecord == null) {
+            return;
+        }
+
+        long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::requestCreateSessionOnHandler,
+                        routerRecord.mUserRecord.mHandler,
+                        routerRecord, route, uniqueRequestId, sessionHints));
+    }
+
+    private void selectRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        final IBinder binder = router.asBinder();
+        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+        if (routerRecord == null) {
+            return;
+        }
+
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::selectRouteOnHandler,
+                        routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+    }
+
+    private void deselectRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        final IBinder binder = router.asBinder();
+        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+        if (routerRecord == null) {
+            return;
+        }
+
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::deselectRouteOnHandler,
+                        routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+    }
+
+    private void transferToRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        final IBinder binder = router.asBinder();
+        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+        if (routerRecord == null) {
+            return;
+        }
+
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::transferToRouteOnHandler,
+                        routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+    }
+
+    private void setSessionVolumeWithRouter2Locked(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, int volume) {
         final IBinder binder = router.asBinder();
         RouterRecord routerRecord = mAllRouterRecords.get(binder);
 
-        if (routerRecord != null) {
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::setSessionVolume,
-                            routerRecord.mUserRecord.mHandler, sessionId, volume));
+        if (routerRecord == null) {
+            return;
         }
+
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::setSessionVolumeOnHandler,
+                        routerRecord.mUserRecord.mHandler, uniqueSessionId, volume));
     }
 
-    private void registerManagerLocked(IMediaRouter2Manager manager,
-            int uid, int pid, String packageName, int userId, boolean trusted) {
-        final IBinder binder = manager.asBinder();
-        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-        if (managerRecord == null) {
-            UserRecord userRecord = getOrCreateUserRecordLocked(userId);
-            managerRecord = new ManagerRecord(userRecord, manager, uid, pid, packageName, trusted);
-            try {
-                binder.linkToDeath(managerRecord, 0);
-            } catch (RemoteException ex) {
-                throw new RuntimeException("Media router manager died prematurely.", ex);
-            }
+    private void releaseSessionWithRouter2Locked(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId) {
+        final IBinder binder = router.asBinder();
+        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
 
-            userRecord.mManagerRecords.add(managerRecord);
-            mAllManagerRecords.put(binder, managerRecord);
-
-            userRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::notifyRoutesToManager,
-                            userRecord.mHandler, manager));
-
-            for (RouterRecord routerRecord : userRecord.mRouterRecords) {
-                // TODO: Do not use notifyPreferredFeaturesChangedToManagers since it updates all
-                // managers. Instead, Notify only to the manager that is currently being registered.
-
-                // TODO: UserRecord <-> routerRecord, why do they reference each other?
-                // How about removing mUserRecord from routerRecord?
-                routerRecord.mUserRecord.mHandler.sendMessage(
-                        obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
-                            routerRecord.mUserRecord.mHandler, routerRecord));
-            }
+        if (routerRecord == null) {
+            return;
         }
+
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::releaseSessionOnHandler,
+                        routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId));
     }
 
-    private void unregisterManagerLocked(IMediaRouter2Manager manager, boolean died) {
-        ManagerRecord managerRecord = mAllManagerRecords.remove(manager.asBinder());
-        if (managerRecord != null) {
-            UserRecord userRecord = managerRecord.mUserRecord;
-            userRecord.mManagerRecords.remove(managerRecord);
-            managerRecord.dispose();
-            disposeUserIfNeededLocked(userRecord); // since manager removed from user
-        }
-    }
+    ////////////////////////////////////////////////////////////
+    ////  ***Locked methods related to MediaRouter2Manager
+    ////   - Should have @NonNull/@Nullable on all arguments
+    ////////////////////////////////////////////////////////////
 
-    private void requestClientCreateSessionLocked(IMediaRouter2Manager manager,
-            String packageName, MediaRoute2Info route, int requestId) {
-        ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder());
-        if (managerRecord != null) {
-            RouterRecord routerRecord =
-                    managerRecord.mUserRecord.findRouterRecordLocked(packageName);
-            if (routerRecord == null) {
-                Slog.w(TAG, "Ignoring session creation for unknown router.");
-            }
-            long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
-            if (routerRecord != null && managerRecord.mTrusted) {
-                //TODO: Use MediaRouter2's OnCreateSessionListener to send proper session hints.
-                requestCreateSessionLocked(routerRecord.mRouter, route,
-                        uniqueRequestId, null /* sessionHints */);
-            }
-        }
-    }
-
-    private void setRouteVolumeLocked(IMediaRouter2Manager manager, MediaRoute2Info route,
-            int volume) {
-        final IBinder binder = manager.asBinder();
-        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
-        if (managerRecord != null) {
-            managerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::setRouteVolume,
-                            managerRecord.mUserRecord.mHandler, route, volume));
-        }
-    }
-
-    private void setSessionVolumeLocked(IMediaRouter2Manager manager, String sessionId,
-            int volume) {
-        final IBinder binder = manager.asBinder();
-        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
-        if (managerRecord != null) {
-            managerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::setSessionVolume,
-                            managerRecord.mUserRecord.mHandler, sessionId, volume));
-        }
-    }
-
-    private List<RoutingSessionInfo> getActiveSessionsLocked(IMediaRouter2Manager manager) {
+    private List<RoutingSessionInfo> getActiveSessionsLocked(
+            @NonNull IMediaRouter2Manager manager) {
         final IBinder binder = manager.asBinder();
         ManagerRecord managerRecord = mAllManagerRecords.get(binder);
 
@@ -713,6 +696,177 @@
         return sessionInfos;
     }
 
+    private void registerManagerLocked(@NonNull IMediaRouter2Manager manager,
+            int uid, int pid, @NonNull String packageName, int userId, boolean trusted) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord != null) {
+            Slog.w(TAG, "Same manager already exists. packageName=" + packageName);
+            return;
+        }
+
+        UserRecord userRecord = getOrCreateUserRecordLocked(userId);
+        managerRecord = new ManagerRecord(userRecord, manager, uid, pid, packageName, trusted);
+        try {
+            binder.linkToDeath(managerRecord, 0);
+        } catch (RemoteException ex) {
+            throw new RuntimeException("Media router manager died prematurely.", ex);
+        }
+
+        userRecord.mManagerRecords.add(managerRecord);
+        mAllManagerRecords.put(binder, managerRecord);
+
+        userRecord.mHandler.sendMessage(obtainMessage(UserHandler::notifyRoutesToManager,
+                userRecord.mHandler, manager));
+
+        for (RouterRecord routerRecord : userRecord.mRouterRecords) {
+            // TODO: Do not use notifyPreferredFeaturesChangedToManagers since it updates all
+            // managers. Instead, Notify only to the manager that is currently being registered.
+
+            // TODO: UserRecord <-> routerRecord, why do they reference each other?
+            // How about removing mUserRecord from routerRecord?
+            routerRecord.mUserRecord.mHandler.sendMessage(
+                    obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
+                        routerRecord.mUserRecord.mHandler, routerRecord));
+        }
+    }
+
+    private void unregisterManagerLocked(@NonNull IMediaRouter2Manager manager, boolean died) {
+        ManagerRecord managerRecord = mAllManagerRecords.remove(manager.asBinder());
+        if (managerRecord == null) {
+            return;
+        }
+        UserRecord userRecord = managerRecord.mUserRecord;
+        userRecord.mManagerRecords.remove(managerRecord);
+        managerRecord.dispose();
+        disposeUserIfNeededLocked(userRecord); // since manager removed from user
+    }
+
+    private void setRouteVolumeWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+            @NonNull MediaRoute2Info route, int volume) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord == null) {
+            return;
+        }
+        managerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::setRouteVolumeOnHandler,
+                        managerRecord.mUserRecord.mHandler, route, volume));
+    }
+
+    private void requestCreateSessionWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+            @NonNull String packageName, @NonNull MediaRoute2Info route, int requestId) {
+        ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder());
+        if (managerRecord == null || !managerRecord.mTrusted) {
+            return;
+        }
+
+        RouterRecord routerRecord = managerRecord.mUserRecord.findRouterRecordLocked(packageName);
+        if (routerRecord == null) {
+            Slog.w(TAG, "Ignoring session creation for unknown router.");
+            return;
+        }
+
+        long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
+        //TODO: Use MediaRouter2's OnCreateSessionListener to send proper session hints.
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::requestCreateSessionOnHandler,
+                        routerRecord.mUserRecord.mHandler,
+                        routerRecord, route, uniqueRequestId, null /* sessionHints */));
+    }
+
+    private void selectRouteWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord == null) {
+            return;
+        }
+
+        // Can be null if the session is system's.
+        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+                .findRouterforSessionLocked(uniqueSessionId);
+
+        managerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::selectRouteOnHandler,
+                        managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+    }
+
+    private void deselectRouteWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord == null) {
+            return;
+        }
+
+        // Can be null if the session is system's.
+        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+                .findRouterforSessionLocked(uniqueSessionId);
+
+        managerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::deselectRouteOnHandler,
+                        managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+    }
+
+    private void transferToRouteWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord == null) {
+            return;
+        }
+
+        // Can be null if the session is system's.
+        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+                .findRouterforSessionLocked(uniqueSessionId);
+
+        managerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::transferToRouteOnHandler,
+                        managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+    }
+
+    private void setSessionVolumeWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+            @NonNull String uniqueSessionId, int volume) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord == null) {
+            return;
+        }
+
+        managerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::setSessionVolumeOnHandler,
+                        managerRecord.mUserRecord.mHandler, uniqueSessionId, volume));
+    }
+
+    private void releaseSessionWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+            @NonNull String uniqueSessionId) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord == null) {
+            return;
+        }
+
+        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+                .findRouterforSessionLocked(uniqueSessionId);
+
+        managerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::releaseSessionOnHandler,
+                        managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId));
+    }
+
+    ////////////////////////////////////////////////////////////
+    ////  ***Locked methods used by both router2 and manager
+    ////   - Should have @NonNull/@Nullable on all arguments
+    ////////////////////////////////////////////////////////////
+
     private UserRecord getOrCreateUserRecordLocked(int userId) {
         UserRecord userRecord = mUserRecords.get(userId);
         if (userRecord == null) {
@@ -726,79 +880,7 @@
         return userRecord;
     }
 
-    private void selectRouteWithManagerLocked(IMediaRouter2Manager manager, String sessionId,
-            MediaRoute2Info route) {
-        final IBinder binder = manager.asBinder();
-        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
-        if (managerRecord == null) {
-            Slog.w(TAG, "selectRouteWithManagerLocked: Ignoring unknown manager.");
-            return;
-        }
-        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
-                .findRouterforSessionLocked(sessionId);
-
-        managerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::selectRouteOnHandler,
-                            managerRecord.mUserRecord.mHandler,
-                            routerRecord, sessionId, route));
-    }
-
-    private void deselectRouteWithManagerLocked(IMediaRouter2Manager manager, String sessionId,
-            MediaRoute2Info route) {
-        final IBinder binder = manager.asBinder();
-        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
-        if (managerRecord == null) {
-            Slog.w(TAG, "deselectRouteWithManagerLocked: Ignoring unknown manager.");
-            return;
-        }
-        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
-                .findRouterforSessionLocked(sessionId);
-
-        managerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::deselectRouteOnHandler,
-                        managerRecord.mUserRecord.mHandler,
-                        routerRecord, sessionId, route));
-    }
-
-    private void transferToRouteWithManagerLocked(IMediaRouter2Manager manager, String sessionId,
-            MediaRoute2Info route) {
-        final IBinder binder = manager.asBinder();
-        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
-        if (managerRecord == null) {
-            Slog.w(TAG, "transferClientRouteLocked: Ignoring unknown manager.");
-            return;
-        }
-        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
-                .findRouterforSessionLocked(sessionId);
-
-        managerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::transferToRouteOnHandler,
-                        managerRecord.mUserRecord.mHandler,
-                        routerRecord, sessionId, route));
-    }
-
-    private void releaseSessionWithManagerLocked(IMediaRouter2Manager manager, String sessionId) {
-        final IBinder binder = manager.asBinder();
-        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
-        if (managerRecord == null) {
-            Slog.w(TAG, "releaseSessionWithManagerLocked: Ignoring unknown manager.");
-            return;
-        }
-
-        RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
-                .findRouterforSessionLocked(sessionId);
-
-        managerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::releaseSessionOnHandler,
-                        managerRecord.mUserRecord.mHandler,
-                        routerRecord, sessionId));
-    }
-
-    private void disposeUserIfNeededLocked(UserRecord userRecord) {
+    private void disposeUserIfNeededLocked(@NonNull UserRecord userRecord) {
         // If there are no records left and the user is no longer current then go ahead
         // and purge the user record and all of its associated state.  If the user is current
         // then leave it alone since we might be connected to a route or want to query
@@ -948,7 +1030,7 @@
 
         private boolean mRunning;
 
-        UserHandler(MediaRouter2ServiceImpl service, UserRecord userRecord) {
+        UserHandler(@NonNull MediaRouter2ServiceImpl service, @NonNull UserRecord userRecord) {
             super(Looper.getMainLooper(), null, true);
             mServiceRef = new WeakReference<>(service);
             mUserRecord = userRecord;
@@ -974,14 +1056,14 @@
         }
 
         @Override
-        public void onAddProviderService(MediaRoute2ProviderServiceProxy proxy) {
+        public void onAddProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy) {
             proxy.setCallback(this);
             mMediaProviders.add(proxy);
             proxy.updateDiscoveryPreference(mUserRecord.mCompositeDiscoveryPreference);
         }
 
         @Override
-        public void onRemoveProviderService(MediaRoute2ProviderServiceProxy proxy) {
+        public void onRemoveProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy) {
             mMediaProviders.remove(proxy);
         }
 
@@ -1012,19 +1094,19 @@
         }
 
         @Override
-        public void onSessionReleased(MediaRoute2Provider provider,
-                RoutingSessionInfo sessionInfo) {
+        public void onSessionReleased(@NonNull MediaRoute2Provider provider,
+                @NonNull RoutingSessionInfo sessionInfo) {
             sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionReleasedOnHandler,
                     this, provider, sessionInfo));
         }
 
         @Nullable
-        public RouterRecord findRouterforSessionLocked(@NonNull String sessionId) {
-            return mSessionToRouterMap.get(sessionId);
+        public RouterRecord findRouterforSessionLocked(@NonNull String uniqueSessionId) {
+            return mSessionToRouterMap.get(uniqueSessionId);
         }
 
         //TODO: notify session info updates
-        private void onProviderStateChangedOnHandler(MediaRoute2Provider provider) {
+        private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) {
             int providerIndex = getProviderInfoIndex(provider.getUniqueId());
             MediaRoute2ProviderInfo providerInfo = provider.getProviderInfo();
             MediaRoute2ProviderInfo prevInfo =
@@ -1096,7 +1178,7 @@
             }
         }
 
-        private int getProviderInfoIndex(String providerId) {
+        private int getProviderInfoIndex(@NonNull String providerId) {
             for (int i = 0; i < mLastProviderInfos.size(); i++) {
                 MediaRoute2ProviderInfo providerInfo = mLastProviderInfos.get(i);
                 if (TextUtils.equals(providerInfo.getUniqueId(), providerId)) {
@@ -1106,8 +1188,8 @@
             return -1;
         }
 
-        private void requestCreateSessionOnHandler(RouterRecord routerRecord,
-                MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
+        private void requestCreateSessionOnHandler(@NonNull RouterRecord routerRecord,
+                @NonNull MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
 
             final MediaRoute2Provider provider = findProvider(route.getProviderId());
             if (provider == null) {
@@ -1126,8 +1208,9 @@
                     requestId, sessionHints);
         }
 
+        // routerRecord can be null if the session is system's.
         private void selectRouteOnHandler(@Nullable RouterRecord routerRecord,
-                String uniqueSessionId, MediaRoute2Info route) {
+                @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
             if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route,
                     "selecting")) {
                 return;
@@ -1142,8 +1225,9 @@
             provider.selectRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
         }
 
+        // routerRecord can be null if the session is system's.
         private void deselectRouteOnHandler(@Nullable RouterRecord routerRecord,
-                String uniqueSessionId, MediaRoute2Info route) {
+                @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
             if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route,
                     "deselecting")) {
                 return;
@@ -1158,8 +1242,9 @@
             provider.deselectRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
         }
 
-        private void transferToRouteOnHandler(RouterRecord routerRecord,
-                String uniqueSessionId, MediaRoute2Info route) {
+        // routerRecord can be null if the session is system's.
+        private void transferToRouteOnHandler(@Nullable RouterRecord routerRecord,
+                @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
             if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route,
                     "transferring to")) {
                 return;
@@ -1171,17 +1256,12 @@
             if (provider == null) {
                 return;
             }
-            provider.transferToRoute(getOriginalId(uniqueSessionId),
-                    route.getOriginalId());
+            provider.transferToRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
         }
 
         private boolean checkArgumentsForSessionControl(@Nullable RouterRecord routerRecord,
-                String uniqueSessionId, MediaRoute2Info route, @NonNull String description) {
-            if (route == null) {
-                Slog.w(TAG, "Ignoring " + description + " null route");
-                return false;
-            }
-
+                @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route,
+                @NonNull String description) {
             final String providerId = route.getProviderId();
             final MediaRoute2Provider provider = findProvider(providerId);
             if (provider == null) {
@@ -1190,12 +1270,6 @@
                 return false;
             }
 
-            if (TextUtils.isEmpty(uniqueSessionId)) {
-                Slog.w(TAG, "Ignoring " + description + " route with empty unique session ID. "
-                        + "route=" + route);
-                return false;
-            }
-
             // Bypass checking router if it's the system session (routerRecord should be null)
             if (TextUtils.equals(getProviderId(uniqueSessionId), mSystemProvider.getUniqueId())) {
                 return true;
@@ -1225,12 +1299,7 @@
         }
 
         private void releaseSessionOnHandler(@NonNull RouterRecord routerRecord,
-                String uniqueSessionId) {
-            if (TextUtils.isEmpty(uniqueSessionId)) {
-                Slog.w(TAG, "Ignoring releasing session with empty unique session ID.");
-                return;
-            }
-
+                @NonNull String uniqueSessionId) {
             final RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId);
             if (matchingRecord != routerRecord) {
                 Slog.w(TAG, "Ignoring releasing session from non-matching router."
@@ -1265,7 +1334,6 @@
 
         private void onSessionCreatedOnHandler(@NonNull MediaRoute2Provider provider,
                 @NonNull RoutingSessionInfo sessionInfo, long requestId) {
-
             notifySessionCreatedToManagers(getManagers(), sessionInfo);
 
             if (requestId == MediaRoute2ProviderService.REQUEST_ID_UNKNOWN) {
@@ -1377,8 +1445,8 @@
             notifySessionReleased(routerRecord, sessionInfo);
         }
 
-        private void notifySessionCreated(RouterRecord routerRecord,
-                RoutingSessionInfo sessionInfo, int requestId) {
+        private void notifySessionCreated(@NonNull RouterRecord routerRecord,
+                @NonNull RoutingSessionInfo sessionInfo, int requestId) {
             try {
                 routerRecord.mRouter.notifySessionCreated(sessionInfo, requestId);
             } catch (RemoteException ex) {
@@ -1387,7 +1455,8 @@
             }
         }
 
-        private void notifySessionCreationFailed(RouterRecord routerRecord, int requestId) {
+        private void notifySessionCreationFailed(@NonNull RouterRecord routerRecord,
+                int requestId) {
             try {
                 routerRecord.mRouter.notifySessionCreated(/* sessionInfo= */ null, requestId);
             } catch (RemoteException ex) {
@@ -1396,8 +1465,8 @@
             }
         }
 
-        private void notifySessionInfoChanged(RouterRecord routerRecord,
-                RoutingSessionInfo sessionInfo) {
+        private void notifySessionInfoChanged(@NonNull RouterRecord routerRecord,
+                @NonNull RoutingSessionInfo sessionInfo) {
             try {
                 routerRecord.mRouter.notifySessionInfoChanged(sessionInfo);
             } catch (RemoteException ex) {
@@ -1406,8 +1475,8 @@
             }
         }
 
-        private void notifySessionReleased(RouterRecord routerRecord,
-                RoutingSessionInfo sessionInfo) {
+        private void notifySessionReleased(@NonNull RouterRecord routerRecord,
+                @NonNull RoutingSessionInfo sessionInfo) {
             try {
                 routerRecord.mRouter.notifySessionReleased(sessionInfo);
             } catch (RemoteException ex) {
@@ -1416,21 +1485,21 @@
             }
         }
 
-        private void setRouteVolume(MediaRoute2Info route, int volume) {
+        private void setRouteVolumeOnHandler(@NonNull MediaRoute2Info route, int volume) {
             final MediaRoute2Provider provider = findProvider(route.getProviderId());
             if (provider != null) {
                 provider.setRouteVolume(route.getOriginalId(), volume);
             }
         }
 
-        private void setSessionVolume(String sessionId, int volume) {
-            final MediaRoute2Provider provider = findProvider(getProviderId(sessionId));
+        private void setSessionVolumeOnHandler(@NonNull String uniqueSessionId, int volume) {
+            final MediaRoute2Provider provider = findProvider(getProviderId(uniqueSessionId));
             if (provider == null) {
                 Slog.w(TAG, "setSessionVolume: couldn't find provider for session "
-                        + "id=" + sessionId);
+                        + "id=" + uniqueSessionId);
                 return;
             }
-            provider.setSessionVolume(getOriginalId(sessionId), volume);
+            provider.setSessionVolume(getOriginalId(uniqueSessionId), volume);
         }
 
         private List<IMediaRouter2> getRouters() {
@@ -1461,7 +1530,7 @@
             return managers;
         }
 
-        private void notifyRoutesToRouter(IMediaRouter2 router) {
+        private void notifyRoutesToRouter(@NonNull IMediaRouter2 router) {
             List<MediaRoute2Info> routes = new ArrayList<>();
             for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) {
                 routes.addAll(providerInfo.getRoutes());
@@ -1476,8 +1545,8 @@
             }
         }
 
-        private void notifyRoutesAddedToRouters(List<IMediaRouter2> routers,
-                List<MediaRoute2Info> routes) {
+        private void notifyRoutesAddedToRouters(@NonNull List<IMediaRouter2> routers,
+                @NonNull List<MediaRoute2Info> routes) {
             for (IMediaRouter2 router : routers) {
                 try {
                     router.notifyRoutesAdded(routes);
@@ -1487,8 +1556,8 @@
             }
         }
 
-        private void notifyRoutesRemovedToRouters(List<IMediaRouter2> routers,
-                List<MediaRoute2Info> routes) {
+        private void notifyRoutesRemovedToRouters(@NonNull List<IMediaRouter2> routers,
+                @NonNull List<MediaRoute2Info> routes) {
             for (IMediaRouter2 router : routers) {
                 try {
                     router.notifyRoutesRemoved(routes);
@@ -1498,8 +1567,8 @@
             }
         }
 
-        private void notifyRoutesChangedToRouters(List<IMediaRouter2> routers,
-                List<MediaRoute2Info> routes) {
+        private void notifyRoutesChangedToRouters(@NonNull List<IMediaRouter2> routers,
+                @NonNull List<MediaRoute2Info> routes) {
             for (IMediaRouter2 router : routers) {
                 try {
                     router.notifyRoutesChanged(routes);
@@ -1509,8 +1578,8 @@
             }
         }
 
-        private void notifySessionInfoChangedToRouters(List<IMediaRouter2> routers,
-                RoutingSessionInfo sessionInfo) {
+        private void notifySessionInfoChangedToRouters(@NonNull List<IMediaRouter2> routers,
+                @NonNull RoutingSessionInfo sessionInfo) {
             for (IMediaRouter2 router : routers) {
                 try {
                     router.notifySessionInfoChanged(sessionInfo);
@@ -1520,7 +1589,7 @@
             }
         }
 
-        private void notifyRoutesToManager(IMediaRouter2Manager manager) {
+        private void notifyRoutesToManager(@NonNull IMediaRouter2Manager manager) {
             List<MediaRoute2Info> routes = new ArrayList<>();
             for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) {
                 routes.addAll(providerInfo.getRoutes());
@@ -1535,8 +1604,8 @@
             }
         }
 
-        private void notifyRoutesAddedToManagers(List<IMediaRouter2Manager> managers,
-                List<MediaRoute2Info> routes) {
+        private void notifyRoutesAddedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+                @NonNull List<MediaRoute2Info> routes) {
             for (IMediaRouter2Manager manager : managers) {
                 try {
                     manager.notifyRoutesAdded(routes);
@@ -1546,8 +1615,8 @@
             }
         }
 
-        private void notifyRoutesRemovedToManagers(List<IMediaRouter2Manager> managers,
-                List<MediaRoute2Info> routes) {
+        private void notifyRoutesRemovedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+                @NonNull List<MediaRoute2Info> routes) {
             for (IMediaRouter2Manager manager : managers) {
                 try {
                     manager.notifyRoutesRemoved(routes);
@@ -1557,8 +1626,8 @@
             }
         }
 
-        private void notifyRoutesChangedToManagers(List<IMediaRouter2Manager> managers,
-                List<MediaRoute2Info> routes) {
+        private void notifyRoutesChangedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+                @NonNull List<MediaRoute2Info> routes) {
             for (IMediaRouter2Manager manager : managers) {
                 try {
                     manager.notifyRoutesChanged(routes);
@@ -1568,8 +1637,8 @@
             }
         }
 
-        private void notifySessionCreatedToManagers(List<IMediaRouter2Manager> managers,
-                RoutingSessionInfo sessionInfo) {
+        private void notifySessionCreatedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+                @NonNull RoutingSessionInfo sessionInfo) {
             for (IMediaRouter2Manager manager : managers) {
                 try {
                     manager.notifySessionCreated(sessionInfo);
@@ -1580,7 +1649,8 @@
             }
         }
 
-        private void notifySessionInfosChangedToManagers(List<IMediaRouter2Manager> managers) {
+        private void notifySessionInfosChangedToManagers(
+                @NonNull List<IMediaRouter2Manager> managers) {
             for (IMediaRouter2Manager manager : managers) {
                 try {
                     manager.notifySessionsUpdated();
@@ -1591,7 +1661,7 @@
             }
         }
 
-        private void notifyPreferredFeaturesChangedToManagers(RouterRecord routerRecord) {
+        private void notifyPreferredFeaturesChangedToManagers(@NonNull RouterRecord routerRecord) {
             MediaRouter2ServiceImpl service = mServiceRef.get();
             if (service == null) {
                 return;
@@ -1632,7 +1702,7 @@
             }
         }
 
-        private MediaRoute2Provider findProvider(String providerId) {
+        private MediaRoute2Provider findProvider(@Nullable String providerId) {
             for (MediaRoute2Provider provider : mMediaProviders) {
                 if (TextUtils.equals(provider.getUniqueId(), providerId)) {
                     return provider;
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 777a8fe..8e38114 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -19,7 +19,6 @@
 import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
 import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO;
 
-import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -42,15 +41,13 @@
 import android.util.Log;
 
 import com.android.internal.R;
-import com.android.internal.annotations.GuardedBy;
 
-import java.util.Collections;
-import java.util.List;
 import java.util.Objects;
 
 /**
  * Provides routes for local playbacks such as phone speaker, wired headset, or Bluetooth speakers.
  */
+// TODO: check thread safety. We may need to use lock to protect variables.
 class SystemMediaRoute2Provider extends MediaRoute2Provider {
     private static final String TAG = "MR2SystemProvider";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -68,19 +65,16 @@
             SystemMediaRoute2Provider.class.getPackageName$(),
             SystemMediaRoute2Provider.class.getName());
 
-    @GuardedBy("mLock")
     private String mSelectedRouteId;
     MediaRoute2Info mDefaultRoute;
-    @NonNull List<MediaRoute2Info> mBluetoothRoutes = Collections.EMPTY_LIST;
     final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
 
     final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
         @Override
         public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
-            mHandler.post(new Runnable() {
-                @Override public void run() {
-                    updateAudioRoutes(newRoutes);
-                }
+            mHandler.post(() -> {
+                updateDefaultRoute(newRoutes);
+                notifyProviderState();
             });
         }
     };
@@ -97,11 +91,15 @@
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         mAudioService = IAudioService.Stub.asInterface(
                 ServiceManager.getService(Context.AUDIO_SERVICE));
+        AudioRoutesInfo newAudioRoutes = null;
+        try {
+            newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
+        } catch (RemoteException e) {
+        }
+        updateDefaultRoute(newAudioRoutes);
 
-        initializeDefaultRoute();
         mBtRouteProvider = BluetoothRouteProvider.getInstance(context, (routes) -> {
-            mBluetoothRoutes = routes;
-            publishRoutes();
+            publishProviderState();
 
             boolean sessionInfoChanged;
             synchronized (mLock) {
@@ -111,7 +109,15 @@
                 notifySessionInfoUpdated();
             }
         });
-        initializeSessionInfo();
+
+        mHandler.post(() -> notifyProviderState());
+
+        //TODO: clean up this
+        // This is required because it is not instantiated in the main thread and
+        // BluetoothRoutesUpdatedListener can be called before here
+        synchronized (mLock) {
+            updateSessionInfosIfNeededLocked();
+        }
 
         mContext.registerReceiver(new VolumeChangeReceiver(),
                 new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
@@ -164,65 +170,21 @@
         // Do nothing since we don't support grouping volume yet.
     }
 
-    private void initializeDefaultRoute() {
-        mDefaultRoute = new MediaRoute2Info.Builder(
-                DEFAULT_ROUTE_ID,
-                mContext.getResources().getText(R.string.default_audio_route_name).toString())
-                .setVolumeHandling(mAudioManager.isVolumeFixed()
-                        ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
-                        : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
-                .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
-                .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
-                .addFeature(FEATURE_LIVE_AUDIO)
-                .addFeature(FEATURE_LIVE_VIDEO)
-                .build();
-
-        AudioRoutesInfo newAudioRoutes = null;
-        try {
-            newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
-        } catch (RemoteException e) {
-        }
-        if (newAudioRoutes != null) {
-            // This will select the active BT route if there is one and the current
-            // selected route is the default system route, or if there is no selected
-            // route yet.
-            updateAudioRoutes(newAudioRoutes);
-        }
-    }
-
-    private void initializeSessionInfo() {
-        mBluetoothRoutes = mBtRouteProvider.getAllBluetoothRoutes();
-
-        MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
-        builder.addRoute(mDefaultRoute);
-        for (MediaRoute2Info route : mBluetoothRoutes) {
-            builder.addRoute(route);
-        }
-        setProviderState(builder.build());
-        mHandler.post(() -> notifyProviderState());
-
-        //TODO: clean up this
-        // This is required because it is not instantiated in the main thread and
-        // BluetoothRoutesUpdatedListener can be called before this function
-        synchronized (mLock) {
-            updateSessionInfosIfNeededLocked();
-        }
-    }
-
-    private void updateAudioRoutes(AudioRoutesInfo newRoutes) {
+    private void updateDefaultRoute(AudioRoutesInfo newRoutes) {
         int name = R.string.default_audio_route_name;
-        mCurAudioRoutesInfo.mainType = newRoutes.mainType;
-        if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
-                || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
-            name = com.android.internal.R.string.default_audio_route_name_headphones;
-        } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
-            name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
-        } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
-            name = com.android.internal.R.string.default_audio_route_name_hdmi;
-        } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
-            name = com.android.internal.R.string.default_audio_route_name_usb;
+        if (newRoutes != null) {
+            mCurAudioRoutesInfo.mainType = newRoutes.mainType;
+            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
+                    || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
+                name = com.android.internal.R.string.default_audio_route_name_headphones;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
+                name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
+                name = com.android.internal.R.string.default_audio_route_name_hdmi;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
+                name = com.android.internal.R.string.default_audio_route_name_usb;
+            }
         }
-
         mDefaultRoute = new MediaRoute2Info.Builder(
                 DEFAULT_ROUTE_ID, mContext.getResources().getText(name).toString())
                 .setVolumeHandling(mAudioManager.isVolumeFixed()
@@ -232,9 +194,20 @@
                 .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
                 .addFeature(FEATURE_LIVE_AUDIO)
                 .addFeature(FEATURE_LIVE_VIDEO)
+                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
                 .build();
+        updateProviderState();
+    }
 
-        publishRoutes();
+    private void updateProviderState() {
+        MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
+        builder.addRoute(mDefaultRoute);
+        if (mBtRouteProvider != null) {
+            for (MediaRoute2Info route : mBtRouteProvider.getAllBluetoothRoutes()) {
+                builder.addRoute(route);
+            }
+        }
+        setProviderState(builder.build());
     }
 
     /**
@@ -246,21 +219,21 @@
         RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder(
                 SYSTEM_SESSION_ID, "" /* clientPackageName */)
                 .setSystemSession(true);
-        String activeBtDeviceAddress = mBtRouteProvider.getSelectedRouteId();
-        mSelectedRouteId = TextUtils.isEmpty(activeBtDeviceAddress) ? mDefaultRoute.getId()
-                : activeBtDeviceAddress;
-        builder.addSelectedRoute(mSelectedRouteId);
 
-        if (!TextUtils.isEmpty(activeBtDeviceAddress)) {
+        MediaRoute2Info selectedRoute = mBtRouteProvider.getSelectedRoute();
+        if (selectedRoute == null) {
+            selectedRoute = mDefaultRoute;
+        } else {
             builder.addTransferableRoute(mDefaultRoute.getId());
         }
+        mSelectedRouteId = selectedRoute.getId();
+        builder.addSelectedRoute(mSelectedRouteId);
 
-        for (MediaRoute2Info route : mBluetoothRoutes) {
-            if (!TextUtils.equals(mSelectedRouteId, route.getId())) {
-                builder.addTransferableRoute(route.getId());
-            }
+        for (MediaRoute2Info route : mBtRouteProvider.getTransferableRoutes()) {
+            builder.addTransferableRoute(route.getId());
         }
 
+
         RoutingSessionInfo newSessionInfo = builder.setProviderId(mUniqueId).build();
         if (Objects.equals(oldSessionInfo, newSessionInfo)) {
             return false;
@@ -271,13 +244,9 @@
         }
     }
 
-    void publishRoutes() {
-        MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
-        builder.addRoute(mDefaultRoute);
-        for (MediaRoute2Info route : mBluetoothRoutes) {
-            builder.addRoute(route);
-        }
-        setAndNotifyProviderState(builder.build());
+    void publishProviderState() {
+        updateProviderState();
+        notifyProviderState();
     }
 
     void notifySessionInfoUpdated() {
@@ -306,24 +275,14 @@
                     AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
 
             if (newVolume != oldVolume) {
-                String activeBtDeviceAddress = mBtRouteProvider.getSelectedRouteId();
-                if (!TextUtils.isEmpty(activeBtDeviceAddress)) {
-                    for (int i = mBluetoothRoutes.size() - 1; i >= 0; i--) {
-                        MediaRoute2Info route = mBluetoothRoutes.get(i);
-                        if (TextUtils.equals(activeBtDeviceAddress, route.getId())) {
-                            mBluetoothRoutes.set(i,
-                                    new MediaRoute2Info.Builder(route)
-                                            .setVolume(newVolume)
-                                            .build());
-                            break;
-                        }
-                    }
-                } else {
+                if (TextUtils.equals(mDefaultRoute.getId(), mSelectedRouteId)) {
                     mDefaultRoute = new MediaRoute2Info.Builder(mDefaultRoute)
                             .setVolume(newVolume)
                             .build();
+                } else {
+                    mBtRouteProvider.setSelectedRouteVolume(newVolume);
                 }
-                publishRoutes();
+                publishProviderState();
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b3b8159..b61a2ce 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6224,6 +6224,13 @@
         return mRemoteAnimationDefinition;
     }
 
+    @Override
+    void applyFixedRotationTransform(DisplayInfo info, DisplayFrames displayFrames,
+            Configuration config) {
+        super.applyFixedRotationTransform(info, displayFrames, config);
+        ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
+    }
+
     void setRequestedOrientation(int requestedOrientation) {
         setOrientation(requestedOrientation, mayFreezeScreenLocked());
         mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged(
@@ -6462,11 +6469,20 @@
 
     @Override
     void resolveOverrideConfiguration(Configuration newParentConfiguration) {
-        Configuration resolvedConfig = getResolvedOverrideConfiguration();
+        super.resolveOverrideConfiguration(newParentConfiguration);
+        final Configuration resolvedConfig = getResolvedOverrideConfiguration();
+        if (isFixedRotationTransforming()) {
+            // The resolved configuration is applied with rotated display configuration. If this
+            // activity matches its parent (the following resolving procedures are no-op), then it
+            // can use the resolved configuration directly. Otherwise (e.g. fixed aspect ratio),
+            // the rotated configuration is used as parent configuration to compute the actual
+            // resolved configuration. It is like putting the activity in a rotated container.
+            mTmpConfig.setTo(resolvedConfig);
+            newParentConfiguration = mTmpConfig;
+        }
         if (mCompatDisplayInsets != null) {
             resolveSizeCompatModeConfiguration(newParentConfiguration);
         } else {
-            super.resolveOverrideConfiguration(newParentConfiguration);
             // We ignore activities' requested orientation in multi-window modes. Task level may
             // take them into consideration when calculating bounds.
             if (getParent() != null && getParent().inMultiWindowMode()) {
@@ -6495,7 +6511,6 @@
      * inheriting the parent bounds.
      */
     private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration) {
-        super.resolveOverrideConfiguration(newParentConfiguration);
         final Configuration resolvedConfig = getResolvedOverrideConfiguration();
         final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
 
@@ -6669,28 +6684,26 @@
         mTmpPrevBounds.set(getBounds());
         super.onConfigurationChanged(newParentConfig);
 
-        final Rect overrideBounds = getResolvedOverrideBounds();
-        if (task != null && !overrideBounds.isEmpty()
-                // If the changes come from change-listener, the incoming parent configuration is
-                // still the old one. Make sure their orientations are the same to reduce computing
-                // the compatibility bounds for the intermediate state.
-                && (task.getConfiguration().orientation == newParentConfig.orientation)) {
-            final Rect taskBounds = task.getBounds();
-            // Since we only center the activity horizontally, if only the fixed height is smaller
-            // than its container, the override bounds don't need to take effect.
-            if ((overrideBounds.width() != taskBounds.width()
-                    || overrideBounds.height() > taskBounds.height())) {
-                calculateCompatBoundsTransformation(newParentConfig);
-                updateSurfacePosition();
-            } else if (mSizeCompatBounds != null) {
+        if (shouldUseSizeCompatMode()) {
+            final Rect overrideBounds = getResolvedOverrideBounds();
+            if (task != null && !overrideBounds.isEmpty()) {
+                final Rect taskBounds = task.getBounds();
+                // Since we only center the activity horizontally, if only the fixed height is
+                // smaller than its container, the override bounds don't need to take effect.
+                if ((overrideBounds.width() != taskBounds.width()
+                        || overrideBounds.height() > taskBounds.height())) {
+                    calculateCompatBoundsTransformation(newParentConfig);
+                    updateSurfacePosition();
+                } else if (mSizeCompatBounds != null) {
+                    mSizeCompatBounds = null;
+                    mSizeCompatScale = 1f;
+                    updateSurfacePosition();
+                }
+            } else if (overrideBounds.isEmpty()) {
                 mSizeCompatBounds = null;
                 mSizeCompatScale = 1f;
                 updateSurfacePosition();
             }
-        } else if (overrideBounds.isEmpty()) {
-            mSizeCompatBounds = null;
-            mSizeCompatScale = 1f;
-            updateSurfacePosition();
         }
 
         final int newWinMode = getWindowingMode();
@@ -7710,6 +7723,12 @@
             return;
         }
         win.getAnimationFrames(outFrame, outInsets, outStableInsets, outSurfaceInsets);
+        if (isFixedRotationTransforming()) {
+            // This activity has been rotated but the display is still in old rotation. Because the
+            // animation applies in display space coordinates, the rotated animation frames need to
+            // be unrotated to avoid being cropped.
+            unrotateAnimationFrames(outFrame, outInsets, outStableInsets, outSurfaceInsets);
+        }
     }
 
     void setPictureInPictureParams(PictureInPictureParams p) {
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index ab8e975..46596e35 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -2854,6 +2854,11 @@
 
     boolean navigateUpTo(ActivityRecord srec, Intent destIntent, int resultCode,
             Intent resultData) {
+        if (!srec.attachedToProcess()) {
+            // Nothing to do if the caller is not attached, because this method should be called
+            // from an alive activity.
+            return false;
+        }
         final Task task = srec.getTask();
 
         if (!mChildren.contains(task) || !task.hasChild(srec)) {
@@ -2915,14 +2920,14 @@
         resultData = resultDataHolder[0];
 
         if (parent != null && foundParentInTask) {
+            final int callingUid = srec.info.applicationInfo.uid;
             final int parentLaunchMode = parent.info.launchMode;
             final int destIntentFlags = destIntent.getFlags();
             if (parentLaunchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE ||
                     parentLaunchMode == ActivityInfo.LAUNCH_SINGLE_TASK ||
                     parentLaunchMode == ActivityInfo.LAUNCH_SINGLE_TOP ||
                     (destIntentFlags & Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) {
-                parent.deliverNewIntentLocked(srec.info.applicationInfo.uid, destIntent,
-                        srec.packageName);
+                parent.deliverNewIntentLocked(callingUid, destIntent, srec.packageName);
             } else {
                 try {
                     ActivityInfo aInfo = AppGlobals.getPackageManager().getActivityInfo(
@@ -2935,11 +2940,11 @@
                             .setActivityInfo(aInfo)
                             .setResultTo(parent.appToken)
                             .setCallingPid(-1)
-                            .setCallingUid(parent.launchedFromUid)
-                            .setCallingPackage(parent.launchedFromPackage)
+                            .setCallingUid(callingUid)
+                            .setCallingPackage(srec.packageName)
                             .setCallingFeatureId(parent.launchedFromFeatureId)
                             .setRealCallingPid(-1)
-                            .setRealCallingUid(parent.launchedFromUid)
+                            .setRealCallingUid(callingUid)
                             .setComponentSpecified(true)
                             .execute();
                     foundParentInTask = res == ActivityManager.START_SUCCESS;
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 014cb76..8cf0881 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -528,6 +528,12 @@
         }
     }
 
+    private void notifyAppTransitionTimeoutLocked() {
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onAppTransitionTimeoutLocked();
+        }
+    }
+
     private int notifyAppTransitionStartingLocked(int transit, long duration,
             long statusBarAnimationStartTime, long statusBarAnimationDuration) {
         int redoLayout = 0;
@@ -2300,6 +2306,7 @@
             if (dc == null) {
                 return;
             }
+            notifyAppTransitionTimeoutLocked();
             if (isTransitionSet() || !dc.mOpeningApps.isEmpty() || !dc.mClosingApps.isEmpty()
                     || !dc.mChangingApps.isEmpty()) {
                 ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 9bd380a..33dd9cf 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -113,13 +113,6 @@
      * @see #mFullConfiguration
      */
     public void onConfigurationChanged(Configuration newParentConfig) {
-        onConfigurationChanged(newParentConfig, true /*forwardToChildren*/);
-    }
-
-    // TODO(root-unify): Consolidate with onConfigurationChanged() method above once unification is
-    //  done. This is only currently need during the process of unification where we don't want
-    //  configuration forwarded to a child from both parents.
-    public void onConfigurationChanged(Configuration newParentConfig, boolean forwardToChildren) {
         mResolvedTmpConfig.setTo(mResolvedOverrideConfiguration);
         resolveOverrideConfiguration(newParentConfig);
         mFullConfiguration.setTo(newParentConfig);
@@ -141,11 +134,9 @@
             mChangeListeners.get(i).onMergedOverrideConfigurationChanged(
                     mMergedOverrideConfiguration);
         }
-        if (forwardToChildren) {
-            for (int i = getChildCount() - 1; i >= 0; --i) {
-                final ConfigurationContainer child = getChildAt(i);
-                child.onConfigurationChanged(mFullConfiguration);
-            }
+        for (int i = getChildCount() - 1; i >= 0; --i) {
+            final ConfigurationContainer child = getChildAt(i);
+            child.onConfigurationChanged(mFullConfiguration);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 8a46771..1fbaadc 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -259,6 +259,7 @@
         implements WindowManagerPolicy.DisplayContentInfo {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "DisplayContent" : TAG_WM;
     private static final String TAG_STACK = TAG + POSTFIX_STACK;
+    private static final int NO_ROTATION = -1;
 
     /** The default scaling mode that scales content automatically. */
     static final int FORCE_SCALING_MODE_AUTO = 0;
@@ -515,6 +516,13 @@
      */
     ActivityRecord mFocusedApp = null;
 
+    /**
+     * The launching activity which is using fixed rotation transformation.
+     *
+     * @see #handleTopActivityLaunchingInDifferentOrientation
+     */
+    ActivityRecord mFixedRotationLaunchingApp;
+
     /** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */
     final ArrayList<WindowState> mWinAddedSinceNullFocus = new ArrayList<>();
 
@@ -1308,6 +1316,9 @@
         if (mDisplayRotation.isWaitingForRemoteRotation()) {
             return;
         }
+        // Clear the record because the display will sync to current rotation.
+        mFixedRotationLaunchingApp = null;
+
         final boolean configUpdated = updateDisplayOverrideConfigurationLocked();
         if (configUpdated) {
             return;
@@ -1367,7 +1378,7 @@
      *         {@link #sendNewConfiguration} if the method returns {@code true}.
      */
     boolean updateOrientation() {
-        return mDisplayRotation.updateOrientation(getOrientation(), false /* forceUpdate */);
+        return updateOrientation(false /* forceUpdate */);
     }
 
     /**
@@ -1390,7 +1401,7 @@
         }
 
         Configuration config = null;
-        if (mDisplayRotation.updateOrientation(getOrientation(), forceUpdate)) {
+        if (updateOrientation(forceUpdate)) {
             // If we changed the orientation but mOrientationChangeComplete is already true,
             // we used seamless rotation, and we don't need to freeze the screen.
             if (freezeDisplayToken != null && !mWmService.mRoot.mOrientationChangeComplete) {
@@ -1421,6 +1432,126 @@
         return config;
     }
 
+    private boolean updateOrientation(boolean forceUpdate) {
+        final int orientation = getOrientation();
+        // The last orientation source is valid only after getOrientation.
+        final WindowContainer orientationSource = getLastOrientationSource();
+        final ActivityRecord r =
+                orientationSource != null ? orientationSource.asActivityRecord() : null;
+        // Currently there is no use case from non-activity.
+        if (r != null && handleTopActivityLaunchingInDifferentOrientation(r)) {
+            mFixedRotationLaunchingApp = r;
+            // Display orientation should be deferred until the top fixed rotation is finished.
+            return false;
+        }
+        return mDisplayRotation.updateOrientation(orientation, forceUpdate);
+    }
+
+    /** @return a valid rotation if the activity can use different orientation than the display. */
+    @Surface.Rotation
+    private int rotationForActivityInDifferentOrientation(@NonNull ActivityRecord r) {
+        if (!mWmService.mIsFixedRotationTransformEnabled) {
+            return NO_ROTATION;
+        }
+        if (r.inMultiWindowMode()
+                || r.getRequestedConfigurationOrientation() == getConfiguration().orientation) {
+            return NO_ROTATION;
+        }
+        final int currentRotation = getRotation();
+        final int rotation = mDisplayRotation.rotationForOrientation(r.getRequestedOrientation(),
+                currentRotation);
+        if (rotation == currentRotation) {
+            return NO_ROTATION;
+        }
+        return rotation;
+    }
+
+    /**
+     * We need to keep display rotation fixed for a while when the activity in different orientation
+     * is launching until the launch animation is done to avoid showing the previous activity
+     * inadvertently in a wrong orientation.
+     *
+     * @return {@code true} if the fixed rotation is started.
+     */
+    private boolean handleTopActivityLaunchingInDifferentOrientation(@NonNull ActivityRecord r) {
+        if (!mWmService.mIsFixedRotationTransformEnabled) {
+            return false;
+        }
+        if (r.isFinishingFixedRotationTransform()) {
+            return false;
+        }
+        if (r.hasFixedRotationTransform()) {
+            // It has been set and not yet finished.
+            return true;
+        }
+        if (!mAppTransition.isTransitionSet()) {
+            // Apply normal rotation animation in case of the activity set different requested
+            // orientation without activity switch.
+            return false;
+        }
+        if (!mOpeningApps.contains(r)
+                // Without screen rotation, the rotation behavior of non-top visible activities is
+                // undefined. So the fixed rotated activity needs to cover the screen.
+                && r.findMainWindow() != mDisplayPolicy.getTopFullscreenOpaqueWindow()) {
+            return false;
+        }
+        final int rotation = rotationForActivityInDifferentOrientation(r);
+        if (rotation == NO_ROTATION) {
+            return false;
+        }
+        if (!r.getParent().matchParentBounds()) {
+            // Because the fixed rotated configuration applies to activity directly, if its parent
+            // has it own policy for bounds, the activity bounds based on parent is unknown.
+            return false;
+        }
+
+        startFixedRotationTransform(r, rotation);
+        mAppTransition.registerListenerLocked(new WindowManagerInternal.AppTransitionListener() {
+            void done() {
+                r.clearFixedRotationTransform();
+                mAppTransition.unregisterListener(this);
+            }
+
+            @Override
+            public void onAppTransitionFinishedLocked(IBinder token) {
+                if (token == r.token) {
+                    done();
+                }
+            }
+
+            @Override
+            public void onAppTransitionCancelledLocked(int transit) {
+                done();
+            }
+
+            @Override
+            public void onAppTransitionTimeoutLocked() {
+                done();
+            }
+        });
+        return true;
+    }
+
+    /** @return {@code true} if the display orientation will be changed. */
+    boolean continueUpdateOrientationForDiffOrienLaunchingApp(WindowToken token) {
+        if (token != mFixedRotationLaunchingApp) {
+            return false;
+        }
+        if (updateOrientation()) {
+            sendNewConfiguration();
+            return true;
+        }
+        return false;
+    }
+
+    private void startFixedRotationTransform(WindowToken token, int rotation) {
+        mTmpConfiguration.unset();
+        final DisplayInfo info = computeScreenConfiguration(mTmpConfiguration, rotation);
+        final WmDisplayCutout cutout = calculateDisplayCutoutForRotation(rotation);
+        final DisplayFrames displayFrames = new DisplayFrames(mDisplayId, info, cutout);
+        token.applyFixedRotationTransform(info, displayFrames, mTmpConfiguration);
+    }
+
     /**
      * Update rotation of the display.
      *
@@ -1622,6 +1753,63 @@
     }
 
     /**
+     * Compute display info and configuration according to the given rotation without changing
+     * current display.
+     */
+    DisplayInfo computeScreenConfiguration(Configuration outConfig, int rotation) {
+        final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+        final int dw = rotated ? mBaseDisplayHeight : mBaseDisplayWidth;
+        final int dh = rotated ? mBaseDisplayWidth : mBaseDisplayHeight;
+        outConfig.windowConfiguration.getBounds().set(0, 0, dw, dh);
+
+        final int uiMode = getConfiguration().uiMode;
+        final DisplayCutout displayCutout =
+                calculateDisplayCutoutForRotation(rotation).getDisplayCutout();
+        computeScreenAppConfiguration(outConfig, dw, dh, rotation, uiMode, displayCutout);
+
+        final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+        displayInfo.rotation = rotation;
+        displayInfo.logicalWidth = dw;
+        displayInfo.logicalHeight = dh;
+        final Rect appBounds = outConfig.windowConfiguration.getAppBounds();
+        displayInfo.appWidth = appBounds.width();
+        displayInfo.appHeight = appBounds.height();
+        displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout;
+        computeSizeRangesAndScreenLayout(displayInfo, rotated, uiMode, dw, dh,
+                mDisplayMetrics.density, outConfig);
+        return displayInfo;
+    }
+
+    /** Compute configuration related to application without changing current display. */
+    private void computeScreenAppConfiguration(Configuration outConfig, int dw, int dh,
+            int rotation, int uiMode, DisplayCutout displayCutout) {
+        final int appWidth = mDisplayPolicy.getNonDecorDisplayWidth(dw, dh, rotation, uiMode,
+                displayCutout);
+        final int appHeight = mDisplayPolicy.getNonDecorDisplayHeight(dw, dh, rotation, uiMode,
+                displayCutout);
+        mDisplayPolicy.getNonDecorInsetsLw(rotation, dw, dh, displayCutout, mTmpRect);
+        final int leftInset = mTmpRect.left;
+        final int topInset = mTmpRect.top;
+        // AppBounds at the root level should mirror the app screen size.
+        outConfig.windowConfiguration.setAppBounds(leftInset /* left */, topInset /* top */,
+                leftInset + appWidth /* right */, topInset + appHeight /* bottom */);
+        outConfig.windowConfiguration.setRotation(rotation);
+        outConfig.orientation = (dw <= dh) ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+
+        final float density = mDisplayMetrics.density;
+        outConfig.screenWidthDp = (int) (mDisplayPolicy.getConfigDisplayWidth(dw, dh, rotation,
+                uiMode, displayCutout) / density);
+        outConfig.screenHeightDp = (int) (mDisplayPolicy.getConfigDisplayHeight(dw, dh, rotation,
+                uiMode, displayCutout) / density);
+        outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale);
+        outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale);
+
+        final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+        outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, uiMode, dw,
+                dh, displayCutout);
+    }
+
+    /**
      * Compute display configuration based on display properties and policy settings.
      * Do not call if mDisplayReady == false.
      */
@@ -1629,42 +1817,19 @@
         final DisplayInfo displayInfo = updateDisplayAndOrientation(config.uiMode, config);
         calculateBounds(displayInfo, mTmpBounds);
         config.windowConfiguration.setBounds(mTmpBounds);
+        config.windowConfiguration.setWindowingMode(getWindowingMode());
+        config.windowConfiguration.setDisplayWindowingMode(getWindowingMode());
 
         final int dw = displayInfo.logicalWidth;
         final int dh = displayInfo.logicalHeight;
-        config.orientation = (dw <= dh) ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
-        config.windowConfiguration.setWindowingMode(getWindowingMode());
-        config.windowConfiguration.setDisplayWindowingMode(getWindowingMode());
-        config.windowConfiguration.setRotation(displayInfo.rotation);
-
-        final float density = mDisplayMetrics.density;
-        config.screenWidthDp =
-                (int)(mDisplayPolicy.getConfigDisplayWidth(dw, dh, displayInfo.rotation,
-                        config.uiMode, displayInfo.displayCutout) / density);
-        config.screenHeightDp =
-                (int)(mDisplayPolicy.getConfigDisplayHeight(dw, dh, displayInfo.rotation,
-                        config.uiMode, displayInfo.displayCutout) / density);
-
-        mDisplayPolicy.getNonDecorInsetsLw(displayInfo.rotation, dw, dh,
-                displayInfo.displayCutout, mTmpRect);
-        final int leftInset = mTmpRect.left;
-        final int topInset = mTmpRect.top;
-        // appBounds at the root level should mirror the app screen size.
-        config.windowConfiguration.setAppBounds(leftInset /* left */, topInset /* top */,
-                leftInset + displayInfo.appWidth /* right */,
-                topInset + displayInfo.appHeight /* bottom */);
-        final boolean rotated = (displayInfo.rotation == Surface.ROTATION_90
-                || displayInfo.rotation == Surface.ROTATION_270);
+        computeScreenAppConfiguration(config, dw, dh, displayInfo.rotation, config.uiMode,
+                displayInfo.displayCutout);
 
         config.screenLayout = (config.screenLayout & ~Configuration.SCREENLAYOUT_ROUND_MASK)
                 | ((displayInfo.flags & Display.FLAG_ROUND) != 0
                 ? Configuration.SCREENLAYOUT_ROUND_YES
                 : Configuration.SCREENLAYOUT_ROUND_NO);
 
-        config.compatScreenWidthDp = (int)(config.screenWidthDp / mCompatibleScreenScale);
-        config.compatScreenHeightDp = (int)(config.screenHeightDp / mCompatibleScreenScale);
-        config.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, config.uiMode, dw,
-                dh, displayInfo.displayCutout);
         config.densityDpi = displayInfo.logicalDensityDpi;
 
         config.colorMode =
@@ -2111,19 +2276,19 @@
     /**
      * In the general case, the orientation is computed from the above app windows first. If none of
      * the above app windows specify orientation, the orientation is computed from the child window
-     * container, e.g. {@link AppWindowToken#getOrientation(int)}.
+     * container, e.g. {@link ActivityRecord#getOrientation(int)}.
      */
     @ScreenOrientation
     @Override
     int getOrientation() {
-        final WindowManagerPolicy policy = mWmService.mPolicy;
+        mLastOrientationSource = null;
 
         if (mIgnoreRotationForApps) {
             return SCREEN_ORIENTATION_USER;
         }
 
         if (mWmService.mDisplayFrozen) {
-            if (policy.isKeyguardLocked()) {
+            if (mWmService.mPolicy.isKeyguardLocked()) {
                 // Use the last orientation the while the display is frozen with the keyguard
                 // locked. This could be the keyguard forced orientation or from a SHOW_WHEN_LOCKED
                 // window. We don't want to check the show when locked window directly though as
@@ -2135,7 +2300,9 @@
                 return getLastOrientation();
             }
         }
-        return mRootDisplayArea.getOrientation();
+        final int rootOrientation = mRootDisplayArea.getOrientation();
+        mLastOrientationSource = mRootDisplayArea.getLastOrientationSource();
+        return rootOrientation;
     }
 
     void updateDisplayInfo() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 3302445..a96e3a61 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1430,7 +1430,7 @@
         }
     }
 
-    private void simulateLayoutDecorWindow(WindowState win, DisplayFrames displayFrames, int uiMode,
+    private void simulateLayoutDecorWindow(WindowState win, DisplayFrames displayFrames,
             InsetsState insetsState, WindowFrames simulatedWindowFrames, Runnable layout) {
         win.setSimulatedWindowFrames(simulatedWindowFrames);
         try {
@@ -1454,7 +1454,7 @@
         final WindowFrames simulatedWindowFrames = new WindowFrames();
         if (mNavigationBar != null) {
             simulateLayoutDecorWindow(
-                    mNavigationBar, displayFrames, uiMode, insetsState, simulatedWindowFrames,
+                    mNavigationBar, displayFrames, insetsState, simulatedWindowFrames,
                     () -> layoutNavigationBar(displayFrames, uiMode, mLastNavVisible,
                             mLastNavTranslucent, mLastNavAllowedHidden,
                             mLastNotificationShadeForcesShowingNavigation,
@@ -1462,7 +1462,7 @@
         }
         if (mStatusBar != null) {
             simulateLayoutDecorWindow(
-                    mStatusBar, displayFrames, uiMode, insetsState, simulatedWindowFrames,
+                    mStatusBar, displayFrames, insetsState, simulatedWindowFrames,
                     () -> layoutStatusBar(displayFrames, mLastSystemUiFlags,
                             false /* isRealLayout */));
         }
@@ -1536,7 +1536,7 @@
         if (updateSysUiVisibility) {
             updateSystemUiVisibilityLw();
         }
-        layoutScreenDecorWindows(displayFrames, null /* transientFrames */);
+        layoutScreenDecorWindows(displayFrames, null /* simulatedFrames */);
         postAdjustDisplayFrames(displayFrames);
         mLastNavVisible = navVisible;
         mLastNavTranslucent = navTranslucent;
@@ -1939,6 +1939,7 @@
         final int requestedSysUiFl = PolicyControl.getSystemUiVisibility(null, attrs);
         final int sysUiFl = requestedSysUiFl | getImpliedSysUiFlagsForLayout(attrs);
 
+        displayFrames = win.getDisplayFrames(displayFrames);
         final WindowFrames windowFrames = win.getWindowFrames();
 
         sTmpLastParentFrame.set(windowFrames.mParentFrame);
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index e71371a..1a0dcb9 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -484,9 +484,11 @@
             prepareNormalRotationAnimation();
         }
 
-        // The display is frozen now, give a remote handler (system ui) some time to reposition
-        // things.
-        startRemoteRotation(oldRotation, mRotation);
+        // TODO(b/147469351): Remove the restriction.
+        if (mDisplayContent.mFixedRotationLaunchingApp == null) {
+            // Give a remote handler (system ui) some time to reposition things.
+            startRemoteRotation(oldRotation, mRotation);
+        }
 
         return true;
     }
@@ -551,7 +553,15 @@
     @VisibleForTesting
     boolean shouldRotateSeamlessly(int oldRotation, int newRotation, boolean forceUpdate) {
         final WindowState w = mDisplayPolicy.getTopFullscreenOpaqueWindow();
-        if (w == null || w != mDisplayContent.mCurrentFocus) {
+        if (w == null) {
+            return false;
+        }
+        // Display doesn't need to be frozen because application has been started in correct
+        // rotation already, so the rest of the windows can use seamless rotation.
+        if (w.mToken.hasFixedRotationTransform()) {
+            return true;
+        }
+        if (w != mDisplayContent.mCurrentFocus) {
             return false;
         }
         // We only enable seamless rotation if the top window has requested it and is in the
diff --git a/services/core/java/com/android/server/wm/SeamlessRotator.java b/services/core/java/com/android/server/wm/SeamlessRotator.java
index c621c48..024da88 100644
--- a/services/core/java/com/android/server/wm/SeamlessRotator.java
+++ b/services/core/java/com/android/server/wm/SeamlessRotator.java
@@ -20,6 +20,7 @@
 import static android.view.Surface.ROTATION_90;
 
 import android.graphics.Matrix;
+import android.graphics.Rect;
 import android.os.IBinder;
 import android.view.DisplayInfo;
 import android.view.Surface.Rotation;
@@ -27,6 +28,7 @@
 import android.view.SurfaceControl.Transaction;
 
 import com.android.server.wm.utils.CoordinateTransforms;
+import com.android.server.wm.utils.InsetUtils;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -45,34 +47,51 @@
     private final float[] mFloat9 = new float[9];
     private final int mOldRotation;
     private final int mNewRotation;
+    private final int mRotationDelta;
+    private final int mW;
+    private final int mH;
 
     public SeamlessRotator(@Rotation int oldRotation, @Rotation int newRotation, DisplayInfo info) {
         mOldRotation = oldRotation;
         mNewRotation = newRotation;
+        mRotationDelta = DisplayContent.deltaRotation(oldRotation, newRotation);
 
         final boolean flipped = info.rotation == ROTATION_90 || info.rotation == ROTATION_270;
-        final int h = flipped ? info.logicalWidth : info.logicalHeight;
-        final int w = flipped ? info.logicalHeight : info.logicalWidth;
+        mH = flipped ? info.logicalWidth : info.logicalHeight;
+        mW = flipped ? info.logicalHeight : info.logicalWidth;
 
         final Matrix tmp = new Matrix();
-        CoordinateTransforms.transformLogicalToPhysicalCoordinates(oldRotation, w, h, mTransform);
-        CoordinateTransforms.transformPhysicalToLogicalCoordinates(newRotation, w, h, tmp);
+        CoordinateTransforms.transformLogicalToPhysicalCoordinates(oldRotation, mW, mH, mTransform);
+        CoordinateTransforms.transformPhysicalToLogicalCoordinates(newRotation, mW, mH, tmp);
         mTransform.postConcat(tmp);
     }
 
     /**
-     * Applies a transform to the {@link WindowState} surface that undoes the effect of the global
-     * display rotation.
+     * Applies a transform to the {@link WindowContainer} surface that undoes the effect of the
+     * global display rotation.
      */
-    public void unrotate(Transaction transaction, WindowState win) {
+    public void unrotate(Transaction transaction, WindowContainer win) {
         transaction.setMatrix(win.getSurfaceControl(), mTransform, mFloat9);
-
         // WindowState sets the position of the window so transform the position and update it.
         final float[] winSurfacePos = {win.mLastSurfacePosition.x, win.mLastSurfacePosition.y};
         mTransform.mapPoints(winSurfacePos);
         transaction.setPosition(win.getSurfaceControl(), winSurfacePos[0], winSurfacePos[1]);
     }
 
+    /** Rotates the frame from {@link #mNewRotation} to {@link #mOldRotation}. */
+    void unrotateFrame(Rect inOut) {
+        if (mRotationDelta == ROTATION_90) {
+            inOut.set(inOut.top, mH - inOut.right, inOut.bottom, mH - inOut.left);
+        } else if (mRotationDelta == ROTATION_270) {
+            inOut.set(mW - inOut.bottom, inOut.left, mW - inOut.top, inOut.right);
+        }
+    }
+
+    /** Rotates the insets from {@link #mNewRotation} to {@link #mOldRotation}. */
+    void unrotateInsets(Rect inOut) {
+        InsetUtils.rotateInsets(inOut, mRotationDelta);
+    }
+
     /**
      * Returns the rotation of the display before it started rotating.
      *
@@ -95,10 +114,8 @@
      * it.
      */
     public void finish(WindowState win, boolean timeout) {
-        mTransform.reset();
         final Transaction t = win.getPendingTransaction();
-        t.setMatrix(win.mSurfaceControl, mTransform, mFloat9);
-        t.setPosition(win.mSurfaceControl, win.mLastSurfacePosition.x, win.mLastSurfacePosition.y);
+        finish(t, win);
         if (win.mWinAnimator.mSurfaceController != null && !timeout) {
             t.deferTransactionUntil(win.mSurfaceControl,
                     win.getDeferTransactionBarrier(), win.getFrameNumber());
@@ -107,6 +124,13 @@
         }
     }
 
+    /** Removes the transform and restore to the original last position. */
+    void finish(Transaction t, WindowContainer win) {
+        mTransform.reset();
+        t.setMatrix(win.mSurfaceControl, mTransform, mFloat9);
+        t.setPosition(win.mSurfaceControl, win.mLastSurfacePosition.x, win.mLastSurfacePosition.y);
+    }
+
     public void dump(PrintWriter pw) {
         pw.print("{old="); pw.print(mOldRotation); pw.print(", new="); pw.print(mNewRotation);
         pw.print("}");
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 294c36a..da996dc 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -141,6 +141,12 @@
     @ActivityInfo.ScreenOrientation
     protected int mOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
 
+    /**
+     * The window container which decides its orientation since the last time
+     * {@link #getOrientation(int) was called.
+     */
+    protected WindowContainer mLastOrientationSource;
+
     private final Pools.SynchronizedPool<ForAllWindowsConsumerWrapper> mConsumerWrapperPool =
             new Pools.SynchronizedPool<>(3);
 
@@ -1061,6 +1067,7 @@
      * @return The orientation as specified by this branch or the window hierarchy.
      */
     int getOrientation(int candidate) {
+        mLastOrientationSource = null;
         if (!fillsParent()) {
             // Ignore containers that don't completely fill their parents.
             return SCREEN_ORIENTATION_UNSET;
@@ -1072,6 +1079,7 @@
         // if none of the children have a better candidate for the orientation.
         if (mOrientation != SCREEN_ORIENTATION_UNSET
                 && mOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+            mLastOrientationSource = this;
             return mOrientation;
         }
 
@@ -1087,6 +1095,7 @@
                 // can find one. Else return SCREEN_ORIENTATION_BEHIND so the caller can choose to
                 // look behind this container.
                 candidate = orientation;
+                mLastOrientationSource = wc;
                 continue;
             }
 
@@ -1100,6 +1109,7 @@
                 ProtoLog.v(WM_DEBUG_ORIENTATION, "%s is requesting orientation %d (%s)",
                         wc.toString(), orientation,
                         ActivityInfo.screenOrientationToString(orientation));
+                mLastOrientationSource = wc;
                 return orientation;
             }
         }
@@ -1108,6 +1118,22 @@
     }
 
     /**
+     * @return The deepest source which decides the orientation of this window container since the
+     *         last time {@link #getOrientation(int) was called.
+     */
+    @Nullable
+    WindowContainer getLastOrientationSource() {
+        final WindowContainer source = mLastOrientationSource;
+        if (source != null && source != this) {
+            final WindowContainer nextSource = source.getLastOrientationSource();
+            if (nextSource != null) {
+                return nextSource;
+            }
+        }
+        return source;
+    }
+
+    /**
      * Returns true if this container is opaque and fills all the space made available by its parent
      * container.
      *
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 59eee9c..240f566 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -120,6 +120,11 @@
         public void onAppTransitionCancelledLocked(int transit) {}
 
         /**
+         * Called when an app transition is timed out.
+         */
+        public void onAppTransitionTimeoutLocked() {}
+
+        /**
          * Called when an app transition gets started
          *
          * @param transit transition type indicating what kind of transition gets run, must be one
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index e01e8d22..e1f713e 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -412,6 +412,10 @@
 
     private static final int ANIMATION_COMPLETED_TIMEOUT_MS = 5000;
 
+    // TODO(b/143053092): Remove the settings if it becomes stable.
+    private static final String FIXED_ROTATION_TRANSFORM_SETTING_NAME = "fixed_rotation_transform";
+    boolean mIsFixedRotationTransformEnabled;
+
     final WindowManagerConstants mConstants;
 
     final WindowTracing mWindowTracing;
@@ -738,6 +742,8 @@
                 DEVELOPMENT_ENABLE_SIZECOMPAT_FREEFORM);
         private final Uri mRenderShadowsInCompositorUri = Settings.Global.getUriFor(
                 DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR);
+        private final Uri mFixedRotationTransformUri = Settings.Global.getUriFor(
+                FIXED_ROTATION_TRANSFORM_SETTING_NAME);
 
         public SettingsObserver() {
             super(new Handler());
@@ -762,6 +768,8 @@
                     UserHandle.USER_ALL);
             resolver.registerContentObserver(mRenderShadowsInCompositorUri, false, this,
                     UserHandle.USER_ALL);
+            resolver.registerContentObserver(mFixedRotationTransformUri, false, this,
+                    UserHandle.USER_ALL);
         }
 
         @Override
@@ -805,6 +813,11 @@
                 return;
             }
 
+            if (mFixedRotationTransformUri.equals(uri)) {
+                updateFixedRotationTransform();
+                return;
+            }
+
             @UpdateAnimationScaleMode
             final int mode;
             if (mWindowAnimationScaleUri.equals(uri)) {
@@ -821,6 +834,12 @@
             mH.sendMessage(m);
         }
 
+        void loadSettings() {
+            updateSystemUiSettings();
+            updatePointerLocation();
+            updateFixedRotationTransform();
+        }
+
         void updateSystemUiSettings() {
             boolean changed;
             synchronized (mGlobalLock) {
@@ -884,6 +903,11 @@
 
             mAtmService.mSizeCompatFreeform = sizeCompatFreeform;
         }
+
+        void updateFixedRotationTransform() {
+            mIsFixedRotationTransformEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+                    FIXED_ROTATION_TRANSFORM_SETTING_NAME, 0) != 0;
+        }
     }
 
     private void setShadowRenderer() {
@@ -1651,7 +1675,7 @@
                     outFrame, outContentInsets, outStableInsets, outDisplayCutout)) {
                 res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS;
             }
-            outInsetsState.set(displayContent.getInsetsPolicy().getInsetsForDispatch(win),
+            outInsetsState.set(win.getInsetsState(),
                     win.mClient instanceof IWindow.Stub /* copySource */);
 
             if (mInTouchMode) {
@@ -2389,7 +2413,7 @@
                     outStableInsets);
             outCutout.set(win.getWmDisplayCutout().getDisplayCutout());
             outBackdropFrame.set(win.getBackdropFrame(win.getFrameLw()));
-            outInsetsState.set(displayContent.getInsetsPolicy().getInsetsForDispatch(win),
+            outInsetsState.set(win.getInsetsState(),
                     win.mClient instanceof IWindow.Stub /* copySource */);
             if (DEBUG) {
                 Slog.v(TAG_WM, "Relayout given client " + client.asBinder()
@@ -4574,8 +4598,7 @@
         mTaskSnapshotController.systemReady();
         mHasWideColorGamutSupport = queryWideColorGamutSupport();
         mHasHdrSupport = queryHdrSupport();
-        UiThread.getHandler().post(mSettingsObserver::updateSystemUiSettings);
-        UiThread.getHandler().post(mSettingsObserver::updatePointerLocation);
+        UiThread.getHandler().post(mSettingsObserver::loadSettings);
         IVrManager vrManager = IVrManager.Stub.asInterface(
                 ServiceManager.getService(Context.VR_SERVICE));
         if (vrManager != null) {
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index c7d00ec..03dc4c9 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -221,7 +221,9 @@
             // has been sent to client by {@link android.app.IApplicationThread#bindApplication}.
             // If this process is system server, it is fine because system is booting and a new
             // configuration will update when display is ready.
-            setLastReportedConfiguration(getConfiguration());
+            if (thread != null) {
+                setLastReportedConfiguration(getConfiguration());
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 46081c5..78d6b9631 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -696,6 +696,11 @@
             return;
         }
 
+        if (mToken.hasFixedRotationTransform()) {
+            // The transform of its surface is handled by fixed rotation.
+            return;
+        }
+
         if (mPendingSeamlessRotate != null) {
             oldRotation = mPendingSeamlessRotate.getOldRotation();
         }
@@ -1000,6 +1005,7 @@
         final boolean isFullscreenAndFillsDisplay = !inMultiWindowMode() && matchesDisplayBounds();
         final boolean windowsAreFloating = task != null && task.isFloating();
         final DisplayContent dc = getDisplayContent();
+        final DisplayInfo displayInfo = getDisplayInfo();
         final WindowFrames windowFrames = getLayoutingWindowFrames();
 
         mInsetFrame.set(getBounds());
@@ -1086,7 +1092,7 @@
             layoutXDiff = mInsetFrame.left - windowFrames.mContainingFrame.left;
             layoutYDiff = mInsetFrame.top - windowFrames.mContainingFrame.top;
             layoutContainingFrame = mInsetFrame;
-            mTmpRect.set(0, 0, dc.getDisplayInfo().logicalWidth, dc.getDisplayInfo().logicalHeight);
+            mTmpRect.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight);
             subtractInsets(windowFrames.mDisplayFrame, layoutContainingFrame, layoutDisplayFrame,
                     mTmpRect);
             if (!layoutInParentFrame()) {
@@ -1161,9 +1167,8 @@
                     windowFrames.mDisplayFrame);
             windowFrames.calculateDockedDividerInsets(c.getDisplayCutout().getSafeInsets());
         } else {
-            getDisplayContent().getBounds(mTmpRect);
-            windowFrames.calculateInsets(
-                    windowsAreFloating, isFullscreenAndFillsDisplay, mTmpRect);
+            windowFrames.calculateInsets(windowsAreFloating, isFullscreenAndFillsDisplay,
+                    getDisplayFrames(dc.mDisplayFrames).mUnrestricted);
         }
 
         windowFrames.setDisplayCutout(
@@ -1186,12 +1191,8 @@
 
         if (mIsWallpaper && (fw != windowFrames.mFrame.width()
                 || fh != windowFrames.mFrame.height())) {
-            final DisplayContent displayContent = getDisplayContent();
-            if (displayContent != null) {
-                final DisplayInfo displayInfo = displayContent.getDisplayInfo();
-                getDisplayContent().mWallpaperController.updateWallpaperOffset(this,
-                        displayInfo.logicalWidth, displayInfo.logicalHeight, false);
-            }
+            dc.mWallpaperController.updateWallpaperOffset(this,
+                    displayInfo.logicalWidth, displayInfo.logicalHeight, false /* sync */);
         }
 
         // Calculate relative frame
@@ -1467,9 +1468,28 @@
         }
     }
 
+    DisplayFrames getDisplayFrames(DisplayFrames originalFrames) {
+        final DisplayFrames diplayFrames = mToken.getFixedRotationTransformDisplayFrames();
+        if (diplayFrames != null) {
+            return diplayFrames;
+        }
+        return originalFrames;
+    }
+
     DisplayInfo getDisplayInfo() {
-        final DisplayContent displayContent = getDisplayContent();
-        return displayContent != null ? displayContent.getDisplayInfo() : null;
+        final DisplayInfo displayInfo = mToken.getFixedRotationTransformDisplayInfo();
+        if (displayInfo != null) {
+            return displayInfo;
+        }
+        return getDisplayContent().getDisplayInfo();
+    }
+
+    InsetsState getInsetsState() {
+        final InsetsState insetsState = mToken.getFixedRotationTransformInsetsState();
+        if (insetsState != null) {
+            return insetsState;
+        }
+        return getDisplayContent().getInsetsPolicy().getInsetsForDispatch(this);
     }
 
     @Override
@@ -1990,6 +2010,11 @@
     }
 
     private boolean matchesDisplayBounds() {
+        final Rect displayBounds = mToken.getFixedRotationTransformDisplayBounds();
+        if (displayBounds != null) {
+            // If the rotated display bounds are available, the window bounds are also rotated.
+            return displayBounds.equals(getBounds());
+        }
         return getDisplayContent().getBounds().equals(getBounds());
     }
 
@@ -4786,7 +4811,7 @@
         if (!displayContent.isDefaultDisplay && !displayContent.supportsSystemDecorations()) {
             // On a different display there is no system decor. Crop the window
             // by the screen boundaries.
-            final DisplayInfo displayInfo = displayContent.getDisplayInfo();
+            final DisplayInfo displayInfo = getDisplayInfo();
             policyCrop.set(0, 0, mWindowFrames.mCompatFrame.width(),
                     mWindowFrames.mCompatFrame.height());
             policyCrop.intersect(-mWindowFrames.mCompatFrame.left, -mWindowFrames.mCompatFrame.top,
@@ -5012,7 +5037,7 @@
             return;
         }
 
-        final DisplayInfo displayInfo = getDisplayContent().getDisplayInfo();
+        final DisplayInfo displayInfo = getDisplayInfo();
         anim.initialize(mWindowFrames.mFrame.width(), mWindowFrames.mFrame.height(),
                 displayInfo.appWidth, displayInfo.appHeight);
         anim.restrictDuration(MAX_ANIMATION_DURATION);
@@ -5550,7 +5575,7 @@
     }
 
     /**
-     * If the transient frame is set, the computed result won't be used in real layout. So this
+     * If the simulated frame is set, the computed result won't be used in real layout. So this
      * frames must be cleared when the simulated computation is done.
      */
     void setSimulatedWindowFrames(WindowFrames windowFrames) {
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 43cd66d..1180566 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -36,15 +36,20 @@
 import static com.android.server.wm.WindowTokenProto.WINDOW_CONTAINER;
 
 import android.annotation.CallSuper;
+import android.content.res.Configuration;
+import android.graphics.Rect;
 import android.os.Debug;
 import android.os.IBinder;
 import android.util.proto.ProtoOutputStream;
+import android.view.DisplayInfo;
+import android.view.InsetsState;
 import android.view.SurfaceControl;
 
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.protolog.common.ProtoLog;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Comparator;
 
 /**
@@ -84,6 +89,57 @@
     /** The owner has {@link android.Manifest.permission#MANAGE_APP_TOKENS} */
     final boolean mOwnerCanManageAppTokens;
 
+    private FixedRotationTransformState mFixedRotationTransformState;
+
+    /**
+     * Used to fix the transform of the token to be rotated to a rotation different than it's
+     * display. The window frames and surfaces corresponding to this token will be layouted and
+     * rotated by the given rotated display info, frames and insets.
+     */
+    private static class FixedRotationTransformState {
+        final DisplayInfo mDisplayInfo;
+        final DisplayFrames mDisplayFrames;
+        final InsetsState mInsetsState;
+        final Configuration mRotatedOverrideConfiguration;
+        final SeamlessRotator mRotator;
+        final ArrayList<WindowContainer<?>> mRotatedContainers = new ArrayList<>();
+        boolean mIsTransforming = true;
+
+        FixedRotationTransformState(DisplayInfo rotatedDisplayInfo,
+                DisplayFrames rotatedDisplayFrames, InsetsState rotatedInsetsState,
+                Configuration rotatedConfig, int currentRotation) {
+            mDisplayInfo = rotatedDisplayInfo;
+            mDisplayFrames = rotatedDisplayFrames;
+            mInsetsState = rotatedInsetsState;
+            mRotatedOverrideConfiguration = rotatedConfig;
+            // This will use unrotate as rotate, so the new and old rotation are inverted.
+            mRotator = new SeamlessRotator(rotatedDisplayInfo.rotation, currentRotation,
+                    rotatedDisplayInfo);
+        }
+
+        /**
+         * Transforms the window container from the next rotation to the current rotation for
+         * showing the window in a display with different rotation.
+         */
+        void transform(WindowContainer<?> container) {
+            mRotator.unrotate(container.getPendingTransaction(), container);
+            if (!mRotatedContainers.contains(container)) {
+                mRotatedContainers.add(container);
+            }
+        }
+
+        /**
+         * Resets the transformation of the window containers which have been rotated. This should
+         * be called when the window has the same rotation as display.
+         */
+        void resetTransform() {
+            for (int i = mRotatedContainers.size() - 1; i >= 0; i--) {
+                final WindowContainer<?> c = mRotatedContainers.get(i);
+                mRotator.finish(c.getPendingTransaction(), c);
+            }
+        }
+    }
+
     /**
      * Compares two child window of this token and returns -1 if the first is lesser than the
      * second in terms of z-order and 1 otherwise.
@@ -274,6 +330,107 @@
         return builder;
     }
 
+    boolean hasFixedRotationTransform() {
+        return mFixedRotationTransformState != null;
+    }
+
+    boolean isFinishingFixedRotationTransform() {
+        return mFixedRotationTransformState != null
+                && !mFixedRotationTransformState.mIsTransforming;
+    }
+
+    boolean isFixedRotationTransforming() {
+        return mFixedRotationTransformState != null
+                && mFixedRotationTransformState.mIsTransforming;
+    }
+
+    DisplayInfo getFixedRotationTransformDisplayInfo() {
+        return isFixedRotationTransforming() ? mFixedRotationTransformState.mDisplayInfo : null;
+    }
+
+    DisplayFrames getFixedRotationTransformDisplayFrames() {
+        return isFixedRotationTransforming() ? mFixedRotationTransformState.mDisplayFrames : null;
+    }
+
+    Rect getFixedRotationTransformDisplayBounds() {
+        return isFixedRotationTransforming()
+                ? mFixedRotationTransformState.mRotatedOverrideConfiguration.windowConfiguration
+                        .getBounds()
+                : null;
+    }
+
+    InsetsState getFixedRotationTransformInsetsState() {
+        return isFixedRotationTransforming() ? mFixedRotationTransformState.mInsetsState : null;
+    }
+
+    /** Applies the rotated layout environment to this token in the simulated rotated display. */
+    void applyFixedRotationTransform(DisplayInfo info, DisplayFrames displayFrames,
+            Configuration config) {
+        if (mFixedRotationTransformState != null) {
+            return;
+        }
+        final InsetsState insetsState = new InsetsState();
+        mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames, insetsState,
+                mDisplayContent.getConfiguration().uiMode);
+        mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames,
+                insetsState, new Configuration(config), mDisplayContent.getRotation());
+        onConfigurationChanged(getParent().getConfiguration());
+    }
+
+    /** Clears the transformation and continue updating the orientation change of display. */
+    void clearFixedRotationTransform() {
+        if (mFixedRotationTransformState == null) {
+            return;
+        }
+        mFixedRotationTransformState.resetTransform();
+        // Clear the flag so if the display will be updated to the same orientation, the transform
+        // won't take effect. The state is cleared at the end, because it is used to indicate that
+        // other windows can use seamless rotation when applying rotation to display.
+        mFixedRotationTransformState.mIsTransforming = false;
+        final boolean changed =
+                mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp(this);
+        // If it is not the launching app or the display is not rotated, make sure the merged
+        // override configuration is restored from parent.
+        if (!changed) {
+            onMergedOverrideConfigurationChanged();
+        }
+        mFixedRotationTransformState = null;
+    }
+
+    @Override
+    void resolveOverrideConfiguration(Configuration newParentConfig) {
+        super.resolveOverrideConfiguration(newParentConfig);
+        if (isFixedRotationTransforming()) {
+            // Apply the rotated configuration to current resolved configuration, so the merged
+            // override configuration can update to the same state.
+            getResolvedOverrideConfiguration().updateFrom(
+                    mFixedRotationTransformState.mRotatedOverrideConfiguration);
+        }
+    }
+
+    @Override
+    void updateSurfacePosition() {
+        super.updateSurfacePosition();
+        if (isFixedRotationTransforming()) {
+            // The window is layouted in a simulated rotated display but the real display hasn't
+            // rotated, so here transforms its surface to fit in the real display.
+            mFixedRotationTransformState.transform(this);
+        }
+    }
+
+    /**
+     * Converts the rotated animation frames and insets back to display space for local animation.
+     * It should only be called when {@link #hasFixedRotationTransform} is true.
+     */
+    void unrotateAnimationFrames(Rect outFrame, Rect outInsets, Rect outStableInsets,
+            Rect outSurfaceInsets) {
+        final SeamlessRotator rotator = mFixedRotationTransformState.mRotator;
+        rotator.unrotateFrame(outFrame);
+        rotator.unrotateInsets(outInsets);
+        rotator.unrotateInsets(outStableInsets);
+        rotator.unrotateInsets(outSurfaceInsets);
+    }
+
     @CallSuper
     @Override
     public void dumpDebug(ProtoOutputStream proto, long fieldId,
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
index 393d8b8..52fc3de 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
@@ -54,8 +54,12 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 
+import android.app.ActivityManager;
+import android.app.IApplicationThread;
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
 import android.os.UserHandle;
@@ -1109,6 +1113,37 @@
     }
 
     @Test
+    public void testNavigateUpTo() {
+        final ActivityStartController controller = mock(ActivityStartController.class);
+        final ActivityStarter starter = new ActivityStarter(controller,
+                mService, mService.mStackSupervisor, mock(ActivityStartInterceptor.class));
+        doReturn(controller).when(mService).getActivityStartController();
+        spyOn(starter);
+        doReturn(ActivityManager.START_SUCCESS).when(starter).execute();
+
+        final ActivityRecord firstActivity = new ActivityBuilder(mService).setTask(mTask).build();
+        final ActivityRecord secondActivity = new ActivityBuilder(mService).setTask(mTask)
+                .setUid(firstActivity.getUid() + 1).build();
+        doReturn(starter).when(controller).obtainStarter(eq(firstActivity.intent), anyString());
+
+        final IApplicationThread thread = secondActivity.app.getThread();
+        secondActivity.app.setThread(null);
+        // This should do nothing from a non-attached caller.
+        assertFalse(mStack.navigateUpTo(secondActivity /* source record */,
+                firstActivity.intent /* destIntent */, 0 /* resultCode */, null /* resultData */));
+
+        secondActivity.app.setThread(thread);
+        assertTrue(mStack.navigateUpTo(secondActivity /* source record */,
+                firstActivity.intent /* destIntent */, 0 /* resultCode */, null /* resultData */));
+        // The firstActivity uses default launch mode, so the activities between it and itself will
+        // be finished.
+        assertTrue(secondActivity.finishing);
+        assertTrue(firstActivity.finishing);
+        // The caller uid of the new activity should be the current real caller.
+        assertEquals(starter.mRequest.callingUid, secondActivity.getUid());
+    }
+
+    @Test
     public void testResetTaskWithFinishingActivities() {
         final ActivityRecord taskTop =
                 new ActivityBuilder(mService).setStack(mStack).setCreateTask(true).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
index c93dc97..b6b3bc6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
@@ -238,6 +238,7 @@
             aInfo.applicationInfo.uid = mUid;
             aInfo.processName = mProcessName;
             aInfo.packageName = mComponent.getPackageName();
+            aInfo.name = mComponent.getClassName();
             if (mTargetActivity != null) {
                 aInfo.targetActivity = mTargetActivity;
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 3b6816a..1a8f2a6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -995,6 +995,35 @@
     }
 
     @Test
+    public void testApplyTopFixedRotationTransform() {
+        mWm.mIsFixedRotationTransformEnabled = true;
+        final Configuration config90 = new Configuration();
+        mDisplayContent.getDisplayRotation().setRotation(ROTATION_90);
+        mDisplayContent.computeScreenConfiguration(config90);
+        mDisplayContent.onRequestedOverrideConfigurationChanged(config90);
+
+        final Configuration config = new Configuration();
+        mDisplayContent.getDisplayRotation().setRotation(Surface.ROTATION_0);
+        mDisplayContent.computeScreenConfiguration(config);
+        mDisplayContent.onRequestedOverrideConfigurationChanged(config);
+
+        final ActivityRecord app = mAppWindow.mActivityRecord;
+        mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_ACTIVITY_OPEN,
+                false /* alwaysKeepCurrent */);
+        mDisplayContent.mOpeningApps.add(app);
+        app.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+        assertTrue(app.isFixedRotationTransforming());
+        assertEquals(config.orientation, mDisplayContent.getConfiguration().orientation);
+        assertEquals(config90.orientation, app.getConfiguration().orientation);
+
+        mDisplayContent.mAppTransition.notifyAppTransitionFinishedLocked(app.token);
+
+        assertFalse(app.hasFixedRotationTransform());
+        assertEquals(config90.orientation, mDisplayContent.getConfiguration().orientation);
+    }
+
+    @Test
     public void testRemoteRotation() {
         DisplayContent dc = createNewDisplay();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index da807d8..cf7411e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -638,7 +638,7 @@
         mBuilder.build();
 
         final WindowState win = mock(WindowState.class);
-        win.mActivityRecord = mock(ActivityRecord.class);
+        win.mToken = win.mActivityRecord = mock(ActivityRecord.class);
         final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
         attrs.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
 
diff --git a/tests/net/common/java/android/net/NetworkScoreTest.kt b/tests/net/common/java/android/net/NetworkScoreTest.kt
index a63d58d..3e10992 100644
--- a/tests/net/common/java/android/net/NetworkScoreTest.kt
+++ b/tests/net/common/java/android/net/NetworkScoreTest.kt
@@ -17,6 +17,9 @@
 package android.net
 
 import android.net.NetworkScore.Metrics.BANDWIDTH_UNKNOWN
+import android.net.NetworkScore.POLICY_DEFAULT_SUBSCRIPTION
+import android.net.NetworkScore.POLICY_IGNORE_ON_WIFI
+import android.net.NetworkScore.RANGE_MEDIUM
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import com.android.testutils.assertParcelSane
@@ -39,19 +42,19 @@
         assertEquals(TEST_SCORE, builder.build().getLegacyScore())
         assertParcelSane(builder.build(), 7)
 
-        builder.addPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI)
-                .addPolicy(NetworkScore.POLICY_DEFAULT_SUBSCRIPTION)
+        builder.addPolicy(POLICY_IGNORE_ON_WIFI)
+                .addPolicy(POLICY_DEFAULT_SUBSCRIPTION)
                 .setLinkLayerMetrics(NetworkScore.Metrics(44 /* latency */,
                         380 /* downlinkBandwidth */, BANDWIDTH_UNKNOWN /* uplinkBandwidth */))
                 .setEndToEndMetrics(NetworkScore.Metrics(11 /* latency */,
                         BANDWIDTH_UNKNOWN /* downlinkBandwidth */, 100_000 /* uplinkBandwidth */))
                 .setRange(NetworkScore.RANGE_MEDIUM)
         assertParcelSane(builder.build(), 7)
-        builder.clearPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI)
+        builder.clearPolicy(POLICY_IGNORE_ON_WIFI)
         val ns = builder.build()
         assertParcelSane(ns, 7)
-        assertFalse(ns.hasPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI))
-        assertTrue(ns.hasPolicy(NetworkScore.POLICY_DEFAULT_SUBSCRIPTION))
+        assertFalse(ns.hasPolicy(POLICY_IGNORE_ON_WIFI))
+        assertTrue(ns.hasPolicy(POLICY_DEFAULT_SUBSCRIPTION))
 
         val exitingNs = ns.withExiting(true)
         assertNotEquals(ns.isExiting, exitingNs.isExiting)
@@ -73,4 +76,17 @@
         assertTrue(builder1.build().equals(builder2.build()))
         assertEquals(builder1.build().hashCode(), builder2.build().hashCode())
     }
+
+    @Test
+    fun testBuilderEquals() {
+        val ns = NetworkScore.Builder()
+                .addPolicy(POLICY_IGNORE_ON_WIFI)
+                .addPolicy(POLICY_DEFAULT_SUBSCRIPTION)
+                .setExiting(true)
+                .setEndToEndMetrics(NetworkScore.Metrics(145, 2500, 1430))
+                .setRange(RANGE_MEDIUM)
+                .setSignalStrength(400)
+                .build()
+        assertEquals(ns, NetworkScore.Builder(ns).build())
+    }
 }
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 2e59966..957683e 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -78,6 +78,7 @@
 import static android.net.NetworkPolicyManager.RULE_REJECT_ALL;
 import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.system.OsConstants.IPPROTO_TCP;
 
 import static com.android.server.ConnectivityServiceTestUtilsKt.transportToLegacyType;
 import static com.android.testutils.ConcurrentUtilsKt.await;
@@ -138,6 +139,7 @@
 import android.content.res.Resources;
 import android.location.LocationManager;
 import android.net.CaptivePortalData;
+import android.net.ConnectionInfo;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.ConnectivityManager.PacketKeepalive;
@@ -153,6 +155,7 @@
 import android.net.INetworkPolicyListener;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
+import android.net.InetAddresses;
 import android.net.InterfaceConfiguration;
 import android.net.IpPrefix;
 import android.net.IpSecManager;
@@ -176,6 +179,7 @@
 import android.net.SocketKeepalive;
 import android.net.UidRange;
 import android.net.Uri;
+import android.net.VpnManager;
 import android.net.metrics.IpConnectivityLog;
 import android.net.shared.NetworkMonitorUtils;
 import android.net.shared.PrivateDnsConfig;
@@ -272,6 +276,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 import kotlin.reflect.KClass;
 
@@ -445,15 +450,21 @@
             return mPackageManager;
         }
 
+        private int checkMockedPermission(String permission, Supplier<Integer> ifAbsent) {
+            final Integer granted = mMockedPermissions.get(permission);
+            return granted != null ? granted : ifAbsent.get();
+        }
+
         @Override
         public int checkPermission(String permission, int pid, int uid) {
-            final Integer granted = mMockedPermissions.get(permission);
-            if (granted == null) {
-                // All non-mocked permissions should be held by the test or unnecessary: check as
-                // normal to make sure the code does not rely on unexpected permissions.
-                return super.checkPermission(permission, pid, uid);
-            }
-            return granted;
+            return checkMockedPermission(
+                    permission, () -> super.checkPermission(permission, pid, uid));
+        }
+
+        @Override
+        public int checkCallingOrSelfPermission(String permission) {
+            return checkMockedPermission(
+                    permission, () -> super.checkCallingOrSelfPermission(permission));
         }
 
         @Override
@@ -1002,6 +1013,7 @@
         // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does
         // not inherit from NetworkAgent.
         private TestNetworkAgentWrapper mMockNetworkAgent;
+        private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
 
         private VpnInfo mVpnInfo;
 
@@ -1022,6 +1034,10 @@
             updateCapabilities(null /* defaultNetwork */);
         }
 
+        public void setVpnType(int vpnType) {
+            mVpnType = vpnType;
+        }
+
         @Override
         public int getNetId() {
             if (mMockNetworkAgent == null) {
@@ -1040,6 +1056,11 @@
             return mConnected;  // Similar trickery
         }
 
+        @Override
+        public int getActiveAppVpnType() {
+            return mVpnType;
+        }
+
         private void connect(boolean isAlwaysMetered) {
             mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities());
             mConnected = true;
@@ -6429,6 +6450,90 @@
         assertEquals(Process.INVALID_UID, newNc.getOwnerUid());
     }
 
+    private void setupConnectionOwnerUid(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
+            throws Exception {
+        final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(VPN_USER));
+        establishVpn(new LinkProperties(), vpnOwnerUid, vpnRange);
+        mMockVpn.setVpnType(vpnType);
+
+        final VpnInfo vpnInfo = new VpnInfo();
+        vpnInfo.ownerUid = vpnOwnerUid;
+        mMockVpn.setVpnInfo(vpnInfo);
+    }
+
+    private void setupConnectionOwnerUidAsVpnApp(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
+            throws Exception {
+        setupConnectionOwnerUid(vpnOwnerUid, vpnType);
+
+        // Test as VPN app
+        mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED);
+        mServiceContext.setPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_DENIED);
+    }
+
+    private ConnectionInfo getTestConnectionInfo() throws Exception {
+        return new ConnectionInfo(
+                IPPROTO_TCP,
+                new InetSocketAddress(InetAddresses.parseNumericAddress("1.2.3.4"), 1234),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2.3.4.5"), 2345));
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidPlatformVpn() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_PLATFORM);
+
+        try {
+            mService.getConnectionOwnerUid(getTestConnectionInfo());
+            fail("Expected SecurityException for non-VpnService app");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceWrongUser() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUidAsVpnApp(myUid + 1, VpnManager.TYPE_VPN_SERVICE);
+
+        try {
+            mService.getConnectionOwnerUid(getTestConnectionInfo());
+            fail("Expected SecurityException for non-VpnService app");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceDoesNotThrow() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_SERVICE);
+
+        // TODO: Test the returned UID
+        mService.getConnectionOwnerUid(getTestConnectionInfo());
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceNetworkStackDoesNotThrow() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE);
+        mServiceContext.setPermission(
+                android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED);
+
+        // TODO: Test the returned UID
+        mService.getConnectionOwnerUid(getTestConnectionInfo());
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceMainlineNetworkStackDoesNotThrow()
+            throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE);
+        mServiceContext.setPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED);
+
+        // TODO: Test the returned UID
+        mService.getConnectionOwnerUid(getTestConnectionInfo());
+    }
+
     private TestNetworkAgentWrapper establishVpn(
             LinkProperties lp, int ownerUid, Set<UidRange> vpnRange) throws Exception {
         final TestNetworkAgentWrapper
diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
index eb78529..ac1c518 100644
--- a/tests/net/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -656,8 +656,12 @@
     }
 
     private Vpn createVpnAndSetupUidChecks(int... grantedOps) throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
+        return createVpnAndSetupUidChecks(primaryUser, grantedOps);
+    }
+
+    private Vpn createVpnAndSetupUidChecks(UserInfo user, int... grantedOps) throws Exception {
+        final Vpn vpn = createVpn(user.id);
+        setMockedUsers(user);
 
         when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
                 .thenReturn(Process.myUid());
@@ -726,6 +730,19 @@
     }
 
     @Test
+    public void testProvisionVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile, mKeyStore);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
     public void testDeleteVpnProfile() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
 
@@ -736,6 +753,19 @@
     }
 
     @Test
+    public void testDeleteVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.deleteVpnProfile(TEST_VPN_PKG, mKeyStore);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
     public void testGetVpnProfilePrivileged() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
 
@@ -820,6 +850,32 @@
     }
 
     @Test
+    public void testStartVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testStopVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.stopVpnProfile(TEST_VPN_PKG);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
     public void testSetPackageAuthorizationVpnService() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();