Merge "Document more about volume provider ID."
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/NetworkScore.java b/core/java/android/net/NetworkScore.java
index d2e59eb..d207e79 100644
--- a/core/java/android/net/NetworkScore.java
+++ b/core/java/android/net/NetworkScore.java
@@ -83,6 +83,65 @@
             uplinkBandwidthKBps = uplinkBandwidth;
         }
 
+        /**
+         * Evaluate whether a metrics codes for faster network is faster than another.
+         *
+         * This is a simple comparison of expected speeds. If either of the tested attributes
+         * are unknown, this returns zero. This implementation just assumes downlink bandwidth
+         * is more important than uplink bandwidth, which is more important than latency. This
+         * is not a very good way of evaluating network speed, but it's a start.
+         * TODO : do something more representative of how fast the network feels
+         *
+         * @param other the Metrics to evaluate against
+         * @return a negative integer, zero, or a positive integer as this metrics is worse than,
+         *   equally good as (or unknown), or better than the passed Metrics.
+         * @see #compareToPreferringKnown(Metrics)
+         * @hide
+         */
+        // Can't implement Comparable<Metrics> because this is @hide.
+        public int compareTo(@NonNull final Metrics other) {
+            if (downlinkBandwidthKBps != BANDWIDTH_UNKNOWN
+                    && other.downlinkBandwidthKBps != BANDWIDTH_UNKNOWN) {
+                if (downlinkBandwidthKBps > other.downlinkBandwidthKBps) return 1;
+                if (downlinkBandwidthKBps < other.downlinkBandwidthKBps) return -1;
+            }
+            if (uplinkBandwidthKBps != BANDWIDTH_UNKNOWN
+                    && other.uplinkBandwidthKBps != BANDWIDTH_UNKNOWN) {
+                if (uplinkBandwidthKBps > other.uplinkBandwidthKBps) return 1;
+                if (uplinkBandwidthKBps < other.uplinkBandwidthKBps) return -1;
+            }
+            if (latencyMs != LATENCY_UNKNOWN && other.latencyMs != LATENCY_UNKNOWN) {
+                // Latency : lower is better
+                if (latencyMs > other.latencyMs) return -1;
+                if (latencyMs < other.latencyMs) return 1;
+            }
+            return 0;
+        }
+
+        /**
+         * Evaluate whether a metrics codes for faster network is faster than another.
+         *
+         * This is a simple comparison of expected speeds. If either of the tested attributes
+         * are unknown, this prefers the known attributes. This implementation just assumes
+         * downlink bandwidth is more important than uplink bandwidth, which is more important than
+         * latency. This is not a very good way of evaluating network speed, but it's a start.
+         * TODO : do something more representative of how fast the network feels
+         *
+         * @param other the Metrics to evaluate against
+         * @return a negative integer, zero, or a positive integer as this metrics is worse than,
+         *   equally good as (or unknown), or better than the passed Metrics.
+         * @see #compareTo(Metrics)
+         * @hide
+         */
+        public int compareToPreferringKnown(@NonNull final Metrics other) {
+            if (downlinkBandwidthKBps > other.downlinkBandwidthKBps) return 1;
+            if (downlinkBandwidthKBps < other.downlinkBandwidthKBps) return -1;
+            if (uplinkBandwidthKBps > other.uplinkBandwidthKBps) return 1;
+            if (uplinkBandwidthKBps < other.uplinkBandwidthKBps) return -1;
+            // Latency : lower is better
+            return -Integer.compare(latencyMs, other.latencyMs);
+        }
+
         /** toString */
         public String toString() {
             return "latency = " + latencyMs + " downlinkBandwidth = " + downlinkBandwidthKBps
@@ -346,6 +405,33 @@
         return mLegacyScore;
     }
 
+    /**
+     * Use the metrics to evaluate whether a network is faster than another.
+     *
+     * This is purely based on the metrics, and explicitly ignores policy or exiting. It's
+     * provided to get a decision on two networks when policy can not decide, or to evaluate
+     * how a network is expected to compare to another if it should validate.
+     *
+     * @param other the score to evaluate against
+     * @return whether this network is probably faster than the other
+     * @hide
+     */
+    public boolean probablyFasterThan(@NonNull final NetworkScore other) {
+        if (mLegacyScore > other.mLegacyScore) return true;
+        final int atEndToEnd = mEndToEndMetrics.compareTo(other.mEndToEndMetrics);
+        if (atEndToEnd > 0) return true;
+        if (atEndToEnd < 0) return false;
+        final int atLinkLayer = mLinkLayerMetrics.compareTo(other.mLinkLayerMetrics);
+        if (atLinkLayer > 0) return true;
+        if (atLinkLayer < 0) return false;
+        final int atEndToEndPreferringKnown =
+                mEndToEndMetrics.compareToPreferringKnown(other.mEndToEndMetrics);
+        if (atEndToEndPreferringKnown > 0) return true;
+        if (atEndToEndPreferringKnown < 0) return false;
+        // If this returns 0, neither is "probably faster" so just return false.
+        return mLinkLayerMetrics.compareToPreferringKnown(other.mLinkLayerMetrics) > 0;
+    }
+
     /** Builder for NetworkScore. */
     public static class Builder {
         private int mPolicy = 0;
diff --git a/core/java/com/android/internal/util/FunctionalUtils.java b/core/java/com/android/internal/util/FunctionalUtils.java
index 3c97917..720f4fe 100644
--- a/core/java/com/android/internal/util/FunctionalUtils.java
+++ b/core/java/com/android/internal/util/FunctionalUtils.java
@@ -16,12 +16,16 @@
 
 package com.android.internal.util;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.RemoteException;
 import android.util.ExceptionUtils;
 
+import java.util.Collection;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 /**
@@ -218,4 +222,20 @@
             }
         }
     }
+
+    /**
+     * Find the first element in the list that matches the predicate.
+     *
+     * The standard Java way of doing this is to use streams, which is very expensive.
+     *
+     * @return the first matching element, or null if none.
+     */
+    @Nullable
+    public static <T> T findFirst(@NonNull final Collection<T> haystack,
+            @NonNull final Predicate<T> p) {
+        for (final T needle : haystack) {
+            if (p.test(needle)) return needle;
+        }
+        return null;
+    }
 }
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/connectivity/NetworkRanker.java b/services/core/java/com/android/server/connectivity/NetworkRanker.java
index 1ae7dc5..c536ab2 100644
--- a/services/core/java/com/android/server/connectivity/NetworkRanker.java
+++ b/services/core/java/com/android/server/connectivity/NetworkRanker.java
@@ -16,8 +16,13 @@
 
 package com.android.server.connectivity;
 
+import static android.net.NetworkScore.POLICY_IGNORE_ON_WIFI;
+
+import static com.android.internal.util.FunctionalUtils.findFirst;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 
 import java.util.ArrayList;
@@ -37,15 +42,33 @@
             @NonNull final Collection<NetworkAgentInfo> nais) {
         final ArrayList<NetworkAgentInfo> candidates = new ArrayList<>(nais);
         candidates.removeIf(nai -> !nai.satisfies(request));
+        // Enforce policy.
+        filterBadWifiAvoidancePolicy(candidates);
 
         NetworkAgentInfo bestNetwork = null;
         int bestScore = Integer.MIN_VALUE;
         for (final NetworkAgentInfo nai : candidates) {
-            if (nai.getCurrentScore() > bestScore) {
+            final int score = nai.getCurrentScore();
+            if (score > bestScore) {
                 bestNetwork = nai;
-                bestScore = nai.getCurrentScore();
+                bestScore = score;
             }
         }
         return bestNetwork;
     }
+
+    // If some network with wifi transport is present, drop all networks with POLICY_IGNORE_ON_WIFI.
+    private void filterBadWifiAvoidancePolicy(
+            @NonNull final ArrayList<NetworkAgentInfo> candidates) {
+        final NetworkAgentInfo wifi = findFirst(candidates,
+                nai -> nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+                        && nai.everValidated
+                        // Horrible hack : there is old UI that will let a user say they want to
+                        // override the policy only for this network only at this time and it
+                        // feeds into the following member. This old UI should probably be removed
+                        // but for now keep backward compatibility.
+                        && !nai.avoidUnvalidated);
+        if (null == wifi) return; // No wifi : this policy doesn't apply
+        candidates.removeIf(nai -> nai.getNetworkScore().hasPolicy(POLICY_IGNORE_ON_WIFI));
+    }
 }
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 8e38114..05867ba 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -62,7 +62,7 @@
     private final BluetoothRouteProvider mBtRouteProvider;
 
     private static ComponentName sComponentName = new ComponentName(
-            SystemMediaRoute2Provider.class.getPackageName$(),
+            SystemMediaRoute2Provider.class.getPackage().getName(),
             SystemMediaRoute2Provider.class.getName());
 
     private String mSelectedRouteId;
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 3e10992..a7e33b3 100644
--- a/tests/net/common/java/android/net/NetworkScoreTest.kt
+++ b/tests/net/common/java/android/net/NetworkScoreTest.kt
@@ -16,7 +16,6 @@
 
 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
@@ -35,20 +34,24 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class NetworkScoreTest {
+    private fun makeScoreBuilder() = NetworkScore.Builder()
+            .setLegacyScore(TEST_SCORE)
+            .addPolicy(POLICY_IGNORE_ON_WIFI)
+            .addPolicy(POLICY_DEFAULT_SUBSCRIPTION)
+            .setExiting(false)
+            .setEndToEndMetrics(NetworkScore.Metrics(145 /* latency */,
+                    2500 /* downlinkBandwidth */, 1430 /* uplinkBandwidth */))
+            .setRange(RANGE_MEDIUM)
+            .setSignalStrength(400)
+
     @Test
     fun testParcelNetworkScore() {
         val defaultCap = NetworkCapabilities()
-        val builder = NetworkScore.Builder().setLegacyScore(TEST_SCORE)
-        assertEquals(TEST_SCORE, builder.build().getLegacyScore())
-        assertParcelSane(builder.build(), 7)
+        val legacyBuilder = NetworkScore.Builder().setLegacyScore(TEST_SCORE)
+        assertEquals(TEST_SCORE, legacyBuilder.build().getLegacyScore())
+        assertParcelSane(legacyBuilder.build(), 7)
 
-        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)
+        val builder = makeScoreBuilder()
         assertParcelSane(builder.build(), 7)
         builder.clearPolicy(POLICY_IGNORE_ON_WIFI)
         val ns = builder.build()
@@ -79,14 +82,7 @@
 
     @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()
+        val ns = makeScoreBuilder().build()
         assertEquals(ns, NetworkScore.Builder(ns).build())
     }
 }
diff --git a/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt
index 86c9116..a6b371a 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity
 
+import android.net.NetworkCapabilities
 import android.net.NetworkRequest
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
@@ -35,6 +36,7 @@
     private fun makeNai(satisfy: Boolean, score: Int) = mock(NetworkAgentInfo::class.java).also {
         doReturn(satisfy).`when`(it).satisfies(any())
         doReturn(score).`when`(it).currentScore
+        it.networkCapabilities = NetworkCapabilities()
     }
 
     @Test