Merge "Show the icon of the active mode in the Modes Tile" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 76012bb..9c93c3a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3434,6 +3434,23 @@
 
 package android.companion.virtual {
 
+  @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public final class ActivityPolicyExemption implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.content.ComponentName getComponentName();
+    method public int getDisplayId();
+    method @Nullable public String getPackageName();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.ActivityPolicyExemption> CREATOR;
+  }
+
+  public static final class ActivityPolicyExemption.Builder {
+    ctor public ActivityPolicyExemption.Builder();
+    method @NonNull public android.companion.virtual.ActivityPolicyExemption build();
+    method @NonNull public android.companion.virtual.ActivityPolicyExemption.Builder setComponentName(@NonNull android.content.ComponentName);
+    method @NonNull public android.companion.virtual.ActivityPolicyExemption.Builder setDisplayId(int);
+    method @NonNull public android.companion.virtual.ActivityPolicyExemption.Builder setPackageName(@NonNull String);
+  }
+
   public final class VirtualDevice implements android.os.Parcelable {
     method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomAudioInputSupport();
     method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomCameraSupport();
@@ -3467,9 +3484,7 @@
   public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable {
     method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
     method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull String);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName, int);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull String, int);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
     method public void addSoundEffectListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
     method @NonNull public android.content.Context createContext();
@@ -3494,9 +3509,7 @@
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
     method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
     method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
-    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull String);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName, int);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull String, int);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
     method public void removeSoundEffectListener(@NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
     method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int);
     method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int, int);
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 80764af..dbf9afd 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -112,6 +112,8 @@
 import android.permission.PermissionControllerManager;
 import android.permission.PermissionManager;
 import android.provider.Settings;
+import android.ravenwood.annotation.RavenwoodKeepPartialClass;
+import android.ravenwood.annotation.RavenwoodReplace;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -157,6 +159,7 @@
 import java.util.function.Function;
 
 /** @hide */
+@RavenwoodKeepPartialClass
 public class ApplicationPackageManager extends PackageManager {
     private static final String TAG = "ApplicationPackageManager";
     private static final boolean DEBUG_ICONS = false;
@@ -2163,6 +2166,7 @@
     }
 
     @UnsupportedAppUsage
+    @RavenwoodReplace(reason = "<cinit> crashes due to unsupported class PropertyInvalidatedCache")
     static void configurationChanged() {
         synchronized (sSync) {
             sIconCache.clear();
@@ -2170,6 +2174,10 @@
         }
     }
 
+    private static void configurationChanged$ravenwood() {
+        /* no-op */
+    }
+
     @UnsupportedAppUsage
     protected ApplicationPackageManager(ContextImpl context, IPackageManager pm) {
         mContext = context;
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 2d5dad0..84a4eb4 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -41,6 +41,7 @@
 import android.os.Process;
 import android.os.Trace;
 import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodReplace;
 import android.ravenwood.annotation.RavenwoodThrow;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -1409,6 +1410,7 @@
         return newKey;
     }
 
+    @RavenwoodThrow(reason = "AppInfo update not supported")
     public void appendPendingAppInfoUpdate(@NonNull String[] oldSourceDirs,
             @NonNull ApplicationInfo appInfo) {
         synchronized (mLock) {
@@ -1427,6 +1429,7 @@
         }
     }
 
+    @RavenwoodReplace(reason = "AppInfo update not supported")
     public final void applyAllPendingAppInfoUpdates() {
         synchronized (mLock) {
             if (mPendingAppInfoUpdates != null) {
@@ -1439,6 +1442,10 @@
         }
     }
 
+    private void applyAllPendingAppInfoUpdates$ravenwood() {
+        /* no-op */
+    }
+
     public final boolean applyConfigurationToResources(@NonNull Configuration config,
             @Nullable CompatibilityInfo compat) {
         synchronized (mLock) {
diff --git a/core/java/android/app/admin/Provisioning_OWNERS b/core/java/android/app/admin/Provisioning_OWNERS
index 91b9761..09ebb26 100644
--- a/core/java/android/app/admin/Provisioning_OWNERS
+++ b/core/java/android/app/admin/Provisioning_OWNERS
@@ -1,4 +1,7 @@
 # Assign bugs to android-enterprise-triage@google.com
 ae-provisioning-reviews@google.com
 acjohnston@google.com #{LAST_RESORT_SUGGESTION}
+sinduran@google.com #{LAST_RESORT_SUGGESTION}
+nupursn@google.com #{LAST_RESORT_SUGGESTION}
+shreyacsingh@google.com #{LAST_RESORT_SUGGESTION}
 file:EnterprisePlatform_OWNERS
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 4b6f406..b6240a7 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -54,9 +54,9 @@
      *
      * @hide
      */
-    public AppFunctionManager(IAppFunctionManager mService, Context context) {
-        this.mService = mService;
-        this.mContext = context;
+    public AppFunctionManager(IAppFunctionManager service, Context context) {
+        mService = service;
+        mContext = context;
     }
 
     /**
@@ -114,7 +114,7 @@
                         }
                     });
         } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+            throw e.rethrowFromSystemServer();
         }
     }
 }
diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
index ef37095..28827bb 100644
--- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
@@ -31,6 +31,7 @@
     * @param request the request to execute an app function.
     * @param callback the callback to report the result.
     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)")
     void executeAppFunction(
         in ExecuteAppFunctionAidlRequest request,
         in IExecuteAppFunctionCallback callback
diff --git a/core/java/android/companion/virtual/ActivityPolicyExemption.aidl b/core/java/android/companion/virtual/ActivityPolicyExemption.aidl
new file mode 100644
index 0000000..2f89da3
--- /dev/null
+++ b/core/java/android/companion/virtual/ActivityPolicyExemption.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual;
+
+parcelable ActivityPolicyExemption;
diff --git a/core/java/android/companion/virtual/ActivityPolicyExemption.java b/core/java/android/companion/virtual/ActivityPolicyExemption.java
new file mode 100644
index 0000000..c81bb43
--- /dev/null
+++ b/core/java/android/companion/virtual/ActivityPolicyExemption.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.companion.virtualdevice.flags.Flags;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.Display;
+
+import java.util.Objects;
+
+/**
+ * Specifies an exemption from the current default activity launch policy of a virtual device.
+ *
+ * <p>Note that changing the virtual device's activity launch policy will clear all current
+ * exemptions.</p>
+ *
+ * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
+ * @see VirtualDeviceManager.VirtualDevice#setDevicePolicy
+ * @see VirtualDeviceManager.VirtualDevice#addActivityPolicyExemption(ActivityPolicyExemption)
+ * @see VirtualDeviceManager.VirtualDevice#removeActivityPolicyExemption(ActivityPolicyExemption)
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
+@SystemApi
+public final class ActivityPolicyExemption implements Parcelable {
+
+    private final @Nullable ComponentName mComponentName;
+    private final @Nullable String mPackageName;
+    private final int mDisplayId;
+
+    private ActivityPolicyExemption(@Nullable ComponentName componentName,
+            @Nullable String packageName, int displayId) {
+        mComponentName = componentName;
+        mPackageName = packageName;
+        mDisplayId = displayId;
+    }
+
+    private ActivityPolicyExemption(@NonNull Parcel parcel) {
+        mComponentName = parcel.readTypedObject(ComponentName.CREATOR);
+        mPackageName = parcel.readString8();
+        mDisplayId = parcel.readInt();
+    }
+
+    /**
+     * Returns the exempt component name if this is a component level exemption, {@code null}
+     * otherwise.
+     *
+     * @see Builder#setComponentName(ComponentName)
+     */
+    public @Nullable ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    /**
+     * Returns the exempt package name if this is a package level exemption, {@code null} otherwise.
+     *
+     * @see Builder#setPackageName(String)
+     */
+    public @Nullable String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Returns the display ID relevant for this exemption if it is specific to a single display,
+     * {@link Display#INVALID_DISPLAY} otherwise.
+     *
+     * @see Builder#setDisplayId(int)
+     */
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedObject(mComponentName, flags);
+        dest.writeString8(mPackageName);
+        dest.writeInt(mDisplayId);
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<ActivityPolicyExemption> CREATOR =
+            new Parcelable.Creator<>() {
+                public ActivityPolicyExemption createFromParcel(Parcel in) {
+                    return new ActivityPolicyExemption(in);
+                }
+
+                public ActivityPolicyExemption[] newArray(int size) {
+                    return new ActivityPolicyExemption[size];
+                }
+            };
+
+    /**
+     * Builder for {@link ActivityPolicyExemption}.
+     */
+    public static final class Builder {
+
+        private @Nullable ComponentName mComponentName;
+        private @Nullable String mPackageName;
+        private int mDisplayId = Display.INVALID_DISPLAY;
+
+        /**
+         * Specifies a component level exemption from the current default activity launch policy.
+         *
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
+         * then the specified component will be blocked from launching.
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
+         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the
+         * specified component will be allowed to launch.</p>
+         *
+         * <p>Setting a component name will clear any previously set package name.</p>
+         */
+        public @NonNull Builder setComponentName(@NonNull ComponentName componentName) {
+            mComponentName = Objects.requireNonNull(componentName);
+            mPackageName = null;
+            return this;
+        }
+
+        /**
+         * Specifies a package level exemption from the current default activity launch policy.
+         *
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
+         * then all activities from the specified package will be blocked from launching.
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
+         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then all
+         * activities from the specified package will be allowed to launch.</p>
+         *
+         * <p>Package level exemptions are independent of component level exemptions created via
+         * {@link #setComponentName(ComponentName)}, i.e. removing a package exemption will not
+         * remove any existing component exemptions, even if the component belongs to that package.
+         * </p>
+         *
+         * <p>Setting a package name will clear any previously set component name.</p>
+         */
+        public @NonNull Builder setPackageName(@NonNull String packageName) {
+            mComponentName = null;
+            mPackageName = Objects.requireNonNull(packageName);
+            return this;
+        }
+
+        /**
+         * Makes this exemption specific to the display with the given ID. If unset, or set to
+         * {@link Display#INVALID_DISPLAY}, then the exemption is applied to all displays that
+         * belong to the virtual device.
+         *
+         * @param displayId  the ID of the display, for which to apply the exemption. The display
+         *   must belong to the virtual device.
+         */
+        public @NonNull Builder setDisplayId(int displayId) {
+            mDisplayId = displayId;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ActivityPolicyExemption} instance.
+         *
+         * @throws IllegalArgumentException if neither the component name nor the package name are
+         *   set.
+         */
+        @NonNull
+        public ActivityPolicyExemption build() {
+            if ((mComponentName == null) == (mPackageName == null)) {
+                throw new IllegalArgumentException(
+                        "Either component name or package name must be set");
+            }
+            return new ActivityPolicyExemption(mComponentName, mPackageName, mDisplayId);
+        }
+    }
+}
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 56d5a74..8916ce2 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -17,6 +17,7 @@
 package android.companion.virtual;
 
 import android.app.PendingIntent;
+import android.companion.virtual.ActivityPolicyExemption;
 import android.companion.virtual.IVirtualDeviceActivityListener;
 import android.companion.virtual.IVirtualDeviceIntentInterceptor;
 import android.companion.virtual.IVirtualDeviceSoundEffectListener;
@@ -103,25 +104,13 @@
      * Adds an exemption to the default activity launch policy.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void addActivityPolicyExemption(in ComponentName exemption);
+    void addActivityPolicyExemption(in ActivityPolicyExemption exemption);
 
     /**
      * Removes an exemption to the default activity launch policy.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void removeActivityPolicyExemption(in ComponentName exemption);
-
-    /**
-     * Adds a package level exemption to the default activity launch policy.
-     */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void addActivityPolicyPackageExemption(in String exemption);
-
-    /**
-     * Removes a package level exemption to the default activity launch policy.
-     */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void removeActivityPolicyPackageExemption(in String exemption);
+    void removeActivityPolicyExemption(in ActivityPolicyExemption exemption);
 
     /**
      * Specifies a policy for this virtual device on the given display.
@@ -130,30 +119,6 @@
     void setDevicePolicyForDisplay(int displayId, int policyType, int devicePolicy);
 
     /**
-     * Adds an exemption to the default activity launch policy on the given display.
-     */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void addActivityPolicyExemptionForDisplay(int displayId, in ComponentName exemption);
-
-    /**
-     * Removes an exemption to the default activity launch policy on the given display.
-     */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void removeActivityPolicyExemptionForDisplay(int displayId, in ComponentName exemption);
-
-    /**
-     * Adds a package level exemption to the default activity launch policy on the given display.
-     */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void addActivityPolicyPackageExemptionForDisplay(int displayId, in String exemption);
-
-    /**
-     * Removes a package level exemption to the default activity launch policy on the given display.
-     */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void removeActivityPolicyPackageExemptionForDisplay(int displayId, in String exemption);
-
-    /**
      * Notifies that an audio session being started.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index e83f46a..b7bf2d1 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -306,39 +306,22 @@
         }
     }
 
-    void addActivityPolicyExemption(@NonNull ComponentName componentName) {
+    void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
         try {
-            mVirtualDevice.addActivityPolicyExemption(componentName);
+            mVirtualDevice.addActivityPolicyExemption(exemption);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
-    void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
+    void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
         try {
-            mVirtualDevice.removeActivityPolicyExemption(componentName);
+            mVirtualDevice.removeActivityPolicyExemption(exemption);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
-    void addActivityPolicyPackageExemption(@NonNull String packageName) {
-        try {
-            mVirtualDevice.addActivityPolicyPackageExemption(packageName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    void removeActivityPolicyPackageExemption(@NonNull String packageName) {
-        try {
-            mVirtualDevice.removeActivityPolicyPackageExemption(packageName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-
     void setDevicePolicyForDisplay(int displayId,
             @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
             @VirtualDeviceParams.DevicePolicy int devicePolicy) {
@@ -358,40 +341,6 @@
         }
     }
 
-    void addActivityPolicyExemptionForDisplay(int displayId, @NonNull ComponentName componentName) {
-        try {
-            mVirtualDevice.addActivityPolicyExemptionForDisplay(displayId, componentName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    void removeActivityPolicyExemptionForDisplay(int displayId,
-            @NonNull ComponentName componentName) {
-        try {
-            mVirtualDevice.removeActivityPolicyExemptionForDisplay(displayId, componentName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    void addActivityPolicyPackageExemptionForDisplay(int displayId, @NonNull String packageName) {
-        try {
-            mVirtualDevice.addActivityPolicyPackageExemptionForDisplay(displayId, packageName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    void removeActivityPolicyPackageExemptionForDisplay(int displayId,
-            @NonNull String packageName) {
-        try {
-            mVirtualDevice.removeActivityPolicyPackageExemptionForDisplay(displayId, packageName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
     @NonNull
     VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
         try {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 68a864d..40aa6837 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -765,14 +765,15 @@
          * <p>Note that changing the activity launch policy will clear current set of exempt
          * components.</p>
          *
-         * @see #removeActivityPolicyExemption
+         * @see #removeActivityPolicyExemption(ComponentName)
          * @see #setDevicePolicy
          */
         @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void addActivityPolicyExemption(@NonNull ComponentName componentName) {
-            mVirtualDeviceInternal.addActivityPolicyExemption(
-                    Objects.requireNonNull(componentName));
+            addActivityPolicyExemption(new ActivityPolicyExemption.Builder()
+                    .setComponentName(componentName)
+                    .build());
         }
 
         /**
@@ -788,70 +789,54 @@
          * <p>Note that changing the activity launch policy will clear current set of exempt
          * components.</p>
          *
-         * @see #addActivityPolicyExemption
+         * @see #addActivityPolicyExemption(ComponentName)
          * @see #setDevicePolicy
          */
         @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
-            mVirtualDeviceInternal.removeActivityPolicyExemption(
-                    Objects.requireNonNull(componentName));
+            removeActivityPolicyExemption(new ActivityPolicyExemption.Builder()
+                    .setComponentName(componentName)
+                    .build());
         }
 
         /**
-         * Specifies a package name to be exempt from the current activity launch policy.
+         * Specifies an exemption from the current activity launch policy.
          *
          * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
          * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
-         * then all activities from the specified package will be blocked from launching.
+         * then all exempt activities be blocked from launching.
          * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
          * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then all
-         * activities from the specified package will be allowed to launch.</p>
-         *
-         * <p>Package level exemptions are independent of component level exemptions added via
-         * {@link #addActivityPolicyExemption(String)}, i.e. removing a package exemption will not
-         * remove any existing component exemptions, even if the component belongs to that package.
-         * </p>
+         * exempt activities will be allowed to launch.</p>
          *
          * <p>Note that changing the activity launch policy will clear current set of exempt
          * packages.</p>
+         * <p>Any change to the exemptions will only be applied for new activity launches.</p>
          *
-         * @see #removeActivityPolicyExemption(String)
+         * @see #removeActivityPolicyExemption(ActivityPolicyExemption)
          * @see #setDevicePolicy
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-        public void addActivityPolicyExemption(@NonNull String packageName) {
-            mVirtualDeviceInternal.addActivityPolicyPackageExemption(
-                    Objects.requireNonNull(packageName));
+        public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
+            mVirtualDeviceInternal.addActivityPolicyExemption(Objects.requireNonNull(exemption));
         }
 
         /**
-         * Makes the specified package name adhere to the default activity launch policy.
-         *
-         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
-         * then all activities from the specified package will be allowed to launch.
-         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
-         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then all
-         * activities from the specified package will be blocked from launching.</p>
-         *
-         * <p>Package level exemptions are independent of component level exemptions added via
-         * {@link #addActivityPolicyExemption(String)}, i.e. removing a package exemption will not
-         * remove any existing component exemptions, even if the component belongs to that package.
-         * </p>
+         * Removes an exemption from the current activity launch policy.
          *
          * <p>Note that changing the activity launch policy will clear current set of exempt
          * packages.</p>
+         * <p>Any change to the exemptions will only be applied for new activity launches.</p>
          *
-         * @see #addActivityPolicyExemption(String)
+         * @see #addActivityPolicyExemption(ActivityPolicyExemption)
          * @see #setDevicePolicy
          */
-        @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
+        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-        public void removeActivityPolicyExemption(@NonNull String packageName) {
-            mVirtualDeviceInternal.removeActivityPolicyPackageExemption(
-                    Objects.requireNonNull(packageName));
+        public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
+            mVirtualDeviceInternal.removeActivityPolicyExemption(Objects.requireNonNull(exemption));
         }
 
         /**
@@ -881,142 +866,6 @@
         }
 
         /**
-         * Specifies a component name to be exempt from the given display's activity launch policy.
-         *
-         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
-         * then the specified component will be blocked from launching.
-         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
-         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the
-         * specified component will be allowed to launch.</p>
-         *
-         * <p>Note that changing the activity launch policy will clear current set of exempt
-         * components.</p>
-         * <p>Any change to the exemptions will only be applied for new activity launches.</p>
-         *
-         * @param componentName the component name to be exempt from the activity launch policy.
-         * @param displayId the ID of the display, for which to apply the exemption. The display
-         *   must belong to the virtual device.
-         * @throws IllegalArgumentException if the specified display does not belong to the virtual
-         *   device.
-         *
-         * @see #removeActivityPolicyExemption
-         * @see #setDevicePolicy
-         * @see Display#getDisplayId
-         */
-        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-        public void addActivityPolicyExemption(
-                @NonNull ComponentName componentName, int displayId) {
-            mVirtualDeviceInternal.addActivityPolicyExemptionForDisplay(
-                    displayId, Objects.requireNonNull(componentName));
-        }
-
-        /**
-         * Makes the specified component name adhere to the given display's activity launch policy.
-         *
-         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
-         * then the specified component will be allowed to launch.
-         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
-         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the
-         * specified component will be blocked from launching.</p>
-         *
-         * <p>Note that changing the activity launch policy will clear current set of exempt
-         * components.</p>
-         *
-         * @param componentName the component name to be removed from the exemption list.
-         * @param displayId the ID of the display, for which to apply the exemption. The display
-         *   must belong to the virtual device.
-         * @throws IllegalArgumentException if the specified display does not belong to the virtual
-         *   device.
-         *
-         * @see #addActivityPolicyExemption
-         * @see #setDevicePolicy
-         * @see Display#getDisplayId
-         */
-        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-        public void removeActivityPolicyExemption(
-                @NonNull ComponentName componentName, int displayId) {
-            mVirtualDeviceInternal.removeActivityPolicyExemptionForDisplay(
-                    displayId, Objects.requireNonNull(componentName));
-        }
-
-        /**
-         * Specifies a package name to be exempt from the given display's activity launch policy.
-         *
-         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
-         * then all activities from the specified package will be blocked from launching.
-         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
-         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then all
-         * activities from the specified package will be allowed to launch.</p>
-         *
-         * <p>Note that changing the activity launch policy will clear current set of exempt
-         * packages.</p>
-         * <p>Any change to the exemptions will only be applied for new activity launches.</p>
-         *
-         * <p>Package level exemptions are independent of component level exemptions added via
-         * {@link #addActivityPolicyExemption(String, int)}, i.e. removing a package exemption will
-         * not remove any existing component exemptions, even if the component belongs to that
-         * package.</p>
-         *
-         * @param packageName the package name to be exempt from the activity launch policy. All
-         *   activities from that package will be exempt.
-         * @param displayId the ID of the display, for which to apply the exemption. The display
-         *   must belong to the virtual device.
-         * @throws IllegalArgumentException if the specified display does not belong to the virtual
-         *   device.
-         *
-         * @see #removeActivityPolicyExemption(String, int)
-         * @see #setDevicePolicy
-         * @see Display#getDisplayId
-         */
-        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-        public void addActivityPolicyExemption(@NonNull String packageName, int displayId) {
-            mVirtualDeviceInternal.addActivityPolicyPackageExemptionForDisplay(
-                    displayId, Objects.requireNonNull(packageName));
-        }
-
-        /**
-         * Makes the specified package name adhere to the given display's activity launch policy.
-         *
-         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
-         * then all activities from the specified package will be allowed to launch.
-         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
-         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then all
-         * activities from the specified package will be blocked from launching.</p>
-         *
-         * <p>Note that changing the activity launch policy will clear current set of exempt
-         * packages.</p>
-         *
-         * <p>Package level exemptions are independent of component level exemptions added via
-         * {@link #addActivityPolicyExemption(String, int)}, i.e. removing a package exemption will
-         * not remove any existing component exemptions, even if the component belongs to that
-         * package.</p>
-         *
-         * @param packageName the package name to be removed from the exemption list. All activities
-         *   from that package stop being exempt from the activity launch policy.
-         * @param displayId the ID of the display, for which to apply the exemption. The display
-         *   must belong to the virtual device.
-         * @throws IllegalArgumentException if the specified display does not belong to the virtual
-         *   device.
-         *
-         * @see #addActivityPolicyExemption(String, int)
-         * @see #setDevicePolicy
-         * @see Display#getDisplayId
-         */
-        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-        public void removeActivityPolicyExemption(@NonNull String packageName, int displayId) {
-            mVirtualDeviceInternal.removeActivityPolicyPackageExemptionForDisplay(
-                    displayId, Objects.requireNonNull(packageName));
-        }
-
-        /**
          * Creates a virtual dpad.
          *
          * @param config the configurations of the virtual dpad.
@@ -1401,7 +1250,7 @@
          *   activity to a different display.
          *
          * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
-         * @see VirtualDevice#addActivityPolicyExemption(ComponentName)
+         * @see VirtualDevice#addActivityPolicyExemption(ActivityPolicyExemption)
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
         default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName,
diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java
index da4ecdd..9649cab 100644
--- a/core/java/android/content/AbstractThreadedSyncAdapter.java
+++ b/core/java/android/content/AbstractThreadedSyncAdapter.java
@@ -42,7 +42,7 @@
  * will be invoked on that thread.
  * <p>
  * Syncs can be cancelled at any time by the framework. For example a sync that was not
- * user-initiated and lasts longer than 30 minutes will be considered timed-out and cancelled.
+ * user-initiated and lasts longer than 10 minutes will be considered timed-out and cancelled.
  * Similarly the framework will attempt to determine whether or not an adapter is making progress
  * by monitoring its network activity over the course of a minute. If the network traffic over this
  * window is close enough to zero the sync will be cancelled. You can also request the sync be
diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java
index f929c1f..d6620d1 100644
--- a/core/java/android/content/res/CompatibilityInfo.java
+++ b/core/java/android/content/res/CompatibilityInfo.java
@@ -28,6 +28,7 @@
 import android.os.Build.VERSION_CODES;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.util.DisplayMetrics;
 import android.util.MergedConfiguration;
 import android.view.InsetsSourceControl;
@@ -42,6 +43,7 @@
  * 
  *  {@hide} 
  */
+@RavenwoodKeepWholeClass
 public class CompatibilityInfo implements Parcelable {
     /** default compatibility info object for compatible applications */
     @UnsupportedAppUsage
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index 982224b..ef200c3 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -58,6 +58,7 @@
 import android.os.LocaleList;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.Slog;
@@ -89,6 +90,7 @@
  * with {@link android.app.Activity#getResources}:</p>
  * <pre>Configuration config = getResources().getConfiguration();</pre>
  */
+@RavenwoodKeepWholeClass
 public final class Configuration implements Parcelable, Comparable<Configuration> {
     /** @hide */
     public static final Configuration EMPTY = new Configuration();
diff --git a/core/java/android/content/res/ConfigurationBoundResourceCache.java b/core/java/android/content/res/ConfigurationBoundResourceCache.java
index 5e10a57..9dc097a 100644
--- a/core/java/android/content/res/ConfigurationBoundResourceCache.java
+++ b/core/java/android/content/res/ConfigurationBoundResourceCache.java
@@ -18,6 +18,7 @@
 
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.ActivityInfo.Config;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 /**
  * A Cache class which can be used to cache resource objects that are easy to clone but more
@@ -25,6 +26,7 @@
  *
  * @hide For internal use only.
  */
+@RavenwoodKeepWholeClass
 public class ConfigurationBoundResourceCache<T> extends ThemedResourceCache<ConstantState<T>> {
 
     @UnsupportedAppUsage
diff --git a/core/java/android/content/res/ConstantState.java b/core/java/android/content/res/ConstantState.java
index 09d4a59..cedfe02 100644
--- a/core/java/android/content/res/ConstantState.java
+++ b/core/java/android/content/res/ConstantState.java
@@ -16,6 +16,7 @@
 package android.content.res;
 
 import android.content.pm.ActivityInfo.Config;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 /**
  * A cache class that can provide new instances of a particular resource which may change
@@ -29,6 +30,7 @@
  * changing configurations of each Animator in the set)
  * @hide
  */
+@RavenwoodKeepWholeClass
 abstract public class ConstantState<T> {
 
     /**
diff --git a/core/java/android/content/res/FontResourcesParser.java b/core/java/android/content/res/FontResourcesParser.java
index 24ae31e..8aef45b 100644
--- a/core/java/android/content/res/FontResourcesParser.java
+++ b/core/java/android/content/res/FontResourcesParser.java
@@ -18,6 +18,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Typeface;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Xml;
@@ -36,6 +37,7 @@
  * Parser for xml type font resources.
  * @hide
  */
+@RavenwoodKeepWholeClass
 public class FontResourcesParser {
     private static final String TAG = "FontResourcesParser";
 
diff --git a/core/java/android/content/res/FontScaleConverter.java b/core/java/android/content/res/FontScaleConverter.java
index f4312a9..b2c5afa 100644
--- a/core/java/android/content/res/FontScaleConverter.java
+++ b/core/java/android/content/res/FontScaleConverter.java
@@ -20,6 +20,7 @@
 import android.annotation.AnyThread;
 import android.annotation.FlaggedApi;
 import android.annotation.Nullable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 /**
  * A converter for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
@@ -32,6 +33,7 @@
  * scale them slightly to preserve the visual hierarchy when compared to smaller fonts.
  */
 @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC)
+@RavenwoodKeepWholeClass
 public interface FontScaleConverter {
     /**
      * Converts a dimension in "sp" to "dp".
diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java
index c7237ea..9087a9a 100644
--- a/core/java/android/content/res/FontScaleConverterFactory.java
+++ b/core/java/android/content/res/FontScaleConverterFactory.java
@@ -19,6 +19,7 @@
 import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.util.MathUtils;
 import android.util.SparseArray;
 
@@ -34,6 +35,7 @@
  *
  * @hide
  */
+@RavenwoodKeepWholeClass
 public class FontScaleConverterFactory {
     private static final float SCALE_KEY_MULTIPLIER = 100f;
 
diff --git a/core/java/android/content/res/FontScaleConverterImpl.java b/core/java/android/content/res/FontScaleConverterImpl.java
index 1968c4e..508507a 100644
--- a/core/java/android/content/res/FontScaleConverterImpl.java
+++ b/core/java/android/content/res/FontScaleConverterImpl.java
@@ -17,6 +17,7 @@
 package android.content.res;
 
 import android.annotation.NonNull;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.util.MathUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +34,7 @@
  */
 // Needs to be public so the Kotlin test can see it
 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+@RavenwoodKeepWholeClass
 public class FontScaleConverterImpl implements FontScaleConverter {
 
     /** @hide */
diff --git a/core/java/android/content/res/ThemedResourceCache.java b/core/java/android/content/res/ThemedResourceCache.java
index 690dfcf..c7fcc1a 100644
--- a/core/java/android/content/res/ThemedResourceCache.java
+++ b/core/java/android/content/res/ThemedResourceCache.java
@@ -22,6 +22,7 @@
 import android.content.pm.ActivityInfo.Config;
 import android.content.res.Resources.Theme;
 import android.content.res.Resources.ThemeKey;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.util.ArrayMap;
 import android.util.LongSparseArray;
 
@@ -32,6 +33,7 @@
  *
  * @param <T> type of data to cache
  */
+@RavenwoodKeepWholeClass
 abstract class ThemedResourceCache<T> {
     public static final int UNDEFINED_GENERATION = -1;
     @UnsupportedAppUsage
diff --git a/core/java/android/view/DisplayAdjustments.java b/core/java/android/view/DisplayAdjustments.java
index bb50849..149d992 100644
--- a/core/java/android/view/DisplayAdjustments.java
+++ b/core/java/android/view/DisplayAdjustments.java
@@ -21,10 +21,12 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 import java.util.Objects;
 
 /** @hide */
+@RavenwoodKeepWholeClass
 public class DisplayAdjustments {
     public static final DisplayAdjustments DEFAULT_DISPLAY_ADJUSTMENTS = new DisplayAdjustments();
 
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 61ee13a..be744fd 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -228,6 +228,16 @@
 }
 
 flag {
+  name: "ensure_wallpaper_in_wear_transitions"
+  namespace: "windowing_frontend"
+  description: "Ensure that wallpaper window tokens are always present/available for collection in transitions on Wear"
+  bug: "355596979"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "custom_animations_behind_translucent"
   namespace: "windowing_frontend"
   description: "A change can use its own layer parameters to animate behind a translucent activity"
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 78b5cfe..49ed55d 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -431,15 +431,10 @@
 
         Log.d(LOG_TAG, "Dumping viewer config to trace");
 
-        ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
-
-        if (pis == null) {
-            Slog.w(LOG_TAG, "Failed to get viewer input stream.");
-            return;
-        }
-
         mDataSource.trace(ctx -> {
             try {
+                ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+
                 final ProtoOutputStream os = ctx.newTracePacket();
 
                 os.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos());
diff --git a/core/res/res/layout/app_language_picker_current_locale_item.xml b/core/res/res/layout/app_language_picker_current_locale_item.xml
index 01b9cc5..edd6d64 100644
--- a/core/res/res/layout/app_language_picker_current_locale_item.xml
+++ b/core/res/res/layout/app_language_picker_current_locale_item.xml
@@ -18,26 +18,31 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <FrameLayout
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical">
+    <RelativeLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_weight=".8">
+        android:layout_marginEnd="6dip"
+        android:layout_marginTop="6dip"
+        android:layout_marginBottom="6dip"
+        android:layout_weight="1">
         <include
             android:id="@+id/language_picker_item"
             layout="@layout/language_picker_item" />
-    </FrameLayout>
+    </RelativeLayout>
 
     <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_weight=".2"
+        android:layout_height="match_parent"
         android:gravity="center"
         android:minHeight="?android:attr/listPreferredItemHeight">
         <ImageView
             android:id="@+id/imageView"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:layout_marginHorizontal="16dp"
             android:src="@drawable/ic_check_24dp"
             app:tint="?attr/colorAccentPrimaryVariant"
             android:contentDescription="@*android:string/checked"/>
diff --git a/core/res/res/layout/language_picker_item.xml b/core/res/res/layout/language_picker_item.xml
index 88012a9..3e55f12 100644
--- a/core/res/res/layout/language_picker_item.xml
+++ b/core/res/res/layout/language_picker_item.xml
@@ -21,7 +21,6 @@
           android:gravity="center_vertical"
           android:minHeight="?android:attr/listPreferredItemHeight"
           android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-          android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
           android:textAppearance="?android:attr/textAppearanceListItem"
           android:layoutDirection="locale"
           android:textDirection="locale"
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 2bbaf9c..e86515f 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -164,6 +164,7 @@
         "org.apache.http.legacy",
     ],
     sdk_version: "core_platform",
+    resource_zips: [":FrameworksCoreTests_apks_as_resources"],
 }
 
 // Rules to copy all the test apks to the intermediate raw resource directory
@@ -237,6 +238,7 @@
     static_libs: [
         "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
         "androidx.core_core",
+        "androidx.core_core-ktx",
         "androidx.annotation_annotation",
         "androidx.test.rules",
         "androidx.test.ext.junit",
@@ -255,8 +257,11 @@
         "src/android/content/pm/UserInfoTest.java",
         "src/android/database/CursorWindowTest.java",
         "src/android/os/**/*.java",
+        "src/android/content/res/*.java",
+        "src/android/content/res/*.kt",
         "src/android/telephony/PinResultTest.java",
         "src/android/util/**/*.java",
+        "src/android/view/DisplayAdjustmentsTests.java",
         "src/android/view/DisplayTest.java",
         "src/android/view/DisplayInfoTest.java",
         "src/com/android/internal/logging/**/*.java",
@@ -274,6 +279,10 @@
         ":FrameworksCoreTests-helpers",
         ":FrameworksCoreTestDoubles-sources",
     ],
+    exclude_srcs: [
+        "src/android/content/res/FontScaleConverterActivityTest.java",
+    ],
+    resource_apk: "FrameworksCoreTests-resonly",
     aidl: {
         generate_get_transaction_name: true,
         local_include_dirs: ["aidl"],
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index fc3c2f3..0dcb1ce 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1672,14 +1672,6 @@
             </intent-filter>
         </activity>
 
-        <activity android:name="android.content.res.ResourceCacheActivity"
-                  android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
-            </intent-filter>
-        </activity>
-
         <activity
             android:name="android.print.test.PrintDocumentActivity"
             android:theme="@style/Theme" />
diff --git a/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java b/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java
index 6ffdee1..68882eb 100644
--- a/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java
+++ b/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java
@@ -16,80 +16,94 @@
 
 package android.content.res;
 
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+
+import android.content.Context;
 import android.platform.test.annotations.Presubmit;
-import android.test.ActivityInstrumentationTestCase2;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.TypedValue;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.frameworks.coretests.R;
 
-import java.lang.reflect.InvocationTargetException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 @Presubmit
-public class ConfigurationBoundResourceCacheTest
-        extends ActivityInstrumentationTestCase2<ResourceCacheActivity> {
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ConfigurationBoundResourceCacheTest {
 
-    ConfigurationBoundResourceCache<Float> mCache;
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
 
-    public ConfigurationBoundResourceCacheTest() {
-        super(ResourceCacheActivity.class);
+    private ConfigurationBoundResourceCache<Float> mCache;
+    private Context mContext;
+
+    private void assertEquals(float expected, float actual) {
+        Assert.assertEquals(expected, actual, 0);
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
         mCache = new ConfigurationBoundResourceCache<>();
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
     }
 
-    @SmallTest
+    @Test
     public void testGetEmpty() {
-        final Resources res = getActivity().getResources();
+        final Resources res = mContext.getResources();
         assertNull(mCache.getInstance(-1, res, null));
     }
 
-    @SmallTest
+    @Test
     public void testSetGet() {
         mCache.put(1, null, new DummyFloatConstantState(5f),
                 ThemedResourceCache.UNDEFINED_GENERATION);
-        final Resources res = getActivity().getResources();
+        final Resources res = mContext.getResources();
         assertEquals(5f, mCache.getInstance(1, res, null));
         assertNotSame(5f, mCache.getInstance(1, res, null));
-        assertEquals(null, mCache.getInstance(1, res, getActivity().getTheme()));
+        assertNull(mCache.getInstance(1, res, mContext.getTheme()));
     }
 
-    @SmallTest
+    @Test
     public void testSetGetThemed() {
-        mCache.put(1, getActivity().getTheme(), new DummyFloatConstantState(5f),
+        mCache.put(1, mContext.getTheme(), new DummyFloatConstantState(5f),
                 ThemedResourceCache.UNDEFINED_GENERATION);
-        final Resources res = getActivity().getResources();
-        assertEquals(null, mCache.getInstance(1, res, null));
-        assertEquals(5f, mCache.getInstance(1, res, getActivity().getTheme()));
-        assertNotSame(5f, mCache.getInstance(1, res, getActivity().getTheme()));
+        final Resources res = mContext.getResources();
+        assertNull(mCache.getInstance(1, res, null));
+        assertEquals(5f, mCache.getInstance(1, res, mContext.getTheme()));
+        assertNotSame(5f, mCache.getInstance(1, res, mContext.getTheme()));
     }
 
-    @SmallTest
+    @Test
     public void testMultiThreadPutGet() {
-        mCache.put(1, getActivity().getTheme(), new DummyFloatConstantState(5f),
+        mCache.put(1, mContext.getTheme(), new DummyFloatConstantState(5f),
                 ThemedResourceCache.UNDEFINED_GENERATION);
         mCache.put(1, null, new DummyFloatConstantState(10f),
                 ThemedResourceCache.UNDEFINED_GENERATION);
-        final Resources res = getActivity().getResources();
+        final Resources res = mContext.getResources();
         assertEquals(10f, mCache.getInstance(1, res, null));
         assertNotSame(10f, mCache.getInstance(1, res, null));
-        assertEquals(5f, mCache.getInstance(1, res, getActivity().getTheme()));
-        assertNotSame(5f, mCache.getInstance(1, res, getActivity().getTheme()));
+        assertEquals(5f, mCache.getInstance(1, res, mContext.getTheme()));
+        assertNotSame(5f, mCache.getInstance(1, res, mContext.getTheme()));
     }
 
-    @SmallTest
-    public void testVoidConfigChange()
-            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
+    @Test
+    public void testVoidConfigChange() {
         TypedValue staticValue = new TypedValue();
         long key = 3L;
-        final Resources res = getActivity().getResources();
+        final Resources res = mContext.getResources();
         res.getValue(R.dimen.resource_cache_test_generic, staticValue, true);
         float staticDim = TypedValue.complexToDimension(staticValue.data, res.getDisplayMetrics());
-        mCache.put(key, getActivity().getTheme(),
+        mCache.put(key, mContext.getTheme(),
                 new DummyFloatConstantState(staticDim, staticValue.changingConfigurations),
                 ThemedResourceCache.UNDEFINED_GENERATION);
         final Configuration cfg = res.getConfiguration();
@@ -98,21 +112,20 @@
                 Configuration.ORIENTATION_PORTRAIT
                 : Configuration.ORIENTATION_LANDSCAPE;
         int changes = calcConfigChanges(res, newCnf);
-        assertEquals(staticDim, mCache.getInstance(key, res, getActivity().getTheme()));
+        assertEquals(staticDim, mCache.getInstance(key, res, mContext.getTheme()));
         mCache.onConfigurationChange(changes);
-        assertEquals(staticDim, mCache.getInstance(key, res, getActivity().getTheme()));
+        assertEquals(staticDim, mCache.getInstance(key, res, mContext.getTheme()));
     }
 
-    @SmallTest
-    public void testEffectiveConfigChange()
-            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
+    @Test
+    public void testEffectiveConfigChange() {
         TypedValue changingValue = new TypedValue();
         long key = 4L;
-        final Resources res = getActivity().getResources();
+        final Resources res = mContext.getResources();
         res.getValue(R.dimen.resource_cache_test_orientation_dependent, changingValue, true);
         float changingDim = TypedValue.complexToDimension(changingValue.data,
                 res.getDisplayMetrics());
-        mCache.put(key, getActivity().getTheme(),
+        mCache.put(key, mContext.getTheme(),
                 new DummyFloatConstantState(changingDim, changingValue.changingConfigurations),
                 ThemedResourceCache.UNDEFINED_GENERATION);
 
@@ -123,26 +136,25 @@
                 : Configuration.ORIENTATION_LANDSCAPE;
         int changes = calcConfigChanges(res, newCnf);
         assertEquals(changingDim,
-                mCache.getInstance(key, res, getActivity().getTheme()));
+                mCache.getInstance(key, res, mContext.getTheme()));
         mCache.onConfigurationChange(changes);
-        assertNull(mCache.get(key, getActivity().getTheme()));
+        assertNull(mCache.get(key, mContext.getTheme()));
     }
 
-    @SmallTest
-    public void testConfigChangeMultipleResources()
-            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
+    @Test
+    public void testConfigChangeMultipleResources() {
         TypedValue staticValue = new TypedValue();
         TypedValue changingValue = new TypedValue();
-        final Resources res = getActivity().getResources();
+        final Resources res = mContext.getResources();
         res.getValue(R.dimen.resource_cache_test_generic, staticValue, true);
         res.getValue(R.dimen.resource_cache_test_orientation_dependent, changingValue, true);
         float staticDim = TypedValue.complexToDimension(staticValue.data, res.getDisplayMetrics());
         float changingDim = TypedValue.complexToDimension(changingValue.data,
                 res.getDisplayMetrics());
-        mCache.put(R.dimen.resource_cache_test_generic, getActivity().getTheme(),
+        mCache.put(R.dimen.resource_cache_test_generic, mContext.getTheme(),
                 new DummyFloatConstantState(staticDim, staticValue.changingConfigurations),
                 ThemedResourceCache.UNDEFINED_GENERATION);
-        mCache.put(R.dimen.resource_cache_test_orientation_dependent, getActivity().getTheme(),
+        mCache.put(R.dimen.resource_cache_test_orientation_dependent, mContext.getTheme(),
                 new DummyFloatConstantState(changingDim, changingValue.changingConfigurations),
                 ThemedResourceCache.UNDEFINED_GENERATION);
         final Configuration cfg = res.getConfiguration();
@@ -152,25 +164,24 @@
                 : Configuration.ORIENTATION_LANDSCAPE;
         int changes = calcConfigChanges(res, newCnf);
         assertEquals(staticDim, mCache.getInstance(R.dimen.resource_cache_test_generic, res,
-                getActivity().getTheme()));
+                mContext.getTheme()));
         assertEquals(changingDim,
                 mCache.getInstance(R.dimen.resource_cache_test_orientation_dependent, res,
-                        getActivity().getTheme()));
+                        mContext.getTheme()));
         mCache.onConfigurationChange(changes);
         assertEquals(staticDim, mCache.getInstance(R.dimen.resource_cache_test_generic, res,
-                getActivity().getTheme()));
+                mContext.getTheme()));
         assertNull(mCache.getInstance(R.dimen.resource_cache_test_orientation_dependent, res,
-                getActivity().getTheme()));
+                mContext.getTheme()));
     }
 
-    @SmallTest
-    public void testConfigChangeMultipleThemes()
-            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
+    @Test
+    public void testConfigChangeMultipleThemes() {
         TypedValue[] staticValues = new TypedValue[]{new TypedValue(), new TypedValue()};
         TypedValue[] changingValues = new TypedValue[]{new TypedValue(), new TypedValue()};
         float staticDim = 0;
         float changingDim = 0;
-        final Resources res = getActivity().getResources();
+        final Resources res = mContext.getResources();
         for (int i = 0; i < 2; i++) {
             res.getValue(R.dimen.resource_cache_test_generic, staticValues[i], true);
             staticDim = TypedValue
@@ -180,7 +191,7 @@
                     true);
             changingDim = TypedValue.complexToDimension(changingValues[i].data,
                     res.getDisplayMetrics());
-            final Resources.Theme theme = i == 0 ? getActivity().getTheme() : null;
+            final Resources.Theme theme = i == 0 ? mContext.getTheme() : null;
             mCache.put(R.dimen.resource_cache_test_generic, theme,
                     new DummyFloatConstantState(staticDim, staticValues[i].changingConfigurations),
                     ThemedResourceCache.UNDEFINED_GENERATION);
@@ -196,7 +207,7 @@
                 : Configuration.ORIENTATION_LANDSCAPE;
         int changes = calcConfigChanges(res, newCnf);
         for (int i = 0; i < 2; i++) {
-            final Resources.Theme theme = i == 0 ? getActivity().getTheme() : null;
+            final Resources.Theme theme = i == 0 ? mContext.getTheme() : null;
             assertEquals(staticDim,
                     mCache.getInstance(R.dimen.resource_cache_test_generic, res, theme));
             assertEquals(changingDim,
@@ -205,7 +216,7 @@
         }
         mCache.onConfigurationChange(changes);
         for (int i = 0; i < 2; i++) {
-            final Resources.Theme theme = i == 0 ? getActivity().getTheme() : null;
+            final Resources.Theme theme = i == 0 ? mContext.getTheme() : null;
             assertEquals(staticDim,
                     mCache.getInstance(R.dimen.resource_cache_test_generic, res, theme));
             assertNull(mCache.getInstance(R.dimen.resource_cache_test_orientation_dependent, res,
diff --git a/core/tests/coretests/src/android/content/res/ConfigurationTest.java b/core/tests/coretests/src/android/content/res/ConfigurationTest.java
index 0d5cd72..83c7484 100644
--- a/core/tests/coretests/src/android/content/res/ConfigurationTest.java
+++ b/core/tests/coretests/src/android/content/res/ConfigurationTest.java
@@ -28,23 +28,27 @@
 import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
 import static android.view.Surface.ROTATION_90;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 import android.content.Context;
 import android.os.LocaleList;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.AtomicFile;
 import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.usage.IntervalStatsProto;
 
-import junit.framework.TestCase;
-
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -54,10 +58,14 @@
 /**
  * Build/install/run: bit FrameworksCoreTests:android.content.res.ConfigurationTest
  */
-@RunWith(JUnit4.class)
+@RunWith(AndroidJUnit4.class)
 @SmallTest
 @Presubmit
-public class ConfigurationTest extends TestCase {
+public class ConfigurationTest {
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
+
     @Test
     public void testUpdateFromPreservesRoundBit() {
         Configuration config = new Configuration();
@@ -82,7 +90,7 @@
 
     @Test
     public void testReadWriteProto() throws Exception {
-        final Context context = InstrumentationRegistry.getTargetContext();
+        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
         final File testDir = new File(context.getFilesDir(), "ConfigurationTest");
         testDir.mkdirs();
         final File proto = new File(testDir, "configs");
diff --git a/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java b/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java
index 85f5d69..3fcd372 100644
--- a/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java
+++ b/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java
@@ -26,16 +26,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
-import android.app.Instrumentation;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.frameworks.coretests.R;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.xmlpull.v1.XmlPullParserException;
@@ -51,13 +52,14 @@
 @RunWith(AndroidJUnit4.class)
 public class FontResourcesParserTest {
 
-    private Instrumentation mInstrumentation;
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
+
     private Resources mResources;
 
     @Before
     public void setup() {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mResources = mInstrumentation.getContext().getResources();
+        mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources();
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
index c7d5825..c0a9bc2 100644
--- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
+++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
@@ -20,6 +20,8 @@
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.platform.test.flag.junit.CheckFlagsRule
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.platform.test.flag.junit.RavenwoodFlagsValueProvider
+import android.platform.test.ravenwood.RavenwoodRule
 import android.util.SparseArray
 import androidx.core.util.forEach
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -27,15 +29,14 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.random.Random.Default.nextFloat
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
-import kotlin.math.ceil
-import kotlin.math.floor
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.lang.IllegalStateException
-import kotlin.random.Random.Default.nextFloat
 
 /**
  * Unit tests for FontScaleConverterFactory. Note that some similar tests are in
@@ -46,7 +47,15 @@
 class FontScaleConverterFactoryTest {
 
     @get:Rule
-    val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+    val ravenwoodRule: RavenwoodRule = RavenwoodRule.Builder().build()
+
+    @get:Rule
+    val checkFlagsRule: CheckFlagsRule =
+        if (RavenwoodRule.isOnRavenwood()) {
+            RavenwoodFlagsValueProvider.createAllOnCheckFlagsRule()
+        } else {
+            DeviceFlagsValueProvider.createCheckFlagsRule()
+        }
 
     private var defaultLookupTables: SparseArray<FontScaleConverter>? = null
 
diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt
index 2c61442..0e5d926 100644
--- a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt
+++ b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt
@@ -17,8 +17,10 @@
 package android.content.res
 
 import android.platform.test.annotations.Presubmit
+import android.platform.test.ravenwood.RavenwoodRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -26,6 +28,9 @@
 @RunWith(AndroidJUnit4::class)
 class FontScaleConverterTest {
 
+    @get:Rule
+    val ravenwoodRule: RavenwoodRule = RavenwoodRule.Builder().build()
+
     @Test
     fun straightInterpolation() {
         val table = createTable(8f to 8f, 10f to 10f, 20f to 20f)
diff --git a/core/tests/coretests/src/android/content/res/ResourceCacheActivity.java b/core/tests/coretests/src/android/content/res/ResourceCacheActivity.java
deleted file mode 100644
index f37e549..0000000
--- a/core/tests/coretests/src/android/content/res/ResourceCacheActivity.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-* Copyright (C) 2014 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
-
-package android.content.res;
-
-import android.annotation.Nullable;
-import android.app.Activity;
-import android.os.Bundle;
-
-import java.lang.ref.WeakReference;
-
-public class ResourceCacheActivity extends Activity {
-    static WeakReference<ResourceCacheActivity> lastCreatedInstance;
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        lastCreatedInstance = new WeakReference<ResourceCacheActivity>(this);
-    }
-
-    public static ResourceCacheActivity getLastCreatedInstance() {
-        return lastCreatedInstance == null ? null : lastCreatedInstance.get();
-    }
-}
diff --git a/core/tests/coretests/src/android/content/res/ResourcesDrawableTest.java b/core/tests/coretests/src/android/content/res/ResourcesDrawableTest.java
index ac69a0f..6a09848 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesDrawableTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesDrawableTest.java
@@ -24,22 +24,29 @@
 import android.graphics.drawable.ColorStateListDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.frameworks.coretests.R;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @Presubmit
 @SmallTest
+@DisabledOnRavenwood(blockedBy = Drawable.class)
 @RunWith(AndroidJUnit4.class)
 public class ResourcesDrawableTest {
 
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
+
     @Test
     public void testLoadColorAsDrawable() {
         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java b/core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java
index 26e4349..fdfddc8 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java
@@ -16,29 +16,52 @@
 
 package android.content.res;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 import android.content.Context;
 import android.os.FileUtils;
 import android.os.LocaleList;
 import android.platform.test.annotations.Presubmit;
-import android.test.AndroidTestCase;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.DisplayMetrics;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.frameworks.coretests.R;
 
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.File;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.Locale;
 
 @Presubmit
-public class ResourcesLocaleTest extends AndroidTestCase {
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ResourcesLocaleTest {
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
+
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
 
     private String extractApkAndGetPath(int id) throws Exception {
-        final Resources resources = getContext().getResources();
+        final Resources resources = mContext.getResources();
         try (InputStream is = resources.openRawResource(id)) {
-            File path = new File(getContext().getFilesDir(), resources.getResourceEntryName(id));
+            File path = new File(mContext.getFilesDir(), resources.getResourceEntryName(id));
             FileUtils.copyToFileOrThrow(is, path);
             return path.getAbsolutePath();
         }
@@ -53,6 +76,15 @@
         return new Resources(assets, dm, new Configuration());
     }
 
+    private Resources createResourcesWithSelfApk() {
+        final AssetManager assets = new AssetManager();
+        assertTrue(assets.addAssetPath(mContext.getPackageResourcePath()) != 0);
+
+        final DisplayMetrics dm = new DisplayMetrics();
+        dm.setToDefaults();
+        return new Resources(assets, dm, new Configuration());
+    }
+
     private static void ensureNoLanguage(Resources resources, String language) {
         final String[] supportedLocales = resources.getAssets().getNonSystemLocales();
         for (String languageTag : supportedLocales) {
@@ -65,7 +97,7 @@
         }
     }
 
-    @SmallTest
+    @Test
     public void testEnglishIsAlwaysConsideredSupported() throws Exception {
         final Resources resources = createResourcesWithApk(R.raw.locales);
         ensureNoLanguage(resources, "en");
@@ -82,7 +114,7 @@
                 resources.getConfiguration().getLocales().get(0));
     }
 
-    @SmallTest
+    @Test
     public void testSelectFirstSupportedLanguage() throws Exception {
         final Resources resources = createResourcesWithApk(R.raw.locales);
         ensureNoLanguage(resources, "fr");
@@ -99,7 +131,7 @@
                 resources.getConfiguration().getLocales().get(0));
     }
 
-    @SmallTest
+    @Test
     public void testDeprecatedISOLanguageCode() {
         assertResGetString(Locale.US, R.string.locale_test_res_1, "Testing ID");
         assertResGetString(Locale.forLanguageTag("id"), R.string.locale_test_res_2, "Pengujian IN");
@@ -115,7 +147,8 @@
         LocaleList locales = new LocaleList(locale);
         final Configuration config = new Configuration();
         config.setLocales(locales);
-        Context newContext = getContext().createConfigurationContext(config);
-        assertEquals(expectedString, newContext.getResources().getString(resId));
+        final Resources resources = createResourcesWithSelfApk();
+        resources.updateConfiguration(config, null);
+        assertEquals(expectedString, resources.getString(resId));
     }
 }
diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
index ee1b658..3eefe04 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
@@ -16,27 +16,34 @@
 
 package android.content.res;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
 import android.annotation.NonNull;
 import android.app.ResourcesManager;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.LocaleList;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.annotations.Postsubmit;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.RavenwoodFlagsValueProvider;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.Display;
 import android.view.DisplayAdjustments;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-
-import junit.framework.TestCase;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -49,7 +56,7 @@
 
 @Postsubmit
 @RunWith(AndroidJUnit4.class)
-public class ResourcesManagerTest extends TestCase {
+public class ResourcesManagerTest {
     private static final int SECONDARY_DISPLAY_ID = 1;
     private static final String APP_ONE_RES_DIR = "app_one.apk";
     private static final String APP_ONE_RES_SPLIT_DIR = "app_one_split.apk";
@@ -57,14 +64,20 @@
     private static final String LIB_RES_DIR = "lib.apk";
     private static final String TEST_LIB = "com.android.frameworks.coretests.bdr_helper_app1";
 
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            RavenwoodRule.isOnRavenwood()
+                    ? RavenwoodFlagsValueProvider.createAllOnCheckFlagsRule()
+                    : DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private ResourcesManager mResourcesManager;
     private Map<Integer, DisplayMetrics> mDisplayMetricsMap;
-    private PackageManager mPackageManager;
 
     @Before
     public void setUp() throws Exception {
-        super.setUp();
-
         mDisplayMetricsMap = new HashMap<>();
 
         DisplayMetrics defaultDisplayMetrics = new DisplayMetrics();
@@ -110,12 +123,11 @@
                 return mDisplayMetricsMap.get(displayId);
             }
         };
-
-        mPackageManager = InstrumentationRegistry.getContext().getPackageManager();
     }
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+    private PackageManager getPackageManager() {
+        return InstrumentationRegistry.getInstrumentation().getContext().getPackageManager();
+    }
 
     @Test
     @SmallTest
@@ -356,6 +368,7 @@
     @Test
     @SmallTest
     @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    @DisabledOnRavenwood(blockedBy = PackageManager.class)
     public void testExistingResourcesAfterResourcePathsRegistration()
              throws PackageManager.NameNotFoundException {
         // Inject ResourcesManager instance from this test to the ResourcesManager class so that all
@@ -370,7 +383,7 @@
         assertNotNull(resources);
         ResourcesImpl oriResImpl = resources.getImpl();
 
-        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        ApplicationInfo appInfo = getPackageManager().getApplicationInfo(TEST_LIB, 0);
         Resources.registerResourcePaths(TEST_LIB, appInfo);
 
         assertNotSame(oriResImpl, resources.getImpl());
@@ -390,6 +403,7 @@
     @Test
     @SmallTest
     @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    @DisabledOnRavenwood(blockedBy = PackageManager.class)
     public void testNewResourcesAfterResourcePathsRegistration()
             throws PackageManager.NameNotFoundException {
         // Inject ResourcesManager instance from this test to the ResourcesManager class so that all
@@ -397,7 +411,7 @@
         ResourcesManager oriResourcesManager = ResourcesManager.getInstance();
         ResourcesManager.setInstance(mResourcesManager);
 
-        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        ApplicationInfo appInfo = getPackageManager().getApplicationInfo(TEST_LIB, 0);
         Resources.registerResourcePaths(TEST_LIB, appInfo);
 
         // Create a Resources after register resources' paths for a package.
@@ -420,6 +434,7 @@
     @Test
     @SmallTest
     @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    @DisabledOnRavenwood(blockedBy = PackageManager.class)
     public void testExistingResourcesCreatedByConstructorAfterResourcePathsRegistration()
             throws PackageManager.NameNotFoundException {
         // Inject ResourcesManager instance from this test to the ResourcesManager class so that all
@@ -437,7 +452,7 @@
 
         ResourcesImpl oriResImpl = resources.getImpl();
 
-        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        ApplicationInfo appInfo = getPackageManager().getApplicationInfo(TEST_LIB, 0);
         Resources.registerResourcePaths(TEST_LIB, appInfo);
 
         assertNotSame(oriResImpl, resources.getImpl());
@@ -456,6 +471,7 @@
     @Test
     @SmallTest
     @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    @DisabledOnRavenwood(blockedBy = PackageManager.class)
     public void testNewResourcesWithOutdatedImplAfterResourcePathsRegistration()
             throws PackageManager.NameNotFoundException {
         ResourcesManager oriResourcesManager = ResourcesManager.getInstance();
@@ -467,7 +483,7 @@
         assertNotNull(old_resources);
         ResourcesImpl oldImpl = old_resources.getImpl();
 
-        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        ApplicationInfo appInfo = getPackageManager().getApplicationInfo(TEST_LIB, 0);
         Resources.registerResourcePaths(TEST_LIB, appInfo);
 
         // Create another resources with identical parameters.
diff --git a/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java b/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java
index afbf8db..b86029b 100644
--- a/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java
+++ b/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java
@@ -19,9 +19,11 @@
 import static org.junit.Assert.assertEquals;
 
 import android.content.res.Configuration;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
 @RunWith(AndroidJUnit4.class)
 public class DisplayAdjustmentsTests {
 
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
+
     @Test
     public void testDefaultConstructor_hasEmptyConfiguration() {
         DisplayAdjustments emptyAdjustments = new DisplayAdjustments();
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 39f6d8c..fe8b818 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -183,4 +183,7 @@
     <!-- This is to be overridden to define a list of packages mapped to web links which will be
          parsed and utilized for desktop windowing's app-to-web feature. -->
     <string name="generic_links_list" translatable="false"/>
+
+    <!-- Apps that can trigger Desktop Windowing App handle Education -->
+    <string-array name="desktop_windowing_app_handle_education_allowlist_apps"></string-array>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/integers.xml b/libs/WindowManager/Shell/res/values/integers.xml
index 583bf33..300baea 100644
--- a/libs/WindowManager/Shell/res/values/integers.xml
+++ b/libs/WindowManager/Shell/res/values/integers.xml
@@ -22,4 +22,16 @@
     <integer name="bubbles_overflow_columns">4</integer>
     <!-- Maximum number of bubbles we allow in overflow before we dismiss the oldest one. -->
     <integer name="bubbles_max_overflow">16</integer>
+    <!-- App Handle Education - Minimum number of times an app should have been launched, in order
+         to be eligible to show education in it -->
+    <integer name="desktop_windowing_education_min_app_launch_count">3</integer>
+    <!-- App Handle Education - Interval at which app usage stats should be queried and updated in
+         cache periodically -->
+    <integer name="desktop_windowing_education_app_usage_cache_interval_seconds">86400</integer>
+    <!-- App Handle Education - Time interval in seconds for which we'll analyze app usage
+         stats to determine if minimum usage requirements are met.  -->
+    <integer name="desktop_windowing_education_app_launch_interval_seconds">2592000</integer>
+    <!-- App Handle Education - Required time passed in seconds since device has been setup
+         in order to be eligible to show education -->
+    <integer name="desktop_windowing_education_required_time_since_setup_seconds">604800</integer>
 </resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index ce054a8..d947326 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -72,6 +72,7 @@
 import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
 import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
 import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.draganddrop.GlobalDragListener;
@@ -711,6 +712,14 @@
         return new AppHandleEducationDatastoreRepository(context);
     }
 
+    @WMSingleton
+    @Provides
+    static AppHandleEducationFilter provideAppHandleEducationFilter(
+            Context context,
+            AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository) {
+        return new AppHandleEducationFilter(context, appHandleEducationDatastoreRepository);
+    }
+
     //
     // Drag and drop
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
new file mode 100644
index 0000000..51bdb40
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.desktopmode.education
+
+import android.annotation.IntegerRes
+import android.app.usage.UsageStatsManager
+import android.content.Context
+import android.os.SystemClock
+import android.provider.Settings.Secure
+import com.android.wm.shell.R
+import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
+import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
+import java.time.Duration
+
+/** Filters incoming app handle education triggers based on set conditions. */
+class AppHandleEducationFilter(
+    private val context: Context,
+    private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository
+) {
+  private val usageStatsManager =
+      context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
+
+  /** Returns true if conditions to show app handle education are met, returns false otherwise. */
+  suspend fun shouldShowAppHandleEducation(focusAppPackageName: String): Boolean {
+    val windowingEducationProto = appHandleEducationDatastoreRepository.windowingEducationProto()
+    return isFocusAppInAllowlist(focusAppPackageName) &&
+        !isOtherEducationShowing() &&
+        hasSufficientTimeSinceSetup() &&
+        !isEducationViewedBefore(windowingEducationProto) &&
+        !isFeatureUsedBefore(windowingEducationProto) &&
+        hasMinAppUsage(windowingEducationProto, focusAppPackageName)
+  }
+
+  private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean =
+      focusAppPackageName in
+          context.resources.getStringArray(
+              R.array.desktop_windowing_app_handle_education_allowlist_apps)
+
+  // TODO: b/350953004 - Add checks based on App compat
+  // TODO: b/350951797 - Add checks based on PKT tips education
+  private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing()
+
+  private fun isTaskbarEducationShowing(): Boolean =
+      Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1
+
+  private fun hasSufficientTimeSinceSetup(): Boolean =
+      Duration.ofMillis(SystemClock.elapsedRealtime()) >
+          convertIntegerResourceToDuration(
+              R.integer.desktop_windowing_education_required_time_since_setup_seconds)
+
+  private fun isEducationViewedBefore(windowingEducationProto: WindowingEducationProto): Boolean =
+      windowingEducationProto.hasEducationViewedTimestampMillis()
+
+  private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean =
+      windowingEducationProto.hasFeatureUsedTimestampMillis()
+
+  private suspend fun hasMinAppUsage(
+      windowingEducationProto: WindowingEducationProto,
+      focusAppPackageName: String
+  ): Boolean =
+      (launchCountByPackageName(windowingEducationProto)[focusAppPackageName] ?: 0) >=
+          context.resources.getInteger(R.integer.desktop_windowing_education_min_app_launch_count)
+
+  private suspend fun launchCountByPackageName(
+      windowingEducationProto: WindowingEducationProto
+  ): Map<String, Int> =
+      if (isAppUsageCacheStale(windowingEducationProto)) {
+        // Query and return user stats, update cache in datastore
+        getAndCacheAppUsageStats()
+      } else {
+        // Return cached usage stats
+        windowingEducationProto.appHandleEducation.appUsageStatsMap
+      }
+
+  private fun isAppUsageCacheStale(windowingEducationProto: WindowingEducationProto): Boolean {
+    val currentTime = currentTimeInDuration()
+    val lastUpdateTime =
+        Duration.ofMillis(
+            windowingEducationProto.appHandleEducation.appUsageStatsLastUpdateTimestampMillis)
+    val appUsageStatsCachingInterval =
+        convertIntegerResourceToDuration(
+            R.integer.desktop_windowing_education_app_usage_cache_interval_seconds)
+    return (currentTime - lastUpdateTime) > appUsageStatsCachingInterval
+  }
+
+  private suspend fun getAndCacheAppUsageStats(): Map<String, Int> {
+    val currentTime = currentTimeInDuration()
+    val appUsageStats = queryAppUsageStats()
+    appHandleEducationDatastoreRepository.updateAppUsageStats(appUsageStats, currentTime)
+    return appUsageStats
+  }
+
+  private fun queryAppUsageStats(): Map<String, Int> {
+    val endTime = currentTimeInDuration()
+    val appLaunchInterval =
+        convertIntegerResourceToDuration(
+            R.integer.desktop_windowing_education_app_launch_interval_seconds)
+    val startTime = endTime - appLaunchInterval
+
+    return usageStatsManager
+        .queryAndAggregateUsageStats(startTime.toMillis(), endTime.toMillis())
+        .mapValues { it.value.appLaunchCount }
+  }
+
+  private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration =
+      Duration.ofSeconds(context.resources.getInteger(resourceId).toLong())
+
+  private fun currentTimeInDuration(): Duration = Duration.ofMillis(System.currentTimeMillis())
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
index bf4a2ab..a7fff8a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
@@ -22,12 +22,12 @@
 import androidx.datastore.core.DataStore
 import androidx.datastore.core.DataStoreFactory
 import androidx.datastore.core.Serializer
-import androidx.datastore.dataStore
 import androidx.datastore.dataStoreFile
 import com.android.framework.protobuf.InvalidProtocolBufferException
 import com.android.internal.annotations.VisibleForTesting
 import java.io.InputStream
 import java.io.OutputStream
+import java.time.Duration
 import kotlinx.coroutines.flow.first
 
 /**
@@ -58,6 +58,24 @@
         WindowingEducationProto.getDefaultInstance()
       }
 
+  /**
+   * Updates [AppHandleEducation.appUsageStats] and
+   * [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with
+   * [appUsageStats] and [appUsageStatsLastUpdateTimestamp].
+   */
+  suspend fun updateAppUsageStats(
+      appUsageStats: Map<String, Int>,
+      appUsageStatsLastUpdateTimestamp: Duration
+  ) {
+    val currentAppHandleProto = windowingEducationProto().appHandleEducation.toBuilder()
+    currentAppHandleProto
+        .putAllAppUsageStats(appUsageStats)
+        .setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestamp.toMillis())
+    dataStore.updateData { preferences: WindowingEducationProto ->
+      preferences.toBuilder().setAppHandleEducation(currentAppHandleProto).build()
+    }
+  }
+
   companion object {
     private const val TAG = "AppHandleEducationDatastoreRepository"
     private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
index 4d40738..765021f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -26,13 +26,16 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
 import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
+import com.android.wm.shell.util.createWindowingEducationProto
 import com.google.common.truth.Truth.assertThat
 import java.io.File
+import java.time.Duration
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.test.setMain
@@ -88,35 +91,22 @@
         assertThat(resultProto).isEqualTo(windowingEducationProto)
       }
 
-  private fun createWindowingEducationProto(
-      educationViewedTimestampMillis: Long? = null,
-      featureUsedTimestampMillis: Long? = null,
-      appUsageStats: Map<String, Int>? = null,
-      appUsageStatsLastUpdateTimestampMillis: Long? = null
-  ): WindowingEducationProto =
-      WindowingEducationProto.newBuilder()
-          .apply {
-            if (educationViewedTimestampMillis != null)
-                setEducationViewedTimestampMillis(educationViewedTimestampMillis)
-            if (featureUsedTimestampMillis != null)
-                setFeatureUsedTimestampMillis(featureUsedTimestampMillis)
-            setAppHandleEducation(
-                createAppHandleEducationProto(
-                    appUsageStats, appUsageStatsLastUpdateTimestampMillis))
-          }
-          .build()
+  @Test
+  fun updateAppUsageStats_updatesDatastoreProto() =
+      runTest(StandardTestDispatcher()) {
+        val appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 3)
+        val appUsageStatsLastUpdateTimestamp = Duration.ofMillis(123L)
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = appUsageStats,
+                appUsageStatsLastUpdateTimestampMillis =
+                    appUsageStatsLastUpdateTimestamp.toMillis())
 
-  private fun createAppHandleEducationProto(
-      appUsageStats: Map<String, Int>? = null,
-      appUsageStatsLastUpdateTimestampMillis: Long? = null
-  ): WindowingEducationProto.AppHandleEducation =
-      WindowingEducationProto.AppHandleEducation.newBuilder()
-          .apply {
-            if (appUsageStats != null) putAllAppUsageStats(appUsageStats)
-            if (appUsageStatsLastUpdateTimestampMillis != null)
-                setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestampMillis)
-          }
-          .build()
+        datastoreRepository.updateAppUsageStats(appUsageStats, appUsageStatsLastUpdateTimestamp)
+
+        val result = testDatastore.data.first()
+        assertThat(result).isEqualTo(windowingEducationProto)
+      }
 
   companion object {
     private const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
new file mode 100644
index 0000000..c0d71c0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.desktopmode.education
+
+import android.app.usage.UsageStats
+import android.app.usage.UsageStatsManager
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
+import android.testing.TestableResources
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
+import com.android.wm.shell.util.createWindowingEducationProto
+import com.google.common.truth.Truth.assertThat
+import kotlin.Int.Companion.MAX_VALUE
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AppHandleEducationFilterTest : ShellTestCase() {
+  @Mock private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository
+  @Mock private lateinit var mockUsageStatsManager: UsageStatsManager
+  private lateinit var educationFilter: AppHandleEducationFilter
+  private lateinit var testableResources: TestableResources
+  private lateinit var testableContext: TestableContext
+
+  @Before
+  fun setup() {
+    MockitoAnnotations.initMocks(this)
+    testableContext = TestableContext(mContext)
+    testableResources =
+        testableContext.orCreateTestableResources.apply {
+          addOverride(
+              R.array.desktop_windowing_app_handle_education_allowlist_apps,
+              arrayOf(GMAIL_PACKAGE_NAME))
+          addOverride(R.integer.desktop_windowing_education_required_time_since_setup_seconds, 0)
+          addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
+          addOverride(
+              R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, MAX_VALUE)
+          addOverride(R.integer.desktop_windowing_education_app_launch_interval_seconds, 100)
+        }
+    testableContext.addMockSystemService(Context.USAGE_STATS_SERVICE, mockUsageStatsManager)
+    educationFilter = AppHandleEducationFilter(testableContext, datastoreRepository)
+  }
+
+  @Test
+  fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest {
+    // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation
+    // should return true
+    val windowingEducationProto =
+        createWindowingEducationProto(
+            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
+    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+
+    val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME)
+
+    assertThat(result).isTrue()
+  }
+
+  @Test
+  fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest {
+    // Pass Youtube as current focus app, it is not in allowlist hence #shouldShowAppHandleEducation
+    // should return false
+    testableResources.addOverride(
+        R.array.desktop_windowing_app_handle_education_allowlist_apps, arrayOf(GMAIL_PACKAGE_NAME))
+    val windowingEducationProto =
+        createWindowingEducationProto(
+            appUsageStats = mapOf(YOUTUBE_PACKAGE_NAME to 4),
+            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
+    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+
+    val result = educationFilter.shouldShowAppHandleEducation(YOUTUBE_PACKAGE_NAME)
+
+    assertThat(result).isFalse()
+  }
+
+  @Test
+  fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest {
+    // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation should
+    // return false
+    testableResources.addOverride(
+        R.integer.desktop_windowing_education_required_time_since_setup_seconds, MAX_VALUE)
+    val windowingEducationProto =
+        createWindowingEducationProto(
+            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
+    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+
+    val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME)
+
+    assertThat(result).isFalse()
+  }
+
+  @Test
+  fun shouldShowAppHandleEducation_educationViewedBefore_returnsFalse() = runTest {
+    // Education has been viewed before, hence #shouldShowAppHandleEducation should return false
+    val windowingEducationProto =
+        createWindowingEducationProto(
+            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+            educationViewedTimestampMillis = 123L,
+            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
+    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+
+    val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME)
+
+    assertThat(result).isFalse()
+  }
+
+  @Test
+  fun shouldShowAppHandleEducation_featureUsedBefore_returnsFalse() = runTest {
+    // Feature has been used before, hence #shouldShowAppHandleEducation should return false
+    val windowingEducationProto =
+        createWindowingEducationProto(
+            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+            featureUsedTimestampMillis = 123L,
+            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
+    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+
+    val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME)
+
+    assertThat(result).isFalse()
+  }
+
+  @Test
+  fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest {
+    // Simulate that gmail app has been launched twice before, minimum app launch count is 3, hence
+    // #shouldShowAppHandleEducation should return false
+    testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
+    val windowingEducationProto =
+        createWindowingEducationProto(
+            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
+            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
+    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+
+    val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME)
+
+    assertThat(result).isFalse()
+  }
+
+  @Test
+  fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest {
+    // UsageStats caching interval is set to 0ms, that means caching should happen very frequently
+    testableResources.addOverride(
+        R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, 0)
+    // The DataStore currently holds a proto object where Gmail's app launch count is recorded as 4.
+    // This value exceeds the minimum required count of 3.
+    testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
+    val windowingEducationProto =
+        createWindowingEducationProto(
+            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+            appUsageStatsLastUpdateTimestampMillis = 0)
+    // The mocked UsageStatsManager is configured to return a launch count of 2 for Gmail.
+    // This value is below the minimum required count of 3.
+    `when`(mockUsageStatsManager.queryAndAggregateUsageStats(anyLong(), anyLong()))
+        .thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 }))
+    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+
+    val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME)
+
+    // Result should be false as queried usage stats should be considered to determine the result
+    // instead of cached stats
+    assertThat(result).isFalse()
+  }
+
+  companion object {
+    private const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
+    private const val YOUTUBE_PACKAGE_NAME = "com.google.android.youtube"
+  }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationProtoUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationProtoUtils.kt
new file mode 100644
index 0000000..def4b91
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationProtoUtils.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.util
+
+import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
+
+/**
+ * Constructs a [WindowingEducationProto] object, populating its fields with the provided
+ * parameters.
+ *
+ * Any fields without corresponding parameters will retain their default values.
+ */
+fun createWindowingEducationProto(
+    educationViewedTimestampMillis: Long? = null,
+    featureUsedTimestampMillis: Long? = null,
+    appUsageStats: Map<String, Int>? = null,
+    appUsageStatsLastUpdateTimestampMillis: Long? = null
+): WindowingEducationProto =
+    WindowingEducationProto.newBuilder()
+        .apply {
+          if (educationViewedTimestampMillis != null) {
+            setEducationViewedTimestampMillis(educationViewedTimestampMillis)
+          }
+          if (featureUsedTimestampMillis != null) {
+            setFeatureUsedTimestampMillis(featureUsedTimestampMillis)
+          }
+          setAppHandleEducation(
+              createAppHandleEducationProto(appUsageStats, appUsageStatsLastUpdateTimestampMillis))
+        }
+        .build()
+
+/**
+ * Constructs a [WindowingEducationProto.AppHandleEducation] object, populating its fields with the
+ * provided parameters.
+ *
+ * Any fields without corresponding parameters will retain their default values.
+ */
+fun createAppHandleEducationProto(
+    appUsageStats: Map<String, Int>? = null,
+    appUsageStatsLastUpdateTimestampMillis: Long? = null
+): WindowingEducationProto.AppHandleEducation =
+    WindowingEducationProto.AppHandleEducation.newBuilder()
+        .apply {
+          if (appUsageStats != null) putAllAppUsageStats(appUsageStats)
+          if (appUsageStatsLastUpdateTimestampMillis != null) {
+            setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestampMillis)
+          }
+        }
+        .build()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index ec7c77b..e619814 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -1106,7 +1106,10 @@
             val directionSign = if (transition.isUpOrLeft) -1 else 1
             val isToContent = overscroll.scene == transition.toContent
             val linearProgress = transition.progress.let { if (isToContent) it - 1f else it }
-            val progress = directionSign * overscroll.progressConverter(linearProgress)
+            val progressConverter =
+                overscroll.progressConverter
+                    ?: layoutImpl.state.transitions.defaultProgressConverter
+            val progress = directionSign * progressConverter.convert(linearProgress)
             val rangeProgress = propertySpec.range?.progress(progress) ?: progress
 
             // Interpolate between the value at rest and the over scrolled value.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
index ae5344f..8f1a4141 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
@@ -74,6 +74,12 @@
          * the transition completes/settles.
          */
         val isUserInputOngoing: Flow<Boolean>,
+
+        /** Current progress of the preview part of the transition */
+        val previewProgress: Flow<Float> = flowOf(0f),
+
+        /** Whether the transition is currently in the preview stage or not */
+        val isInPreviewStage: Flow<Boolean> = flowOf(false),
     ) : ObservableTransitionState {
         override fun toString(): String =
             """Transition
@@ -113,6 +119,8 @@
                         progress = snapshotFlow { state.progress },
                         isInitiatedByUserInput = state.isInitiatedByUserInput,
                         isUserInputOngoing = snapshotFlow { state.isUserInputOngoing },
+                        previewProgress = snapshotFlow { state.previewProgress },
+                        isInPreviewStage = snapshotFlow { state.isInPreviewStage }
                     )
                 }
             }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
index 2fbdf7c..cc53a28 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
@@ -85,7 +85,7 @@
         get() = 0f // Currently, velocity is not exposed by predictive back API
 
     override val isInPreviewStage: Boolean
-        get() = progressAnimatable == null && previewTransformationSpec != null
+        get() = previewTransformationSpec != null && currentScene == fromScene
 
     override val progress: Float
         get() = progressAnimatable?.value ?: previewTransformationSpec?.let { 0f } ?: dragProgress
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 3ded1de..d35d956 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -45,6 +45,7 @@
     internal val transitionSpecs: List<TransitionSpecImpl>,
     internal val overscrollSpecs: List<OverscrollSpecImpl>,
     internal val interruptionHandler: InterruptionHandler,
+    internal val defaultProgressConverter: ProgressConverter,
 ) {
     private val transitionCache =
         mutableMapOf<
@@ -147,6 +148,7 @@
                 transitionSpecs = emptyList(),
                 overscrollSpecs = emptyList(),
                 interruptionHandler = DefaultInterruptionHandler,
+                defaultProgressConverter = ProgressConverter.Default,
             )
     }
 }
@@ -282,14 +284,14 @@
      * - 1, the user overscrolled by exactly the [OverscrollBuilder.distance].
      * - Greater than 1, the user overscrolled more than the [OverscrollBuilder.distance].
      */
-    val progressConverter: (Float) -> Float
+    val progressConverter: ProgressConverter?
 }
 
 internal class OverscrollSpecImpl(
     override val scene: SceneKey,
     override val orientation: Orientation,
     override val transformationSpec: TransformationSpecImpl,
-    override val progressConverter: (Float) -> Float,
+    override val progressConverter: ProgressConverter?,
 ) : OverscrollSpec
 
 /**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 5e96381..e38c849 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.content.state.ContentState
+import kotlin.math.tanh
 
 /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
 fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
@@ -49,6 +50,12 @@
     var interruptionHandler: InterruptionHandler
 
     /**
+     * Default [ProgressConverter] used during overscroll. It lets you change a linear progress into
+     * a function of your choice. Defaults to [ProgressConverter.Default].
+     */
+    var defaultOverscrollProgressConverter: ProgressConverter
+
+    /**
      * Define the default animation to be played when transitioning [to] the specified content, from
      * any content. For the animation specification to apply only when transitioning between two
      * specific contents, use [from] instead.
@@ -216,7 +223,7 @@
      * - 1, the user overscrolled by exactly the [distance].
      * - Greater than 1, the user overscrolled more than the [distance].
      */
-    var progressConverter: (Float) -> Float
+    var progressConverter: ProgressConverter?
 
     /** Translate the element(s) matching [matcher] by ([x], [y]) pixels. */
     fun translate(
@@ -510,3 +517,35 @@
         anchorHeight: Boolean = true,
     )
 }
+
+/** This converter lets you change a linear progress into a function of your choice. */
+fun interface ProgressConverter {
+    fun convert(progress: Float): Float
+
+    companion object {
+        /** Keeps scrolling linearly */
+        val Default = linear()
+
+        /**
+         * The scroll stays linear, with [factor] you can control how much resistance there is.
+         *
+         * @param factor If you choose a value between 0f and 1f, the progress will grow more
+         *   slowly, like there's resistance. A value of 1f means there's no resistance.
+         */
+        fun linear(factor: Float = 1f) = ProgressConverter { it * factor }
+
+        /**
+         * This function starts linear and slowly approaches [maxProgress].
+         *
+         * See a [visual representation](https://www.desmos.com/calculator/usgvvf0z1u) of this
+         * function.
+         *
+         * @param maxProgress is the maximum progress value.
+         * @param tilt behaves similarly to the factor in the [linear] function, and allows you to
+         *   control how quickly you get to the [maxProgress].
+         */
+        fun tanh(maxProgress: Float, tilt: Float = 1f) = ProgressConverter {
+            maxProgress * tanh(x = it / (maxProgress * tilt))
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index a63b19a..523e5bdd7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -50,12 +50,14 @@
         impl.transitionSpecs,
         impl.transitionOverscrollSpecs,
         impl.interruptionHandler,
+        impl.defaultOverscrollProgressConverter,
     )
 }
 
 private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
     override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
     override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler
+    override var defaultOverscrollProgressConverter: ProgressConverter = ProgressConverter.Default
 
     val transitionSpecs = mutableListOf<TransitionSpecImpl>()
     val transitionOverscrollSpecs = mutableListOf<OverscrollSpecImpl>()
@@ -271,7 +273,7 @@
 }
 
 internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), OverscrollBuilder {
-    override var progressConverter: (Float) -> Float = { it }
+    override var progressConverter: ProgressConverter? = null
 
     override fun translate(
         matcher: ElementMatcher,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 60cefb0..20b9b49 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -961,6 +961,97 @@
     }
 
     @Test
+    fun elementTransitionWithDistanceDuringOverscrollWithDefaultProgressConverter() {
+        val layoutWidth = 200.dp
+        val layoutHeight = 400.dp
+        var animatedFloat = 0f
+        val state =
+            setupOverscrollScenario(
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight,
+                sceneTransitions = {
+                    // Overscroll progress will be halved
+                    defaultOverscrollProgressConverter = ProgressConverter { it / 2f }
+
+                    overscroll(SceneB, Orientation.Vertical) {
+                        // On overscroll 100% -> Foo should translate by layoutHeight
+                        translate(TestElements.Foo, y = { absoluteDistance })
+                    }
+                },
+                firstScroll = 1f, // 100% scroll
+                animatedFloatRange = 0f..100f,
+                onAnimatedFloat = { animatedFloat = it },
+            )
+
+        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
+        fooElement.assertTopPositionInRootIsEqualTo(0.dp)
+        assertThat(animatedFloat).isEqualTo(100f)
+
+        rule.onRoot().performTouchInput {
+            // Scroll another 100%
+            moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000)
+        }
+
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(animatedFloat).isEqualTo(100f)
+
+        // Scroll 200% (100% scroll + 100% overscroll)
+        assertThat(transition).hasProgress(2f)
+        assertThat(transition).hasOverscrollSpec()
+
+        // Overscroll progress is halved, we are at 50% of the overscroll progress.
+        fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
+        assertThat(animatedFloat).isEqualTo(100f)
+    }
+
+    @Test
+    fun elementTransitionWithDistanceDuringOverscrollWithOverrideDefaultProgressConverter() {
+        val layoutWidth = 200.dp
+        val layoutHeight = 400.dp
+        var animatedFloat = 0f
+        val state =
+            setupOverscrollScenario(
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight,
+                sceneTransitions = {
+                    // Overscroll progress will be linear (by default)
+                    defaultOverscrollProgressConverter = ProgressConverter { it }
+
+                    overscroll(SceneB, Orientation.Vertical) {
+                        // This override the defaultOverscrollProgressConverter
+                        // Overscroll progress will be halved
+                        progressConverter = ProgressConverter { it / 2f }
+                        // On overscroll 100% -> Foo should translate by layoutHeight
+                        translate(TestElements.Foo, y = { absoluteDistance })
+                    }
+                },
+                firstScroll = 1f, // 100% scroll
+                animatedFloatRange = 0f..100f,
+                onAnimatedFloat = { animatedFloat = it },
+            )
+
+        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
+        fooElement.assertTopPositionInRootIsEqualTo(0.dp)
+        assertThat(animatedFloat).isEqualTo(100f)
+
+        rule.onRoot().performTouchInput {
+            // Scroll another 100%
+            moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000)
+        }
+
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(animatedFloat).isEqualTo(100f)
+
+        // Scroll 200% (100% scroll + 100% overscroll)
+        assertThat(transition).hasProgress(2f)
+        assertThat(transition).hasOverscrollSpec()
+
+        // Overscroll progress is halved, we are at 50% of the overscroll progress.
+        fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
+        assertThat(animatedFloat).isEqualTo(100f)
+    }
+
+    @Test
     fun elementTransitionWithDistanceDuringOverscrollWithProgressConverter() {
         val layoutWidth = 200.dp
         val layoutHeight = 400.dp
@@ -972,7 +1063,7 @@
                 sceneTransitions = {
                     overscroll(SceneB, Orientation.Vertical) {
                         // Overscroll progress will be halved
-                        progressConverter = { it / 2f }
+                        progressConverter = ProgressConverter { it / 2f }
 
                         // On overscroll 100% -> Foo should translate by layoutHeight
                         translate(TestElements.Foo, y = { absoluteDistance })
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
index f717301..0543e7f 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
@@ -16,11 +16,16 @@
 
 package com.android.compose.animation.scene
 
+import androidx.activity.BackEventCompat
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.compose.animation.scene.TestScenes.SceneA
 import com.android.compose.animation.scene.TestScenes.SceneB
@@ -36,7 +41,7 @@
 
 @RunWith(AndroidJUnit4::class)
 class ObservableTransitionStateTest {
-    @get:Rule val rule = createComposeRule()
+    @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
 
     @Test
     fun testObservableTransitionState() = runTest {
@@ -145,6 +150,82 @@
         assertThat(currentScene.value).isEqualTo(SceneA)
     }
 
+    @Test
+    fun testObservablePreviewTransitionState() = runTest {
+        val layoutState =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutState(
+                    SceneA,
+                    transitions = transitions { from(SceneA, to = SceneB, preview = {}) }
+                )
+            }
+        rule.setContent {
+            SceneTransitionLayout(layoutState) {
+                scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) }
+                scene(SceneB) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        var observableState: ObservableTransitionState? = null
+        backgroundScope.launch {
+            layoutState.observableTransitionState().collect { observableState = it }
+        }
+
+        fun observableState(): ObservableTransitionState {
+            runCurrent()
+            return observableState!!
+        }
+
+        fun ObservableTransitionState.Transition.previewProgress(): Float {
+            var lastProgress = -1f
+            backgroundScope.launch { previewProgress.collect { lastProgress = it } }
+            runCurrent()
+            return lastProgress
+        }
+
+        fun ObservableTransitionState.Transition.isInPreviewStage(): Boolean {
+            var lastIsInPreviewStage = false
+            backgroundScope.launch { isInPreviewStage.collect { lastIsInPreviewStage = it } }
+            runCurrent()
+            return lastIsInPreviewStage
+        }
+
+        // Start back.
+        val dispatcher = rule.activity.onBackPressedDispatcher
+        rule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(backEvent())
+            dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
+        }
+
+        var state = observableState()
+        assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java)
+        assertThat((state as ObservableTransitionState.Transition).fromScene).isEqualTo(SceneA)
+        assertThat(state.previewProgress()).isEqualTo(0.4f)
+        assertThat(state.isInPreviewStage()).isEqualTo(true)
+
+        // Cancel it.
+        rule.runOnUiThread { dispatcher.dispatchOnBackCancelled() }
+        rule.waitForIdle()
+        state = observableState()
+        assertThat(state).isInstanceOf(ObservableTransitionState.Idle::class.java)
+
+        // Start again and commit it.
+        rule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(backEvent())
+            dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
+            dispatcher.onBackPressed()
+        }
+        state = observableState()
+        assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java)
+        assertThat((state as ObservableTransitionState.Transition).fromScene).isEqualTo(SceneA)
+        assertThat(state.previewProgress()).isEqualTo(0.4f)
+        assertThat(state.isInPreviewStage()).isEqualTo(false)
+
+        rule.waitForIdle()
+        state = observableState()
+        assertThat(state).isInstanceOf(ObservableTransitionState.Idle::class.java)
+    }
+
     // See http://shortn/_hj4Mhikmos for inspiration.
     private fun runTestWithSnapshots(testBody: suspend TestScope.() -> Unit) {
         val globalWriteObserverHandle =
@@ -159,4 +240,13 @@
             globalWriteObserverHandle.dispose()
         }
     }
+
+    private fun backEvent(progress: Float = 0f): BackEventCompat {
+        return BackEventCompat(
+            touchX = 0f,
+            touchY = 0f,
+            progress = progress,
+            swipeEdge = BackEventCompat.EDGE_LEFT,
+        )
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
index 661d4b0..e58cf15 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
 import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
 import com.android.systemui.qs.panels.data.repository.qsPreferencesRepository
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -40,15 +41,16 @@
         testKosmos().apply {
             defaultLargeTilesRepository =
                 object : DefaultLargeTilesRepository {
-                    override val defaultLargeTiles: Set<TileSpec> = setOf(TileSpec.create("large"))
+                    override val defaultLargeTiles: Set<TileSpec> = setOf(largeTile)
                 }
+            currentTilesInteractor.setTiles(listOf(largeTile, smallTile))
         }
     private val underTest = with(kosmos) { iconTilesInteractor }
 
     @Test
     fun isIconTile_returnsCorrectValue() {
-        assertThat(underTest.isIconTile(TileSpec.create("large"))).isFalse()
-        assertThat(underTest.isIconTile(TileSpec.create("small"))).isTrue()
+        assertThat(underTest.isIconTile(largeTile)).isFalse()
+        assertThat(underTest.isIconTile(smallTile)).isTrue()
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
@@ -56,14 +58,21 @@
     fun isIconTile_updatesFromSharedPreferences() =
         with(kosmos) {
             testScope.runTest {
-                // Assert that new tile defaults to icon
-                assertThat(underTest.isIconTile(TileSpec.create("newTile"))).isTrue()
+                val spec = TileSpec.create("newTile")
 
-                qsPreferencesRepository.setLargeTilesSpecs(setOf(TileSpec.create("newTile")))
+                // Assert that new tile defaults to icon
+                assertThat(underTest.isIconTile(spec)).isTrue()
+
+                // Add the tile
+                currentTilesInteractor.addTile(spec)
+                runCurrent()
+
+                // Resize it to large
+                qsPreferencesRepository.setLargeTilesSpecs(setOf(spec))
                 runCurrent()
 
                 // Assert that the new tile was added to the large tiles set
-                assertThat(underTest.isIconTile(TileSpec.create("newTile"))).isFalse()
+                assertThat(underTest.isIconTile(spec)).isFalse()
             }
         }
 
@@ -72,21 +81,57 @@
     fun resize_updatesSharedPreferences() =
         with(kosmos) {
             testScope.runTest {
-                qsPreferencesRepository.setLargeTilesSpecs(setOf())
-                runCurrent()
-
                 val latest by collectLastValue(qsPreferencesRepository.largeTilesSpecs)
-                val spec = TileSpec.create("large")
-
-                // Assert that the tile is added to the large tiles after resizing
-                underTest.resize(spec)
                 runCurrent()
-                assertThat(latest).contains(spec)
 
                 // Assert that the tile is removed from the large tiles after resizing
-                underTest.resize(spec)
+                underTest.resize(largeTile)
                 runCurrent()
-                assertThat(latest).doesNotContain(spec)
+                assertThat(latest).doesNotContain(largeTile)
+
+                // Assert that the tile is added to the large tiles after resizing
+                underTest.resize(largeTile)
+                runCurrent()
+                assertThat(latest).contains(largeTile)
             }
         }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun removingTile_updatesSharedPreferences() =
+        with(kosmos) {
+            testScope.runTest {
+                val latest by collectLastValue(qsPreferencesRepository.largeTilesSpecs)
+                runCurrent()
+
+                // Remove the large tile from the current tiles
+                currentTilesInteractor.removeTiles(listOf(largeTile))
+                runCurrent()
+
+                // Assert that it resized to small
+                assertThat(latest).doesNotContain(largeTile)
+            }
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun resizingNonCurrentTile_doesNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                val latest by collectLastValue(qsPreferencesRepository.largeTilesSpecs)
+                val newTile = TileSpec.create("newTile")
+
+                // Remove the large tile from the current tiles
+                underTest.resize(newTile)
+                runCurrent()
+
+                // Assert that it's still small
+                assertThat(latest).doesNotContain(newTile)
+            }
+        }
+
+    private companion object {
+        private val largeTile = TileSpec.create("large")
+        private val smallTile = TileSpec.create("small")
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
index 56156a8..ef85302 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
@@ -24,8 +24,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
-import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.res.R
@@ -56,11 +55,9 @@
 
     private val kosmos =
         testKosmos().apply {
-            defaultLargeTilesRepository =
-                object : DefaultLargeTilesRepository {
-                    override val defaultLargeTiles: Set<TileSpec> =
-                        tiles.filter { it.spec.startsWith(PREFIX_LARGE) }.toSet()
-                }
+            qsPreferencesInteractor.setLargeTilesSpecs(
+                tiles.filter { it.spec.startsWith(PREFIX_LARGE) }.toSet()
+            )
         }
 
     private val underTest = kosmos.quickQuickSettingsViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
index c44836a..620e90d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -117,7 +117,11 @@
             label,
             activationState,
             secondaryLabel,
-            setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            setOf(
+                QSTileState.UserAction.CLICK,
+                QSTileState.UserAction.TOGGLE_CLICK,
+                QSTileState.UserAction.LONG_CLICK
+            ),
             contentDescription,
             null,
             QSTileState.SideViewIcon.Chevron,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
index e1f3d97..52c476e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
 import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.dialog.WifiStateWorker
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
 import com.android.systemui.statusbar.connectivity.AccessPointController
 import com.android.systemui.util.mockito.mock
@@ -40,6 +41,8 @@
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.times
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @EnabledOnRavenwood
@@ -51,17 +54,20 @@
     private lateinit var underTest: InternetTileUserActionInteractor
 
     @Mock private lateinit var internetDialogManager: InternetDialogManager
+    @Mock private lateinit var wifiStateWorker: WifiStateWorker
     @Mock private lateinit var controller: AccessPointController
 
     @Before
     fun setup() {
         internetDialogManager = mock<InternetDialogManager>()
+        wifiStateWorker = mock<WifiStateWorker>()
         controller = mock<AccessPointController>()
 
         underTest =
             InternetTileUserActionInteractor(
                 kosmos.testScope.coroutineContext,
                 internetDialogManager,
+                wifiStateWorker,
                 controller,
                 inputHandler,
             )
@@ -110,4 +116,24 @@
                 Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
             }
         }
+
+    @Test
+    fun handleSecondaryClickWhenWifiOn() =
+        kosmos.testScope.runTest {
+            whenever(wifiStateWorker.isWifiEnabled).thenReturn(true)
+
+            underTest.handleInput(QSTileInputTestKtx.toggleClick(InternetTileModel.Active()))
+
+            verify(wifiStateWorker, times(1)).isWifiEnabled = eq(false)
+        }
+
+    @Test
+    fun handleSecondaryClickWhenWifiOff() =
+        kosmos.testScope.runTest {
+            whenever(wifiStateWorker.isWifiEnabled).thenReturn(false)
+
+            underTest.handleInput(QSTileInputTestKtx.toggleClick(InternetTileModel.Inactive()))
+
+            verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 355669b..f72a2e8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.scene.data.repository.Idle
 import com.android.systemui.scene.data.repository.setTransition
+import com.android.systemui.scene.domain.interactor.sceneBackInteractor
 import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
@@ -112,6 +113,7 @@
                     { kosmos.sceneInteractor },
                     { kosmos.sceneContainerOcclusionInteractor },
                     { kosmos.keyguardClockInteractor },
+                    { kosmos.sceneBackInteractor },
                 ) {
                 override fun createDarkAnimator(): ObjectAnimator {
                     return mockDarkAnimator
@@ -320,12 +322,23 @@
 
             assertThat(deviceUnlockStatus!!.isUnlocked).isTrue()
 
-            kosmos.sceneInteractor.changeScene(toScene = Scenes.Gone, loggingReason = "reason")
+            kosmos.sceneInteractor.changeScene(
+                toScene = Scenes.Lockscreen,
+                loggingReason = "reason"
+            )
             runCurrent()
-            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
 
             // Call start to begin hydrating based on the scene framework:
             underTest.start()
+            runCurrent()
+
+            assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+
+            kosmos.sceneInteractor.changeScene(toScene = Scenes.Gone, loggingReason = "reason")
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
 
             kosmos.sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = "reason")
             runCurrent()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index d13c750..be44dee 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -169,6 +169,7 @@
         public boolean isTransient = false;
         public String expandedAccessibilityClassName;
         public boolean handlesLongClick = true;
+        public boolean handlesSecondaryClick = false;
         @Nullable
         public Drawable sideViewCustomDrawable;
         public String spec;
@@ -212,6 +213,7 @@
                     || !Objects.equals(other.isTransient, isTransient)
                     || !Objects.equals(other.dualTarget, dualTarget)
                     || !Objects.equals(other.handlesLongClick, handlesLongClick)
+                    || !Objects.equals(other.handlesSecondaryClick, handlesSecondaryClick)
                     || !Objects.equals(other.sideViewCustomDrawable, sideViewCustomDrawable);
             other.spec = spec;
             other.icon = icon;
@@ -227,6 +229,7 @@
             other.dualTarget = dualTarget;
             other.isTransient = isTransient;
             other.handlesLongClick = handlesLongClick;
+            other.handlesSecondaryClick = handlesSecondaryClick;
             other.sideViewCustomDrawable = sideViewCustomDrawable;
             return changed;
         }
@@ -252,6 +255,7 @@
             sb.append(",disabledByPolicy=").append(disabledByPolicy);
             sb.append(",dualTarget=").append(dualTarget);
             sb.append(",isTransient=").append(isTransient);
+            sb.append(",handlesSecondaryClick=").append(handlesSecondaryClick);
             sb.append(",state=").append(state);
             sb.append(",sideViewCustomDrawable=").append(sideViewCustomDrawable);
             return sb.append(']');
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/model/UserDeviceConnectionStatus.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/model/UserDeviceConnectionStatus.kt
new file mode 100644
index 0000000..1a22d3c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/model/UserDeviceConnectionStatus.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputdevice.data.model
+
+data class UserDeviceConnectionStatus(val isConnected: Boolean, val userId: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/UserInputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/UserInputDeviceRepository.kt
new file mode 100644
index 0000000..b8e73a3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/UserInputDeviceRepository.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputdevice.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputdevice.data.model.UserDeviceConnectionStatus
+import com.android.systemui.keyboard.data.repository.KeyboardRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import com.android.systemui.user.data.model.SelectionStatus
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * Allow listening keyboard and touchpad device connection changes for current user. It emits new
+ * value when user is changed.
+ */
+@SysUISingleton
+class UserInputDeviceRepository
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    keyboardRepository: KeyboardRepository,
+    touchpadRepository: TouchpadRepository,
+    userRepository: UserRepository,
+) {
+    private val selectedUserId =
+        userRepository.selectedUser
+            .filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE }
+            .map { it.userInfo.id }
+
+    val isAnyKeyboardConnectedForUser =
+        keyboardRepository.isAnyKeyboardConnected
+            .combine(selectedUserId) { isAnyKeyboardConnected, userId ->
+                UserDeviceConnectionStatus(isAnyKeyboardConnected, userId)
+            }
+            .flowOn(backgroundDispatcher)
+
+    val isAnyTouchpadConnectedForUser =
+        touchpadRepository.isAnyTouchpadConnected
+            .combine(selectedUserId) { isAnyTouchpadConnected, userId ->
+                UserDeviceConnectionStatus(isAnyTouchpadConnected, userId)
+            }
+            .flowOn(backgroundDispatcher)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
index 19b46e3..04aa04d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
@@ -63,7 +63,11 @@
                 if (categories.isEmpty()) {
                     ShortcutsUiState.Inactive
                 } else {
-                    val filteredCategories = filterCategoriesBySearchQuery(query, categories)
+                    /* temporarily hiding launcher shortcut categories until b/327141011
+                     * is completed. */
+                    val categoriesWithLauncherExcluded = excludeLauncherApp(categories)
+                    val filteredCategories =
+                        filterCategoriesBySearchQuery(query, categoriesWithLauncherExcluded)
                     ShortcutsUiState.Active(
                         searchQuery = query,
                         shortcutCategories = filteredCategories,
@@ -77,15 +81,27 @@
                 initialValue = ShortcutsUiState.Inactive
             )
 
+    private suspend fun excludeLauncherApp(
+        categories: List<ShortcutCategory>
+    ): List<ShortcutCategory> {
+        val launcherAppCategory =
+            categories.firstOrNull { it.type is CurrentApp && isLauncherApp(it.type.packageName) }
+        return if (launcherAppCategory != null) {
+            categories - launcherAppCategory
+        } else {
+            categories
+        }
+    }
+
     private suspend fun getDefaultSelectedCategory(
         categories: List<ShortcutCategory>
     ): ShortcutCategoryType? {
         val currentAppShortcuts =
-            categories.firstOrNull { it.type is CurrentApp && !isAppLauncher(it.type.packageName) }
+            categories.firstOrNull { it.type is CurrentApp && !isLauncherApp(it.type.packageName) }
         return currentAppShortcuts?.type ?: categories.firstOrNull()?.type
     }
 
-    private suspend fun isAppLauncher(packageName: String): Boolean {
+    private suspend fun isLauncherApp(packageName: String): Boolean {
         return withContext(backgroundDispatcher) {
             roleManager
                 .getRoleHoldersAsUser(RoleManager.ROLE_HOME, userTracker.userHandle)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
index 6dcdea9..02a607d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
@@ -22,10 +22,12 @@
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
 import com.android.systemui.qs.panels.shared.model.PanelsLog
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
@@ -35,19 +37,38 @@
 @Inject
 constructor(
     repo: DefaultLargeTilesRepository,
+    private val currentTilesInteractor: CurrentTilesInteractor,
     private val preferencesInteractor: QSPreferencesInteractor,
     @PanelsLog private val logBuffer: LogBuffer,
     @Application private val applicationScope: CoroutineScope
 ) {
 
     val largeTilesSpecs =
-        preferencesInteractor.largeTilesSpecs
+        combine(preferencesInteractor.largeTilesSpecs, currentTilesInteractor.currentTiles) {
+                largeTiles,
+                currentTiles ->
+                if (currentTiles.isEmpty()) {
+                    largeTiles
+                } else {
+                    // Only current tiles can be resized, so observe the current tiles and find the
+                    // intersection between them and the large tiles.
+                    val newLargeTiles = largeTiles intersect currentTiles.map { it.spec }.toSet()
+                    if (newLargeTiles != largeTiles) {
+                        preferencesInteractor.setLargeTilesSpecs(newLargeTiles)
+                    }
+                    newLargeTiles
+                }
+            }
             .onEach { logChange(it) }
             .stateIn(applicationScope, SharingStarted.Eagerly, repo.defaultLargeTiles)
 
     fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec)
 
     fun resize(spec: TileSpec) {
+        if (!isCurrent(spec)) {
+            return
+        }
+
         if (largeTilesSpecs.value.contains(spec)) {
             preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec)
         } else {
@@ -55,6 +76,10 @@
         }
     }
 
+    private fun isCurrent(spec: TileSpec): Boolean {
+        return currentTilesInteractor.currentTilesSpecs.contains(spec)
+    }
+
     private fun logChange(specs: Set<TileSpec>) {
         logBuffer.log(
             LOG_BUFFER_LARGE_TILES_SPECS_CHANGE_TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index c06d6d2..6eacb2e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -109,6 +109,7 @@
 import com.android.systemui.qs.panels.ui.model.SpacerGridCell
 import com.android.systemui.qs.panels.ui.model.TileGridCell
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.toUiState
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
@@ -129,7 +130,7 @@
 ) {
     val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
     val uiState = remember(state) { state.toUiState() }
-    val colors = TileDefaults.getColorForState(uiState.state)
+    val colors = TileDefaults.getColorForState(uiState)
 
     TileContainer(
         colors = colors,
@@ -150,9 +151,13 @@
                 secondaryLabel = uiState.secondaryLabel,
                 icon = icon,
                 colors = colors,
-                clickEnabled = true,
-                onClick = tile::onSecondaryClick,
-                onLongClick = tile::onLongClick,
+                toggleClickSupported = state.handlesSecondaryClick,
+                onClick = {
+                    if (state.handlesSecondaryClick) {
+                        tile.onSecondaryClick()
+                    }
+                },
+                onLongClick = { tile.onLongClick(it) },
             )
         }
     }
@@ -168,7 +173,7 @@
     onClick: (Expandable) -> Unit = {},
     onLongClick: (Expandable) -> Unit = {},
     modifier: Modifier = Modifier,
-    content: @Composable BoxScope.() -> Unit,
+    content: @Composable BoxScope.(Expandable) -> Unit,
 ) {
     Column(
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -200,7 +205,7 @@
                         }
                         .tilePadding(),
             ) {
-                content()
+                content(it)
             }
         }
 
@@ -222,36 +227,27 @@
     secondaryLabel: String?,
     icon: Icon,
     colors: TileColors,
-    clickEnabled: Boolean = false,
-    onClick: (Expandable) -> Unit = {},
-    onLongClick: (Expandable) -> Unit = {},
+    toggleClickSupported: Boolean = false,
+    onClick: () -> Unit = {},
+    onLongClick: () -> Unit = {},
 ) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = tileHorizontalArrangement()
     ) {
-        Expandable(
-            color = colors.iconBackground,
-            shape = TileDefaults.TileShape,
-            modifier = Modifier.fillMaxHeight().aspectRatio(1f)
+        // Icon
+        Box(
+            modifier =
+                Modifier.fillMaxHeight().aspectRatio(1f).thenIf(toggleClickSupported) {
+                    Modifier.clip(TileDefaults.TileShape)
+                        .background(colors.iconBackground, { 1f })
+                        .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+                }
         ) {
-            Box(
-                modifier =
-                    Modifier.fillMaxSize().clip(TileDefaults.TileShape).thenIf(clickEnabled) {
-                        Modifier.combinedClickable(
-                            onClick = { onClick(it) },
-                            onLongClick = { onLongClick(it) }
-                        )
-                    }
-            ) {
-                TileIcon(
-                    icon = icon,
-                    color = colors.icon,
-                    modifier = Modifier.align(Alignment.Center)
-                )
-            }
+            TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
         }
 
+        // Labels
         Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
             Text(
                 label,
@@ -743,9 +739,21 @@
     val TileShape = CircleShape
     val IconTileWithLabelHeight = 140.dp
 
+    /** An active tile without dual target uses the active color as background */
     @Composable
     fun activeTileColors(): TileColors =
         TileColors(
+            background = MaterialTheme.colorScheme.primary,
+            iconBackground = MaterialTheme.colorScheme.primary,
+            label = MaterialTheme.colorScheme.onPrimary,
+            secondaryLabel = MaterialTheme.colorScheme.onPrimary,
+            icon = MaterialTheme.colorScheme.onPrimary,
+        )
+
+    /** An active tile with dual target only show the active color on the icon */
+    @Composable
+    fun activeDualTargetTileColors(): TileColors =
+        TileColors(
             background = MaterialTheme.colorScheme.surfaceVariant,
             iconBackground = MaterialTheme.colorScheme.primary,
             label = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -774,9 +782,15 @@
         )
 
     @Composable
-    fun getColorForState(state: Int): TileColors {
-        return when (state) {
-            STATE_ACTIVE -> activeTileColors()
+    fun getColorForState(uiState: TileUiState): TileColors {
+        return when (uiState.state) {
+            STATE_ACTIVE -> {
+                if (uiState.handlesSecondaryClick) {
+                    activeDualTargetTileColors()
+                } else {
+                    activeTileColors()
+                }
+            }
             STATE_INACTIVE -> inactiveTileColors()
             else -> unavailableTileColors()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
index c83e3b2..45051fe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
@@ -25,6 +25,7 @@
     val label: String,
     val secondaryLabel: String,
     val state: Int,
+    val handlesSecondaryClick: Boolean,
     val icon: Supplier<QSTile.Icon?>,
 )
 
@@ -33,6 +34,7 @@
         label?.toString() ?: "",
         secondaryLabel?.toString() ?: "",
         state,
+        handlesSecondaryClick,
         icon?.let { Supplier { icon } } ?: iconSupplier ?: Supplier { null },
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
index 8578bb0..44dd801 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
@@ -50,8 +50,8 @@
         tile.longClick(expandable)
     }
 
-    fun onSecondaryClick(expandable: Expandable?) {
-        tile.secondaryClick(expandable)
+    fun onSecondaryClick() {
+        tile.secondaryClick(null)
     }
 
     fun startListening(token: Any) = tile.setListening(token, true)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 9f41d98..7ceb786 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -114,7 +114,9 @@
 
     @Override
     public BooleanState newTileState() {
-        return new BooleanState();
+        BooleanState s = new BooleanState();
+        s.handlesSecondaryClick = true;
+        return s;
     }
 
     @Override
@@ -141,10 +143,7 @@
             mDialogViewModel.showDialog(expandable);
         } else {
             // Secondary clicks are header clicks, just toggle.
-            final boolean isEnabled = mState.value;
-            // Immediately enter transient enabling state when turning bluetooth on.
-            refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
-            mController.setBluetoothEnabled(!isEnabled);
+            toggleBluetooth();
         }
     }
 
@@ -160,9 +159,7 @@
                     new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0);
             return;
         }
-        if (!mState.value) {
-            mController.setBluetoothEnabled(true);
-        }
+        toggleBluetooth();
     }
 
     @Override
@@ -228,6 +225,13 @@
         state.forceExpandIcon = mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG);
     }
 
+    private void toggleBluetooth() {
+        final boolean isEnabled = mState.value;
+        // Immediately enter transient enabling state when turning bluetooth on.
+        refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
+        mController.setBluetoothEnabled(!isEnabled);
+    }
+
     /**
      * Returns the secondary label to use for the given bluetooth connection in the form of the
      * battery level or bluetooth profile name. If the bluetooth is disabled, there's no connected
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 6d98da4..02f6f80 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -52,6 +52,7 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager;
+import com.android.systemui.qs.tiles.dialog.WifiStateWorker;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.connectivity.AccessPointController;
 import com.android.systemui.statusbar.connectivity.IconState;
@@ -84,6 +85,7 @@
 
     protected final InternetSignalCallback mSignalCallback = new InternetSignalCallback();
     private final InternetDialogManager mInternetDialogManager;
+    private final WifiStateWorker mWifiStateWorker;
     final Handler mHandler;
 
     @Inject
@@ -99,11 +101,13 @@
             QSLogger qsLogger,
             NetworkController networkController,
             AccessPointController accessPointController,
-            InternetDialogManager internetDialogManager
+            InternetDialogManager internetDialogManager,
+            WifiStateWorker wifiStateWorker
     ) {
         super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
         mInternetDialogManager = internetDialogManager;
+        mWifiStateWorker = wifiStateWorker;
         mHandler = mainHandler;
         mController = networkController;
         mAccessPointController = accessPointController;
@@ -115,6 +119,7 @@
     public BooleanState newTileState() {
         BooleanState s = new BooleanState();
         s.forceExpandIcon = true;
+        s.handlesSecondaryClick = true;
         return s;
     }
 
@@ -131,6 +136,13 @@
     }
 
     @Override
+    public void secondaryClick(@Nullable Expandable expandable) {
+        // TODO(b/358352265): Figure out the correct action for the secondary click
+        // Toggle Wifi
+        mWifiStateWorker.setWifiEnabled(!mWifiStateWorker.isWifiEnabled());
+    }
+
+    @Override
     public CharSequence getTileLabel() {
         return mContext.getString(R.string.quick_settings_internet_label);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
index 932dec5..42ef0cd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.dialog.WifiStateWorker
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.connectivity.AccessPointController
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.InternetTileBinder
@@ -55,6 +56,7 @@
     qsLogger: QSLogger,
     viewModel: InternetTileViewModel,
     private val internetDialogManager: InternetDialogManager,
+    private val wifiStateWorker: WifiStateWorker,
     private val accessPointController: AccessPointController,
 ) :
     QSTileImpl<QSTile.BooleanState>(
@@ -81,7 +83,10 @@
         mContext.getString(R.string.quick_settings_internet_label)
 
     override fun newTileState(): QSTile.BooleanState {
-        return QSTile.BooleanState().also { it.forceExpandIcon = true }
+        return QSTile.BooleanState().also {
+            it.forceExpandIcon = true
+            it.handlesSecondaryClick = true
+        }
     }
 
     override fun handleClick(expandable: Expandable?) {
@@ -95,6 +100,12 @@
         }
     }
 
+    override fun secondaryClick(expandable: Expandable?) {
+        // TODO(b/358352265): Figure out the correct action for the secondary click
+        // Toggle wifi
+        wifiStateWorker.isWifiEnabled = !wifiStateWorker.isWifiEnabled
+    }
+
     override fun handleUpdateState(state: QSTile.BooleanState, arg: Any?) {
         state.label = mContext.resources.getString(R.string.quick_settings_internet_label)
         state.expandedAccessibilityClassName = Switch::class.java.name
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt
index 0d15a5b..1d42777 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt
@@ -47,6 +47,7 @@
     private fun QSTileUserAction.getQSEvent(): QSEvent =
         when (this) {
             is QSTileUserAction.Click -> QSEvent.QS_ACTION_CLICK
+            is QSTileUserAction.ToggleClick -> QSEvent.QS_ACTION_SECONDARY_CLICK
             is QSTileUserAction.LongClick -> QSEvent.QS_ACTION_LONG_PRESS
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
index f0d7206..8ec8a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -222,6 +222,7 @@
     private fun QSTileUserAction.toLogString(): String =
         when (this) {
             is QSTileUserAction.Click -> "click"
+            is QSTileUserAction.ToggleClick -> "toggle click"
             is QSTileUserAction.LongClick -> "long click"
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 9e84f01..d8c5af2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -229,7 +229,8 @@
         filter { action ->
             val isFalseAction =
                 when (action) {
-                    is QSTileUserAction.Click ->
+                    is QSTileUserAction.Click,
+                    is QSTileUserAction.ToggleClick ->
                         falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
                     is QSTileUserAction.LongClick ->
                         falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
index bf0f8f6..5053291 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
@@ -57,6 +57,7 @@
                         Intent(Settings.ACTION_AIRPLANE_MODE_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
index 14fc57c..79fcd37 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
@@ -49,6 +49,7 @@
                     }
                 }
                 is QSTileUserAction.LongClick -> {}
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
index d4b4fe0..3bbb9aa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
@@ -48,6 +48,7 @@
                         Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
index 534bd73..dfdec3b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
@@ -49,6 +49,7 @@
                         Intent(Settings.ACTION_COLOR_CORRECTION_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
index 9bdf631..af2bb9d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
@@ -74,6 +74,7 @@
                     click(action.expandable, data.tile.activityLaunchForClick)
                 is QSTileUserAction.LongClick ->
                     longClick(user, action.expandable, data.componentName, data.tile.state)
+                is QSTileUserAction.ToggleClick -> {}
             }
             qsTileLogger.logCustomTileUserActionDelivered(tileSpec)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
index bedd65e..13afc15 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
@@ -42,6 +42,7 @@
                         flashlightController.setFlashlight(!input.data.isEnabled)
                     }
                 }
+                is QSTileUserAction.ToggleClick -> {}
                 else -> {}
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
index d308ec8..6ab5796 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
@@ -66,8 +66,7 @@
                                         INTERACTION_JANK_TAG
                                     )
                                 )
-                                ?.let { dialogTransitionAnimator.show(dialog, it) }
-                                ?: dialog.show()
+                                ?.let { dialogTransitionAnimator.show(dialog, it) } ?: dialog.show()
                         } else {
                             dialog.show()
                         }
@@ -89,8 +88,10 @@
                         Intent(Settings.ACTION_TEXT_READING_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
+
     companion object {
         private const val INTERACTION_JANK_TAG = "font_scaling"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
index e543e4b..8965ef2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -72,6 +72,10 @@
                 else QSTileState.ActivationState.INACTIVE
 
             supportedActions =
-                setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+                setOf(
+                    QSTileState.UserAction.CLICK,
+                    QSTileState.UserAction.TOGGLE_CLICK,
+                    QSTileState.UserAction.LONG_CLICK
+                )
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index c0b089d..a963b28 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.dialog.WifiStateWorker
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 import com.android.systemui.statusbar.connectivity.AccessPointController
@@ -36,6 +37,7 @@
 constructor(
     @Main private val mainContext: CoroutineContext,
     private val internetDialogManager: InternetDialogManager,
+    private val wifiStateWorker: WifiStateWorker,
     private val accessPointController: AccessPointController,
     private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
 ) : QSTileUserActionInteractor<InternetTileModel> {
@@ -53,6 +55,11 @@
                         )
                     }
                 }
+                is QSTileUserAction.ToggleClick -> {
+                    // TODO(b/358352265): Figure out the correct action for the secondary click
+                    // Toggle Wifi
+                    wifiStateWorker.isWifiEnabled = !wifiStateWorker.isWifiEnabled
+                }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
                         action.expandable,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
index d643273..aa83877 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
@@ -49,6 +49,7 @@
                         Intent(Settings.ACTION_COLOR_INVERSION_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
index 77404aa..cca947f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
@@ -68,6 +68,7 @@
                         Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
index 083bf05..eb8b23c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
@@ -41,7 +41,8 @@
     override suspend fun handleInput(input: QSTileInput<ModesTileModel>) {
         with(input) {
             when (action) {
-                is QSTileUserAction.Click -> {
+                is QSTileUserAction.Click,
+                is QSTileUserAction.ToggleClick -> {
                     handleClick(action.expandable)
                 }
                 is QSTileUserAction.LongClick -> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
index 5cee8c4..7076a8f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
@@ -55,6 +55,7 @@
                         Intent(Settings.ACTION_NIGHT_DISPLAY_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt
index 5cb0e18..0a0f0a6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt
@@ -49,6 +49,7 @@
                         Intent(Settings.ACTION_ONE_HANDED_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/domain/interactor/QRCodeScannerTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/domain/interactor/QRCodeScannerTileUserActionInteractor.kt
index 7c0c41e..bb5df02 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/domain/interactor/QRCodeScannerTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/domain/interactor/QRCodeScannerTileUserActionInteractor.kt
@@ -45,6 +45,7 @@
                     }
                 }
                 is QSTileUserAction.LongClick -> {} // no-op
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
index ed5e4fe..de49e70 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
@@ -71,6 +71,7 @@
                         Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
index 34385ea..65712c7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
@@ -46,6 +46,7 @@
                         Intent(Settings.ACTION_AUTO_ROTATE_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
index a5dc66c..252e3f8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
@@ -94,8 +94,7 @@
                             )
                             ?.let { controller ->
                                 dialogTransitionAnimator.show(dialog, controller)
-                            }
-                            ?: dialog.show()
+                            } ?: dialog.show()
                     }
                 }
                 is QSTileUserAction.LongClick -> {
@@ -104,6 +103,7 @@
                         Intent(Settings.ACTION_DATA_SAVER_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
index 5637115..48b39ed 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
@@ -75,6 +75,7 @@
                     }
                 }
                 is QSTileUserAction.LongClick -> {} // no-op
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
index f22a426..d7f64d1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
@@ -82,6 +82,7 @@
                     }
                     qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent)
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
index f8dd1730..8897828 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
@@ -54,6 +54,7 @@
                         Intent(Settings.ACTION_DARK_THEME_SETTINGS)
                     )
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
index 031e4d9..45ae09e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
@@ -49,6 +49,7 @@
                         )
                     }
                 }
+                is QSTileUserAction.ToggleClick -> {}
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
index 30247c4..549f0a7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
@@ -105,6 +105,7 @@
 
     enum class UserAction {
         CLICK,
+        TOGGLE_CLICK,
         LONG_CLICK,
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
index acb2936..bf3bc73 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
@@ -23,5 +23,8 @@
     val expandable: Expandable?
 
     class Click(override val expandable: Expandable?) : QSTileUserAction
+
+    class ToggleClick(override val expandable: Expandable?) : QSTileUserAction
+
     class LongClick(override val expandable: Expandable?) : QSTileUserAction
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 9bcf927..8077c67 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -131,8 +131,8 @@
     }
 
     override fun secondaryClick(expandable: Expandable?) {
-        if (isActionSupported(QSTileState.UserAction.CLICK)) {
-            qsTileViewModel.onActionPerformed(QSTileUserAction.Click(expandable))
+        if (isActionSupported(QSTileState.UserAction.TOGGLE_CLICK)) {
+            qsTileViewModel.onActionPerformed(QSTileUserAction.ToggleClick(expandable))
         }
     }
 
@@ -184,8 +184,7 @@
         }
     }
 
-    override fun isListening(): Boolean =
-        listeningClients.isNotEmpty()
+    override fun isListening(): Boolean = listeningClients.isNotEmpty()
 
     override fun setDetailListening(show: Boolean) {
         // do nothing like QSTileImpl
@@ -238,6 +237,8 @@
                 secondaryLabel = viewModelState.secondaryLabel
                 handlesLongClick =
                     viewModelState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK)
+                handlesSecondaryClick =
+                    viewModelState.supportedActions.contains(QSTileState.UserAction.TOGGLE_CLICK)
 
                 icon =
                     when (val stateIcon = viewModelState.icon()) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneStack.kt b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneStack.kt
index d3e529c..323bb3d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneStack.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneStack.kt
@@ -55,6 +55,9 @@
     }
 }
 
+/** Does this stack contain the given [sceneKey]? O(N) */
+fun SceneStack.contains(sceneKey: SceneKey): Boolean = asIterable().any { it == sceneKey }
+
 /**
  * Returns a new [SceneStack] containing the given [scenes], ordered such that the first argument is
  * the head returned from [peek], then the second, and so forth.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 0957e5a..3422c67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -54,6 +54,9 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.data.model.SceneStack;
+import com.android.systemui.scene.data.model.SceneStackKt;
+import com.android.systemui.scene.domain.interactor.SceneBackInteractor;
 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor;
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
@@ -118,6 +121,7 @@
     private final Lazy<SceneInteractor> mSceneInteractorLazy;
     private final Lazy<SceneContainerOcclusionInteractor> mSceneContainerOcclusionInteractorLazy;
     private final Lazy<KeyguardClockInteractor> mKeyguardClockInteractorLazy;
+    private final Lazy<SceneBackInteractor> mSceneBackInteractorLazy;
     private int mState;
     private int mLastState;
     private int mUpcomingState;
@@ -186,7 +190,8 @@
             Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
             Lazy<SceneInteractor> sceneInteractorLazy,
             Lazy<SceneContainerOcclusionInteractor> sceneContainerOcclusionInteractor,
-            Lazy<KeyguardClockInteractor> keyguardClockInteractorLazy) {
+            Lazy<KeyguardClockInteractor> keyguardClockInteractorLazy,
+            Lazy<SceneBackInteractor> sceneBackInteractorLazy) {
         mUiEventLogger = uiEventLogger;
         mInteractionJankMonitorLazy = interactionJankMonitorLazy;
         mJavaAdapter = javaAdapter;
@@ -196,6 +201,7 @@
         mSceneInteractorLazy = sceneInteractorLazy;
         mSceneContainerOcclusionInteractorLazy = sceneContainerOcclusionInteractor;
         mKeyguardClockInteractorLazy = keyguardClockInteractorLazy;
+        mSceneBackInteractorLazy = sceneBackInteractorLazy;
         for (int i = 0; i < HISTORY_SIZE; i++) {
             mHistoricalRecords[i] = new HistoricalState();
         }
@@ -221,6 +227,7 @@
                     combineFlows(
                         mDeviceUnlockedInteractorLazy.get().getDeviceUnlockStatus(),
                         mSceneInteractorLazy.get().getCurrentScene(),
+                        mSceneBackInteractorLazy.get().getBackStack(),
                         mSceneContainerOcclusionInteractorLazy.get().getInvisibleDueToOcclusion(),
                         this::calculateStateFromSceneFramework),
                     this::onStatusBarStateChanged);
@@ -677,10 +684,15 @@
     private int calculateStateFromSceneFramework(
             DeviceUnlockStatus deviceUnlockStatus,
             SceneKey currentScene,
+            SceneStack backStack,
             boolean isOccluded) {
         SceneContainerFlag.isUnexpectedlyInLegacyMode();
-
-        if (deviceUnlockStatus.isUnlocked() || isOccluded) {
+        if (currentScene.equals(Scenes.Lockscreen)) {
+            return StatusBarState.KEYGUARD;
+        } else if (currentScene.equals(Scenes.Shade)
+                && SceneStackKt.contains(backStack, Scenes.Lockscreen)) {
+            return StatusBarState.SHADE_LOCKED;
+        } else if (deviceUnlockStatus.isUnlocked() || isOccluded) {
             return StatusBarState.SHADE;
         } else {
             return Preconditions.checkNotNull(sStatusBarStateByLockedSceneKey.get(currentScene));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 0e4be8e..8f187f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1487,7 +1487,14 @@
 
     private float updateStackEndHeight() {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
-        float height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop());
+        final float height;
+        if (mMaxDisplayedNotifications != -1) {
+            // The stack intrinsic height already contains the correct value when there is a limit
+            // in the max number of notifications (e.g. as in keyguard).
+            height = mIntrinsicContentHeight;
+        } else {
+            height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop());
+        }
         mAmbientState.setStackEndHeight(height);
         return height;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/UserInputDeviceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/UserInputDeviceRepositoryTest.kt
new file mode 100644
index 0000000..f2e43fc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/UserInputDeviceRepositoryTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputdevice.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.inputdevice.data.model.UserDeviceConnectionStatus
+import com.android.systemui.keyboard.data.repository.keyboardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.touchpad.data.repository.touchpadRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.data.repository.userRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+class UserInputDeviceRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: UserInputDeviceRepository
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+    private val keyboardRepository = kosmos.keyboardRepository
+    private val touchpadRepository = kosmos.touchpadRepository
+    private val userRepository = kosmos.fakeUserRepository
+
+    @Before
+    fun setup() {
+        underTest =
+            UserInputDeviceRepository(
+                kosmos.testDispatcher,
+                keyboardRepository,
+                touchpadRepository,
+                kosmos.userRepository
+            )
+        userRepository.setUserInfos(USER_INFOS)
+    }
+
+    @Test
+    fun emitsNewKeyboardConnectedValueOnUserChanged() =
+        testScope.runTest {
+            val isAnyKeyboardConnected by collectValues(underTest.isAnyKeyboardConnectedForUser)
+            userRepository.setSelectedUserInfo(USER_INFOS[0])
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+            runCurrent()
+
+            userRepository.setSelectedUserInfo(USER_INFOS[1])
+
+            assertThat(isAnyKeyboardConnected)
+                .containsExactly(
+                    UserDeviceConnectionStatus(isConnected = true, USER_INFOS[0].id),
+                    UserDeviceConnectionStatus(isConnected = true, USER_INFOS[1].id)
+                )
+                .inOrder()
+        }
+
+    @Test
+    fun emitsNewTouchpadConnectedValueOnUserChanged() =
+        testScope.runTest {
+            val isAnyTouchpadConnected by collectValues(underTest.isAnyTouchpadConnectedForUser)
+            userRepository.setSelectedUserInfo(USER_INFOS[0])
+            touchpadRepository.setIsAnyTouchpadConnected(true)
+            runCurrent()
+
+            userRepository.setSelectedUserInfo(USER_INFOS[1])
+
+            assertThat(isAnyTouchpadConnected)
+                .containsExactly(
+                    UserDeviceConnectionStatus(isConnected = true, USER_INFOS[0].id),
+                    UserDeviceConnectionStatus(isConnected = true, USER_INFOS[1].id)
+                )
+                .inOrder()
+        }
+
+    companion object {
+        private val USER_INFOS =
+            listOf(
+                UserInfo(100, "First User", 0),
+                UserInfo(101, "Second User", 0),
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
index 79cb51a..828c7b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
@@ -23,7 +23,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
@@ -32,6 +31,8 @@
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.dialog.WifiStateWorker
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.connectivity.AccessPointController
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
@@ -46,7 +47,6 @@
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
@@ -58,6 +58,10 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -87,6 +91,7 @@
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var logger: QSLogger
     @Mock private lateinit var dialogManager: InternetDialogManager
+    @Mock private lateinit var wifiStateWorker: WifiStateWorker
     @Mock private lateinit var accessPointController: AccessPointController
 
     @Before
@@ -122,6 +127,7 @@
                 logger,
                 viewModel,
                 dialogManager,
+                wifiStateWorker,
                 accessPointController
             )
 
@@ -231,6 +237,24 @@
             assertThat(underTest.state.secondaryLabel).isEqualTo(WIFI_SSID)
         }
 
+    @Test
+    fun secondaryClick_turnsWifiOff() {
+        whenever(wifiStateWorker.isWifiEnabled).thenReturn(true)
+
+        underTest.secondaryClick(null)
+
+        verify(wifiStateWorker, times(1)).isWifiEnabled = eq(false)
+    }
+
+    @Test
+    fun secondaryClick_turnsWifiOn() {
+        whenever(wifiStateWorker.isWifiEnabled).thenReturn(false)
+
+        underTest.secondaryClick(null)
+
+        verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true)
+    }
+
     companion object {
         const val WIFI_SSID = "test ssid"
         val ACTIVE_WIFI =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
index 8ea79d7..0cf9604 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
@@ -18,7 +18,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.os.Handler;
@@ -38,6 +41,7 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager;
+import com.android.systemui.qs.tiles.dialog.WifiStateWorker;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.connectivity.AccessPointController;
 import com.android.systemui.statusbar.connectivity.IconState;
@@ -65,6 +69,8 @@
     @Mock
     private InternetDialogManager mInternetDialogManager;
     @Mock
+    private WifiStateWorker mWifiStateWorker;
+    @Mock
     private QsEventLogger mUiEventLogger;
 
     private TestableLooper mTestableLooper;
@@ -89,7 +95,8 @@
             mock(QSLogger.class),
             mNetworkController,
             mAccessPointController,
-                mInternetDialogManager
+            mInternetDialogManager,
+            mWifiStateWorker
         );
 
         mTile.initialize();
@@ -167,4 +174,22 @@
         assertThat(mTile.getState().icon).isEqualTo(
                 QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable));
     }
+
+    @Test
+    public void secondaryClick_turnsWifiOff() {
+        when(mWifiStateWorker.isWifiEnabled()).thenReturn(true);
+
+        mTile.secondaryClick(null);
+
+        verify(mWifiStateWorker, times(1)).setWifiEnabled(eq(false));
+    }
+
+    @Test
+    public void secondaryClick_turnsWifiOn() {
+        when(mWifiStateWorker.isWifiEnabled()).thenReturn(false);
+
+        mTile.secondaryClick(null);
+
+        verify(mWifiStateWorker, times(1)).setWifiEnabled(eq(true));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 8125ef5..523d15c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -468,7 +468,8 @@
                 () -> mKosmos.getDeviceUnlockedInteractor(),
                 () -> mKosmos.getSceneInteractor(),
                 () -> mKosmos.getSceneContainerOcclusionInteractor(),
-                () -> mKosmos.getKeyguardClockInteractor());
+                () -> mKosmos.getKeyguardClockInteractor(),
+                () -> mKosmos.getSceneBackInteractor());
 
         KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
         keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -625,7 +626,8 @@
                                 () -> mKosmos.getDeviceUnlockedInteractor(),
                                 () -> mKosmos.getSceneInteractor(),
                                 () -> mKosmos.getSceneContainerOcclusionInteractor(),
-                                () -> mKosmos.getKeyguardClockInteractor()),
+                                () -> mKosmos.getKeyguardClockInteractor(),
+                                () -> mKosmos.getSceneBackInteractor()),
                         mKeyguardBypassController,
                         mDozeParameters,
                         mScreenOffAnimationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 22b9887..1717f4c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -354,6 +354,20 @@
     }
 
     @Test
+    @EnableSceneContainer
+    public void updateStackEndHeightAndStackHeight_maxNotificationsSet_withSceneContainer() {
+        float stackHeight = 300f;
+        when(mStackSizeCalculator.computeHeight(eq(mStackScroller), anyInt(), anyFloat()))
+                .thenReturn(stackHeight);
+        mStackScroller.setMaxDisplayedNotifications(3); // any non-zero amount
+
+        clearInvocations(mAmbientState);
+        mStackScroller.updateStackEndHeightAndStackHeight(1f);
+
+        verify(mAmbientState).setStackHeight(eq(300f));
+    }
+
+    @Test
     public void updateStackEndHeightAndStackHeight_onlyUpdatesStackHeightDuringSwipeUp() {
         final float expansionFraction = 0.5f;
         mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index e6bd24b..9fe66eb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -50,6 +50,7 @@
 import com.android.systemui.plugins.statusbar.statusBarStateController
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneBackInteractor
 import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.startable.scrimStartable
@@ -115,6 +116,7 @@
     val interactionJankMonitor by lazy { kosmos.interactionJankMonitor }
     val fakeSceneContainerConfig by lazy { kosmos.sceneContainerConfig }
     val sceneInteractor by lazy { kosmos.sceneInteractor }
+    val sceneBackInteractor by lazy { kosmos.sceneBackInteractor }
     val falsingCollector by lazy { kosmos.falsingCollector }
     val powerInteractor by lazy { kosmos.powerInteractor }
     val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index f9f8d23..2deeb25 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.scene.domain.interactor.sceneBackInteractor
 import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -41,5 +42,6 @@
             { sceneInteractor },
             { sceneContainerOcclusionInteractor },
             { keyguardClockInteractor },
+            { sceneBackInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt
index 76dccdb..0c62d0e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt
@@ -20,11 +20,13 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
 
 val Kosmos.iconTilesInteractor by
     Kosmos.Fixture {
         IconTilesInteractor(
             defaultLargeTilesRepository,
+            currentTilesInteractor,
             qsPreferencesInteractor,
             FakeLogBuffer.Factory.create(),
             applicationCoroutineScope
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
index 9cb76bb..3943d1d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
@@ -28,6 +28,12 @@
         expandable: Expandable? = null,
     ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.Click(expandable), data)
 
+    fun <T> toggleClick(
+        data: T,
+        user: UserHandle = UserHandle.CURRENT,
+        expandable: Expandable? = null,
+    ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.ToggleClick(expandable), data)
+
     fun <T> longClick(
         data: T,
         user: UserHandle = UserHandle.CURRENT,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt
index 2f17ca8..53d3c01 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt
@@ -69,6 +69,8 @@
     progress: Flow<Float> = flowOf(0f),
     isInitiatedByUserInput: Boolean = false,
     isUserInputOngoing: Flow<Boolean> = flowOf(false),
+    previewProgress: Flow<Float> = flowOf(0f),
+    isInPreviewStage: Flow<Boolean> = flowOf(false)
 ): ObservableTransitionState.Transition {
     return ObservableTransitionState.Transition(
         fromScene = from,
@@ -76,7 +78,9 @@
         currentScene = currentScene,
         progress = progress,
         isInitiatedByUserInput = isInitiatedByUserInput,
-        isUserInputOngoing = isUserInputOngoing
+        isUserInputOngoing = isUserInputOngoing,
+        previewProgress = previewProgress,
+        isInPreviewStage = isInPreviewStage
     )
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryKosmos.kt
new file mode 100644
index 0000000..91e2396
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.touchpadRepository by Kosmos.Fixture { FakeTouchpadRepository() }
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index 231179b..5cffdec 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -222,8 +222,16 @@
 android.content.res.AssetFileDescriptor
 android.content.res.AssetManager
 android.content.res.AssetManager$Builder
+android.content.res.ConfigurationBoundResourceCache
+android.content.res.Configuration
+android.content.res.CompatibilityInfo
+android.content.res.ConstantState
 android.content.res.DrawableCache
 android.content.res.Element
+android.content.res.FontResourcesParser
+android.content.res.FontScaleConverter
+android.content.res.FontScaleConverterImpl
+android.content.res.FontScaleConverterFactory
 android.content.res.Resources
 android.content.res.Resources$Theme
 android.content.res.ResourceId
@@ -231,6 +239,7 @@
 android.content.res.ResourcesKey
 android.content.res.StringBlock
 android.content.res.TagCounter
+android.content.res.ThemedResourceCache
 android.content.res.TypedArray
 android.content.res.Validator
 android.content.res.XmlBlock
@@ -275,6 +284,7 @@
 
 android.app.ActivityManager
 android.app.ActivityOptions
+android.app.ApplicationPackageManager
 android.app.BroadcastOptions
 android.app.ComponentOptions
 android.app.Instrumentation
@@ -288,6 +298,7 @@
 android.view.Display
 android.view.Display$HdrCapabilities
 android.view.Display$Mode
+android.view.DisplayAdjustments
 android.view.DisplayInfo
 android.view.inputmethod.InputBinding
 
diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index 3062863..2d49128 100644
--- a/ravenwood/texts/ravenwood-framework-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
@@ -69,20 +69,3 @@
 # Just enough to allow ResourcesManager to run
 class android.hardware.display.DisplayManagerGlobal keep
     method getInstance ()Landroid/hardware/display/DisplayManagerGlobal; ignore
-
-# These classes will be properly enabled in follow-up CLs
-
-class android.content.res.FontResourcesParser keepclass
-class android.content.res.FontResourcesParser.FamilyResourceEntry keepclass
-class android.content.res.FontResourcesParser.ProviderResourceEntry keepclass
-class android.content.res.FontResourcesParser.FontFamilyFilesResourceEntry keepclass
-class android.content.res.FontResourcesParser.FontFileResourceEntry keepclass
-class android.content.res.FontScaleConverter keepclass
-class android.content.res.FontScaleConverterImpl keepclass
-class android.content.res.FontScaleConverterFactory keepclass
-class android.content.res.ThemedResourceCache keepclass
-class android.content.res.ConfigurationBoundResourceCache keepclass
-class android.content.res.Configuration keepclass
-class android.content.res.CompatibilityInfo keepclass
-class android.content.res.ConstantState keepclass
-class android.view.DisplayAdjustments keepclass
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 191ec69..6b8e8c7 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -23,10 +23,6 @@
 import android.app.appfunctions.IAppFunctionService;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
-import android.app.appfunctions.ServiceCallHelper;
-import android.app.appfunctions.ServiceCallHelper.RunServiceCallCallback;
-import android.app.appfunctions.ServiceCallHelper.ServiceUsageCompleteListener;
-import android.app.appfunctions.ServiceCallHelperImpl;
 import android.content.Context;
 import android.content.Intent;
 import android.os.UserHandle;
@@ -34,6 +30,8 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
+import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
 
 import java.util.Objects;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -45,12 +43,12 @@
  */
 public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
     private static final String TAG = AppFunctionManagerServiceImpl.class.getSimpleName();
-    private final ServiceCallHelper<IAppFunctionService> mExternalServiceCallHelper;
+    private final RemoteServiceCaller<IAppFunctionService> mRemoteServiceCaller;
     private final CallerValidator mCallerValidator;
     private final ServiceHelper mInternalServiceHelper;
 
     public AppFunctionManagerServiceImpl(@NonNull Context context) {
-        this(new ServiceCallHelperImpl<>(
+        this(new RemoteServiceCallerImpl<>(
                         context,
                         IAppFunctionService.Stub::asInterface, new ThreadPoolExecutor(
                         /*corePoolSize=*/ Runtime.getRuntime().availableProcessors(),
@@ -63,11 +61,11 @@
     }
 
     @VisibleForTesting
-    AppFunctionManagerServiceImpl(ServiceCallHelper<IAppFunctionService> serviceCallHelper,
-                                  CallerValidator apiValidator,
+    AppFunctionManagerServiceImpl(RemoteServiceCaller<IAppFunctionService> remoteServiceCaller,
+                                  CallerValidator callerValidator,
                                   ServiceHelper appFunctionInternalServiceHelper) {
-        mExternalServiceCallHelper = Objects.requireNonNull(serviceCallHelper);
-        mCallerValidator = Objects.requireNonNull(apiValidator);
+        mRemoteServiceCaller = Objects.requireNonNull(remoteServiceCaller);
+        mCallerValidator = Objects.requireNonNull(callerValidator);
         mInternalServiceHelper =
                 Objects.requireNonNull(appFunctionInternalServiceHelper);
     }
@@ -134,7 +132,6 @@
             return;
         }
 
-        // TODO(b/357551503): Offload call to async executor.
         bindAppFunctionServiceUnchecked(requestInternal, serviceIntent, targetUser,
                 safeExecuteAppFunctionCallback,
                 /*bindFlags=*/ Context.BIND_AUTO_CREATE,
@@ -148,12 +145,12 @@
             @NonNull SafeOneTimeExecuteAppFunctionCallback
                     safeExecuteAppFunctionCallback,
             int bindFlags, long timeoutInMillis) {
-        boolean bindServiceResult = mExternalServiceCallHelper.runServiceCall(
+        boolean bindServiceResult = mRemoteServiceCaller.runServiceCall(
                 serviceIntent,
                 bindFlags,
                 timeoutInMillis,
                 targetUser,
-                /*timeOutCallback=*/ new RunServiceCallCallback<IAppFunctionService>() {
+                new RunServiceCallCallback<IAppFunctionService>() {
                     @Override
                     public void onServiceConnected(@NonNull IAppFunctionService service,
                                                    @NonNull ServiceUsageCompleteListener
diff --git a/core/java/android/app/appfunctions/ServiceCallHelper.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
similarity index 96%
rename from core/java/android/app/appfunctions/ServiceCallHelper.java
rename to services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
index cc882bd..98903ae 100644
--- a/core/java/android/app/appfunctions/ServiceCallHelper.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.app.appfunctions;
+package com.android.server.appfunctions;
 
 import android.annotation.NonNull;
 import android.content.Intent;
@@ -27,7 +27,7 @@
  * @param <T> Class of wrapped service.
  * @hide
  */
-public interface ServiceCallHelper<T> {
+public interface RemoteServiceCaller<T> {
 
     /**
      * Initiates service binding and executes a provided method when the service connects. Unbinds
diff --git a/core/java/android/app/appfunctions/ServiceCallHelperImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
similarity index 89%
rename from core/java/android/app/appfunctions/ServiceCallHelperImpl.java
rename to services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
index 2e58546..c19a027 100644
--- a/core/java/android/app/appfunctions/ServiceCallHelperImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.app.appfunctions;
+package com.android.server.appfunctions;
 
 import android.annotation.NonNull;
 import android.content.ComponentName;
@@ -30,27 +30,29 @@
 import java.util.function.Function;
 
 /**
- * An implementation of {@link android.app.appfunctions.ServiceCallHelper} that that is based on
+ * An implementation of {@link RemoteServiceCaller} that that is based on
  * {@link Context#bindService}.
  *
  * @param <T> Class of wrapped service.
  * @hide
  */
-public class ServiceCallHelperImpl<T> implements ServiceCallHelper<T> {
+public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> {
     private static final String TAG = "AppFunctionsServiceCall";
 
-    @NonNull private final Context mContext;
-    @NonNull private final Function<IBinder, T> mInterfaceConverter;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final Function<IBinder, T> mInterfaceConverter;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final Executor mExecutor;
 
     /**
      * @param interfaceConverter A function responsible for converting an IBinder object into the
-     *     desired service interface.
-     * @param executor An Executor instance to dispatch callback.
-     * @param context The system context.
+     *                           desired service interface.
+     * @param executor           An Executor instance to dispatch callback.
+     * @param context            The system context.
      */
-    public ServiceCallHelperImpl(
+    public RemoteServiceCallerImpl(
             @NonNull Context context,
             @NonNull Function<IBinder, T> interfaceConverter,
             @NonNull Executor executor) {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index f332ed9..4eb50a9 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -41,6 +41,7 @@
 import android.app.PendingIntent;
 import android.app.admin.DevicePolicyManager;
 import android.companion.AssociationInfo;
+import android.companion.virtual.ActivityPolicyExemption;
 import android.companion.virtual.IVirtualDevice;
 import android.companion.virtual.IVirtualDeviceActivityListener;
 import android.companion.virtual.IVirtualDeviceIntentInterceptor;
@@ -522,13 +523,37 @@
 
     @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void addActivityPolicyExemption(@NonNull ComponentName componentName) {
+    public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
         super.addActivityPolicyExemption_enforcePermission();
+        final int displayId = exemption.getDisplayId();
+        if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
+            if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+                return;
+            }
+        }
         synchronized (mVirtualDeviceLock) {
-            if (mActivityPolicyExemptions.add(componentName)) {
-                for (int i = 0; i < mVirtualDisplays.size(); i++) {
-                    mVirtualDisplays.valueAt(i).getWindowPolicyController()
-                            .addActivityPolicyExemption(componentName);
+            if (displayId != Display.INVALID_DISPLAY) {
+                checkDisplayOwnedByVirtualDeviceLocked(displayId);
+                if (exemption.getComponentName() != null) {
+                    mVirtualDisplays.get(displayId).getWindowPolicyController()
+                            .addActivityPolicyExemption(exemption.getComponentName());
+                } else if (exemption.getPackageName() != null) {
+                    mVirtualDisplays.get(displayId).getWindowPolicyController()
+                            .addActivityPolicyExemption(exemption.getPackageName());
+                }
+            } else {
+                if (exemption.getComponentName() != null
+                        && mActivityPolicyExemptions.add(exemption.getComponentName())) {
+                    for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                        mVirtualDisplays.valueAt(i).getWindowPolicyController()
+                                .addActivityPolicyExemption(exemption.getComponentName());
+                    }
+                } else if (exemption.getPackageName() != null
+                        && mActivityPolicyPackageExemptions.add(exemption.getPackageName())) {
+                    for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                        mVirtualDisplays.valueAt(i).getWindowPolicyController()
+                                .addActivityPolicyExemption(exemption.getPackageName());
+                    }
                 }
             }
         }
@@ -536,112 +561,42 @@
 
     @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
+    public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
         super.removeActivityPolicyExemption_enforcePermission();
-        synchronized (mVirtualDeviceLock) {
-            if (mActivityPolicyExemptions.remove(componentName)) {
-                for (int i = 0; i < mVirtualDisplays.size(); i++) {
-                    mVirtualDisplays.valueAt(i).getWindowPolicyController()
-                            .removeActivityPolicyExemption(componentName);
-                }
+        final int displayId = exemption.getDisplayId();
+        if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
+            if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+                return;
             }
         }
-    }
-
-    @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void addActivityPolicyPackageExemption(@NonNull String packageName) {
-        super.addActivityPolicyPackageExemption_enforcePermission();
-        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
-            return;
-        }
         synchronized (mVirtualDeviceLock) {
-            if (mActivityPolicyPackageExemptions.add(packageName)) {
-                for (int i = 0; i < mVirtualDisplays.size(); i++) {
-                    mVirtualDisplays.valueAt(i).getWindowPolicyController()
-                            .addActivityPolicyExemption(packageName);
+            if (displayId != Display.INVALID_DISPLAY) {
+                checkDisplayOwnedByVirtualDeviceLocked(displayId);
+                if (exemption.getComponentName() != null) {
+                    mVirtualDisplays.get(displayId).getWindowPolicyController()
+                            .removeActivityPolicyExemption(exemption.getComponentName());
+                } else if (exemption.getPackageName() != null) {
+                    mVirtualDisplays.get(displayId).getWindowPolicyController()
+                            .removeActivityPolicyExemption(exemption.getPackageName());
+                }
+            } else {
+                if (exemption.getComponentName() != null
+                        && mActivityPolicyExemptions.remove(exemption.getComponentName())) {
+                    for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                        mVirtualDisplays.valueAt(i).getWindowPolicyController()
+                                .removeActivityPolicyExemption(exemption.getComponentName());
+                    }
+                } else if (exemption.getPackageName() != null
+                        && mActivityPolicyPackageExemptions.remove(exemption.getPackageName())) {
+                    for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                        mVirtualDisplays.valueAt(i).getWindowPolicyController()
+                                .removeActivityPolicyExemption(exemption.getPackageName());
+                    }
                 }
             }
         }
     }
 
-    @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void removeActivityPolicyPackageExemption(@NonNull String packageName) {
-        super.removeActivityPolicyPackageExemption_enforcePermission();
-        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
-            return;
-        }
-        synchronized (mVirtualDeviceLock) {
-            if (mActivityPolicyPackageExemptions.remove(packageName)) {
-                for (int i = 0; i < mVirtualDisplays.size(); i++) {
-                    mVirtualDisplays.valueAt(i).getWindowPolicyController()
-                            .removeActivityPolicyExemption(packageName);
-                }
-            }
-        }
-    }
-
-    @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void addActivityPolicyExemptionForDisplay(
-            int displayId, @NonNull ComponentName componentName) {
-        super.addActivityPolicyExemptionForDisplay_enforcePermission();
-        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
-            return;
-        }
-        synchronized (mVirtualDeviceLock) {
-            checkDisplayOwnedByVirtualDeviceLocked(displayId);
-            mVirtualDisplays.get(displayId).getWindowPolicyController()
-                    .addActivityPolicyExemption(componentName);
-        }
-    }
-
-    @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void removeActivityPolicyExemptionForDisplay(
-            int displayId, @NonNull ComponentName componentName) {
-        super.removeActivityPolicyExemptionForDisplay_enforcePermission();
-        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
-            return;
-        }
-        synchronized (mVirtualDeviceLock) {
-            checkDisplayOwnedByVirtualDeviceLocked(displayId);
-            mVirtualDisplays.get(displayId).getWindowPolicyController()
-                    .removeActivityPolicyExemption(componentName);
-        }
-    }
-
-    @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void addActivityPolicyPackageExemptionForDisplay(
-            int displayId, @NonNull String packageName) {
-        super.addActivityPolicyPackageExemptionForDisplay_enforcePermission();
-        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
-            return;
-        }
-        synchronized (mVirtualDeviceLock) {
-            checkDisplayOwnedByVirtualDeviceLocked(displayId);
-            mVirtualDisplays.get(displayId).getWindowPolicyController()
-                    .addActivityPolicyExemption(packageName);
-        }
-    }
-
-    @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void removeActivityPolicyPackageExemptionForDisplay(
-            int displayId, @NonNull String packageName) {
-        super.removeActivityPolicyPackageExemptionForDisplay_enforcePermission();
-        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
-            return;
-        }
-        synchronized (mVirtualDeviceLock) {
-            checkDisplayOwnedByVirtualDeviceLocked(displayId);
-            mVirtualDisplays.get(displayId).getWindowPolicyController()
-                    .removeActivityPolicyExemption(packageName);
-        }
-    }
-
     private void sendPendingIntent(int displayId, PendingIntent pendingIntent)
             throws PendingIntent.CanceledException {
         final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId);
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 27024a7..a27360d 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -125,9 +125,9 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.util.Preconditions;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
 import com.android.server.EventLogTags;
 import com.android.server.LockGuard;
-import com.android.server.RescueParty;
 import com.android.server.ServiceThread;
 import com.android.server.SystemService;
 import com.android.server.UiThread;
@@ -4031,7 +4031,7 @@
             }
         }
         if (mHandler == null || !mSystemReady) {
-            if (RescueParty.isRecoveryTriggeredReboot()) {
+            if (CrashRecoveryHelper.isRecoveryTriggeredReboot()) {
                 // If we're stuck in a really low-level reboot loop, and a
                 // rescue party is trying to prompt the user for a factory data
                 // reset, we must GET TO DA CHOPPA!
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index 4b4e442..d209ea9 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -59,8 +59,8 @@
 import android.view.WindowManager;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
 import com.android.server.LocalServices;
-import com.android.server.RescueParty;
 import com.android.server.statusbar.StatusBarManagerInternal;
 
 import java.io.File;
@@ -339,7 +339,7 @@
                             com.android.internal.R.string.reboot_to_update_reboot));
             }
         } else if (mReason != null && mReason.equals(PowerManager.REBOOT_RECOVERY)) {
-            if (RescueParty.isRecoveryTriggeredReboot()) {
+            if (CrashRecoveryHelper.isRecoveryTriggeredReboot()) {
                 // We're not actually doing a factory reset yet; we're rebooting
                 // to ask the user if they'd like to reset, so give them a less
                 // scary dialog message.
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index e91097c..1c786e6 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -492,23 +492,19 @@
         PackageManager pm = mContext.getPackageManager();
 
         if (Flags.refactorCrashrecovery() && provideInfoOfApkInApex()) {
-            // Check if the package is listed among the system modules.
-            boolean isApex = false;
-            try {
-                isApex = (pm.getModuleInfo(packageName, 0 /* flags */) != null);
-            } catch (PackageManager.NameNotFoundException e) {
-                //pass
-            }
-
-            // Check if the package is an APK inside an APEX.
-            boolean isApkInApex = false;
+            // Check if the package is listed among the system modules or is an
+            // APK inside an updatable APEX.
             try {
                 final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */);
-                isApkInApex = (pkg.getApexPackageName() != null);
+                String apexPackageName = pkg.getApexPackageName();
+                if (apexPackageName != null) {
+                    packageName = apexPackageName;
+                }
+
+                return pm.getModuleInfo(packageName, 0 /* flags */) != null;
             } catch (PackageManager.NameNotFoundException e) {
-                // pass
+                return false;
             }
-            return isApex || isApkInApex;
         } else {
             // Check if the package is an APK inside an APEX. If it is, use the parent APEX package
             // when querying PackageManager.
diff --git a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
index 3dba57f..4abf806 100644
--- a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
+++ b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
@@ -56,9 +56,14 @@
         Change() {}
 
         Change(@NonNull Change other) {
+            copyFrom(other);
+        }
+
+        void copyFrom(@NonNull Change other) {
             mAlpha = other.mAlpha;
             mBlurRadius = other.mBlurRadius;
             mDimmingContainer = other.mDimmingContainer;
+            mGeometryParent = other.mGeometryParent;
             mRelativeLayer = other.mRelativeLayer;
         }
 
@@ -83,8 +88,8 @@
         }
     }
 
-    private Change mCurrentProperties = new Change();
-    private Change mRequestedProperties = new Change();
+    private final Change mCurrentProperties = new Change();
+    private final Change mRequestedProperties = new Change();
     private AnimationSpec mAlphaAnimationSpec;
 
     private final AnimationAdapterFactory mAnimationAdapterFactory;
@@ -123,12 +128,15 @@
      * {@link Change#setRequestedAppearance(float, int)}
      */
     void applyChanges(@NonNull SurfaceControl.Transaction t, @NonNull Dimmer.DimState dim) {
+        final Change startProperties = new Change(mCurrentProperties);
+        mCurrentProperties.copyFrom(mRequestedProperties);
+
         if (mRequestedProperties.mDimmingContainer == null) {
             Log.e(TAG, this + " does not have a dimming container. Have you forgotten to "
                     + "call adjustRelativeLayer?");
             return;
         }
-        if (mRequestedProperties.mDimmingContainer.mSurfaceControl == null) {
+        if (mRequestedProperties.mDimmingContainer.getSurfaceControl() == null) {
             Log.w(TAG, "container " + mRequestedProperties.mDimmingContainer
                     + "does not have a surface");
             dim.remove(t);
@@ -137,52 +145,49 @@
 
         dim.ensureVisible(t);
         reparent(dim.mDimSurface,
-                mRequestedProperties.mGeometryParent != mCurrentProperties.mGeometryParent
+                startProperties.mGeometryParent != mRequestedProperties.mGeometryParent
                         ? mRequestedProperties.mGeometryParent.getSurfaceControl() : null,
                 mRequestedProperties.mDimmingContainer.getSurfaceControl(),
                 mRequestedProperties.mRelativeLayer, t);
 
-        if (!mCurrentProperties.hasSameVisualProperties(mRequestedProperties)) {
+        if (!startProperties.hasSameVisualProperties(mRequestedProperties)) {
             stopCurrentAnimation(dim.mDimSurface);
 
             if (dim.mSkipAnimation
                     // If the container doesn't change but requests a dim change, then it is
                     // directly providing us the animated values
-                    || (mRequestedProperties.hasSameDimmingContainer(mCurrentProperties)
+                    || (startProperties.hasSameDimmingContainer(mRequestedProperties)
                     && dim.isDimming())) {
                 ProtoLog.d(WM_DEBUG_DIMMER,
                         "%s skipping animation and directly setting alpha=%f, blur=%d",
-                        dim, mRequestedProperties.mAlpha,
+                        dim, startProperties.mAlpha,
                         mRequestedProperties.mBlurRadius);
-                setAlphaBlur(dim.mDimSurface, mRequestedProperties.mAlpha,
-                        mRequestedProperties.mBlurRadius, t);
+                setCurrentAlphaBlur(dim.mDimSurface, t);
                 dim.mSkipAnimation = false;
             } else {
-                startAnimation(t, dim);
+                startAnimation(t, dim, startProperties, mRequestedProperties);
             }
-
         } else if (!dim.isDimming()) {
             // We are not dimming, so we tried the exit animation but the alpha is already 0,
             // therefore, let's just remove this surface
             dim.remove(t);
         }
-        mCurrentProperties = new Change(mRequestedProperties);
     }
 
     private void startAnimation(
-            @NonNull SurfaceControl.Transaction t, @NonNull Dimmer.DimState dim) {
+            @NonNull SurfaceControl.Transaction t, @NonNull Dimmer.DimState dim,
+            @NonNull Change from, @NonNull Change to) {
         ProtoLog.v(WM_DEBUG_DIMMER, "Starting animation on %s", dim);
-        mAlphaAnimationSpec = getRequestedAnimationSpec();
+        mAlphaAnimationSpec = getRequestedAnimationSpec(from, to);
         mLocalAnimationAdapter = mAnimationAdapterFactory.get(mAlphaAnimationSpec,
                 dim.mHostContainer.mWmService.mSurfaceAnimationRunner);
 
-        float targetAlpha = mRequestedProperties.mAlpha;
-        int targetBlur = mRequestedProperties.mBlurRadius;
+        float targetAlpha = to.mAlpha;
 
         mLocalAnimationAdapter.startAnimation(dim.mDimSurface, t,
                 ANIMATION_TYPE_DIMMER, /* finishCallback */ (type, animator) -> {
                     synchronized (dim.mHostContainer.mWmService.mGlobalLock) {
-                        setAlphaBlur(dim.mDimSurface, targetAlpha, targetBlur, t);
+                        setCurrentAlphaBlur(dim.mDimSurface, t);
                         if (targetAlpha == 0f && !dim.isDimming()) {
                             dim.remove(t);
                         }
@@ -207,15 +212,15 @@
     }
 
     @NonNull
-    private AnimationSpec getRequestedAnimationSpec() {
-        final float startAlpha = Math.max(mCurrentProperties.mAlpha, 0f);
-        final int startBlur = Math.max(mCurrentProperties.mBlurRadius, 0);
-        long duration = (long) (getDimDuration(mRequestedProperties.mDimmingContainer)
-                * Math.abs(mRequestedProperties.mAlpha - startAlpha));
+    private static AnimationSpec getRequestedAnimationSpec(Change from, Change to) {
+        final float startAlpha = Math.max(from.mAlpha, 0f);
+        final int startBlur = Math.max(from.mBlurRadius, 0);
+        long duration = (long) (getDimDuration(to.mDimmingContainer)
+                * Math.abs(to.mAlpha - startAlpha));
 
         final AnimationSpec spec =  new AnimationSpec(
-                new AnimationSpec.AnimationExtremes<>(startAlpha, mRequestedProperties.mAlpha),
-                new AnimationSpec.AnimationExtremes<>(startBlur, mRequestedProperties.mBlurRadius),
+                new AnimationSpec.AnimationExtremes<>(startAlpha, to.mAlpha),
+                new AnimationSpec.AnimationExtremes<>(startBlur, to.mBlurRadius),
                 duration
         );
         ProtoLog.v(WM_DEBUG_DIMMER, "Dim animation requested: %s", spec);
@@ -225,7 +230,7 @@
     /**
      * Change the geometry and relative parent of this dim layer
      */
-    void reparent(@NonNull SurfaceControl dimLayer,
+    static void reparent(@NonNull SurfaceControl dimLayer,
                   @Nullable SurfaceControl newGeometryParent,
                   @NonNull SurfaceControl relativeParent,
                   int relativePosition,
@@ -240,17 +245,16 @@
         }
     }
 
-    void setAlphaBlur(@NonNull SurfaceControl sc, float alpha, int blur,
-                      @NonNull SurfaceControl.Transaction t) {
+    void setCurrentAlphaBlur(@NonNull SurfaceControl sc, @NonNull SurfaceControl.Transaction t) {
         try {
-            t.setAlpha(sc, alpha);
-            t.setBackgroundBlurRadius(sc, blur);
+            t.setAlpha(sc, mCurrentProperties.mAlpha);
+            t.setBackgroundBlurRadius(sc, mCurrentProperties.mBlurRadius);
         } catch (NullPointerException e) {
             Log.w(TAG , "Tried to change look of dim " + sc + " after remove",  e);
         }
     }
 
-    private long getDimDuration(@NonNull WindowContainer<?> container) {
+    private static long getDimDuration(@NonNull WindowContainer<?> container) {
         // Use the same duration as the animation on the WindowContainer
         AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation();
         final float durationScale = container.mWmService.getTransitionAnimationScaleLocked();
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 4ca4730..862f84d 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -3426,6 +3426,25 @@
         return null;
     }
 
+    /** Returns the top direct activity if it should be idle but has not yet been reported. */
+    @Nullable
+    private static ActivityRecord getNotYetIdleActivity(@NonNull TaskFragment visibleTf) {
+        for (int i = visibleTf.getChildCount() - 1; i >= 0; i--) {
+            final ActivityRecord r = visibleTf.getChildAt(i).asActivityRecord();
+            if (r == null || r.finishing) {
+                continue;
+            }
+            if (!r.idle && (r.isState(RESUMED)
+                    // Its process is not attached yet and it may resume later.
+                    || (r.app == null && r.isFocusable()))) {
+                return r;
+            }
+            // Only check the top running activity.
+            break;
+        }
+        return null;
+    }
+
     boolean allResumedActivitiesIdle() {
         for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
             final DisplayContent display = getChildAt(displayNdx);
@@ -3434,19 +3453,31 @@
                 continue;
             }
 
-            final boolean foundNotIdle = display.forAllLeafTaskFragments(tf -> {
-                if (!tf.isVisibleRequested()) {
+            final boolean foundNotIdle = display.forAllLeafTasks(task -> {
+                if (!task.isVisibleRequested()) {
                     return false;
                 }
-                // Note that only activities that will be resumed can report idle.
-                final ActivityRecord r = tf.topRunningActivity();
-                if (r != null && !r.idle && (r.isState(RESUMED)
-                        // Its process is not attached yet and it may resume later.
-                        || (r.app == null && r.isFocusable()))) {
-                    ProtoLog.d(WM_DEBUG_STATES, "allResumedActivitiesIdle: %s not idle", r);
+                final ActivityRecord notIdle = getNotYetIdleActivity(task);
+                if (notIdle != null) {
+                    ProtoLog.d(WM_DEBUG_STATES, "allResumedActivitiesIdle: %s not idle", notIdle);
                     return true;
                 }
-                return false;
+                if (task.isLeafTaskFragment()) {
+                    // The task doesn't contain child TaskFragment.
+                    return false;
+                }
+                return task.forAllLeafTaskFragments(tf -> {
+                    if (!tf.isVisibleRequested()) {
+                        return false;
+                    }
+                    final ActivityRecord tfNotIdle = getNotYetIdleActivity(tf);
+                    if (tfNotIdle != null) {
+                        ProtoLog.d(WM_DEBUG_STATES, "allResumedActivitiesIdle: %s not idle",
+                                tfNotIdle);
+                        return true;
+                    }
+                    return false;
+                });
             });
             if (foundNotIdle) {
                 return false;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index b768bb1..e76e94d 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1804,7 +1804,7 @@
                 // If on a rotation leash, the wallpaper token surface needs to be shown explicitly
                 // because shell only gets the leash and the wallpaper token surface is not allowed
                 // to be changed by non-transition logic until the transition is finished.
-                if (Flags.ensureWallpaperInTransitions() && wp.isVisibleRequested()
+                if (wp.mWmService.mFlags.mEnsureWallpaperInTransitions && wp.isVisibleRequested()
                         && wp.getFixedRotationLeash() != null) {
                     transaction.show(wp.mSurfaceControl);
                 }
@@ -2216,7 +2216,8 @@
             if (wallpaper != null) {
                 if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) {
                     wallpaper.commitVisibility(showWallpaper);
-                } else if (Flags.ensureWallpaperInTransitions() && wallpaper.isVisible()
+                } else if (wallpaper.mWmService.mFlags.mEnsureWallpaperInTransitions
+                        && wallpaper.isVisible()
                         && !showWallpaper && !wallpaper.getDisplayContent().isKeyguardLocked()
                         && !wallpaperIsOwnTarget(wallpaper)) {
                     wallpaper.setVisibleRequested(false);
@@ -2934,7 +2935,7 @@
                     // Use parent rotation because shell doesn't know the surface is rotated.
                     endRotation = parent.getWindowConfiguration().getRotation();
                 }
-            } else if (isWallpaper(target) && Flags.ensureWallpaperInTransitions()
+            } else if (isWallpaper(target) && target.mWmService.mFlags.mEnsureWallpaperInTransitions
                     && target.getRelativeDisplayRotation() != 0
                     && !target.mTransitionController.useShellTransitionsRotation()) {
                 // If the wallpaper is "fixed-rotated", shell is unaware of this, so use the
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index db95d96..4536f24 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -59,7 +59,6 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.internal.util.ToBooleanFunction;
 import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
-import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -759,7 +758,7 @@
 
     void collectTopWallpapers(Transition transition) {
         if (mFindResults.hasTopShowWhenLockedWallpaper()) {
-            if (Flags.ensureWallpaperInTransitions()) {
+            if (mService.mFlags.mEnsureWallpaperInTransitions) {
                 transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper.mToken);
             } else {
                 transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper);
@@ -767,7 +766,7 @@
 
         }
         if (mFindResults.hasTopHideWhenLockedWallpaper()) {
-            if (Flags.ensureWallpaperInTransitions()) {
+            if (mService.mFlags.mEnsureWallpaperInTransitions) {
                 transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper.mToken);
             } else {
                 transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper);
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 31156de..384d111 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -32,7 +32,6 @@
 import android.util.SparseArray;
 
 import com.android.internal.protolog.ProtoLog;
-import com.android.window.flags.Flags;
 
 import java.util.function.Consumer;
 
@@ -85,7 +84,7 @@
     public void prepareSurfaces() {
         super.prepareSurfaces();
 
-        if (Flags.ensureWallpaperInTransitions()) {
+        if (mWmService.mFlags.mEnsureWallpaperInTransitions) {
             // Similar to Task.prepareSurfaces, outside of transitions we need to apply visibility
             // changes directly. In transitions the transition player will take care of applying the
             // visibility change.
diff --git a/services/core/java/com/android/server/wm/WindowManagerFlags.java b/services/core/java/com/android/server/wm/WindowManagerFlags.java
index f3e6a18..7ef8d8d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerFlags.java
+++ b/services/core/java/com/android/server/wm/WindowManagerFlags.java
@@ -16,6 +16,9 @@
 
 package com.android.server.wm;
 
+import android.app.AppGlobals;
+import android.content.pm.PackageManager;
+
 import com.android.window.flags.Flags;
 
 /**
@@ -53,5 +56,26 @@
     final boolean mRespectNonTopVisibleFixedOrientation =
             Flags.respectNonTopVisibleFixedOrientation();
 
+    final boolean mEnsureWallpaperInTransitions;
+
     /* End Available Flags */
+
+    WindowManagerFlags() {
+        boolean isWatch;
+        try {
+            isWatch = AppGlobals.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_WATCH, 0 /* version */);
+        } catch (Throwable e) {
+            isWatch = false;
+        }
+        /*
+         * Wallpaper enablement is separated on Wear vs Phone as the latter appears to still exhibit
+         * regressions when enabled (for example b/353870983). These don't exist on Wear likely
+         * due to differences in SysUI/transition implementations. Wear enablement is required for
+         * 25Q2 while phone doesn't have as pressing a constraint and will wait to resolve any
+         * outstanding issues prior to roll-out.
+         */
+        mEnsureWallpaperInTransitions = (isWatch && Flags.ensureWallpaperInWearTransitions())
+                || Flags.ensureWallpaperInTransitions();
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 87c0084..d8df645 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7999,7 +7999,8 @@
             }
             boolean allWindowsDrawn = false;
             synchronized (mGlobalLock) {
-                if (mRoot.getDefaultDisplay().mDisplayUpdater.waitForTransition(message)) {
+                if (displayId == DEFAULT_DISPLAY
+                        && mRoot.getDefaultDisplay().mDisplayUpdater.waitForTransition(message)) {
                     // Use the ready-to-play of transition as the signal.
                     return;
                 }
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 24a2a62..b40cf56 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -67,9 +67,8 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 
-import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.ProtoLog;
-import com.android.window.flags.Flags;
+import com.android.internal.protolog.common.LogLevel;
 import com.android.server.policy.WindowManagerPolicy;
 
 import java.io.PrintWriter;
@@ -413,7 +412,7 @@
             ProtoLog.i(WM_SHOW_SURFACE_ALLOC, "SURFACE DESTROY: %s. %s",
                     mWin, new RuntimeException().fillInStackTrace());
             destroySurface(t);
-            if (Flags.ensureWallpaperInTransitions()) {
+            if (mService.mFlags.mEnsureWallpaperInTransitions) {
                 if (mWallpaperControllerLocked.isWallpaperTarget(mWin)) {
                     mWin.requestUpdateWallpaperIfNeeded();
                 }
@@ -464,7 +463,7 @@
 
         if (!w.isOnScreen()) {
             hide(t, "prepareSurfaceLocked");
-            if (!w.mIsWallpaper || !Flags.ensureWallpaperInTransitions()) {
+            if (!w.mIsWallpaper || !mService.mFlags.mEnsureWallpaperInTransitions) {
                 mWallpaperControllerLocked.hideWallpapers(w);
             }
 
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 8332b8b..a4dfaa1 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -275,26 +275,15 @@
         launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver);
     }
 
-    private void traceOnAppStart(String packageName) {
-        if (mIProfcollect == null) {
-            return;
-        }
-
-        if (Utils.withFrequency("applaunch_trace_freq", 2)) {
-            BackgroundThread.get().getThreadHandler().post(() -> {
-                try {
-                    mIProfcollect.trace_system("applaunch");
-                } catch (RemoteException e) {
-                    Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
-                }
-            });
-        }
-    }
-
     private class AppLaunchObserver extends ActivityMetricsLaunchObserver {
         @Override
         public void onIntentStarted(Intent intent, long timestampNanos) {
-            traceOnAppStart(intent.getPackage());
+            if (mIProfcollect == null) {
+                return;
+            }
+            if (Utils.withFrequency("applaunch_trace_freq", 2)) {
+                Utils.traceSystem(mIProfcollect, "applaunch");
+            }
         }
     }
 
@@ -316,13 +305,7 @@
         }
         if (Utils.withFrequency("dex2oat_trace_freq", 25)) {
             // Dex2oat could take a while before it starts. Add a short delay before start tracing.
-            BackgroundThread.get().getThreadHandler().postDelayed(() -> {
-                try {
-                    mIProfcollect.trace_system("dex2oat");
-                } catch (RemoteException e) {
-                    Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
-                }
-            }, 1000);
+            Utils.traceSystem(mIProfcollect, "dex2oat", /* delayMs */ 1000);
         }
     }
 
@@ -385,20 +368,10 @@
                     return;
                 }
                 if (Utils.withFrequency("camera_trace_freq", 10)) {
-                    final int traceDuration = 5000;
-                    final String traceTag = "camera";
-                    BackgroundThread.get().getThreadHandler().post(() -> {
-                        if (mIProfcollect == null) {
-                            return;
-                        }
-                        try {
-                            mIProfcollect.trace_process(traceTag,
-                                "android.hardware.camera.provider",
-                                traceDuration);
-                        } catch (RemoteException e) {
-                            Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
-                        }
-                    });
+                    Utils.traceProcess(mIProfcollect,
+                            "camera",
+                            "android.hardware.camera.provider",
+                            /* durationMs */ 5000);
                 }
             }
         }, null);
diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java
index d5ef14c..8508802 100644
--- a/services/profcollect/src/com/android/server/profcollect/Utils.java
+++ b/services/profcollect/src/com/android/server/profcollect/Utils.java
@@ -16,17 +16,67 @@
 
 package com.android.server.profcollect;
 
+import static com.android.server.profcollect.ProfcollectForwardingService.LOG_TAG;
+
+import android.os.RemoteException;
 import android.provider.DeviceConfig;
+import android.util.Log;
+
+import com.android.internal.os.BackgroundThread;
 
 import java.util.concurrent.ThreadLocalRandom;
 
 public final class Utils {
 
-  public static boolean withFrequency(String configName, int defaultFrequency) {
+    public static boolean withFrequency(String configName, int defaultFrequency) {
         int threshold = DeviceConfig.getInt(
                 DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, configName, defaultFrequency);
         int randomNum = ThreadLocalRandom.current().nextInt(100);
         return randomNum < threshold;
     }
 
+    public static boolean traceSystem(IProfCollectd mIProfcollect, String eventName) {
+        if (mIProfcollect == null) {
+            return false;
+        }
+        BackgroundThread.get().getThreadHandler().post(() -> {
+            try {
+                mIProfcollect.trace_system(eventName);
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+            }
+        });
+        return true;
+    }
+
+    public static boolean traceSystem(IProfCollectd mIProfcollect, String eventName, int delayMs) {
+        if (mIProfcollect == null) {
+            return false;
+        }
+        BackgroundThread.get().getThreadHandler().postDelayed(() -> {
+            try {
+                mIProfcollect.trace_system(eventName);
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+            }
+        }, delayMs);
+        return true;
+    }
+
+    public static boolean traceProcess(IProfCollectd mIProfcollect,
+            String eventName, String processName, int durationMs) {
+        if (mIProfcollect == null) {
+            return false;
+        }
+        BackgroundThread.get().getThreadHandler().post(() -> {
+            try {
+                mIProfcollect.trace_process(eventName,
+                        processName,
+                        durationMs);
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+            }
+        });
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/appfunctions/OWNERS b/services/tests/servicestests/src/com/android/server/appfunctions/OWNERS
new file mode 100644
index 0000000..7fa8917
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appfunctions/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1627156
+include platform/frameworks/base:/core/java/android/app/appfunctions/OWNERS
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index f843386..affaad6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -16,12 +16,16 @@
 
 package com.android.server.wm;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.WindowStateAnimator.HAS_DRAWN;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -37,6 +41,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.LocalServices;
 import com.android.server.wm.TransitionController.OnStartCollect;
 import com.android.window.flags.Flags;
 
@@ -57,18 +62,29 @@
 public class DisplayContentDeferredUpdateTests extends WindowTestsBase {
 
     // The fields to override the current DisplayInfo.
-    private String mUniqueId;
+    private String mUniqueId = "initial_unique_id";
+    private String mSecondaryUniqueId = "secondary_initial_unique_id";
     private int mColorMode;
     private int mLogicalDensityDpi;
 
+    private DisplayContent mSecondaryDisplayContent;
+
     private final Message mScreenUnblocker = mock(Message.class);
+    private final Message mSecondaryScreenUnblocker = mock(Message.class);
+
+    private WindowManagerInternal mWmInternal;
 
     @Before
     public void before() {
+        when(mScreenUnblocker.getTarget()).thenReturn(mWm.mH);
         doReturn(true).when(mDisplayContent).getLastHasContent();
-        mockTransitionsController(/* enabled= */ true);
-        mockRemoteDisplayChangeController();
-        performInitialDisplayUpdate();
+
+        mockTransitionsController();
+
+        mockRemoteDisplayChangeController(mDisplayContent);
+        performInitialDisplayUpdate(mDisplayContent);
+
+        mWmInternal = LocalServices.getService(WindowManagerInternal.class);
     }
 
     @Test
@@ -245,24 +261,90 @@
         verify(mScreenUnblocker, never()).sendToTarget();
     }
 
-    private void mockTransitionsController(boolean enabled) {
-        spyOn(mDisplayContent.mTransitionController);
-        when(mDisplayContent.mTransitionController.isShellTransitionsEnabled()).thenReturn(enabled);
-        doReturn(true).when(mDisplayContent.mTransitionController).startCollectOrQueue(any(),
-                any());
+    @Test
+    public void testTwoDisplayUpdateAtTheSameTime_bothDisplaysAreUnblocked() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH);
+        prepareSecondaryDisplay();
+
+        final WindowState defaultDisplayWindow = createWindow(/* parent= */ null,
+                TYPE_BASE_APPLICATION, mDisplayContent, "DefaultDisplayWindow");
+        final WindowState secondaryDisplayWindow = createWindow(/* parent= */ null,
+                TYPE_BASE_APPLICATION, mSecondaryDisplayContent, "SecondaryDisplayWindow");
+        makeWindowVisibleAndNotDrawn(defaultDisplayWindow, secondaryDisplayWindow);
+
+        // Mark as display switching only for the default display as we filter out
+        // non-default display switching events in the display policy
+        mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ true);
+
+        mWmInternal.waitForAllWindowsDrawn(mScreenUnblocker,
+                /* timeout= */ Integer.MAX_VALUE, DEFAULT_DISPLAY);
+        mWmInternal.waitForAllWindowsDrawn(mSecondaryScreenUnblocker,
+                /* timeout= */ Integer.MAX_VALUE, mSecondaryDisplayContent.getDisplayId());
+
+        // Perform display update for both displays at the same time
+        mUniqueId = "new_default_display_unique_id";
+        mSecondaryUniqueId = "new_secondary_display_unique_id";
+        mDisplayContent.requestDisplayUpdate(mock(Runnable.class));
+        mSecondaryDisplayContent.requestDisplayUpdate(mock(Runnable.class));
+
+        when(mDisplayContent.mTransitionController.inTransition()).thenReturn(true);
+
+        // Notify that both transitions started collecting
+        captureStartTransitionCollection().getAllValues().forEach((callback) ->
+                callback.onCollectStarted(/* deferred= */ true));
+
+        // Verify that screens are not unblocked yet
+        verify(mScreenUnblocker, never()).sendToTarget();
+        verify(mSecondaryScreenUnblocker, never()).sendToTarget();
+
+        // Make all secondary display windows drawn
+        secondaryDisplayWindow.mWinAnimator.mDrawState = HAS_DRAWN;
+        mWm.mRoot.performSurfacePlacement();
+
+        // Verify that only secondary screen is unblocked as it uses
+        // the legacy waitForAllWindowsDrawn path
+        verify(mScreenUnblocker, never()).sendToTarget();
+        verify(mSecondaryScreenUnblocker).sendToTarget();
+
+        // Mark start transactions as presented
+        when(mDisplayContent.mTransitionController.inTransition()).thenReturn(false);
+        captureRequestedTransition().getAllValues().forEach(
+                this::makeTransitionTransactionCompleted);
+
+        // Verify that the default screen unblocker is sent only after start transaction
+        // of the Shell transition is presented
+        verify(mScreenUnblocker).sendToTarget();
     }
 
-    private void mockRemoteDisplayChangeController() {
-        spyOn(mDisplayContent.mRemoteDisplayChangeController);
-        doReturn(true).when(mDisplayContent.mRemoteDisplayChangeController)
+    private void prepareSecondaryDisplay() {
+        mSecondaryDisplayContent = createNewDisplay();
+        when(mSecondaryScreenUnblocker.getTarget()).thenReturn(mWm.mH);
+        doReturn(true).when(mSecondaryDisplayContent).getLastHasContent();
+        mockRemoteDisplayChangeController(mSecondaryDisplayContent);
+        performInitialDisplayUpdate(mSecondaryDisplayContent);
+    }
+
+    private void mockTransitionsController() {
+        spyOn(mDisplayContent.mTransitionController);
+        when(mDisplayContent.mTransitionController.isShellTransitionsEnabled())
+                .thenReturn(true);
+        doReturn(mock(Transition.class)).when(mDisplayContent.mTransitionController)
+                .createTransition(anyInt(), anyInt());
+        doReturn(true).when(mDisplayContent.mTransitionController)
+                .startCollectOrQueue(any(), any());
+    }
+
+    private void mockRemoteDisplayChangeController(DisplayContent displayContent) {
+        spyOn(displayContent.mRemoteDisplayChangeController);
+        doReturn(true).when(displayContent.mRemoteDisplayChangeController)
                 .performRemoteDisplayChange(anyInt(), anyInt(), any(), any());
     }
 
     private ArgumentCaptor<OnStartCollect> captureStartTransitionCollection() {
         ArgumentCaptor<OnStartCollect> callbackCaptor =
                 ArgumentCaptor.forClass(OnStartCollect.class);
-        verify(mDisplayContent.mTransitionController, atLeast(1)).startCollectOrQueue(any(),
-                callbackCaptor.capture());
+        verify(mDisplayContent.mTransitionController, atLeast(1))
+                .startCollectOrQueue(any(), callbackCaptor.capture());
         return callbackCaptor;
     }
 
@@ -283,20 +365,23 @@
         }
     }
 
-    private void performInitialDisplayUpdate() {
-        mUniqueId = "initial_unique_id";
+    private void performInitialDisplayUpdate(DisplayContent displayContent) {
         mColorMode = 0;
         mLogicalDensityDpi = 400;
 
-        spyOn(mDisplayContent.mDisplay);
+        spyOn(displayContent.mDisplay);
         doAnswer(invocation -> {
             DisplayInfo info = invocation.getArgument(0);
-            info.uniqueId = mUniqueId;
+            if (displayContent.isDefaultDisplay) {
+                info.uniqueId = mUniqueId;
+            } else {
+                info.uniqueId = mSecondaryUniqueId;
+            }
             info.colorMode = mColorMode;
             info.logicalDensityDpi = mLogicalDensityDpi;
             return null;
-        }).when(mDisplayContent.mDisplay).getDisplayInfo(any());
+        }).when(displayContent.mDisplay).getDisplayInfo(any());
         Runnable onUpdated = mock(Runnable.class);
-        mDisplayContent.requestDisplayUpdate(onUpdated);
+        displayContent.requestDisplayUpdate(onUpdated);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 4ab2fcf..f1db713 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -224,6 +224,22 @@
         activity1.idle = false;
         activity1.setVisibleRequested(false);
         assertThat(mWm.mRoot.allResumedActivitiesIdle()).isTrue();
+
+        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(activity2.getTask()).build();
+        final ActivityRecord activity3 = new ActivityBuilder(mAtm).build();
+        taskFragment.addChild(activity3);
+        taskFragment.setVisibleRequested(true);
+        activity3.setState(RESUMED, "test");
+        activity3.idle = true;
+        assertThat(mWm.mRoot.allResumedActivitiesIdle()).isTrue();
+
+        activity3.idle = false;
+        assertThat(mWm.mRoot.allResumedActivitiesIdle()).isFalse();
+
+        activity2.idle = false;
+        activity3.idle = true;
+        assertThat(mWm.mRoot.allResumedActivitiesIdle()).isFalse();
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 4a8a2e6..bcf4ebc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -55,6 +55,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
+import static com.android.server.wm.WindowStateAnimator.DRAW_PENDING;
 import static com.android.server.wm.WindowStateAnimator.HAS_DRAWN;
 
 import static org.junit.Assert.assertEquals;
@@ -693,6 +694,13 @@
         }
     }
 
+    static void makeWindowVisibleAndNotDrawn(WindowState... windows) {
+        makeWindowVisible(windows);
+        for (WindowState win : windows) {
+            win.mWinAnimator.mDrawState = DRAW_PENDING;
+        }
+    }
+
     static void makeLastConfigReportedToClient(WindowState w, boolean visible) {
         w.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
                 new MergedConfiguration(), new ActivityWindowInfo(), true /* useLatestConfig */,
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index 4826f42..05a68e9 100644
--- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -756,6 +756,48 @@
                 .isEqualTo("My null args: 0, 0, false");
     }
 
+    @Test
+    public void handlesConcurrentTracingSessions() throws IOException {
+        PerfettoTraceMonitor traceMonitor1 =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(true)
+                        .build();
+
+        PerfettoTraceMonitor traceMonitor2 =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(true)
+                        .build();
+
+        final ResultWriter writer2 = new ResultWriter()
+                .forScenario(new ScenarioBuilder()
+                        .forClass(createTempFile("temp", "").getName()).build())
+                .withOutputDir(mTracingDirectory)
+                .setRunComplete();
+
+        try {
+            traceMonitor1.start();
+            traceMonitor2.start();
+
+            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+                    LogDataType.BOOLEAN, new Object[]{true});
+        } finally {
+            traceMonitor1.stop(mWriter);
+            traceMonitor2.stop(writer2);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protologFromMonitor1 = reader.readProtoLogTrace();
+
+        final ResultReader reader2 = new ResultReader(writer2.write(), mTraceConfig);
+        final ProtoLogTrace protologFromMonitor2 = reader2.readProtoLogTrace();
+
+        Truth.assertThat(protologFromMonitor1.messages).hasSize(1);
+        Truth.assertThat(protologFromMonitor1.messages.get(0).getMessage())
+                .isEqualTo("My Test Debug Log Message true");
+
+        Truth.assertThat(protologFromMonitor2.messages).hasSize(1);
+        Truth.assertThat(protologFromMonitor2.messages.get(0).getMessage())
+                .isEqualTo("My Test Debug Log Message true");
+    }
+
     private enum TestProtoLogGroup implements IProtoLogGroup {
         TEST_GROUP(true, true, false, "TEST_TAG");