Merge "Fix typo"
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/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto
index bf17c55..0aaf41e 100644
--- a/core/proto/android/app/settings_enums.proto
+++ b/core/proto/android/app/settings_enums.proto
@@ -2622,4 +2622,9 @@
     // CATEGORY: SETTINGS
     // OS: R
     NOTIFICATION_CONVERSATION_LIST_SETTINGS = 1834;
+
+    // Panel for Media Output Group operation
+    // CATEGORY: SETTINGS
+    // OS: R
+    PANEL_MEDIA_OUTPUT_GROUP = 1835;
 }
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/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/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/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/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/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();