Merge "Don’t bring App to FG if only creator is allowed to do BAL(1/2)" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index f09036b..207abb2 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -33157,7 +33157,6 @@
   public static class PerformanceHintManager.Session implements java.io.Closeable {
     method public void close();
     method public void reportActualWorkDuration(long);
-    method @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public void reportActualWorkDuration(@NonNull android.os.WorkDuration);
     method @FlaggedApi("android.os.adpf_prefer_power_efficiency") public void setPreferPowerEfficiency(boolean);
     method public void setThreads(@NonNull int[]);
     method public void updateTargetWorkDuration(long);
@@ -33500,7 +33499,6 @@
     method public static boolean setCurrentTimeMillis(long);
     method public static void sleep(long);
     method public static long uptimeMillis();
-    method @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public static long uptimeNanos();
   }
 
   public class TestLooperManager {
@@ -33766,22 +33764,6 @@
     method @RequiresPermission(android.Manifest.permission.VIBRATE) public final void vibrate(@NonNull android.os.CombinedVibration, @Nullable android.os.VibrationAttributes);
   }
 
-  @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public final class WorkDuration implements android.os.Parcelable {
-    ctor public WorkDuration();
-    ctor public WorkDuration(long, long, long, long);
-    method public int describeContents();
-    method public long getActualCpuDurationNanos();
-    method public long getActualGpuDurationNanos();
-    method public long getActualTotalDurationNanos();
-    method public long getWorkPeriodStartTimestampNanos();
-    method public void setActualCpuDurationNanos(long);
-    method public void setActualGpuDurationNanos(long);
-    method public void setActualTotalDurationNanos(long);
-    method public void setWorkPeriodStartTimestampNanos(long);
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.os.WorkDuration> CREATOR;
-  }
-
   public class WorkSource implements android.os.Parcelable {
     ctor public WorkSource();
     ctor public WorkSource(android.os.WorkSource);
diff --git a/core/java/android/os/IHintSession.aidl b/core/java/android/os/IHintSession.aidl
index fe85da2..6b43e73 100644
--- a/core/java/android/os/IHintSession.aidl
+++ b/core/java/android/os/IHintSession.aidl
@@ -17,8 +17,6 @@
 
 package android.os;
 
-import android.os.WorkDuration;
-
 /** {@hide} */
 oneway interface IHintSession {
     void updateTargetWorkDuration(long targetDurationNanos);
@@ -26,5 +24,4 @@
     void close();
     void sendHint(int hint);
     void setMode(int mode, boolean enabled);
-    void reportActualWorkDuration2(in WorkDuration[] workDurations);
 }
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index e005910..11084b8 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -103,7 +103,7 @@
      * Any call in this class will change its internal data, so you must do your own thread
      * safety to protect from racing.
      *
-     * All timings should be in {@link SystemClock#uptimeNanos()}.
+     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
      */
     public static class Session implements Closeable {
         private long mNativeSessionPtr;
@@ -269,40 +269,6 @@
         public @Nullable int[] getThreadIds() {
             return nativeGetThreadIds(mNativeSessionPtr);
         }
-
-        /**
-         * Reports the work duration for the last cycle of work.
-         *
-         * The system will attempt to adjust the core placement of the threads within the thread
-         * group and/or the frequency of the core on which they are run to bring the actual duration
-         * close to the target duration.
-         *
-         * @param workDuration the work duration of each component.
-         * @throws IllegalArgumentException if work period start timestamp is not positive, or
-         *         actual total duration is not positive, or actual CPU duration is not positive,
-         *         or actual GPU duration is negative.
-         */
-        @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION)
-        public void reportActualWorkDuration(@NonNull WorkDuration workDuration) {
-            if (workDuration.mWorkPeriodStartTimestampNanos <= 0) {
-                throw new IllegalArgumentException(
-                    "the work period start timestamp should be positive.");
-            }
-            if (workDuration.mActualTotalDurationNanos <= 0) {
-                throw new IllegalArgumentException("the actual total duration should be positive.");
-            }
-            if (workDuration.mActualCpuDurationNanos <= 0) {
-                throw new IllegalArgumentException("the actual CPU duration should be positive.");
-            }
-            if (workDuration.mActualGpuDurationNanos < 0) {
-                throw new IllegalArgumentException(
-                    "the actual GPU duration should be non negative.");
-            }
-            nativeReportActualWorkDuration(mNativeSessionPtr,
-                    workDuration.mWorkPeriodStartTimestampNanos,
-                    workDuration.mActualTotalDurationNanos,
-                    workDuration.mActualCpuDurationNanos, workDuration.mActualGpuDurationNanos);
-        }
     }
 
     private static native long nativeAcquireManager();
@@ -319,7 +285,4 @@
     private static native void nativeSetThreads(long nativeSessionPtr, int[] tids);
     private static native void nativeSetPreferPowerEfficiency(long nativeSessionPtr,
             boolean enabled);
-    private static native void nativeReportActualWorkDuration(long nativeSessionPtr,
-            long workPeriodStartTimestampNanos, long actualTotalDurationNanos,
-            long actualCpuDurationNanos, long actualGpuDurationNanos);
 }
diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java
index e2a5833..49a0bd3 100644
--- a/core/java/android/os/SystemClock.java
+++ b/core/java/android/os/SystemClock.java
@@ -16,7 +16,6 @@
 
 package android.os;
 
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.app.IAlarmManager;
 import android.app.time.UnixEpochTime;
@@ -203,8 +202,8 @@
      * Returns nanoseconds since boot, not counting time spent in deep sleep.
      *
      * @return nanoseconds of non-sleep uptime since boot.
+     * @hide
      */
-    @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION)
     @CriticalNative
     @android.ravenwood.annotation.RavenwoodReplace
     public static native long uptimeNanos();
diff --git a/core/java/android/os/WorkDuration.java b/core/java/android/os/WorkDuration.java
deleted file mode 100644
index 4fdc34f..0000000
--- a/core/java/android/os/WorkDuration.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.os;
-
-import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-
-import java.util.Objects;
-
-/**
- * WorkDuration contains the measured time in nano seconds of the workload
- * in each component, see
- * {@link PerformanceHintManager.Session#reportActualWorkDuration(WorkDuration)}.
- *
- * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
- */
-@FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION)
-public final class WorkDuration implements Parcelable {
-    long mWorkPeriodStartTimestampNanos = 0;
-    long mActualTotalDurationNanos = 0;
-    long mActualCpuDurationNanos = 0;
-    long mActualGpuDurationNanos = 0;
-    long mTimestampNanos = 0;
-
-    public static final @NonNull Creator<WorkDuration> CREATOR = new Creator<>() {
-        @Override
-        public WorkDuration createFromParcel(Parcel in) {
-            return new WorkDuration(in);
-        }
-
-        @Override
-        public WorkDuration[] newArray(int size) {
-            return new WorkDuration[size];
-        }
-    };
-
-    public WorkDuration() {}
-
-    public WorkDuration(long workPeriodStartTimestampNanos,
-                      long actualTotalDurationNanos,
-                      long actualCpuDurationNanos,
-                      long actualGpuDurationNanos) {
-        mWorkPeriodStartTimestampNanos = workPeriodStartTimestampNanos;
-        mActualTotalDurationNanos = actualTotalDurationNanos;
-        mActualCpuDurationNanos = actualCpuDurationNanos;
-        mActualGpuDurationNanos = actualGpuDurationNanos;
-    }
-
-    /**
-     * @hide
-     */
-    public WorkDuration(long workPeriodStartTimestampNanos,
-                      long actualTotalDurationNanos,
-                      long actualCpuDurationNanos,
-                      long actualGpuDurationNanos,
-                      long timestampNanos) {
-        mWorkPeriodStartTimestampNanos = workPeriodStartTimestampNanos;
-        mActualTotalDurationNanos = actualTotalDurationNanos;
-        mActualCpuDurationNanos = actualCpuDurationNanos;
-        mActualGpuDurationNanos = actualGpuDurationNanos;
-        mTimestampNanos = timestampNanos;
-    }
-
-    WorkDuration(@NonNull Parcel in) {
-        mWorkPeriodStartTimestampNanos = in.readLong();
-        mActualTotalDurationNanos = in.readLong();
-        mActualCpuDurationNanos = in.readLong();
-        mActualGpuDurationNanos = in.readLong();
-        mTimestampNanos = in.readLong();
-    }
-
-    /**
-     * Sets the work period start timestamp in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public void setWorkPeriodStartTimestampNanos(long workPeriodStartTimestampNanos) {
-        if (workPeriodStartTimestampNanos <= 0) {
-            throw new IllegalArgumentException(
-                "the work period start timestamp should be positive.");
-        }
-        mWorkPeriodStartTimestampNanos = workPeriodStartTimestampNanos;
-    }
-
-    /**
-     * Sets the actual total duration in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public void setActualTotalDurationNanos(long actualTotalDurationNanos) {
-        if (actualTotalDurationNanos <= 0) {
-            throw new IllegalArgumentException("the actual total duration should be positive.");
-        }
-        mActualTotalDurationNanos = actualTotalDurationNanos;
-    }
-
-    /**
-     * Sets the actual CPU duration in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public void setActualCpuDurationNanos(long actualCpuDurationNanos) {
-        if (actualCpuDurationNanos <= 0) {
-            throw new IllegalArgumentException("the actual CPU duration should be positive.");
-        }
-        mActualCpuDurationNanos = actualCpuDurationNanos;
-    }
-
-    /**
-     * Sets the actual GPU duration in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public void setActualGpuDurationNanos(long actualGpuDurationNanos) {
-        if (actualGpuDurationNanos < 0) {
-            throw new IllegalArgumentException("the actual GPU duration should be non negative.");
-        }
-        mActualGpuDurationNanos = actualGpuDurationNanos;
-    }
-
-    /**
-     * Returns the work period start timestamp based in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public long getWorkPeriodStartTimestampNanos() {
-        return mWorkPeriodStartTimestampNanos;
-    }
-
-    /**
-     * Returns the actual total duration in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public long getActualTotalDurationNanos() {
-        return mActualTotalDurationNanos;
-    }
-
-    /**
-     * Returns the actual CPU duration in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public long getActualCpuDurationNanos() {
-        return mActualCpuDurationNanos;
-    }
-
-    /**
-     * Returns the actual GPU duration in nanoseconds.
-     *
-     * All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.
-     */
-    public long getActualGpuDurationNanos() {
-        return mActualGpuDurationNanos;
-    }
-
-    /**
-     * @hide
-     */
-    public long getTimestampNanos() {
-        return mTimestampNanos;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeLong(mWorkPeriodStartTimestampNanos);
-        dest.writeLong(mActualTotalDurationNanos);
-        dest.writeLong(mActualCpuDurationNanos);
-        dest.writeLong(mActualGpuDurationNanos);
-        dest.writeLong(mTimestampNanos);
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof WorkDuration)) {
-            return false;
-        }
-        WorkDuration workDuration = (WorkDuration) obj;
-        return workDuration.mTimestampNanos == this.mTimestampNanos
-            && workDuration.mWorkPeriodStartTimestampNanos == this.mWorkPeriodStartTimestampNanos
-            && workDuration.mActualTotalDurationNanos == this.mActualTotalDurationNanos
-            && workDuration.mActualCpuDurationNanos == this.mActualCpuDurationNanos
-            && workDuration.mActualGpuDurationNanos == this.mActualGpuDurationNanos;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mWorkPeriodStartTimestampNanos, mActualTotalDurationNanos,
-                            mActualCpuDurationNanos, mActualGpuDurationNanos, mTimestampNanos);
-    }
-}
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index d405d1d..a78f221 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -61,11 +61,4 @@
     namespace: "backstage_power"
     description: "Guards a new API in PowerManager to check if battery saver is supported or not."
     bug: "305067031"
-}
-
-flag {
-    name: "adpf_gpu_report_actual_work_duration"
-    namespace: "game"
-    description: "Guards the ADPF GPU APIs."
-    bug: "284324521"
-}
+}
\ No newline at end of file
diff --git a/core/jni/android_os_PerformanceHintManager.cpp b/core/jni/android_os_PerformanceHintManager.cpp
index aebe7ea..95bf49f 100644
--- a/core/jni/android_os_PerformanceHintManager.cpp
+++ b/core/jni/android_os_PerformanceHintManager.cpp
@@ -16,16 +16,15 @@
 
 #define LOG_TAG "PerfHint-jni"
 
-#include <android/performance_hint.h>
+#include "jni.h"
+
 #include <dlfcn.h>
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/ScopedPrimitiveArray.h>
 #include <utils/Log.h>
-
 #include <vector>
 
 #include "core_jni_helpers.h"
-#include "jni.h"
 
 namespace android {
 
@@ -45,11 +44,6 @@
 typedef int (*APH_setThreads)(APerformanceHintSession*, const pid_t*, size_t);
 typedef void (*APH_getThreadIds)(APerformanceHintSession*, int32_t* const, size_t* const);
 typedef void (*APH_setPreferPowerEfficiency)(APerformanceHintSession*, bool);
-typedef void (*APH_reportActualWorkDuration2)(APerformanceHintSession*, AWorkDuration*);
-
-typedef AWorkDuration* (*AWD_create)();
-typedef void (*AWD_setTimeNanos)(AWorkDuration*, int64_t);
-typedef void (*AWD_release)(AWorkDuration*);
 
 bool gAPerformanceHintBindingInitialized = false;
 APH_getManager gAPH_getManagerFn = nullptr;
@@ -62,14 +56,6 @@
 APH_setThreads gAPH_setThreadsFn = nullptr;
 APH_getThreadIds gAPH_getThreadIdsFn = nullptr;
 APH_setPreferPowerEfficiency gAPH_setPreferPowerEfficiencyFn = nullptr;
-APH_reportActualWorkDuration2 gAPH_reportActualWorkDuration2Fn = nullptr;
-
-AWD_create gAWD_createFn = nullptr;
-AWD_setTimeNanos gAWD_setWorkPeriodStartTimestampNanosFn = nullptr;
-AWD_setTimeNanos gAWD_setActualTotalDurationNanosFn = nullptr;
-AWD_setTimeNanos gAWD_setActualCpuDurationNanosFn = nullptr;
-AWD_setTimeNanos gAWD_setActualGpuDurationNanosFn = nullptr;
-AWD_release gAWD_releaseFn = nullptr;
 
 void ensureAPerformanceHintBindingInitialized() {
     if (gAPerformanceHintBindingInitialized) return;
@@ -126,46 +112,9 @@
             (APH_setPreferPowerEfficiency)dlsym(handle_,
                                                 "APerformanceHint_setPreferPowerEfficiency");
     LOG_ALWAYS_FATAL_IF(gAPH_setPreferPowerEfficiencyFn == nullptr,
-                        "Failed to find required symbol "
+                        "Failed to find required symbol"
                         "APerformanceHint_setPreferPowerEfficiency!");
 
-    gAPH_reportActualWorkDuration2Fn =
-            (APH_reportActualWorkDuration2)dlsym(handle_,
-                                                 "APerformanceHint_reportActualWorkDuration2");
-    LOG_ALWAYS_FATAL_IF(gAPH_reportActualWorkDuration2Fn == nullptr,
-                        "Failed to find required symbol "
-                        "APerformanceHint_reportActualWorkDuration2!");
-
-    gAWD_createFn = (AWD_create)dlsym(handle_, "AWorkDuration_create");
-    LOG_ALWAYS_FATAL_IF(gAWD_createFn == nullptr,
-                        "Failed to find required symbol AWorkDuration_create!");
-
-    gAWD_setWorkPeriodStartTimestampNanosFn =
-            (AWD_setTimeNanos)dlsym(handle_, "AWorkDuration_setWorkPeriodStartTimestampNanos");
-    LOG_ALWAYS_FATAL_IF(gAWD_setWorkPeriodStartTimestampNanosFn == nullptr,
-                        "Failed to find required symbol "
-                        "AWorkDuration_setWorkPeriodStartTimestampNanos!");
-
-    gAWD_setActualTotalDurationNanosFn =
-            (AWD_setTimeNanos)dlsym(handle_, "AWorkDuration_setActualTotalDurationNanos");
-    LOG_ALWAYS_FATAL_IF(gAWD_setActualTotalDurationNanosFn == nullptr,
-                        "Failed to find required symbol "
-                        "AWorkDuration_setActualTotalDurationNanos!");
-
-    gAWD_setActualCpuDurationNanosFn =
-            (AWD_setTimeNanos)dlsym(handle_, "AWorkDuration_setActualCpuDurationNanos");
-    LOG_ALWAYS_FATAL_IF(gAWD_setActualCpuDurationNanosFn == nullptr,
-                        "Failed to find required symbol AWorkDuration_setActualCpuDurationNanos!");
-
-    gAWD_setActualGpuDurationNanosFn =
-            (AWD_setTimeNanos)dlsym(handle_, "AWorkDuration_setActualGpuDurationNanos");
-    LOG_ALWAYS_FATAL_IF(gAWD_setActualGpuDurationNanosFn == nullptr,
-                        "Failed to find required symbol AWorkDuration_setActualGpuDurationNanos!");
-
-    gAWD_releaseFn = (AWD_release)dlsym(handle_, "AWorkDuration_release");
-    LOG_ALWAYS_FATAL_IF(gAWD_releaseFn == nullptr,
-                        "Failed to find required symbol AWorkDuration_release!");
-
     gAPerformanceHintBindingInitialized = true;
 }
 
@@ -289,25 +238,6 @@
                                     enabled);
 }
 
-static void nativeReportActualWorkDuration2(JNIEnv* env, jclass clazz, jlong nativeSessionPtr,
-                                            jlong workPeriodStartTimestampNanos,
-                                            jlong actualTotalDurationNanos,
-                                            jlong actualCpuDurationNanos,
-                                            jlong actualGpuDurationNanos) {
-    ensureAPerformanceHintBindingInitialized();
-
-    AWorkDuration* workDuration = gAWD_createFn();
-    gAWD_setWorkPeriodStartTimestampNanosFn(workDuration, workPeriodStartTimestampNanos);
-    gAWD_setActualTotalDurationNanosFn(workDuration, actualTotalDurationNanos);
-    gAWD_setActualCpuDurationNanosFn(workDuration, actualCpuDurationNanos);
-    gAWD_setActualGpuDurationNanosFn(workDuration, actualGpuDurationNanos);
-
-    gAPH_reportActualWorkDuration2Fn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr),
-                                     workDuration);
-
-    gAWD_releaseFn(workDuration);
-}
-
 static const JNINativeMethod gPerformanceHintMethods[] = {
         {"nativeAcquireManager", "()J", (void*)nativeAcquireManager},
         {"nativeGetPreferredUpdateRateNanos", "(J)J", (void*)nativeGetPreferredUpdateRateNanos},
@@ -319,7 +249,6 @@
         {"nativeSetThreads", "(J[I)V", (void*)nativeSetThreads},
         {"nativeGetThreadIds", "(J)[I", (void*)nativeGetThreadIds},
         {"nativeSetPreferPowerEfficiency", "(JZ)V", (void*)nativeSetPreferPowerEfficiency},
-        {"nativeReportActualWorkDuration", "(JJJJJ)V", (void*)nativeReportActualWorkDuration2},
 };
 
 int register_android_os_PerformanceHintManager(JNIEnv* env) {
diff --git a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
index 9b4dec4..20ba427 100644
--- a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
+++ b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
@@ -182,42 +182,4 @@
         s.setPreferPowerEfficiency(true);
         s.setPreferPowerEfficiency(true);
     }
-
-    @Test
-    public void testReportActualWorkDurationWithWorkDurationClass() {
-        Session s = createSession();
-        assumeNotNull(s);
-        s.updateTargetWorkDuration(16);
-        s.reportActualWorkDuration(new WorkDuration(1, 12, 8, 6));
-        s.reportActualWorkDuration(new WorkDuration(1, 33, 14, 20));
-        s.reportActualWorkDuration(new WorkDuration(1, 14, 10, 6));
-    }
-
-    @Test
-    public void testReportActualWorkDurationWithWorkDurationClass_IllegalArgument() {
-        Session s = createSession();
-        assumeNotNull(s);
-        s.updateTargetWorkDuration(16);
-        assertThrows(IllegalArgumentException.class, () -> {
-            s.reportActualWorkDuration(new WorkDuration(-1, 12, 8, 6));
-        });
-        assertThrows(IllegalArgumentException.class, () -> {
-            s.reportActualWorkDuration(new WorkDuration(0, 12, 8, 6));
-        });
-        assertThrows(IllegalArgumentException.class, () -> {
-            s.reportActualWorkDuration(new WorkDuration(1, -1, 8, 6));
-        });
-        assertThrows(IllegalArgumentException.class, () -> {
-            s.reportActualWorkDuration(new WorkDuration(1, 0, 8, 6));
-        });
-        assertThrows(IllegalArgumentException.class, () -> {
-            s.reportActualWorkDuration(new WorkDuration(1, 12, -1, 6));
-        });
-        assertThrows(IllegalArgumentException.class, () -> {
-            s.reportActualWorkDuration(new WorkDuration(1, 12, 0, 6));
-        });
-        assertThrows(IllegalArgumentException.class, () -> {
-            s.reportActualWorkDuration(new WorkDuration(1, 12, 8, -1));
-        });
-    }
 }
diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java
index 2ec4524..d659ddd 100644
--- a/graphics/java/android/graphics/BaseRecordingCanvas.java
+++ b/graphics/java/android/graphics/BaseRecordingCanvas.java
@@ -402,8 +402,8 @@
     }
 
     @Override
-    public final void drawDoubleRoundRect(@NonNull RectF outer, float[] outerRadii,
-            @NonNull RectF inner, float[] innerRadii, @NonNull Paint paint) {
+    public final void drawDoubleRoundRect(@NonNull RectF outer, @NonNull float[] outerRadii,
+            @NonNull RectF inner, @NonNull float[] innerRadii, @NonNull Paint paint) {
         nDrawDoubleRoundRect(mNativeCanvasWrapper,
                 outer.left, outer.top, outer.right, outer.bottom, outerRadii,
                 inner.left, inner.top, inner.right, inner.bottom, innerRadii,
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 9f2a9ac..fea6c5f 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -336,13 +336,6 @@
     APerformanceHint_closeSession; # introduced=Tiramisu
     APerformanceHint_setThreads; # introduced=UpsideDownCake
     APerformanceHint_setPreferPowerEfficiency; # introduced=VanillaIceCream
-    APerformanceHint_reportActualWorkDuration2; # introduced=VanillaIceCream
-    AWorkDuration_create; # introduced=VanillaIceCream
-    AWorkDuration_release; # introduced=VanillaIceCream
-    AWorkDuration_setWorkPeriodStartTimestampNanos; # introduced=VanillaIceCream
-    AWorkDuration_setActualTotalDurationNanos; # introduced=VanillaIceCream
-    AWorkDuration_setActualCpuDurationNanos; # introduced=VanillaIceCream
-    AWorkDuration_setActualGpuDurationNanos; # introduced=VanillaIceCream
   local:
     *;
 };
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index c4c8128..c25df6e 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -18,14 +18,12 @@
 
 #include <aidl/android/hardware/power/SessionHint.h>
 #include <aidl/android/hardware/power/SessionMode.h>
-#include <android/WorkDuration.h>
 #include <android/os/IHintManager.h>
 #include <android/os/IHintSession.h>
 #include <android/performance_hint.h>
 #include <binder/Binder.h>
 #include <binder/IBinder.h>
 #include <binder/IServiceManager.h>
-#include <inttypes.h>
 #include <performance_hint_private.h>
 #include <utils/SystemClock.h>
 
@@ -77,13 +75,10 @@
     int setThreads(const int32_t* threadIds, size_t size);
     int getThreadIds(int32_t* const threadIds, size_t* size);
     int setPreferPowerEfficiency(bool enabled);
-    int reportActualWorkDuration(AWorkDuration* workDuration);
 
 private:
     friend struct APerformanceHintManager;
 
-    int reportActualWorkDurationInternal(WorkDuration* workDuration);
-
     sp<IHintManager> mHintManager;
     sp<IHintSession> mHintSession;
     // HAL preferred update rate
@@ -97,7 +92,8 @@
     // Last hint reported from sendHint indexed by hint value
     std::vector<int64_t> mLastHintSentTimestamp;
     // Cached samples
-    std::vector<WorkDuration> mActualWorkDurations;
+    std::vector<int64_t> mActualDurationsNanos;
+    std::vector<int64_t> mTimestampsNanos;
 };
 
 static IHintManager* gIHintManagerForTesting = nullptr;
@@ -199,7 +195,8 @@
      * Most of the workload is target_duration dependent, so now clear the cached samples
      * as they are most likely obsolete.
      */
-    mActualWorkDurations.clear();
+    mActualDurationsNanos.clear();
+    mTimestampsNanos.clear();
     mFirstTargetMetTimestamp = 0;
     mLastTargetMetTimestamp = 0;
     return 0;
@@ -210,10 +207,43 @@
         ALOGE("%s: actualDurationNanos must be positive", __FUNCTION__);
         return EINVAL;
     }
+    int64_t now = elapsedRealtimeNano();
+    mActualDurationsNanos.push_back(actualDurationNanos);
+    mTimestampsNanos.push_back(now);
 
-    WorkDuration workDuration(0, actualDurationNanos, actualDurationNanos, 0);
+    if (actualDurationNanos >= mTargetDurationNanos) {
+        // Reset timestamps if we are equal or over the target.
+        mFirstTargetMetTimestamp = 0;
+    } else {
+        // Set mFirstTargetMetTimestamp for first time meeting target.
+        if (!mFirstTargetMetTimestamp || !mLastTargetMetTimestamp ||
+            (now - mLastTargetMetTimestamp > 2 * mPreferredRateNanos)) {
+            mFirstTargetMetTimestamp = now;
+        }
+        /**
+         * Rate limit the change if the update is over mPreferredRateNanos since first
+         * meeting target and less than mPreferredRateNanos since last meeting target.
+         */
+        if (now - mFirstTargetMetTimestamp > mPreferredRateNanos &&
+            now - mLastTargetMetTimestamp <= mPreferredRateNanos) {
+            return 0;
+        }
+        mLastTargetMetTimestamp = now;
+    }
 
-    return reportActualWorkDurationInternal(&workDuration);
+    binder::Status ret =
+            mHintSession->reportActualWorkDuration(mActualDurationsNanos, mTimestampsNanos);
+    if (!ret.isOk()) {
+        ALOGE("%s: HintSession reportActualWorkDuration failed: %s", __FUNCTION__,
+              ret.exceptionMessage().c_str());
+        mFirstTargetMetTimestamp = 0;
+        mLastTargetMetTimestamp = 0;
+        return EPIPE;
+    }
+    mActualDurationsNanos.clear();
+    mTimestampsNanos.clear();
+
+    return 0;
 }
 
 int APerformanceHintSession::sendHint(SessionHint hint) {
@@ -292,67 +322,6 @@
     return OK;
 }
 
-int APerformanceHintSession::reportActualWorkDuration(AWorkDuration* aWorkDuration) {
-    WorkDuration* workDuration = static_cast<WorkDuration*>(aWorkDuration);
-    if (workDuration->workPeriodStartTimestampNanos <= 0) {
-        ALOGE("%s: workPeriodStartTimestampNanos must be positive", __FUNCTION__);
-        return EINVAL;
-    }
-    if (workDuration->actualTotalDurationNanos <= 0) {
-        ALOGE("%s: actualDurationNanos must be positive", __FUNCTION__);
-        return EINVAL;
-    }
-    if (workDuration->actualCpuDurationNanos <= 0) {
-        ALOGE("%s: cpuDurationNanos must be positive", __FUNCTION__);
-        return EINVAL;
-    }
-    if (workDuration->actualGpuDurationNanos < 0) {
-        ALOGE("%s: gpuDurationNanos must be non negative", __FUNCTION__);
-        return EINVAL;
-    }
-
-    return reportActualWorkDurationInternal(workDuration);
-}
-
-int APerformanceHintSession::reportActualWorkDurationInternal(WorkDuration* workDuration) {
-    int64_t actualTotalDurationNanos = workDuration->actualTotalDurationNanos;
-    int64_t now = uptimeNanos();
-    workDuration->timestampNanos = now;
-    mActualWorkDurations.push_back(std::move(*workDuration));
-
-    if (actualTotalDurationNanos >= mTargetDurationNanos) {
-        // Reset timestamps if we are equal or over the target.
-        mFirstTargetMetTimestamp = 0;
-    } else {
-        // Set mFirstTargetMetTimestamp for first time meeting target.
-        if (!mFirstTargetMetTimestamp || !mLastTargetMetTimestamp ||
-            (now - mLastTargetMetTimestamp > 2 * mPreferredRateNanos)) {
-            mFirstTargetMetTimestamp = now;
-        }
-        /**
-         * Rate limit the change if the update is over mPreferredRateNanos since first
-         * meeting target and less than mPreferredRateNanos since last meeting target.
-         */
-        if (now - mFirstTargetMetTimestamp > mPreferredRateNanos &&
-            now - mLastTargetMetTimestamp <= mPreferredRateNanos) {
-            return 0;
-        }
-        mLastTargetMetTimestamp = now;
-    }
-
-    binder::Status ret = mHintSession->reportActualWorkDuration2(mActualWorkDurations);
-    if (!ret.isOk()) {
-        ALOGE("%s: HintSession reportActualWorkDuration failed: %s", __FUNCTION__,
-              ret.exceptionMessage().c_str());
-        mFirstTargetMetTimestamp = 0;
-        mLastTargetMetTimestamp = 0;
-        return ret.exceptionCode() == binder::Status::EX_ILLEGAL_ARGUMENT ? EINVAL : EPIPE;
-    }
-    mActualWorkDurations.clear();
-
-    return 0;
-}
-
 // ===================================== C API
 APerformanceHintManager* APerformanceHint_getManager() {
     return APerformanceHintManager::getInstance();
@@ -407,64 +376,6 @@
     return session->setPreferPowerEfficiency(enabled);
 }
 
-int APerformanceHint_reportActualWorkDuration2(APerformanceHintSession* session,
-                                               AWorkDuration* workDuration) {
-    if (session == nullptr || workDuration == nullptr) {
-        ALOGE("Invalid value: (session %p, workDuration %p)", session, workDuration);
-        return EINVAL;
-    }
-    return session->reportActualWorkDuration(workDuration);
-}
-
-AWorkDuration* AWorkDuration_create() {
-    WorkDuration* workDuration = new WorkDuration();
-    return static_cast<AWorkDuration*>(workDuration);
-}
-
-void AWorkDuration_release(AWorkDuration* aWorkDuration) {
-    if (aWorkDuration == nullptr) {
-        ALOGE("%s: aWorkDuration is nullptr", __FUNCTION__);
-    }
-    delete aWorkDuration;
-}
-
-void AWorkDuration_setWorkPeriodStartTimestampNanos(AWorkDuration* aWorkDuration,
-                                                    int64_t workPeriodStartTimestampNanos) {
-    if (aWorkDuration == nullptr || workPeriodStartTimestampNanos <= 0) {
-        ALOGE("%s: Invalid value. (AWorkDuration: %p, workPeriodStartTimestampNanos: %" PRIi64 ")",
-              __FUNCTION__, aWorkDuration, workPeriodStartTimestampNanos);
-    }
-    static_cast<WorkDuration*>(aWorkDuration)->workPeriodStartTimestampNanos =
-            workPeriodStartTimestampNanos;
-}
-
-void AWorkDuration_setActualTotalDurationNanos(AWorkDuration* aWorkDuration,
-                                               int64_t actualTotalDurationNanos) {
-    if (aWorkDuration == nullptr || actualTotalDurationNanos <= 0) {
-        ALOGE("%s: Invalid value. (AWorkDuration: %p, actualTotalDurationNanos: %" PRIi64 ")",
-              __FUNCTION__, aWorkDuration, actualTotalDurationNanos);
-    }
-    static_cast<WorkDuration*>(aWorkDuration)->actualTotalDurationNanos = actualTotalDurationNanos;
-}
-
-void AWorkDuration_setActualCpuDurationNanos(AWorkDuration* aWorkDuration,
-                                             int64_t actualCpuDurationNanos) {
-    if (aWorkDuration == nullptr || actualCpuDurationNanos <= 0) {
-        ALOGE("%s: Invalid value. (AWorkDuration: %p, actualCpuDurationNanos: %" PRIi64 ")",
-              __FUNCTION__, aWorkDuration, actualCpuDurationNanos);
-    }
-    static_cast<WorkDuration*>(aWorkDuration)->actualCpuDurationNanos = actualCpuDurationNanos;
-}
-
-void AWorkDuration_setActualGpuDurationNanos(AWorkDuration* aWorkDuration,
-                                             int64_t actualGpuDurationNanos) {
-    if (aWorkDuration == nullptr || actualGpuDurationNanos < 0) {
-        ALOGE("%s: Invalid value. (AWorkDuration: %p, actualGpuDurationNanos: %" PRIi64 ")",
-              __FUNCTION__, aWorkDuration, actualGpuDurationNanos);
-    }
-    static_cast<WorkDuration*>(aWorkDuration)->actualGpuDurationNanos = actualGpuDurationNanos;
-}
-
 void APerformanceHint_setIHintManagerForTesting(void* iManager) {
     delete gHintManagerForTesting;
     gHintManagerForTesting = nullptr;
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 4553b49..22d33b1 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -16,7 +16,6 @@
 
 #define LOG_TAG "PerformanceHintNativeTest"
 
-#include <android/WorkDuration.h>
 #include <android/os/IHintManager.h>
 #include <android/os/IHintSession.h>
 #include <android/performance_hint.h>
@@ -61,8 +60,6 @@
     MOCK_METHOD(Status, setMode, (int32_t mode, bool enabled), (override));
     MOCK_METHOD(Status, close, (), (override));
     MOCK_METHOD(IBinder*, onAsBinder, (), (override));
-    MOCK_METHOD(Status, reportActualWorkDuration2,
-                (const ::std::vector<android::os::WorkDuration>& workDurations), (override));
 };
 
 class PerformanceHintTest : public Test {
@@ -123,7 +120,6 @@
     std::vector<int64_t> actualDurations;
     actualDurations.push_back(20);
     EXPECT_CALL(*iSession, reportActualWorkDuration(Eq(actualDurations), _)).Times(Exactly(1));
-    EXPECT_CALL(*iSession, reportActualWorkDuration2(_)).Times(Exactly(1));
     result = APerformanceHint_reportActualWorkDuration(session, actualDurationNanos);
     EXPECT_EQ(0, result);
 
@@ -242,125 +238,4 @@
     APerformanceHintSession* session =
             APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration);
     ASSERT_TRUE(session);
-}
-
-MATCHER_P(WorkDurationEq, expected, "") {
-    if (arg.size() != expected.size()) {
-        *result_listener << "WorkDuration vectors are different sizes. Expected: "
-                         << expected.size() << ", Actual: " << arg.size();
-        return false;
-    }
-    for (int i = 0; i < expected.size(); ++i) {
-        android::os::WorkDuration expectedWorkDuration = expected[i];
-        android::os::WorkDuration actualWorkDuration = arg[i];
-        if (!expectedWorkDuration.equalsWithoutTimestamp(actualWorkDuration)) {
-            *result_listener << "WorkDuration at [" << i << "] is different: "
-                             << "Expected: " << expectedWorkDuration
-                             << ", Actual: " << actualWorkDuration;
-            return false;
-        }
-    }
-    return true;
-}
-
-TEST_F(PerformanceHintTest, TestAPerformanceHint_reportActualWorkDuration2) {
-    APerformanceHintManager* manager = createManager();
-
-    std::vector<int32_t> tids;
-    tids.push_back(1);
-    tids.push_back(2);
-    int64_t targetDuration = 56789L;
-
-    StrictMock<MockIHintSession>* iSession = new StrictMock<MockIHintSession>();
-    sp<IHintSession> session_sp(iSession);
-
-    EXPECT_CALL(*mMockIHintManager, createHintSession(_, Eq(tids), Eq(targetDuration), _))
-            .Times(Exactly(1))
-            .WillRepeatedly(DoAll(SetArgPointee<3>(std::move(session_sp)), Return(Status())));
-
-    APerformanceHintSession* session =
-            APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration);
-    ASSERT_TRUE(session);
-
-    int64_t targetDurationNanos = 10;
-    EXPECT_CALL(*iSession, updateTargetWorkDuration(Eq(targetDurationNanos))).Times(Exactly(1));
-    int result = APerformanceHint_updateTargetWorkDuration(session, targetDurationNanos);
-    EXPECT_EQ(0, result);
-
-    usleep(2); // Sleep for longer than preferredUpdateRateNanos.
-    {
-        std::vector<android::os::WorkDuration> actualWorkDurations;
-        android::os::WorkDuration workDuration(1, 20, 13, 8);
-        actualWorkDurations.push_back(workDuration);
-
-        EXPECT_CALL(*iSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations)))
-                .Times(Exactly(1));
-        result = APerformanceHint_reportActualWorkDuration2(session,
-                                                            static_cast<AWorkDuration*>(
-                                                                    &workDuration));
-        EXPECT_EQ(0, result);
-    }
-
-    {
-        std::vector<android::os::WorkDuration> actualWorkDurations;
-        android::os::WorkDuration workDuration(-1, 20, 13, 8);
-        actualWorkDurations.push_back(workDuration);
-
-        EXPECT_CALL(*iSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations)))
-                .Times(Exactly(1));
-        result = APerformanceHint_reportActualWorkDuration2(session,
-                                                            static_cast<AWorkDuration*>(
-                                                                    &workDuration));
-        EXPECT_EQ(22, result);
-    }
-    {
-        std::vector<android::os::WorkDuration> actualWorkDurations;
-        android::os::WorkDuration workDuration(1, -20, 13, 8);
-        actualWorkDurations.push_back(workDuration);
-
-        EXPECT_CALL(*iSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations)))
-                .Times(Exactly(1));
-        result = APerformanceHint_reportActualWorkDuration2(session,
-                                                            static_cast<AWorkDuration*>(
-                                                                    &workDuration));
-        EXPECT_EQ(22, result);
-    }
-    {
-        std::vector<android::os::WorkDuration> actualWorkDurations;
-        android::os::WorkDuration workDuration(1, 20, -13, 8);
-        actualWorkDurations.push_back(workDuration);
-
-        EXPECT_CALL(*iSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations)))
-                .Times(Exactly(1));
-        result = APerformanceHint_reportActualWorkDuration2(session,
-                                                            static_cast<AWorkDuration*>(
-                                                                    &workDuration));
-        EXPECT_EQ(EINVAL, result);
-    }
-    {
-        std::vector<android::os::WorkDuration> actualWorkDurations;
-        android::os::WorkDuration workDuration(1, 20, 13, -8);
-        actualWorkDurations.push_back(workDuration);
-
-        EXPECT_CALL(*iSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations)))
-                .Times(Exactly(1));
-        result = APerformanceHint_reportActualWorkDuration2(session,
-                                                            static_cast<AWorkDuration*>(
-                                                                    &workDuration));
-        EXPECT_EQ(EINVAL, result);
-    }
-
-    EXPECT_CALL(*iSession, close()).Times(Exactly(1));
-    APerformanceHint_closeSession(session);
-}
-
-TEST_F(PerformanceHintTest, TestAWorkDuration) {
-    AWorkDuration* aWorkDuration = AWorkDuration_create();
-    ASSERT_NE(aWorkDuration, nullptr);
-
-    AWorkDuration_setWorkPeriodStartTimestampNanos(aWorkDuration, 1);
-    AWorkDuration_setActualTotalDurationNanos(aWorkDuration, 20);
-    AWorkDuration_setActualCpuDurationNanos(aWorkDuration, 13);
-    AWorkDuration_setActualGpuDurationNanos(aWorkDuration, 8);
-    AWorkDuration_release(aWorkDuration);
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 7eb7dac..57af2ba 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -19,7 +19,6 @@
 import android.app.AlertDialog
 import android.app.Dialog
 import android.content.DialogInterface
-import android.content.res.Configuration
 import androidx.compose.animation.Crossfade
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.snap
@@ -54,8 +53,6 @@
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
-import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
-import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -68,7 +65,6 @@
 import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.text.style.TextOverflow
@@ -83,8 +79,8 @@
 import com.android.compose.animation.scene.SceneTransitionLayout
 import com.android.compose.animation.scene.transitions
 import com.android.compose.modifiers.thenIf
-import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
@@ -93,8 +89,8 @@
 import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.fold.ui.composable.FoldPosture
 import com.android.systemui.fold.ui.composable.foldPosture
+import com.android.systemui.fold.ui.helper.FoldPosture
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
@@ -149,10 +145,7 @@
 ) {
     val backgroundColor = MaterialTheme.colorScheme.surface
     val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState()
-    val layout =
-        calculateLayout(
-            isSideBySideSupported = isSideBySideSupported,
-        )
+    val layout = calculateLayout(isSideBySideSupported = isSideBySideSupported)
 
     Box(modifier) {
         Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) {
@@ -163,27 +156,27 @@
         val isFullScreenUserSwitcherEnabled = viewModel.isUserSwitcherVisible
 
         when (layout) {
-            Layout.STANDARD ->
+            BouncerSceneLayout.STANDARD ->
                 StandardLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
                     modifier = childModifier,
                 )
-            Layout.SIDE_BY_SIDE ->
+            BouncerSceneLayout.SIDE_BY_SIDE ->
                 SideBySideLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
                     isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
                     modifier = childModifier,
                 )
-            Layout.STACKED ->
+            BouncerSceneLayout.STACKED ->
                 StackedLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
                     isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
                     modifier = childModifier,
                 )
-            Layout.SPLIT ->
+            BouncerSceneLayout.SPLIT ->
                 SplitLayout(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
@@ -728,58 +721,10 @@
     }
 }
 
-@Composable
-private fun calculateLayout(
-    isSideBySideSupported: Boolean,
-): Layout {
-    val windowSizeClass = LocalWindowSizeClass.current
-    val width = windowSizeClass.widthSizeClass
-    val height = windowSizeClass.heightSizeClass
-    val isLarge = width > WindowWidthSizeClass.Compact && height > WindowHeightSizeClass.Compact
-    val isTall =
-        when (height) {
-            WindowHeightSizeClass.Expanded -> width < WindowWidthSizeClass.Expanded
-            WindowHeightSizeClass.Medium -> width < WindowWidthSizeClass.Medium
-            else -> false
-        }
-    val isSquare =
-        when (width) {
-            WindowWidthSizeClass.Compact -> height == WindowHeightSizeClass.Compact
-            WindowWidthSizeClass.Medium -> height == WindowHeightSizeClass.Medium
-            WindowWidthSizeClass.Expanded -> height == WindowHeightSizeClass.Expanded
-            else -> false
-        }
-    val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
-
-    return when {
-        // Small and tall devices (i.e. phone/folded in portrait) or square device not in landscape
-        // mode (unfolded with hinge along horizontal plane).
-        (!isLarge && isTall) || (isSquare && !isLandscape) -> Layout.STANDARD
-        // Small and wide devices (i.e. phone/folded in landscape).
-        !isLarge -> Layout.SPLIT
-        // Large and tall devices (i.e. tablet in portrait).
-        isTall -> Layout.STACKED
-        // Large and wide/square devices (i.e. tablet in landscape, unfolded).
-        else -> if (isSideBySideSupported) Layout.SIDE_BY_SIDE else Layout.STANDARD
-    }
-}
-
 interface BouncerSceneDialogFactory {
     operator fun invoke(): AlertDialog
 }
 
-/** Enumerates all known adaptive layout configurations. */
-private enum class Layout {
-    /** The default UI with the bouncer laid out normally. */
-    STANDARD,
-    /** The bouncer is displayed vertically stacked with the user switcher. */
-    STACKED,
-    /** The bouncer is displayed side-by-side with the user switcher or an empty space. */
-    SIDE_BY_SIDE,
-    /** The bouncer is split in two with both sides shown side-by-side. */
-    SPLIT,
-}
-
 /** Enumerates all supported user-input area visibilities. */
 private enum class UserInputAreaVisibility {
     /**
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
new file mode 100644
index 0000000..08b7559
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.bouncer.ui.composable
+
+import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
+import com.android.systemui.bouncer.ui.helper.SizeClass
+import com.android.systemui.bouncer.ui.helper.calculateLayoutInternal
+
+/**
+ * Returns the [BouncerSceneLayout] that should be used by the bouncer scene. If
+ * [isSideBySideSupported] is `false`, then [BouncerSceneLayout.SIDE_BY_SIDE] is replaced by
+ * [BouncerSceneLayout.STANDARD].
+ */
+@Composable
+fun calculateLayout(
+    isSideBySideSupported: Boolean,
+): BouncerSceneLayout {
+    val windowSizeClass = LocalWindowSizeClass.current
+
+    return calculateLayoutInternal(
+        width = windowSizeClass.widthSizeClass.toEnum(),
+        height = windowSizeClass.heightSizeClass.toEnum(),
+        isSideBySideSupported = isSideBySideSupported,
+    )
+}
+
+private fun WindowWidthSizeClass.toEnum(): SizeClass {
+    return when (this) {
+        WindowWidthSizeClass.Compact -> SizeClass.COMPACT
+        WindowWidthSizeClass.Medium -> SizeClass.MEDIUM
+        WindowWidthSizeClass.Expanded -> SizeClass.EXPANDED
+        else -> error("Unsupported WindowWidthSizeClass \"$this\"")
+    }
+}
+
+private fun WindowHeightSizeClass.toEnum(): SizeClass {
+    return when (this) {
+        WindowHeightSizeClass.Compact -> SizeClass.COMPACT
+        WindowHeightSizeClass.Medium -> SizeClass.MEDIUM
+        WindowHeightSizeClass.Expanded -> SizeClass.EXPANDED
+        else -> error("Unsupported WindowHeightSizeClass \"$this\"")
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
index 1c993cf..e77ade9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
@@ -23,19 +23,9 @@
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowInfoTracker
-
-sealed interface FoldPosture {
-    /** A foldable device that's fully closed/folded or a device that doesn't support folding. */
-    data object Folded : FoldPosture
-    /** A foldable that's halfway open with the hinge held vertically. */
-    data object Book : FoldPosture
-    /** A foldable that's halfway open with the hinge held horizontally. */
-    data object Tabletop : FoldPosture
-    /** A foldable that's fully unfolded / flat. */
-    data object FullyUnfolded : FoldPosture
-}
+import com.android.systemui.fold.ui.helper.FoldPosture
+import com.android.systemui.fold.ui.helper.foldPostureInternal
 
 /** Returns the [FoldPosture] of the device currently. */
 @Composable
@@ -48,32 +38,6 @@
         initialValue = FoldPosture.Folded,
         key1 = layoutInfo,
     ) {
-        value =
-            layoutInfo
-                ?.displayFeatures
-                ?.firstNotNullOfOrNull { it as? FoldingFeature }
-                .let { foldingFeature ->
-                    when (foldingFeature?.state) {
-                        null -> FoldPosture.Folded
-                        FoldingFeature.State.HALF_OPENED ->
-                            foldingFeature.orientation.toHalfwayPosture()
-                        FoldingFeature.State.FLAT ->
-                            if (foldingFeature.isSeparating) {
-                                // Dual screen device.
-                                foldingFeature.orientation.toHalfwayPosture()
-                            } else {
-                                FoldPosture.FullyUnfolded
-                            }
-                        else -> error("Unsupported state \"${foldingFeature.state}\"")
-                    }
-                }
-    }
-}
-
-private fun FoldingFeature.Orientation.toHalfwayPosture(): FoldPosture {
-    return when (this) {
-        FoldingFeature.Orientation.HORIZONTAL -> FoldPosture.Tabletop
-        FoldingFeature.Orientation.VERTICAL -> FoldPosture.Book
-        else -> error("Unsupported orientation \"$this\"")
+        value = foldPostureInternal(layoutInfo)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
new file mode 100644
index 0000000..61b2057
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 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.fold.ui.helper
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FoldPostureTest : SysuiTestCase() {
+
+    @Test
+    fun foldPosture_whenNull_returnsFolded() {
+        assertThat(foldPostureInternal(null)).isEqualTo(FoldPosture.Folded)
+    }
+
+    @Test
+    fun foldPosture_whenHalfOpenHorizontally_returnsTabletop() {
+        assertThat(
+                foldPostureInternal(
+                    createWindowLayoutInfo(
+                        state = FoldingFeature.State.HALF_OPENED,
+                        orientation = FoldingFeature.Orientation.HORIZONTAL,
+                    )
+                )
+            )
+            .isEqualTo(FoldPosture.Tabletop)
+    }
+
+    @Test
+    fun foldPosture_whenHalfOpenVertically_returnsBook() {
+        assertThat(
+                foldPostureInternal(
+                    createWindowLayoutInfo(
+                        state = FoldingFeature.State.HALF_OPENED,
+                        orientation = FoldingFeature.Orientation.VERTICAL,
+                    )
+                )
+            )
+            .isEqualTo(FoldPosture.Book)
+    }
+
+    @Test
+    fun foldPosture_whenFlatAndNotSeparating_returnsFullyUnfolded() {
+        assertThat(
+                foldPostureInternal(
+                    createWindowLayoutInfo(
+                        state = FoldingFeature.State.FLAT,
+                        orientation = FoldingFeature.Orientation.HORIZONTAL,
+                        isSeparating = false,
+                    )
+                )
+            )
+            .isEqualTo(FoldPosture.FullyUnfolded)
+    }
+
+    @Test
+    fun foldPosture_whenFlatAndSeparatingHorizontally_returnsTabletop() {
+        assertThat(
+                foldPostureInternal(
+                    createWindowLayoutInfo(
+                        state = FoldingFeature.State.FLAT,
+                        isSeparating = true,
+                        orientation = FoldingFeature.Orientation.HORIZONTAL,
+                    )
+                )
+            )
+            .isEqualTo(FoldPosture.Tabletop)
+    }
+
+    @Test
+    fun foldPosture_whenFlatAndSeparatingVertically_returnsBook() {
+        assertThat(
+                foldPostureInternal(
+                    createWindowLayoutInfo(
+                        state = FoldingFeature.State.FLAT,
+                        isSeparating = true,
+                        orientation = FoldingFeature.Orientation.VERTICAL,
+                    )
+                )
+            )
+            .isEqualTo(FoldPosture.Book)
+    }
+
+    private fun createWindowLayoutInfo(
+        state: FoldingFeature.State,
+        orientation: FoldingFeature.Orientation = FoldingFeature.Orientation.VERTICAL,
+        isSeparating: Boolean = false,
+        occlusionType: FoldingFeature.OcclusionType = FoldingFeature.OcclusionType.NONE,
+    ): WindowLayoutInfo {
+        return WindowLayoutInfo(
+            listOf(
+                object : FoldingFeature {
+                    override val bounds: Rect = Rect(0, 0, 100, 100)
+                    override val isSeparating: Boolean = isSeparating
+                    override val occlusionType: FoldingFeature.OcclusionType = occlusionType
+                    override val orientation: FoldingFeature.Orientation = orientation
+                    override val state: FoldingFeature.State = state
+                }
+            )
+        )
+    }
+}
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index b8f4c0f..7ab44e7 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -53,6 +53,8 @@
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:layout_gravity="center_vertical"
+                android:focusable="true"
+                android:importantForAccessibility="no"
                 android:tint="?attr/shadeActive"
                 android:visibility="gone" />
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
new file mode 100644
index 0000000..5385442
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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.bouncer.ui.helper
+
+import androidx.annotation.VisibleForTesting
+
+/** Enumerates all known adaptive layout configurations. */
+enum class BouncerSceneLayout {
+    /** The default UI with the bouncer laid out normally. */
+    STANDARD,
+    /** The bouncer is displayed vertically stacked with the user switcher. */
+    STACKED,
+    /** The bouncer is displayed side-by-side with the user switcher or an empty space. */
+    SIDE_BY_SIDE,
+    /** The bouncer is split in two with both sides shown side-by-side. */
+    SPLIT,
+}
+
+/** Enumerates the supported window size classes. */
+enum class SizeClass {
+    COMPACT,
+    MEDIUM,
+    EXPANDED,
+}
+
+/**
+ * Internal version of `calculateLayout` in the System UI Compose library, extracted here to allow
+ * for testing that's not dependent on Compose.
+ */
+@VisibleForTesting
+fun calculateLayoutInternal(
+    width: SizeClass,
+    height: SizeClass,
+    isSideBySideSupported: Boolean,
+): BouncerSceneLayout {
+    return when (height) {
+        SizeClass.COMPACT -> BouncerSceneLayout.SPLIT
+        SizeClass.MEDIUM ->
+            when (width) {
+                SizeClass.COMPACT -> BouncerSceneLayout.STANDARD
+                SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD
+                SizeClass.EXPANDED -> BouncerSceneLayout.SIDE_BY_SIDE
+            }
+        SizeClass.EXPANDED ->
+            when (width) {
+                SizeClass.COMPACT -> BouncerSceneLayout.STANDARD
+                SizeClass.MEDIUM -> BouncerSceneLayout.STACKED
+                SizeClass.EXPANDED -> BouncerSceneLayout.SIDE_BY_SIDE
+            }
+    }.takeIf { it != BouncerSceneLayout.SIDE_BY_SIDE || isSideBySideSupported }
+        ?: BouncerSceneLayout.STANDARD
+}
diff --git a/packages/SystemUI/src/com/android/systemui/fold/ui/helper/FoldPosture.kt b/packages/SystemUI/src/com/android/systemui/fold/ui/helper/FoldPosture.kt
new file mode 100644
index 0000000..bc1cc4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fold/ui/helper/FoldPosture.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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.fold.ui.helper
+
+import androidx.annotation.VisibleForTesting
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+
+sealed interface FoldPosture {
+    /** A foldable device that's fully closed/folded or a device that doesn't support folding. */
+    data object Folded : FoldPosture
+    /** A foldable that's halfway open with the hinge held vertically. */
+    data object Book : FoldPosture
+    /** A foldable that's halfway open with the hinge held horizontally. */
+    data object Tabletop : FoldPosture
+    /** A foldable that's fully unfolded / flat. */
+    data object FullyUnfolded : FoldPosture
+}
+
+/**
+ * Internal version of `foldPosture` in the System UI Compose library, extracted here to allow for
+ * testing that's not dependent on Compose.
+ */
+@VisibleForTesting
+fun foldPostureInternal(layoutInfo: WindowLayoutInfo?): FoldPosture {
+    return layoutInfo
+        ?.displayFeatures
+        ?.firstNotNullOfOrNull { it as? FoldingFeature }
+        .let { foldingFeature ->
+            when (foldingFeature?.state) {
+                null -> FoldPosture.Folded
+                FoldingFeature.State.HALF_OPENED -> foldingFeature.orientation.toHalfwayPosture()
+                FoldingFeature.State.FLAT ->
+                    if (foldingFeature.isSeparating) {
+                        // Dual screen device.
+                        foldingFeature.orientation.toHalfwayPosture()
+                    } else {
+                        FoldPosture.FullyUnfolded
+                    }
+                else -> error("Unsupported state \"${foldingFeature.state}\"")
+            }
+        }
+}
+
+private fun FoldingFeature.Orientation.toHalfwayPosture(): FoldPosture {
+    return when (this) {
+        FoldingFeature.Orientation.HORIZONTAL -> FoldPosture.Tabletop
+        FoldingFeature.Orientation.VERTICAL -> FoldPosture.Book
+        else -> error("Unsupported orientation \"$this\"")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index fa18b35b..052c0da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -12,6 +12,7 @@
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.util.AttributeSet;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -22,6 +23,7 @@
 import android.widget.Scroller;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.viewpager.widget.PagerAdapter;
 import androidx.viewpager.widget.ViewPager;
 
@@ -43,6 +45,7 @@
     private static final int NO_PAGE = -1;
 
     private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
+    private static final int SINGLE_PAGE_SCROLL_DURATION_MILLIS = 300;
     private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
     private static final long BOUNCE_ANIMATION_DURATION = 450L;
     private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
@@ -63,8 +66,9 @@
     private PageListener mPageListener;
 
     private boolean mListening;
-    private Scroller mScroller;
+    @VisibleForTesting Scroller mScroller;
 
+    /* set of animations used to indicate which tiles were just revealed  */
     @Nullable
     private AnimatorSet mBounceAnimatorSet;
     private float mLastExpansion;
@@ -306,6 +310,38 @@
         mPageIndicator = indicator;
         mPageIndicator.setNumPages(mPages.size());
         mPageIndicator.setLocation(mPageIndicatorPosition);
+        mPageIndicator.setOnKeyListener((view, keyCode, keyEvent) -> {
+            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+                // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need
+                // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we
+                // have a chance to intercept ACTION_UP.
+                if (keyEvent.getAction() == KeyEvent.ACTION_UP && mScroller.isFinished()) {
+                    scrollByX(getDeltaXForKeyboardScrolling(keyCode),
+                            SINGLE_PAGE_SCROLL_DURATION_MILLIS);
+                }
+                return true;
+            }
+            return false;
+        });
+    }
+
+    private int getDeltaXForKeyboardScrolling(int keyCode) {
+        if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && getCurrentItem() != 0) {
+            return -getWidth();
+        } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+                && getCurrentItem() != mPages.size() - 1) {
+            return getWidth();
+        }
+        return 0;
+    }
+
+    private void scrollByX(int x, int durationMillis) {
+        if (x != 0) {
+            mScroller.startScroll(/* startX= */ getScrollX(), /* startY= */ getScrollY(),
+                    /* dx= */ x, /* dy= */ 0, /* duration= */ durationMillis);
+            // scroller just sets its state, we need to invalidate view to actually start scrolling
+            postInvalidateOnAnimation();
+        }
     }
 
     @Override
@@ -596,9 +632,7 @@
         });
         setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
         int dx = getWidth() * lastPageNumber;
-        mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
-                REVEAL_SCROLL_DURATION_MILLIS);
-        postInvalidateOnAnimation();
+        scrollByX(isLayoutRtl() ? -dx : dx, REVEAL_SCROLL_DURATION_MILLIS);
     }
 
     private boolean shouldNotRunAnimation(Set<String> tilesToReveal) {
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index b30bc56..bc5090f 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -30,12 +30,11 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.systemui.Gefingerpoken;
+import com.android.systemui.res.R;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.haptics.slider.SeekableSliderEventProducer;
-import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.res.R;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.util.ViewController;
@@ -282,7 +281,6 @@
         private final VibratorHelper mVibratorHelper;
         private final SystemClock mSystemClock;
         private final CoroutineDispatcher mMainDispatcher;
-        private final ActivityStarter mActivityStarter;
 
         @Inject
         public Factory(
@@ -290,14 +288,13 @@
                 UiEventLogger uiEventLogger,
                 VibratorHelper vibratorHelper,
                 SystemClock clock,
-                @Main CoroutineDispatcher mainDispatcher,
-                ActivityStarter activityStarter) {
+                @Main CoroutineDispatcher mainDispatcher
+        ) {
             mFalsingManager = falsingManager;
             mUiEventLogger = uiEventLogger;
             mVibratorHelper = vibratorHelper;
             mSystemClock = clock;
             mMainDispatcher = mainDispatcher;
-            mActivityStarter = activityStarter;
         }
 
         /**
@@ -313,8 +310,6 @@
             int layout = getLayout();
             BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context)
                     .inflate(layout, viewRoot, false);
-            root.setActivityStarter(mActivityStarter);
-
             BrightnessSliderHapticPlugin plugin;
             if (hapticBrightnessSlider()) {
                 plugin = new BrightnessSliderHapticPluginImpl(
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
index 5ecf07f..c885492 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
@@ -33,7 +33,6 @@
 
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.systemui.Gefingerpoken;
-import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.res.R;
 
 /**
@@ -42,7 +41,6 @@
  */
 public class BrightnessSliderView extends FrameLayout {
 
-    private ActivityStarter mActivityStarter;
     @NonNull
     private ToggleSeekBar mSlider;
     private DispatchTouchEventListener mListener;
@@ -59,10 +57,6 @@
         super(context, attrs);
     }
 
-    public void setActivityStarter(@NonNull ActivityStarter activityStarter) {
-        mActivityStarter = activityStarter;
-    }
-
     // Inflated from quick_settings_brightness_dialog
     @Override
     protected void onFinishInflate() {
@@ -71,7 +65,6 @@
 
         mSlider = requireViewById(R.id.slider);
         mSlider.setAccessibilityLabel(getContentDescription().toString());
-        mSlider.setActivityStarter(mActivityStarter);
 
         // Finds the progress drawable. Assumes brightness_progress_drawable.xml
         try {
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java
index 6ec10da..a5a0ae7 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java
@@ -23,9 +23,8 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.SeekBar;
 
-import androidx.annotation.NonNull;
-
 import com.android.settingslib.RestrictedLockUtils;
+import com.android.systemui.Dependency;
 import com.android.systemui.plugins.ActivityStarter;
 
 public class ToggleSeekBar extends SeekBar {
@@ -33,8 +32,6 @@
 
     private RestrictedLockUtils.EnforcedAdmin mEnforcedAdmin = null;
 
-    private ActivityStarter mActivityStarter;
-
     public ToggleSeekBar(Context context) {
         super(context);
     }
@@ -52,7 +49,7 @@
         if (mEnforcedAdmin != null) {
             Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
                     mContext, mEnforcedAdmin);
-            mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
+            Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(intent, 0);
             return true;
         }
         if (!isEnabled()) {
@@ -77,8 +74,4 @@
     public void setEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin) {
         mEnforcedAdmin = admin;
     }
-
-    public void setActivityStarter(@NonNull ActivityStarter activityStarter) {
-        mActivityStarter = activityStarter;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
index de334bb..2338be2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
@@ -85,7 +85,9 @@
 
     public void setFooterVisibility(@Visibility int visibility) {
         mFooterVisibility = visibility;
-        setSecondaryVisible(visibility == View.VISIBLE, false);
+        setSecondaryVisible(/* visible = */ visibility == View.VISIBLE,
+                /* animate = */false,
+                /* onAnimationEnded = */ null);
     }
 
     public void setFooterText(@StringRes int text) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 19bce89..fa2748c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
@@ -41,6 +42,7 @@
     private val groupExpansionManagerImpl: GroupExpansionManagerImpl,
     private val notificationIconAreaController: NotificationIconAreaController,
     private val renderListInteractor: RenderNotificationListInteractor,
+    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
 ) : Coordinator {
 
     override fun attach(pipeline: NotifPipeline) {
@@ -50,7 +52,13 @@
 
     fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
         traceSection("StackCoordinator.onAfterRenderList") {
-            controller.setNotifStats(calculateNotifStats(entries))
+            val notifStats = calculateNotifStats(entries)
+            if (FooterViewRefactor.isEnabled) {
+                activeNotificationsInteractor.setNotifStats(notifStats)
+            }
+            // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the footer
+            //  visibility is handled in the new stack.
+            controller.setNotifStats(notifStats)
             if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) {
                 renderListInteractor.setRenderedList(entries)
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
index 12ee54d..5ed82cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar.notification.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
@@ -37,6 +38,9 @@
 
     /** Are any already-seen notifications currently filtered out of the active list? */
     val hasFilteredOutSeenNotifications = MutableStateFlow(false)
+
+    /** Stats about the list of notifications attached to the shade */
+    val notifStats = MutableStateFlow(NotifStats.empty)
 }
 
 /** Represents the notification list, comprised of groups and individual notifications. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 542f3c4..31893b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -15,6 +15,7 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -52,4 +53,14 @@
      */
     val areAnyNotificationsPresentValue: Boolean
         get() = repository.activeNotifications.value.renderList.isNotEmpty()
+
+    /** Are there are any notifications that can be cleared by the "Clear all" button? */
+    val hasClearableNotifications: Flow<Boolean> =
+        repository.notifStats
+            .map { it.hasClearableAlertingNotifs || it.hasClearableSilentNotifs }
+            .distinctUntilChanged()
+
+    fun setNotifStats(notifStats: NotifStats) {
+        repository.notifStats.value = notifStats
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index 10a43d5..3184d5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -46,6 +46,7 @@
 import com.android.systemui.util.DumpUtilsKt;
 
 import java.io.PrintWriter;
+import java.util.function.Consumer;
 
 public class FooterView extends StackScrollerDecorView {
     private static final String TAG = "FooterView";
@@ -63,9 +64,13 @@
     private String mSeenNotifsFilteredText;
     private Drawable mSeenNotifsFilteredIcon;
 
+    private @StringRes int mClearAllButtonTextId;
+    private @StringRes int mClearAllButtonDescriptionId;
     private @StringRes int mMessageStringId;
     private @DrawableRes int mMessageIconId;
 
+    private OnClickListener mClearAllButtonClickListener;
+
     public FooterView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -84,12 +89,18 @@
         return isSecondaryVisible();
     }
 
+    /** See {@link this#setClearAllButtonVisible(boolean, boolean, Consumer)}. */
+    public void setClearAllButtonVisible(boolean visible, boolean animate) {
+        setClearAllButtonVisible(visible, animate, /* onAnimationEnded = */ null);
+    }
+
     /**
      * Set the visibility of the "Clear all" button to {@code visible}. Animate the change if
      * {@code animate} is true.
      */
-    public void setClearAllButtonVisible(boolean visible, boolean animate) {
-        setSecondaryVisible(visible, animate);
+    public void setClearAllButtonVisible(boolean visible, boolean animate,
+            Consumer<Boolean> onAnimationEnded) {
+        setSecondaryVisible(visible, animate, onAnimationEnded);
     }
 
     @Override
@@ -106,6 +117,42 @@
         });
     }
 
+    /** Set the text label for the "Clear all" button. */
+    public void setClearAllButtonText(@StringRes int textId) {
+        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
+        if (mClearAllButtonTextId == textId) {
+            return; // nothing changed
+        }
+        mClearAllButtonTextId = textId;
+        updateClearAllButtonText();
+    }
+
+    private void updateClearAllButtonText() {
+        if (mClearAllButtonTextId == 0) {
+            return; // not initialized yet
+        }
+        mClearAllButton.setText(getContext().getString(mClearAllButtonTextId));
+    }
+
+    /** Set the accessibility content description for the "Clear all" button. */
+    public void setClearAllButtonDescription(@StringRes int contentDescriptionId) {
+        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+            return;
+        }
+        if (mClearAllButtonDescriptionId == contentDescriptionId) {
+            return; // nothing changed
+        }
+        mClearAllButtonDescriptionId = contentDescriptionId;
+        updateClearAllButtonDescription();
+    }
+
+    private void updateClearAllButtonDescription() {
+        if (mClearAllButtonDescriptionId == 0) {
+            return; // not initialized yet
+        }
+        mClearAllButton.setContentDescription(getContext().getString(mClearAllButtonDescriptionId));
+    }
+
     /** Set the string for a message to be shown instead of the buttons. */
     public void setMessageString(@StringRes int messageId) {
         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
@@ -181,6 +228,10 @@
 
     /** Set onClickListener for the clear all (end) button. */
     public void setClearAllButtonClickListener(OnClickListener listener) {
+        if (FooterViewRefactor.isEnabled()) {
+            if (mClearAllButtonClickListener == listener) return;
+            mClearAllButtonClickListener = listener;
+        }
         mClearAllButton.setOnClickListener(listener);
     }
 
@@ -214,7 +265,28 @@
             mManageButton.setText(mManageNotificationText);
             mManageButton.setContentDescription(mManageNotificationText);
         }
-        if (!FooterViewRefactor.isEnabled()) {
+        if (FooterViewRefactor.isEnabled()) {
+            updateClearAllButtonText();
+            updateClearAllButtonDescription();
+
+            updateMessageString();
+            updateMessageIcon();
+        } else {
+            // NOTE: Prior to the refactor, `updateResources` set the class properties to the right
+            // string values. It was always being called together with `updateContent`, which
+            // deals with actually associating those string values with the correct views
+            // (buttons or text).
+            // In the new code, the resource IDs are being set in the view binder (through
+            // setMessageString and similar setters). The setters themselves now deal with
+            // updating both the resource IDs and the views where appropriate (as in, calling
+            // `updateMessageString` when the resource ID changes). This eliminates the need for
+            // `updateResources`, which will eventually be removed. There are, however, still
+            // situations in which we want to update the views even if the resource IDs didn't
+            // change, such as configuration changes.
+            mClearAllButton.setText(R.string.clear_all_notifications_text);
+            mClearAllButton.setContentDescription(
+                    mContext.getString(R.string.accessibility_clear_all));
+
             mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
             mSeenNotifsFooterTextView
                     .setCompoundDrawablesRelative(mSeenNotifsFilteredIcon, null, null, null);
@@ -230,16 +302,8 @@
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         updateColors();
-        mClearAllButton.setText(R.string.clear_all_notifications_text);
-        mClearAllButton.setContentDescription(
-                mContext.getString(R.string.accessibility_clear_all));
         updateResources();
         updateContent();
-
-        if (FooterViewRefactor.isEnabled()) {
-            updateMessageString();
-            updateMessageIcon();
-        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 6d823437..0299114 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -16,10 +16,14 @@
 
 package com.android.systemui.statusbar.notification.footer.ui.viewbinder
 
+import android.view.View
 import androidx.lifecycle.lifecycleScope
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
+import com.android.systemui.util.ui.value
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.launch
 
@@ -28,9 +32,31 @@
     fun bind(
         footer: FooterView,
         viewModel: FooterViewModel,
+        clearAllNotifications: View.OnClickListener,
     ): DisposableHandle {
+        // Listen for changes when the view is attached.
         return footer.repeatWhenAttached {
-            // Listen for changes when the view is attached.
+            lifecycleScope.launch {
+                viewModel.clearAllButton.collect { button ->
+                    if (button.isVisible.isAnimating) {
+                        footer.setClearAllButtonVisible(
+                            button.isVisible.value,
+                            /* animate = */ true,
+                        ) { _ ->
+                            button.isVisible.stopAnimating()
+                        }
+                    } else {
+                        footer.setClearAllButtonVisible(
+                            button.isVisible.value,
+                            /* animate = */ false,
+                        )
+                    }
+                    footer.setClearAllButtonText(button.labelId)
+                    footer.setClearAllButtonDescription(button.accessibilityDescriptionId)
+                    footer.setClearAllButtonClickListener(clearAllNotifications)
+                }
+            }
+
             lifecycleScope.launch {
                 viewModel.message.collect { message ->
                     footer.setFooterLabelVisible(message.visible)
diff --git a/core/java/android/os/WorkDuration.aidl b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
similarity index 65%
rename from core/java/android/os/WorkDuration.aidl
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
index 0f61204..ea5abef 100644
--- a/core/java/android/os/WorkDuration.aidl
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
@@ -14,6 +14,13 @@
  * limitations under the License.
  */
 
-package android.os;
+package com.android.systemui.statusbar.notification.footer.ui.viewmodel
 
-parcelable WorkDuration cpp_header "android/WorkDuration.h";
\ No newline at end of file
+import android.annotation.StringRes
+import com.android.systemui.util.ui.AnimatedValue
+
+data class FooterButtonViewModel(
+    @StringRes val labelId: Int,
+    @StringRes val accessibilityDescriptionId: Int,
+    val isVisible: AnimatedValue<Boolean>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 3d68a7b..721bea1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -18,18 +18,51 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.ui.AnimatableEvent
+import com.android.systemui.util.ui.toAnimatedValueFlow
 import dagger.Module
 import dagger.Provides
 import java.util.Optional
 import javax.inject.Provider
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 
 /** ViewModel for [FooterView]. */
-class FooterViewModel(seenNotificationsInteractor: SeenNotificationsInteractor) {
+class FooterViewModel(
+    activeNotificationsInteractor: ActiveNotificationsInteractor,
+    seenNotificationsInteractor: SeenNotificationsInteractor,
+    shadeInteractor: ShadeInteractor,
+) {
+    val clearAllButton: Flow<FooterButtonViewModel> =
+        activeNotificationsInteractor.hasClearableNotifications
+            .sample(
+                combine(
+                        shadeInteractor.isShadeFullyExpanded,
+                        shadeInteractor.isShadeTouchable,
+                        ::Pair
+                    )
+                    .onStart { emit(Pair(false, false)) }
+            ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) ->
+                val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+                AnimatableEvent(hasClearableNotifications, shouldAnimate)
+            }
+            .toAnimatedValueFlow()
+            .map { visible ->
+                FooterButtonViewModel(
+                    labelId = R.string.clear_all_notifications_text,
+                    accessibilityDescriptionId = R.string.accessibility_clear_all,
+                    isVisible = visible,
+                )
+            }
+
     val message: Flow<FooterMessageViewModel> =
         seenNotificationsInteractor.hasFilteredOutSeenNotifications.map { hasFilteredOutNotifs ->
             FooterMessageViewModel(
@@ -45,10 +78,18 @@
     @Provides
     @SysUISingleton
     fun provideOptional(
+        activeNotificationsInteractor: Provider<ActiveNotificationsInteractor>,
         seenNotificationsInteractor: Provider<SeenNotificationsInteractor>,
+        shadeInteractor: Provider<ShadeInteractor>,
     ): Optional<FooterViewModel> {
         return if (FooterViewRefactor.isEnabled) {
-            Optional.of(FooterViewModel(seenNotificationsInteractor.get()))
+            Optional.of(
+                FooterViewModel(
+                    activeNotificationsInteractor.get(),
+                    seenNotificationsInteractor.get(),
+                    shadeInteractor.get()
+                )
+            )
         } else {
             Optional.empty()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 9f2b0a6..8e82442 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -204,6 +204,11 @@
     override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble()
 }
 
+class BubbleAppSuspendedSuppressor :
+    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "app is suspended") {
+    override fun shouldSuppress(entry: NotificationEntry) = entry.ranking.isSuspended
+}
+
 class BubbleNoMetadataSuppressor() :
     VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") {
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 2fffd37..334e08d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -56,6 +56,14 @@
         }
     }
 
+    fun logSuspendedAppBubble(entry: NotificationEntry) {
+        buffer.log(TAG, DEBUG, {
+            str1 = entry.logKey
+        }, {
+            "No bubble up: notification: app $str1 is suspended"
+        })
+    }
+
     fun logNoBubbleNoMetadata(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
             str1 = entry.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index 4045380..510086d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -204,6 +204,11 @@
             return false;
         }
 
+        if (entry.getRanking().isSuspended()) {
+            mLogger.logSuspendedAppBubble(entry);
+            return false;
+        }
+
         if (entry.getBubbleMetadata() == null
                 || (entry.getBubbleMetadata().getShortcutId() == null
                     && entry.getBubbleMetadata().getIntent() == null)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 39a87de..73dd5f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -159,6 +159,7 @@
         addFilter(PulseLockscreenVisibilityPrivateSuppressor())
         addFilter(PulseLowImportanceSuppressor())
         addFilter(BubbleNotAllowedSuppressor())
+        addFilter(BubbleAppSuspendedSuppressor())
         addFilter(BubbleNoMetadataSuppressor())
         addFilter(HunGroupAlertBehaviorSuppressor())
         addFilter(HunJustLaunchedFsiSuppressor())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index ec90a8d..162e8af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -54,7 +54,7 @@
         mContent = findContentView();
         mSecondaryView = findSecondaryView();
         setVisible(false /* visible */, false /* animate */);
-        setSecondaryVisible(false /* visible */, false /* animate */);
+        setSecondaryVisible(false /* visible */, false /* animate */, null /* onAnimationEnd */);
         setOutlineProvider(null);
     }
 
@@ -155,15 +155,23 @@
     /**
      * Set the secondary view of this layout to visible.
      *
-     * @param visible should the secondary view be visible
-     * @param animate should the change be animated
+     * @param visible          True if the contents should be visible.
+     * @param animate          True if we should fade to new visibility.
+     * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
+     *                         parameter that represents whether the animation was cancelled.
      */
-    protected void setSecondaryVisible(boolean visible, boolean animate) {
+    protected void setSecondaryVisible(boolean visible, boolean animate,
+            Consumer<Boolean> onAnimationEnded) {
         if (mIsSecondaryVisible != visible) {
             mSecondaryAnimating = animate;
             mIsSecondaryVisible = visible;
-            setViewVisible(mSecondaryView, visible, animate,
-                    (cancelled) -> onSecondaryVisibilityAnimationEnd());
+            Consumer<Boolean> onAnimationEndedWrapper = (cancelled) -> {
+                onContentVisibilityAnimationEnd();
+                if (onAnimationEnded != null) {
+                    onAnimationEnded.accept(cancelled);
+                }
+            };
+            setViewVisible(mSecondaryView, visible, animate, onAnimationEndedWrapper);
         }
 
         if (!mSecondaryAnimating) {
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 6a34f98..283a593 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
@@ -4608,13 +4608,15 @@
         if (mManageButtonClickListener != null) {
             mFooterView.setManageButtonClickListener(mManageButtonClickListener);
         }
-        mFooterView.setClearAllButtonClickListener(v -> {
-            if (mFooterClearAllListener != null) {
-                mFooterClearAllListener.onClearAll();
-            }
-            clearNotifications(ROWS_ALL, true /* closeShade */);
-            footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
-        });
+        if (!FooterViewRefactor.isEnabled()) {
+            mFooterView.setClearAllButtonClickListener(v -> {
+                if (mFooterClearAllListener != null) {
+                    mFooterClearAllListener.onClearAll();
+                }
+                clearNotifications(ROWS_ALL, true /* closeShade */);
+                footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
+            });
+        }
         if (FooterViewRefactor.isEnabled()) {
             updateFooter();
         }
@@ -4687,9 +4689,9 @@
         }
         boolean animate = mIsExpanded && mAnimationsEnabled;
         mFooterView.setVisible(visible, animate);
-        mFooterView.setClearAllButtonVisible(showDismissView, animate);
         mFooterView.showHistory(showHistory);
         if (!FooterViewRefactor.isEnabled()) {
+            mFooterView.setClearAllButtonVisible(showDismissView, animate);
             mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
         }
     }
@@ -5355,11 +5357,15 @@
         return viewsToRemove;
     }
 
+    /** Clear all clearable notifications when the user requests it. */
+    public void clearAllNotifications() {
+        clearNotifications(ROWS_ALL, /* closeShade = */ true);
+    }
+
     /**
      * Collects a list of visible rows, and animates them away in a staggered fashion as if they
      * were dismissed. Notifications are dismissed in the backend via onClearAllAnimationsEnd.
      */
-    @VisibleForTesting
     void clearNotifications(@SelectedRows int selection, boolean closeShade) {
         // Animate-swipe all dismissable notifications, then animate the shade closed
         final ArrayList<View> viewsToAnimateAway = getVisibleViewsToAnimateAway(selection);
@@ -5659,6 +5665,7 @@
     }
 
     void setFooterClearAllListener(FooterClearAllListener listener) {
+        FooterViewRefactor.assertInLegacyMode();
         mFooterClearAllListener = listener;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index d5ec0c5..e6315fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -470,7 +470,7 @@
 
                 @Override
                 public void onSnooze(StatusBarNotification sbn,
-                                     NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
+                        NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
                     mNotificationsController.setNotificationSnoozed(sbn, snoozeOption);
                 }
 
@@ -592,7 +592,7 @@
 
                 @Override
                 public boolean updateSwipeProgress(View animView, boolean dismissable,
-                                                   float swipeProgress) {
+                        float swipeProgress) {
                     // Returning true prevents alpha fading.
                     return false;
                 }
@@ -759,8 +759,10 @@
         mView.setClearAllAnimationListener(this::onAnimationEnd);
         mView.setClearAllListener((selection) -> mUiEventLogger.log(
                 NotificationPanelEvent.fromSelection(selection)));
-        mView.setFooterClearAllListener(() ->
-                mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+        if (!FooterViewRefactor.isEnabled()) {
+            mView.setFooterClearAllListener(() ->
+                    mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+        }
         mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
         mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
             @Override
@@ -1090,7 +1092,7 @@
     }
 
     public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
-                                    boolean cancelAnimators) {
+            boolean cancelAnimators) {
         mView.setOverScrollAmount(amount, onTop, animate, cancelAnimators);
     }
 
@@ -1408,14 +1410,14 @@
      * Return whether there are any clearable notifications
      */
     public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
-        // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the clear all
-        //  button in the refactored code
+        // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
+        //  visibility in the refactored code
         return hasNotifications(selection, true /* clearable */);
     }
 
     public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
-        // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the clear all
-        //  button in the refactored code
+        // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
+        //  visibility in the refactored code
         boolean hasAlertingMatchingClearable = isClearable
                 ? mNotifStats.getHasClearableAlertingNotifs()
                 : mNotifStats.getHasNonClearableAlertingNotifs();
@@ -1454,7 +1456,7 @@
     public RemoteInputController.Delegate createDelegate() {
         return new RemoteInputController.Delegate() {
             public void setRemoteInputActive(NotificationEntry entry,
-                                             boolean remoteInputActive) {
+                    boolean remoteInputActive) {
                 mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
                 entry.notifyHeightChanged(true /* needsAnimation */);
                 updateFooter();
@@ -1573,7 +1575,7 @@
     }
 
     private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove,
-                                @SelectedRows int selectedRows) {
+            @SelectedRows int selectedRows) {
         if (selectedRows == ROWS_ALL) {
             mNotifCollection.dismissAllNotifications(
                     mLockscreenUserManager.getCurrentUserId());
@@ -1665,7 +1667,7 @@
      * Set rounded rect clipping bounds on this view.
      */
     public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
-                                         int bottomRadius) {
+            int bottomRadius) {
         mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius);
     }
 
@@ -2021,7 +2023,7 @@
     private class NotifStackControllerImpl implements NotifStackController {
         @Override
         public void setNotifStats(@NonNull NotifStats notifStats) {
-            // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once clear all visibility
+            // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once footer visibility
             // is handled in the refactored stack.
             mNotifStats = notifStats;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 54df4ab..4554085 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -19,6 +19,8 @@
 import android.view.LayoutInflater
 import androidx.lifecycle.lifecycleScope
 import com.android.app.tracing.traceSection
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.nano.MetricsProto
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.common.ui.reinflateAndBindLatest
 import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
@@ -46,6 +48,7 @@
 @Inject
 constructor(
     private val viewModel: NotificationListViewModel,
+    private val metricsLogger: MetricsLogger,
     private val configuration: ConfigurationState,
     private val configurationController: ConfigurationController,
     private val falsingManager: FalsingManager,
@@ -100,7 +103,17 @@
                     attachToRoot = false,
                 ) { footerView: FooterView ->
                     traceSection("bind FooterView") {
-                        val disposableHandle = FooterViewBinder.bind(footerView, footerViewModel)
+                        val disposableHandle =
+                            FooterViewBinder.bind(
+                                footerView,
+                                footerViewModel,
+                                clearAllNotifications = {
+                                    metricsLogger.action(
+                                        MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
+                                    )
+                                    parentView.clearAllNotifications()
+                                },
+                            )
                         parentView.setFooterView(footerView)
                         return@reinflateAndBindLatest disposableHandle
                     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
new file mode 100644
index 0000000..395d712
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2023 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.bouncer.ui.helper
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.SIDE_BY_SIDE
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.SPLIT
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STACKED
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STANDARD
+import com.google.common.truth.Truth.assertThat
+import java.util.Locale
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+class BouncerSceneLayoutTest : SysuiTestCase() {
+
+    data object Phone :
+        Device(
+            name = "phone",
+            width = SizeClass.COMPACT,
+            height = SizeClass.EXPANDED,
+            naturallyHeld = Vertically,
+        )
+    data object Tablet :
+        Device(
+            name = "tablet",
+            width = SizeClass.EXPANDED,
+            height = SizeClass.MEDIUM,
+            naturallyHeld = Horizontally,
+        )
+    data object Folded :
+        Device(
+            name = "folded",
+            width = SizeClass.COMPACT,
+            height = SizeClass.MEDIUM,
+            naturallyHeld = Vertically,
+        )
+    data object Unfolded :
+        Device(
+            name = "unfolded",
+            width = SizeClass.EXPANDED,
+            height = SizeClass.MEDIUM,
+            naturallyHeld = Vertically,
+            widthWhenUnnaturallyHeld = SizeClass.MEDIUM,
+            heightWhenUnnaturallyHeld = SizeClass.MEDIUM,
+        )
+    data object TallerFolded :
+        Device(
+            name = "taller folded",
+            width = SizeClass.COMPACT,
+            height = SizeClass.EXPANDED,
+            naturallyHeld = Vertically,
+        )
+    data object TallerUnfolded :
+        Device(
+            name = "taller unfolded",
+            width = SizeClass.EXPANDED,
+            height = SizeClass.EXPANDED,
+            naturallyHeld = Vertically,
+        )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun testCases() =
+            listOf(
+                    Phone to
+                        Expected(
+                            whenNaturallyHeld = STANDARD,
+                            whenUnnaturallyHeld = SPLIT,
+                        ),
+                    Tablet to
+                        Expected(
+                            whenNaturallyHeld = SIDE_BY_SIDE,
+                            whenUnnaturallyHeld = STACKED,
+                        ),
+                    Folded to
+                        Expected(
+                            whenNaturallyHeld = STANDARD,
+                            whenUnnaturallyHeld = SPLIT,
+                        ),
+                    Unfolded to
+                        Expected(
+                            whenNaturallyHeld = SIDE_BY_SIDE,
+                            whenUnnaturallyHeld = STANDARD,
+                        ),
+                    TallerFolded to
+                        Expected(
+                            whenNaturallyHeld = STANDARD,
+                            whenUnnaturallyHeld = SPLIT,
+                        ),
+                    TallerUnfolded to
+                        Expected(
+                            whenNaturallyHeld = SIDE_BY_SIDE,
+                            whenUnnaturallyHeld = SIDE_BY_SIDE,
+                        ),
+                )
+                .flatMap { (device, expected) ->
+                    buildList {
+                        // Holding the device in its natural orientation (vertical or horizontal):
+                        add(
+                            TestCase(
+                                device = device,
+                                held = device.naturallyHeld,
+                                expected = expected.layout(heldNaturally = true),
+                            )
+                        )
+
+                        if (expected.whenNaturallyHeld == SIDE_BY_SIDE) {
+                            add(
+                                TestCase(
+                                    device = device,
+                                    held = device.naturallyHeld,
+                                    isSideBySideSupported = false,
+                                    expected = STANDARD,
+                                )
+                            )
+                        }
+
+                        // Holding the device the other way:
+                        add(
+                            TestCase(
+                                device = device,
+                                held = device.naturallyHeld.flip(),
+                                expected = expected.layout(heldNaturally = false),
+                            )
+                        )
+
+                        if (expected.whenUnnaturallyHeld == SIDE_BY_SIDE) {
+                            add(
+                                TestCase(
+                                    device = device,
+                                    held = device.naturallyHeld.flip(),
+                                    isSideBySideSupported = false,
+                                    expected = STANDARD,
+                                )
+                            )
+                        }
+                    }
+                }
+    }
+
+    @Parameterized.Parameter @JvmField var testCase: TestCase? = null
+
+    @Test
+    fun calculateLayout() {
+        testCase?.let { nonNullTestCase ->
+            with(nonNullTestCase) {
+                assertThat(
+                        calculateLayoutInternal(
+                            width = device.width(whenHeld = held),
+                            height = device.height(whenHeld = held),
+                            isSideBySideSupported = isSideBySideSupported,
+                        )
+                    )
+                    .isEqualTo(expected)
+            }
+        }
+    }
+
+    data class TestCase(
+        val device: Device,
+        val held: Held,
+        val expected: BouncerSceneLayout,
+        val isSideBySideSupported: Boolean = true,
+    ) {
+        override fun toString(): String {
+            return buildString {
+                append(device.name)
+                append(" width: ${device.width(held).name.lowercase(Locale.US)}")
+                append(" height: ${device.height(held).name.lowercase(Locale.US)}")
+                append(" when held $held")
+                if (!isSideBySideSupported) {
+                    append(" (side-by-side not supported)")
+                }
+            }
+        }
+    }
+
+    data class Expected(
+        val whenNaturallyHeld: BouncerSceneLayout,
+        val whenUnnaturallyHeld: BouncerSceneLayout,
+    ) {
+        fun layout(heldNaturally: Boolean): BouncerSceneLayout {
+            return if (heldNaturally) {
+                whenNaturallyHeld
+            } else {
+                whenUnnaturallyHeld
+            }
+        }
+    }
+
+    sealed class Device(
+        val name: String,
+        private val width: SizeClass,
+        private val height: SizeClass,
+        val naturallyHeld: Held,
+        private val widthWhenUnnaturallyHeld: SizeClass = height,
+        private val heightWhenUnnaturallyHeld: SizeClass = width,
+    ) {
+        fun width(whenHeld: Held): SizeClass {
+            return if (isHeldNaturally(whenHeld)) {
+                width
+            } else {
+                widthWhenUnnaturallyHeld
+            }
+        }
+
+        fun height(whenHeld: Held): SizeClass {
+            return if (isHeldNaturally(whenHeld)) {
+                height
+            } else {
+                heightWhenUnnaturallyHeld
+            }
+        }
+
+        private fun isHeldNaturally(whenHeld: Held): Boolean {
+            return whenHeld == naturallyHeld
+        }
+    }
+
+    sealed class Held {
+        abstract fun flip(): Held
+    }
+    data object Vertically : Held() {
+        override fun flip(): Held {
+            return Horizontally
+        }
+    }
+    data object Horizontally : Held() {
+        override fun flip(): Held {
+            return Vertically
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
new file mode 100644
index 0000000..db9e548
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
@@ -0,0 +1,86 @@
+package com.android.systemui.qs
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.view.KeyEvent
+import android.view.View
+import android.widget.Scroller
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class PagedTileLayoutTest : SysuiTestCase() {
+
+    @Mock private lateinit var pageIndicator: PageIndicator
+    @Captor private lateinit var captor: ArgumentCaptor<View.OnKeyListener>
+
+    private lateinit var pageTileLayout: TestPagedTileLayout
+    private lateinit var scroller: Scroller
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        pageTileLayout = TestPagedTileLayout(mContext)
+        pageTileLayout.setPageIndicator(pageIndicator)
+        verify(pageIndicator).setOnKeyListener(captor.capture())
+        setViewWidth(pageTileLayout, width = PAGE_WIDTH)
+        scroller = pageTileLayout.mScroller
+    }
+
+    private fun setViewWidth(view: View, width: Int) {
+        view.left = 0
+        view.right = width
+    }
+
+    @Test
+    fun scrollsRight_afterRightArrowPressed_whenFocusOnPagerIndicator() {
+        pageTileLayout.currentPageIndex = 0
+
+        sendUpEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
+
+        assertThat(scroller.isFinished).isFalse() // aka we're scrolling
+        assertThat(scroller.finalX).isEqualTo(scroller.currX + PAGE_WIDTH)
+    }
+
+    @Test
+    fun scrollsLeft_afterLeftArrowPressed_whenFocusOnPagerIndicator() {
+        pageTileLayout.currentPageIndex = 1 // we won't scroll left if we're on the first page
+
+        sendUpEvent(KeyEvent.KEYCODE_DPAD_LEFT)
+
+        assertThat(scroller.isFinished).isFalse() // aka we're scrolling
+        assertThat(scroller.finalX).isEqualTo(scroller.currX - PAGE_WIDTH)
+    }
+
+    private fun sendUpEvent(keyCode: Int) {
+        val event = KeyEvent(KeyEvent.ACTION_UP, keyCode)
+        captor.value.onKey(pageIndicator, keyCode, event)
+    }
+
+    /**
+     * Custom PagedTileLayout to easy mock "currentItem" i.e. currently visible page. Setting this
+     * up otherwise would require setting adapter etc
+     */
+    class TestPagedTileLayout(context: Context) : PagedTileLayout(context, null) {
+
+        var currentPageIndex: Int = 0
+
+        override fun getCurrentItem(): Int {
+            return currentPageIndex
+        }
+    }
+
+    companion object {
+        const val PAGE_WIDTH = 200
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 428574b..fa5fad0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
@@ -57,6 +58,7 @@
     @Mock private lateinit var groupExpansionManagerImpl: GroupExpansionManagerImpl
     @Mock private lateinit var notificationIconAreaController: NotificationIconAreaController
     @Mock private lateinit var renderListInteractor: RenderNotificationListInteractor
+    @Mock private lateinit var activeNotificationsInteractor: ActiveNotificationsInteractor
     @Mock private lateinit var stackController: NotifStackController
     @Mock private lateinit var section: NotifSection
 
@@ -75,6 +77,7 @@
                 groupExpansionManagerImpl,
                 notificationIconAreaController,
                 renderListInteractor,
+                activeNotificationsInteractor,
             )
         coordinator.attach(pipeline)
         afterRenderListListener = withArgCaptor {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index b24cafd..4ab3cd4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -72,4 +72,58 @@
             assertThat(areAnyNotificationsPresent).isFalse()
             assertThat(underTest.areAnyNotificationsPresentValue).isFalse()
         }
+
+    @Test
+    fun testHasClearableNotifications_whenHasClearableAlertingNotifs() =
+        testComponent.runTest {
+            val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+            activeNotificationListRepository.notifStats.value =
+                NotifStats(
+                    numActiveNotifs = 2,
+                    hasNonClearableAlertingNotifs = false,
+                    hasClearableAlertingNotifs = true,
+                    hasNonClearableSilentNotifs = false,
+                    hasClearableSilentNotifs = false,
+                )
+            runCurrent()
+
+            assertThat(hasClearable).isTrue()
+        }
+
+    @Test
+    fun testHasClearableNotifications_whenHasClearableSilentNotifs() =
+        testComponent.runTest {
+            val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+            activeNotificationListRepository.notifStats.value =
+                NotifStats(
+                    numActiveNotifs = 2,
+                    hasNonClearableAlertingNotifs = false,
+                    hasClearableAlertingNotifs = false,
+                    hasNonClearableSilentNotifs = false,
+                    hasClearableSilentNotifs = true,
+                )
+            runCurrent()
+
+            assertThat(hasClearable).isTrue()
+        }
+
+    @Test
+    fun testHasClearableNotifications_whenHasNoClearableNotifs() =
+        testComponent.runTest {
+            val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+            activeNotificationListRepository.notifStats.value =
+                NotifStats(
+                    numActiveNotifs = 2,
+                    hasNonClearableAlertingNotifs = false,
+                    hasClearableAlertingNotifs = false,
+                    hasNonClearableSilentNotifs = false,
+                    hasClearableSilentNotifs = false,
+                )
+            runCurrent()
+
+            assertThat(hasClearable).isFalse()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
index a64ac67..22c5bae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
@@ -114,9 +114,46 @@
     }
 
     @Test
+    public void testSetClearAllButtonText_resourceOnlyFetchedOnce() {
+        int resId = R.string.clear_all_notifications_text;
+        mView.setClearAllButtonText(resId);
+        verify(mSpyContext).getString(eq(resId));
+
+        clearInvocations(mSpyContext);
+
+        assertThat(((TextView) mView.findViewById(R.id.dismiss_text))
+                .getText().toString()).contains("Clear all");
+
+        // Set it a few more times, it shouldn't lead to the resource being fetched again
+        mView.setClearAllButtonText(resId);
+        mView.setClearAllButtonText(resId);
+
+        verify(mSpyContext, never()).getString(anyInt());
+    }
+
+    @Test
+    public void testSetClearAllButtonDescription_resourceOnlyFetchedOnce() {
+        int resId = R.string.accessibility_clear_all;
+        mView.setClearAllButtonDescription(resId);
+        verify(mSpyContext).getString(eq(resId));
+
+        clearInvocations(mSpyContext);
+
+        assertThat(((TextView) mView.findViewById(R.id.dismiss_text))
+                .getContentDescription().toString()).contains("Clear all notifications");
+
+        // Set it a few more times, it shouldn't lead to the resource being fetched again
+        mView.setClearAllButtonDescription(resId);
+        mView.setClearAllButtonDescription(resId);
+
+        verify(mSpyContext, never()).getString(anyInt());
+    }
+
+    @Test
     public void testSetMessageString_resourceOnlyFetchedOnce() {
-        mView.setMessageString(R.string.unlock_to_see_notif_text);
-        verify(mSpyContext).getString(eq(R.string.unlock_to_see_notif_text));
+        int resId = R.string.unlock_to_see_notif_text;
+        mView.setMessageString(resId);
+        verify(mSpyContext).getString(eq(resId));
 
         clearInvocations(mSpyContext);
 
@@ -124,22 +161,23 @@
                 .getText().toString()).contains("Unlock");
 
         // Set it a few more times, it shouldn't lead to the resource being fetched again
-        mView.setMessageString(R.string.unlock_to_see_notif_text);
-        mView.setMessageString(R.string.unlock_to_see_notif_text);
+        mView.setMessageString(resId);
+        mView.setMessageString(resId);
 
         verify(mSpyContext, never()).getString(anyInt());
     }
 
     @Test
     public void testSetMessageIcon_resourceOnlyFetchedOnce() {
-        mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
-        verify(mSpyContext).getDrawable(eq(R.drawable.ic_friction_lock_closed));
+        int resId = R.drawable.ic_friction_lock_closed;
+        mView.setMessageIcon(resId);
+        verify(mSpyContext).getDrawable(eq(resId));
 
         clearInvocations(mSpyContext);
 
         // Set it a few more times, it shouldn't lead to the resource being fetched again
-        mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
-        mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
+        mView.setMessageIcon(resId);
+        mView.setMessageIcon(resId);
 
         verify(mSpyContext, never()).getDrawable(anyInt());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 57a7c3c..94dcf7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -18,37 +18,222 @@
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.value
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
+import dagger.BindsInstance
+import dagger.Component
+import java.util.Optional
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class FooterViewModelTest : SysuiTestCase() {
-    private val repository = ActiveNotificationListRepository()
-    private val interactor = SeenNotificationsInteractor(repository)
-    private val underTest = FooterViewModel(interactor)
+    private lateinit var footerViewModel: FooterViewModel
 
-    @Test
-    fun testMessageVisible_whenFilteredNotifications() = runTest {
-        val message by collectLastValue(underTest.message)
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                ActivatableNotificationViewModelModule::class,
+                FooterViewModelModule::class,
+                HeadlessSystemUserModeModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<Optional<FooterViewModel>> {
+        val activeNotificationListRepository: ActiveNotificationListRepository
+        val configurationRepository: FakeConfigurationRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+        val shadeRepository: FakeShadeRepository
+        val powerRepository: FakePowerRepository
 
-        repository.hasFilteredOutSeenNotifications.value = true
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                featureFlags: FakeFeatureFlagsClassicModule,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+    }
 
-        assertThat(message?.visible).isTrue()
+    private val dozeParameters: DozeParameters = mock()
+
+    private val testComponent: TestComponent =
+        DaggerFooterViewModelTest_TestComponent.factory()
+            .create(
+                test = this,
+                featureFlags =
+                    FakeFeatureFlagsClassicModule {
+                        set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true)
+                    },
+                mocks =
+                    TestMocksModule(
+                        dozeParameters = dozeParameters,
+                    )
+            )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
+
+        // The underTest in the component is Optional, because that matches the provider we
+        // currently have for the footer view model.
+        footerViewModel = testComponent.underTest.get()
     }
 
     @Test
-    fun testMessageVisible_whenNoFilteredNotifications() = runTest {
-        val message by collectLastValue(underTest.message)
+    fun testMessageVisible_whenFilteredNotifications() =
+        testComponent.runTest {
+            val message by collectLastValue(footerViewModel.message)
 
-        repository.hasFilteredOutSeenNotifications.value = false
+            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
 
-        assertThat(message?.visible).isFalse()
-    }
+            assertThat(message?.visible).isTrue()
+        }
+
+    @Test
+    fun testMessageVisible_whenNoFilteredNotifications() =
+        testComponent.runTest {
+            val message by collectLastValue(footerViewModel.message)
+
+            activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+
+            assertThat(message?.visible).isFalse()
+        }
+
+    @Test
+    fun testClearAllButtonVisible_whenHasClearableNotifs() =
+        testComponent.runTest {
+            val button by collectLastValue(footerViewModel.clearAllButton)
+
+            activeNotificationListRepository.notifStats.value =
+                NotifStats(
+                    numActiveNotifs = 2,
+                    hasNonClearableAlertingNotifs = false,
+                    hasClearableAlertingNotifs = true,
+                    hasNonClearableSilentNotifs = false,
+                    hasClearableSilentNotifs = true,
+                )
+            runCurrent()
+
+            assertThat(button?.isVisible?.value).isTrue()
+        }
+
+    @Test
+    fun testClearAllButtonVisible_whenHasNoClearableNotifs() =
+        testComponent.runTest {
+            val button by collectLastValue(footerViewModel.clearAllButton)
+
+            activeNotificationListRepository.notifStats.value =
+                NotifStats(
+                    numActiveNotifs = 2,
+                    hasNonClearableAlertingNotifs = false,
+                    hasClearableAlertingNotifs = false,
+                    hasNonClearableSilentNotifs = false,
+                    hasClearableSilentNotifs = false,
+                )
+            runCurrent()
+
+            assertThat(button?.isVisible?.value).isFalse()
+        }
+
+    @Test
+    fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() =
+        testComponent.runTest {
+            val button by collectLastValue(footerViewModel.clearAllButton)
+            runCurrent()
+
+            // WHEN shade is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            shadeRepository.setLegacyShadeExpansion(1f)
+            // AND QS not expanded
+            shadeRepository.setQsExpansion(0f)
+            // AND device is awake
+            powerRepository.updateWakefulness(
+                rawState = WakefulnessState.AWAKE,
+                lastWakeReason = WakeSleepReason.POWER_BUTTON,
+                lastSleepReason = WakeSleepReason.OTHER,
+            )
+            runCurrent()
+
+            // AND there are clearable notifications
+            activeNotificationListRepository.notifStats.value =
+                NotifStats(
+                    numActiveNotifs = 2,
+                    hasNonClearableAlertingNotifs = false,
+                    hasClearableAlertingNotifs = true,
+                    hasNonClearableSilentNotifs = false,
+                    hasClearableSilentNotifs = true,
+                )
+            runCurrent()
+
+            // THEN button visibility should animate
+            assertThat(button?.isVisible?.isAnimating).isTrue()
+        }
+
+    @Test
+    fun testClearAllButtonAnimating_whenShadeNotExpanded() =
+        testComponent.runTest {
+            val button by collectLastValue(footerViewModel.clearAllButton)
+            runCurrent()
+
+            // WHEN shade is collapsed
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            shadeRepository.setLegacyShadeExpansion(0f)
+            // AND QS not expanded
+            shadeRepository.setQsExpansion(0f)
+            // AND device is awake
+            powerRepository.updateWakefulness(
+                rawState = WakefulnessState.AWAKE,
+                lastWakeReason = WakeSleepReason.POWER_BUTTON,
+                lastSleepReason = WakeSleepReason.OTHER,
+            )
+            runCurrent()
+
+            // AND there are clearable notifications
+            activeNotificationListRepository.notifStats.value =
+                NotifStats(
+                    numActiveNotifs = 2,
+                    hasNonClearableAlertingNotifs = false,
+                    hasClearableAlertingNotifs = true,
+                    hasNonClearableSilentNotifs = false,
+                    hasClearableSilentNotifs = true,
+                )
+            runCurrent()
+
+            // THEN button visibility should not animate
+            assertThat(button?.isVisible?.isAnimating).isFalse()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 3e331a6..0a9bac9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -999,11 +999,25 @@
         assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse();
     }
 
+    @Test
+    public void shouldNotBubbleUp_suspended() {
+        assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createSuspendedBubble()))
+                .isFalse();
+    }
+
+    private NotificationEntry createSuspendedBubble() {
+        return createBubble(null, null, true);
+    }
+
     private NotificationEntry createBubble() {
-        return createBubble(null, null);
+        return createBubble(null, null, false);
     }
 
     private NotificationEntry createBubble(String groupKey, Integer groupAlert) {
+        return createBubble(groupKey, groupAlert, false);
+    }
+
+    private NotificationEntry createBubble(String groupKey, Integer groupAlert, Boolean suspended) {
         Notification.BubbleMetadata data = new Notification.BubbleMetadata.Builder(
                 PendingIntent.getActivity(mContext, 0,
                         new Intent().setPackage(mContext.getPackageName()),
@@ -1031,6 +1045,7 @@
                 .setNotification(n)
                 .setImportance(IMPORTANCE_HIGH)
                 .setCanBubble(true)
+                .setSuspended(suspended)
                 .build();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index a3b7e8c..7babff5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -601,6 +601,13 @@
     }
 
     @Test
+    fun testShouldNotBubble_bubbleAppSuspended() {
+        ensureBubbleState()
+        assertShouldNotBubble(buildBubbleEntry { packageSuspended = true })
+        assertNoEventsLogged()
+    }
+
+    @Test
     fun testShouldNotFsi_noFullScreenIntent() {
         forEachFsiState {
             assertShouldNotFsi(buildFsiEntry { hasFsi = false })
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 9d8f979..f3b2ef3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -552,7 +552,8 @@
                     mAsync = true;
                 } else if (opt.equals("--splashscreen-show-icon")) {
                     mShowSplashScreen = true;
-                } else if (opt.equals("--dismiss-keyguard-if-insecure")) {
+                } else if (opt.equals("--dismiss-keyguard-if-insecure")
+                      || opt.equals("--dismiss-keyguard")) {
                     mDismissKeyguardIfInsecure = true;
                 } else {
                     return false;
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a158b18..c24d6a0 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -527,12 +527,27 @@
     @Override
     public boolean canHandleVolumeKey() {
         if (isPlaybackTypeLocal()) {
+            if (DEBUG) {
+                Log.d(TAG, "Local MediaSessionRecord can handle volume key");
+            }
             return true;
         }
         if (mVolumeControlType == VOLUME_CONTROL_FIXED) {
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "Local MediaSessionRecord with FIXED volume control can't handle volume"
+                            + " key");
+            }
             return false;
         }
         if (mVolumeAdjustmentForRemoteGroupSessions) {
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "Volume adjustment for remote group sessions allowed so MediaSessionRecord"
+                            + " can handle volume key");
+            }
             return true;
         }
         // See b/228021646 for details.
@@ -540,7 +555,18 @@
         List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName);
         boolean foundNonSystemSession = false;
         boolean remoteSessionAllowVolumeAdjustment = true;
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "Found "
+                            + sessions.size()
+                            + " routing sessions for package name "
+                            + mPackageName);
+        }
         for (RoutingSessionInfo session : sessions) {
+            if (DEBUG) {
+                Log.d(TAG, "Found routingSessionInfo: " + session);
+            }
             if (!session.isSystemSession()) {
                 foundNonSystemSession = true;
                 if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) {
@@ -549,9 +575,15 @@
             }
         }
         if (!foundNonSystemSession) {
-            Log.d(TAG, "Package " + mPackageName
-                    + " has a remote media session but no associated routing session");
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "Package "
+                                + mPackageName
+                                + " has a remote media session but no associated routing session");
+            }
         }
+
         return foundNonSystemSession && remoteSessionAllowVolumeAdjustment;
     }
 
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index ee3b746..dd39fb0 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -32,8 +32,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemProperties;
-import android.os.WorkDuration;
-import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseIntArray;
@@ -197,9 +195,6 @@
 
         private static native void nativeSetMode(long halPtr, int mode, boolean enabled);
 
-        private static native void nativeReportActualWorkDuration(long halPtr,
-                                                                  WorkDuration[] workDurations);
-
         /** Wrapper for HintManager.nativeInit */
         public void halInit() {
             nativeInit();
@@ -257,10 +252,6 @@
             nativeSetMode(halPtr, mode, enabled);
         }
 
-        /** Wrapper for HintManager.nativeReportActualWorkDuration */
-        public void halReportActualWorkDuration(long halPtr, WorkDuration[] workDurations) {
-            nativeReportActualWorkDuration(halPtr, workDurations);
-        }
     }
 
     @VisibleForTesting
@@ -633,52 +624,6 @@
             }
         }
 
-        @Override
-        public void reportActualWorkDuration2(WorkDuration[] workDurations) {
-            synchronized (this) {
-                if (mHalSessionPtr == 0 || !mUpdateAllowed) {
-                    return;
-                }
-                Preconditions.checkArgument(workDurations.length != 0, "the count"
-                        + " of work durations shouldn't be 0.");
-                for (WorkDuration workDuration : workDurations) {
-                    validateWorkDuration(workDuration);
-                }
-                mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, workDurations);
-            }
-        }
-
-        void validateWorkDuration(WorkDuration workDuration) {
-            if (DEBUG) {
-                Slogf.d(TAG, "WorkDuration(" + workDuration.getTimestampNanos() + ", "
-                        + workDuration.getWorkPeriodStartTimestampNanos() + ", "
-                        + workDuration.getActualTotalDurationNanos() + ", "
-                        + workDuration.getActualCpuDurationNanos() + ", "
-                        + workDuration.getActualGpuDurationNanos() + ")");
-            }
-            if (workDuration.getWorkPeriodStartTimestampNanos() <= 0) {
-                throw new IllegalArgumentException(
-                    TextUtils.formatSimple(
-                            "Work period start timestamp (%d) should be greater than 0",
-                            workDuration.getWorkPeriodStartTimestampNanos()));
-            }
-            if (workDuration.getActualTotalDurationNanos() <= 0) {
-                throw new IllegalArgumentException(
-                    TextUtils.formatSimple("Actual total duration (%d) should be greater than 0",
-                            workDuration.getActualTotalDurationNanos()));
-            }
-            if (workDuration.getActualCpuDurationNanos() <= 0) {
-                throw new IllegalArgumentException(
-                    TextUtils.formatSimple("Actual CPU duration (%d) should be greater than 0",
-                            workDuration.getActualCpuDurationNanos()));
-            }
-            if (workDuration.getActualGpuDurationNanos() < 0) {
-                throw new IllegalArgumentException(
-                    TextUtils.formatSimple("Actual GPU duration (%d) should be non negative",
-                            workDuration.getActualGpuDurationNanos()));
-            }
-        }
-
         private void onProcStateChanged(boolean updateAllowed) {
             updateHintAllowed(updateAllowed);
         }
diff --git a/services/core/java/com/android/server/timedetector/TEST_MAPPING b/services/core/java/com/android/server/timedetector/TEST_MAPPING
index 5c37680..17d327e 100644
--- a/services/core/java/com/android/server/timedetector/TEST_MAPPING
+++ b/services/core/java/com/android/server/timedetector/TEST_MAPPING
@@ -7,10 +7,7 @@
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
-    }
-  ],
-  // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
-  "postsubmit": [
+    },
     {
       "name": "FrameworksTimeServicesTests"
     }
diff --git a/services/core/java/com/android/server/timezonedetector/TEST_MAPPING b/services/core/java/com/android/server/timezonedetector/TEST_MAPPING
index 63dd7b4..358618a 100644
--- a/services/core/java/com/android/server/timezonedetector/TEST_MAPPING
+++ b/services/core/java/com/android/server/timezonedetector/TEST_MAPPING
@@ -7,15 +7,15 @@
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
+    },
+    {
+      "name": "FrameworksTimeServicesTests"
     }
   ],
   // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
   "postsubmit": [
     {
       "name": "CtsLocationTimeZoneManagerHostTest"
-    },
-    {
-      "name": "FrameworksTimeServicesTests"
     }
   ]
 }
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
index ccd9bd0..7edf445 100644
--- a/services/core/jni/com_android_server_hint_HintManagerService.cpp
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -20,7 +20,6 @@
 
 #include <aidl/android/hardware/power/IPower.h>
 #include <android-base/stringprintf.h>
-#include <inttypes.h>
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/ScopedPrimitiveArray.h>
 #include <powermanager/PowerHalController.h>
@@ -39,15 +38,6 @@
 
 namespace android {
 
-static struct {
-    jclass clazz{};
-    jfieldID workPeriodStartTimestampNanos{};
-    jfieldID actualTotalDurationNanos{};
-    jfieldID actualCpuDurationNanos{};
-    jfieldID actualGpuDurationNanos{};
-    jfieldID timestampNanos{};
-} gWorkDurationInfo;
-
 static power::PowerHalController gPowerHalController;
 static std::unordered_map<jlong, std::shared_ptr<IPowerHintSession>> gSessionMap;
 static std::mutex gSessionMapLock;
@@ -190,26 +180,6 @@
     setMode(session_ptr, static_cast<SessionMode>(mode), enabled);
 }
 
-static void nativeReportActualWorkDuration2(JNIEnv* env, jclass /* clazz */, jlong session_ptr,
-                                            jobjectArray jWorkDurations) {
-    int size = env->GetArrayLength(jWorkDurations);
-    std::vector<WorkDuration> workDurations(size);
-    for (int i = 0; i < size; i++) {
-        jobject workDuration = env->GetObjectArrayElement(jWorkDurations, i);
-        workDurations[i].workPeriodStartTimestampNanos =
-                env->GetLongField(workDuration, gWorkDurationInfo.workPeriodStartTimestampNanos);
-        workDurations[i].durationNanos =
-                env->GetLongField(workDuration, gWorkDurationInfo.actualTotalDurationNanos);
-        workDurations[i].cpuDurationNanos =
-                env->GetLongField(workDuration, gWorkDurationInfo.actualCpuDurationNanos);
-        workDurations[i].gpuDurationNanos =
-                env->GetLongField(workDuration, gWorkDurationInfo.actualGpuDurationNanos);
-        workDurations[i].timeStampNanos =
-                env->GetLongField(workDuration, gWorkDurationInfo.timestampNanos);
-    }
-    reportActualWorkDuration(session_ptr, workDurations);
-}
-
 // ----------------------------------------------------------------------------
 static const JNINativeMethod sHintManagerServiceMethods[] = {
         /* name, signature, funcPtr */
@@ -224,23 +194,9 @@
         {"nativeSendHint", "(JI)V", (void*)nativeSendHint},
         {"nativeSetThreads", "(J[I)V", (void*)nativeSetThreads},
         {"nativeSetMode", "(JIZ)V", (void*)nativeSetMode},
-        {"nativeReportActualWorkDuration", "(J[Landroid/os/WorkDuration;)V",
-         (void*)nativeReportActualWorkDuration2},
 };
 
 int register_android_server_HintManagerService(JNIEnv* env) {
-    gWorkDurationInfo.clazz = env->FindClass("android/os/WorkDuration");
-    gWorkDurationInfo.workPeriodStartTimestampNanos =
-            env->GetFieldID(gWorkDurationInfo.clazz, "mWorkPeriodStartTimestampNanos", "J");
-    gWorkDurationInfo.actualTotalDurationNanos =
-            env->GetFieldID(gWorkDurationInfo.clazz, "mActualTotalDurationNanos", "J");
-    gWorkDurationInfo.actualCpuDurationNanos =
-            env->GetFieldID(gWorkDurationInfo.clazz, "mActualCpuDurationNanos", "J");
-    gWorkDurationInfo.actualGpuDurationNanos =
-            env->GetFieldID(gWorkDurationInfo.clazz, "mActualGpuDurationNanos", "J");
-    gWorkDurationInfo.timestampNanos =
-            env->GetFieldID(gWorkDurationInfo.clazz, "mTimestampNanos", "J");
-
     return jniRegisterNativeMethods(env,
                                     "com/android/server/power/hint/"
                                     "HintManagerService$NativeWrapper",
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 3748527..d09aa89 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -44,7 +44,6 @@
 import android.os.IHintSession;
 import android.os.PerformanceHintManager;
 import android.os.Process;
-import android.os.WorkDuration;
 import android.util.Log;
 
 import com.android.server.FgThread;
@@ -90,11 +89,6 @@
     private static final long[] DURATIONS_ZERO = new long[] {};
     private static final long[] TIMESTAMPS_ZERO = new long[] {};
     private static final long[] TIMESTAMPS_TWO = new long[] {1L, 2L};
-    private static final WorkDuration[] WORK_DURATIONS_THREE = new WorkDuration[] {
-        new WorkDuration(1L, 11L, 8L, 4L, 1L),
-        new WorkDuration(2L, 13L, 8L, 6L, 2L),
-        new WorkDuration(3L, 333333333L, 8L, 333333333L, 3L),
-    };
 
     @Mock private Context mContext;
     @Mock private HintManagerService.NativeWrapper mNativeWrapperMock;
@@ -599,55 +593,4 @@
         }
         a.close();
     }
-
-    @Test
-    public void testReportActualWorkDuration2() throws Exception {
-        HintManagerService service = createService();
-        IBinder token = new Binder();
-
-        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
-
-        a.updateTargetWorkDuration(100L);
-        a.reportActualWorkDuration2(WORK_DURATIONS_THREE);
-        verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration(anyLong(),
-                eq(WORK_DURATIONS_THREE));
-
-        assertThrows(IllegalArgumentException.class, () -> {
-            a.reportActualWorkDuration2(new WorkDuration[] {});
-        });
-
-        assertThrows(IllegalArgumentException.class, () -> {
-            a.reportActualWorkDuration2(new WorkDuration[] {new WorkDuration(0L, 11L, 8L, 4L, 1L)});
-        });
-
-        assertThrows(IllegalArgumentException.class, () -> {
-            a.reportActualWorkDuration2(new WorkDuration[] {new WorkDuration(1L, 0L, 8L, 4L, 1L)});
-        });
-
-        assertThrows(IllegalArgumentException.class, () -> {
-            a.reportActualWorkDuration2(new WorkDuration[] {new WorkDuration(1L, 11L, 0L, 4L, 1L)});
-        });
-
-        assertThrows(IllegalArgumentException.class, () -> {
-            a.reportActualWorkDuration2(
-                    new WorkDuration[] {new WorkDuration(1L, 11L, 8L, -1L, 1L)});
-        });
-
-        reset(mNativeWrapperMock);
-        // Set session to background, then the duration would not be updated.
-        service.mUidObserver.onUidStateChanged(
-                a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
-
-        // Using CountDownLatch to ensure above onUidStateChanged() job was digested.
-        final CountDownLatch latch = new CountDownLatch(1);
-        FgThread.getHandler().post(() -> {
-            latch.countDown();
-        });
-        latch.await();
-
-        assertFalse(service.mUidObserver.isUidForeground(a.mUid));
-        a.reportActualWorkDuration2(WORK_DURATIONS_THREE);
-        verify(mNativeWrapperMock, never()).halReportActualWorkDuration(anyLong(), any(), any());
-    }
 }