Merge "Fix broken documentation in accessibility" into main
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index baed7f9..39800f7 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -637,15 +637,15 @@
         /**
          * Specifies a component name to be exempt from the current activity launch policy.
          *
-         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVIY} allows activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT},
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
          * then the specified component will be blocked from launching.
-         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}, then
-         * the specified component will be allowed to launch.</p>
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
+         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the
+         * specified component will be allowed to launch.</p>
          *
-         * <p>Note that changing the activity launch policy will not affect current set of exempt
-         * components and it needs to be updated separately.</p>
+         * <p>Note that changing the activity launch policy will clear current set of exempt
+         * components.</p>
          *
          * @see #removeActivityPolicyExemption
          * @see #setDevicePolicy
@@ -660,15 +660,15 @@
         /**
          * Makes the specified component name to adhere to the default activity launch policy.
          *
-         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVIY} allows activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT},
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
          * then the specified component will be allowed to launch.
-         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity
-         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}, then
-         * the specified component will be blocked from launching.</p>
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
+         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the
+         * specified component will be blocked from launching.</p>
          *
-         * <p>Note that changing the activity launch policy will not affect current set of exempt
-         * components and it needs to be updated separately.</p>
+         * <p>Note that changing the activity launch policy will clear current set of exempt
+         * components.</p>
          *
          * @see #addActivityPolicyExemption
          * @see #setDevicePolicy
diff --git a/core/java/android/service/controls/Control.java b/core/java/android/service/controls/Control.java
index 3b757d6..33978be 100644
--- a/core/java/android/service/controls/Control.java
+++ b/core/java/android/service/controls/Control.java
@@ -50,7 +50,7 @@
  * and zone. Some of these values are defined by the user and/or the {@link ControlsProviderService}
  * and will be used to display the control as well as group them for management.
  * <p>
- * Each object will have an associated {@link DeviceTypes.DeviceType}. This will determine the icons and colors
+ * Each object will have an associated {@link DeviceTypes}. This will determine the icons and colors
  * used to display it.
  * <p>
  * An {@link Intent} linking to the provider Activity that expands on this {@link Control} and
@@ -420,7 +420,7 @@
      * This fixes the values relating to state of the {@link Control} as required by
      * {@link ControlsProviderService#createPublisherForAllAvailable}:
      * <ul>
-     *     <li> Status: {@link Status#STATUS_UNKNOWN}
+     *     <li> Status: {@link #STATUS_UNKNOWN}
      *     <li> Control template: {@link ControlTemplate#getNoTemplateObject}
      *     <li> Status text: {@code ""}
      *     <li> Auth Required: {@code true}
@@ -620,7 +620,7 @@
      *     <li> Device type: {@link DeviceTypes#TYPE_UNKNOWN}
      *     <li> Title: {@code ""}
      *     <li> Subtitle: {@code ""}
-     *     <li> Status: {@link Status#STATUS_UNKNOWN}
+     *     <li> Status: {@link #STATUS_UNKNOWN}
      *     <li> Control template: {@link ControlTemplate#getNoTemplateObject}
      *     <li> Status text: {@code ""}
      *     <li> Auth Required: {@code true}
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index fce87db..0272bb9 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -155,7 +155,7 @@
      * The user has interacted with a Control. The action is dictated by the type of
      * {@link ControlAction} that was sent. A response can be sent via
      * {@link Consumer#accept}, with the Integer argument being one of the provided
-     * {@link ControlAction.ResponseResult}. The Integer should indicate whether the action
+     * {@link ControlAction} response results. The Integer should indicate whether the action
      * was received successfully, or if additional prompts should be presented to
      * the user. Any visual control updates should be sent via the Publisher.
 
diff --git a/core/java/android/service/controls/actions/ControlAction.java b/core/java/android/service/controls/actions/ControlAction.java
index 10f526d..4e38222 100644
--- a/core/java/android/service/controls/actions/ControlAction.java
+++ b/core/java/android/service/controls/actions/ControlAction.java
@@ -154,7 +154,7 @@
     public static final @ResponseResult int RESPONSE_CHALLENGE_PASSPHRASE = 5;
 
     /**
-     * The {@link ActionType} associated with this class.
+     * The action type associated with this class.
      */
     public abstract @ActionType int getActionType();
 
diff --git a/core/java/android/service/controls/templates/ControlTemplate.java b/core/java/android/service/controls/templates/ControlTemplate.java
index 3902d6a..0dd950d 100644
--- a/core/java/android/service/controls/templates/ControlTemplate.java
+++ b/core/java/android/service/controls/templates/ControlTemplate.java
@@ -137,7 +137,7 @@
     }
 
     /**
-     * The {@link TemplateType} associated with this class.
+     * The template type associated with this class.
      */
     public abstract @TemplateType int getTemplateType();
 
diff --git a/core/java/android/service/watchdog/ExplicitHealthCheckService.java b/core/java/android/service/watchdog/ExplicitHealthCheckService.java
index 49e00d6..7befbfb 100644
--- a/core/java/android/service/watchdog/ExplicitHealthCheckService.java
+++ b/core/java/android/service/watchdog/ExplicitHealthCheckService.java
@@ -151,7 +151,7 @@
      */
     @NonNull public abstract List<String> onGetRequestedPackages();
 
-    private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
+    private final Handler mHandler = Handler.createAsync(Looper.getMainLooper());
     @Nullable private RemoteCallback mCallback;
 
     @Override
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index cdf5eec3..70e1896 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -2197,6 +2197,9 @@
             float xOffset, float yOffset, float xPrecision, float yPrecision,
             long downTimeNanos, long eventTimeNanos,
             int pointerCount, PointerProperties[] pointerIds, PointerCoords[] pointerCoords) {
+        if (action == ACTION_CANCEL) {
+            flags |= FLAG_CANCELED;
+        }
         mNativePtr = nativeInitialize(mNativePtr, deviceId, source, displayId, action, flags,
                 edgeFlags, metaState, buttonState, classification, xOffset, yOffset,
                 xPrecision, yPrecision, downTimeNanos, eventTimeNanos, pointerCount, pointerIds,
@@ -2387,6 +2390,11 @@
         nativeSetFlags(mNativePtr, tainted ? flags | FLAG_TAINTED : flags & ~FLAG_TAINTED);
     }
 
+    private void setCanceled(boolean canceled) {
+        final int flags = getFlags();
+        nativeSetFlags(mNativePtr, canceled ? flags | FLAG_CANCELED : flags & ~FLAG_CANCELED);
+    }
+
     /** @hide */
     public  boolean isTargetAccessibilityFocus() {
         final int flags = getFlags();
@@ -3510,6 +3518,14 @@
      * Sets this event's action.
      */
     public final void setAction(int action) {
+        final int actionMasked = action & ACTION_MASK;
+        if (actionMasked == ACTION_CANCEL) {
+            setCanceled(true);
+        } else if (actionMasked == ACTION_POINTER_UP) {
+            // Do nothing - we don't know what the real intent here is
+        } else {
+            setCanceled(false);
+        }
         nativeSetAction(mNativePtr, action);
     }
 
@@ -4157,6 +4173,7 @@
     /** @hide */
     @Override
     public final void cancel() {
+        setCanceled(true);
         setAction(ACTION_CANCEL);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
index 67dc642..e1dea3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
@@ -25,7 +25,8 @@
     private var _isBubbleBarEnabled =
             SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
 
-    override val isBubbleBarEnabled = _isBubbleBarEnabled
+    override val isBubbleBarEnabled
+        get() = _isBubbleBarEnabled
 
     override fun refresh() {
         _isBubbleBarEnabled = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 1943b34..67531ad 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -30,6 +30,8 @@
 import com.android.systemui.log.table.TableLogBuffer;
 import com.android.systemui.log.table.TableLogBufferFactory;
 import com.android.systemui.qs.QSFragmentLegacy;
+import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository;
+import com.android.systemui.qs.pipeline.shared.TileSpec;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.wakelock.WakeLockLog;
@@ -37,6 +39,9 @@
 import dagger.Module;
 import dagger.Provides;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Dagger module for providing instances of {@link LogBuffer}.
  */
@@ -173,8 +178,35 @@
     @Provides
     @SysUISingleton
     @QSLog
-    public static LogBuffer provideQuickSettingsLogBuffer(LogBufferFactory factory) {
-        return factory.create("QSLog", 700 /* maxSize */, false /* systrace */);
+    public static LogBuffer provideQuickSettingsLogBuffer(
+            LogBufferFactory factory,
+            QSPipelineFlagsRepository flags
+    ) {
+        if (flags.getPipelineTilesEnabled()) {
+            // we use
+            return factory.create("QSLog", 450 /* maxSize */, false /* systrace */);
+        } else {
+            return factory.create("QSLog", 700 /* maxSize */, false /* systrace */);
+        }
+    }
+
+    /**
+     * Provides a logging buffer for all logs related to Quick Settings tiles. This LogBuffer is
+     * unique for each tile.
+     * go/qs-tile-refactor
+     */
+    @Provides
+    @QSTilesDefaultLog
+    public static LogBuffer provideQuickSettingsTilesLogBuffer(LogBufferFactory factory) {
+        return factory.create("QSTileLog", 25 /* maxSize */, false /* systrace */);
+    }
+
+    @Provides
+    @QSTilesLogBuffers
+    public static Map<TileSpec, LogBuffer> provideQuickSettingsTilesLogBufferCache() {
+        final Map<TileSpec, LogBuffer> buffers = new HashMap<>();
+        // Add chatty buffers here
+        return buffers;
     }
 
     /** Provides a logging buffer for logs related to Quick Settings configuration. */
@@ -420,7 +452,7 @@
 
     /**
      * Provides a {@link LogBuffer} for use by
-     *  {@link com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepositoryImpl}.
+     * {@link com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepositoryImpl}.
      */
     @Provides
     @SysUISingleton
@@ -431,7 +463,7 @@
 
     /**
      * Provides a {@link LogBuffer} for use by classes in the
-     *  {@link com.android.systemui.keyguard.bouncer} package.
+     * {@link com.android.systemui.keyguard.bouncer} package.
      */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesDefaultLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesDefaultLog.kt
new file mode 100644
index 0000000..6575cdd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesDefaultLog.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * A default [com.android.systemui.log.LogBuffer] for QS tiles messages. It's used exclusively in
+ * [com.android.systemui.qs.tiles.base.logging.QSTileLogger]. If you need to increase it for you
+ * tile, add one to the map provided by the [QSTilesLogBuffers]
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class QSTilesDefaultLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesLogBuffers.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesLogBuffers.kt
new file mode 100644
index 0000000..62d49fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesLogBuffers.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * Provides a map with custom [com.android.systemui.log.LogBuffer] for QS tiles messages. Add
+ * buffers to it when the tile needs to be more verbose and the default buffer provided by
+ * [QSTilesDefaultLog] is not enough.
+ *
+ * This is not a multibinding. Add new logs directly to [LogModule]
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class QSTilesLogBuffers
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesVerboseLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesVerboseLog.java
new file mode 100644
index 0000000..b0c2f8c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesVerboseLog.java
@@ -0,0 +1,36 @@
+/*
+ * 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.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for QS tiles messages. It's used exclusively in
+ * {@link com.android.systemui.qs.tiles.base.logging.QSTileLogger}
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface QSTilesVerboseLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 19012e2..fa18b35b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -27,11 +27,11 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSPanel.QSTileLayout;
 import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.res.R;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -562,6 +562,12 @@
         if (shouldNotRunAnimation(tilesToReveal)) {
             return;
         }
+        // This method has side effects (beings the fake drag, if it returns true). If we have
+        // decided that we want to do a tile reveal, we do a last check to verify that we can
+        // actually perform a fake drag.
+        if (!beginFakeDrag()) {
+            return;
+        }
 
         final int lastPageNumber = mPages.size() - 1;
         final TileLayout lastPage = mPages.get(lastPageNumber);
@@ -596,8 +602,10 @@
     }
 
     private boolean shouldNotRunAnimation(Set<String> tilesToReveal) {
+        // None of these have side effects. That way, we don't need to rely on short-circuiting
+        // behavior
         boolean noAnimationNeeded = tilesToReveal.isEmpty() || mPages.size() < 2;
-        boolean scrollingInProgress = getScrollX() != 0 || !beginFakeDrag();
+        boolean scrollingInProgress = getScrollX() != 0 || !isFakeDragging();
         // checking mRunningInTestHarness to disable animation in functional testing as it caused
         // flakiness and is not needed there. Alternative solutions were more complex and would
         // still be either potentially flaky or modify internal data.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index 128c237..051eeb0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -72,7 +72,8 @@
 import dagger.assisted.AssistedFactory;
 import dagger.assisted.AssistedInject;
 
-public class CustomTile extends QSTileImpl<State> implements TileChangeListener {
+public class CustomTile extends QSTileImpl<State> implements TileChangeListener,
+        CustomTileInterface {
     public static final String PREFIX = "custom(";
 
     private static final long CUSTOM_STALE_TIMEOUT = DateUtils.HOUR_IN_MILLIS;
@@ -181,7 +182,8 @@
     private void updateDefaultTileAndIcon() {
         try {
             PackageManager pm = mUserContext.getPackageManager();
-            int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE;
+            int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                    | PackageManager.MATCH_DIRECT_BOOT_AWARE;
             if (isSystemApp(pm)) {
                 flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
             }
@@ -213,7 +215,7 @@
      * Compare two icons, only works for resources.
      */
     private boolean iconEquals(@Nullable android.graphics.drawable.Icon icon1,
-                               @Nullable android.graphics.drawable.Icon icon2) {
+            @Nullable android.graphics.drawable.Icon icon2) {
         if (icon1 == icon2) {
             return true;
         }
@@ -252,10 +254,12 @@
         }
     }
 
+    @Override
     public int getUser() {
         return mUser;
     }
 
+    @Override
     public ComponentName getComponent() {
         return mComponent;
     }
@@ -265,6 +269,7 @@
         return super.populate(logMaker).setComponentName(mComponent);
     }
 
+    @Override
     public Tile getQsTile() {
         // TODO(b/191145007) Move to background thread safely
         updateDefaultTileAndIcon();
@@ -276,6 +281,7 @@
      *
      * @param tile tile populated with state to apply
      */
+    @Override
     public void updateTileState(Tile tile, int appUid) {
         mServiceUid = appUid;
         // This comes from a binder call IQSService.updateQsTile
@@ -310,10 +316,12 @@
         mTile.setState(tile.getState());
     }
 
+    @Override
     public void onDialogShown() {
         mIsShowingDialog = true;
     }
 
+    @Override
     public void onDialogHidden() {
         mIsShowingDialog = false;
         try {
@@ -507,6 +515,7 @@
         return mComponent.getPackageName();
     }
 
+    @Override
     public void startUnlockAndRun() {
         mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
             try {
@@ -518,8 +527,10 @@
 
     /**
      * Starts an {@link android.app.Activity}
+     *
      * @param pendingIntent A PendingIntent for an Activity to be launched immediately.
      */
+    @Override
     public void startActivityAndCollapse(PendingIntent pendingIntent) {
         if (!pendingIntent.isActivity()) {
             Log.i(TAG, "Intent not for activity.");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileInterface.kt b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileInterface.kt
new file mode 100644
index 0000000..9e02320
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileInterface.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.qs.external
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.service.quicksettings.Tile
+
+interface CustomTileInterface {
+
+    val user: Int
+    val qsTile: Tile
+    val component: ComponentName
+
+    fun getTileSpec(): String
+
+    fun refreshState()
+    fun updateTileState(tile: Tile, uid: Int)
+
+    fun onDialogShown()
+    fun onDialogHidden()
+
+    fun startActivityAndCollapse(pendingIntent: PendingIntent)
+
+    fun startUnlockAndRun()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index fc24022..acee8e9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -67,9 +67,10 @@
     static final int REDUCED_MAX_BOUND = 1;
     private static final String TAG = "TileServices";
 
-    private final ArrayMap<CustomTile, TileServiceManager> mServices = new ArrayMap<>();
-    private final SparseArrayMap<ComponentName, CustomTile> mTiles = new SparseArrayMap<>();
-    private final ArrayMap<IBinder, CustomTile> mTokenMap = new ArrayMap<>();
+    private final ArrayMap<CustomTileInterface, TileServiceManager> mServices = new ArrayMap<>();
+    private final SparseArrayMap<ComponentName, CustomTileInterface> mTiles =
+            new SparseArrayMap<>();
+    private final ArrayMap<IBinder, CustomTileInterface> mTokenMap = new ArrayMap<>();
     private final Context mContext;
     private final Handler mMainHandler;
     private final Provider<Handler> mHandlerProvider;
@@ -120,7 +121,7 @@
         return mHost;
     }
 
-    public TileServiceManager getTileWrapper(CustomTile tile) {
+    public TileServiceManager getTileWrapper(CustomTileInterface tile) {
         ComponentName component = tile.getComponent();
         int userId = tile.getUser();
         TileServiceManager service = onCreateTileService(component, mBroadcastDispatcher);
@@ -140,7 +141,7 @@
                 broadcastDispatcher, mUserTracker, mCustomTileAddedRepository, mBackgroundExecutor);
     }
 
-    public void freeService(CustomTile tile, TileServiceManager service) {
+    public void freeService(CustomTileInterface tile, TileServiceManager service) {
         synchronized (mServices) {
             service.setBindAllowed(false);
             service.handleDestroy();
@@ -184,7 +185,7 @@
         }
     }
 
-    private int verifyCaller(CustomTile tile) {
+    private int verifyCaller(CustomTileInterface tile) {
         try {
             String packageName = tile.getComponent().getPackageName();
             int uid = mContext.getPackageManager().getPackageUidAsUser(packageName,
@@ -201,7 +202,7 @@
     private void requestListening(ComponentName component) {
         synchronized (mServices) {
             int userId = mUserTracker.getUserId();
-            CustomTile customTile = getTileForUserAndComponent(userId, component);
+            CustomTileInterface customTile = getTileForUserAndComponent(userId, component);
             if (customTile == null) {
                 Log.d(TAG, "Couldn't find tile for " + component + "(" + userId + ")");
                 return;
@@ -227,7 +228,7 @@
 
     @Override
     public void updateQsTile(Tile tile, IBinder token) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             int uid = verifyCaller(customTile);
             synchronized (mServices) {
@@ -247,7 +248,7 @@
 
     @Override
     public void onStartSuccessful(IBinder token) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             verifyCaller(customTile);
             synchronized (mServices) {
@@ -267,7 +268,7 @@
 
     @Override
     public void onShowDialog(IBinder token) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             verifyCaller(customTile);
             customTile.onDialogShown();
@@ -278,7 +279,7 @@
 
     @Override
     public void onDialogHidden(IBinder token) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             verifyCaller(customTile);
             Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(false);
@@ -288,7 +289,7 @@
 
     @Override
     public void onStartActivity(IBinder token) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             verifyCaller(customTile);
             mPanelInteractor.forceCollapsePanels();
@@ -301,7 +302,7 @@
     }
 
     @VisibleForTesting
-    protected void startActivity(CustomTile customTile, PendingIntent pendingIntent) {
+    protected void startActivity(CustomTileInterface customTile, PendingIntent pendingIntent) {
         if (customTile != null) {
             verifyCaller(customTile);
             customTile.startActivityAndCollapse(pendingIntent);
@@ -310,7 +311,7 @@
 
     @Override
     public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             verifyCaller(customTile);
             try {
@@ -340,7 +341,7 @@
     @Nullable
     @Override
     public Tile getTile(IBinder token) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             verifyCaller(customTile);
             return customTile.getQsTile();
@@ -367,7 +368,7 @@
 
     @Override
     public void startUnlockAndRun(IBinder token) {
-        CustomTile customTile = getTileForToken(token);
+        CustomTileInterface customTile = getTileForToken(token);
         if (customTile != null) {
             verifyCaller(customTile);
             customTile.startUnlockAndRun();
@@ -385,14 +386,14 @@
     }
 
     @Nullable
-    public CustomTile getTileForToken(IBinder token) {
+    public CustomTileInterface getTileForToken(IBinder token) {
         synchronized (mServices) {
             return mTokenMap.get(token);
         }
     }
 
     @Nullable
-    private CustomTile getTileForUserAndComponent(int userId, ComponentName component) {
+    private CustomTileInterface getTileForUserAndComponent(int userId, ComponentName component) {
         synchronized (mServices) {
             return mTiles.get(userId, component);
         }
@@ -419,11 +420,6 @@
     };
 
     private static final Comparator<TileServiceManager> SERVICE_SORT =
-            new Comparator<TileServiceManager>() {
-        @Override
-        public int compare(TileServiceManager left, TileServiceManager right) {
-            return -Integer.compare(left.getBindPriority(), right.getBindPriority());
-        }
-    };
+            (left, right) -> -Integer.compare(left.getBindPriority(), right.getBindPriority());
 
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt
index 38fe34e..42d3f81 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt
@@ -18,8 +18,6 @@
 
 import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
 import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl
-import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
-import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl
 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl
 import dagger.Binds
@@ -28,7 +26,6 @@
 /** Dagger module to provide/bind footer actions singletons. */
 @Module
 interface FooterActionsModule {
-    @Binds fun userSwitcherRepository(impl: UserSwitcherRepositoryImpl): UserSwitcherRepository
 
     @Binds
     fun foregroundServicesRepository(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index 8b2c3de..c91ed13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
@@ -38,10 +38,10 @@
 import com.android.systemui.qs.QSSecurityFooterUtils
 import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
 import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
-import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
 import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
 import com.android.systemui.security.data.repository.SecurityRepository
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.UserSwitcherRepository
 import com.android.systemui.user.domain.interactor.UserInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt
new file mode 100644
index 0000000..0d15a5b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.qs.tiles.base.analytics
+
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.QSEvent
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Tracks QS tiles analytic events to [UiEventLogger]. */
+@SysUISingleton
+class QSTileAnalytics
+@Inject
+constructor(
+    private val uiEventLogger: UiEventLogger,
+) {
+
+    fun trackUserAction(config: QSTileConfig, action: QSTileUserAction) {
+        logAction(config, action)
+    }
+
+    private fun logAction(config: QSTileConfig, action: QSTileUserAction) {
+        uiEventLogger.logWithInstanceId(
+            action.getQSEvent(),
+            0,
+            config.metricsSpec,
+            config.instanceId,
+        )
+    }
+
+    private fun QSTileUserAction.getQSEvent(): QSEvent =
+        when (this) {
+            is QSTileUserAction.Click -> QSEvent.QS_ACTION_CLICK
+            is QSTileUserAction.LongClick -> QSEvent.QS_ACTION_LONG_PRESS
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
new file mode 100644
index 0000000..70a683b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.qs.tiles.base.logging
+
+import androidx.annotation.GuardedBy
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.QSTilesDefaultLog
+import com.android.systemui.log.dagger.QSTilesLogBuffers
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.StatusBarState
+import javax.inject.Inject
+import javax.inject.Provider
+
+@SysUISingleton
+class QSTileLogger
+@Inject
+constructor(
+    @QSTilesLogBuffers logBuffers: Map<TileSpec, LogBuffer>,
+    @QSTilesDefaultLog private val defaultLogBufferProvider: Provider<LogBuffer>,
+    private val mStatusBarStateController: StatusBarStateController,
+) {
+    @GuardedBy("logBufferCache") private val logBufferCache = logBuffers.toMutableMap()
+
+    /**
+     * Tracks user action when it's first received by the ViewModel and before it reaches the
+     * pipeline
+     */
+    fun logUserAction(
+        userAction: QSTileUserAction,
+        tileSpec: TileSpec,
+        hasData: Boolean,
+        hasTileState: Boolean,
+    ) {
+        tileSpec
+            .getLogBuffer()
+            .log(
+                tileSpec.getLogTag(),
+                LogLevel.DEBUG,
+                {
+                    str1 = userAction.toLogString()
+                    int1 = mStatusBarStateController.state
+                    bool1 = hasTileState
+                    bool2 = hasData
+                },
+                {
+                    "tile $str1: " +
+                        "statusBarState=${StatusBarState.toString(int1)}, " +
+                        "hasState=$bool1, " +
+                        "hasData=$bool2"
+                }
+            )
+    }
+
+    /** Tracks user action when it's rejected by false gestures */
+    fun logUserActionRejectedByFalsing(
+        userAction: QSTileUserAction,
+        tileSpec: TileSpec,
+    ) {
+        tileSpec
+            .getLogBuffer()
+            .log(
+                tileSpec.getLogTag(),
+                LogLevel.DEBUG,
+                { str1 = userAction.toLogString() },
+                { "tile $str1: rejected by falsing" }
+            )
+    }
+
+    /** Tracks user action when it's rejected according to the policy */
+    fun logUserActionRejectedByPolicy(
+        userAction: QSTileUserAction,
+        tileSpec: TileSpec,
+    ) {
+        tileSpec
+            .getLogBuffer()
+            .log(
+                tileSpec.getLogTag(),
+                LogLevel.DEBUG,
+                { str1 = userAction.toLogString() },
+                { "tile $str1: rejected by policy" }
+            )
+    }
+
+    /**
+     * Tracks user actions when it reaches the pipeline and mixes with the last tile state and data
+     */
+    fun <T> logUserActionPipeline(
+        tileSpec: TileSpec,
+        userAction: QSTileUserAction,
+        tileState: QSTileState,
+        data: T,
+    ) {
+        tileSpec
+            .getLogBuffer()
+            .log(
+                tileSpec.getLogTag(),
+                LogLevel.DEBUG,
+                {
+                    str1 = userAction.toLogString()
+                    str2 = tileState.toLogString()
+                    str3 = data.toString().take(DATA_MAX_LENGTH)
+                },
+                {
+                    "tile $str1 pipeline: " +
+                        "statusBarState=${StatusBarState.toString(int1)}, " +
+                        "state=$str2, " +
+                        "data=$str3"
+                }
+            )
+    }
+
+    /** Tracks state changes based on the data and trigger event. */
+    fun <T> logStateUpdate(
+        tileSpec: TileSpec,
+        trigger: StateUpdateTrigger,
+        tileState: QSTileState,
+        data: T,
+    ) {
+        tileSpec
+            .getLogBuffer()
+            .log(
+                tileSpec.getLogTag(),
+                LogLevel.DEBUG,
+                {
+                    str1 = trigger.toLogString()
+                    str2 = tileState.toLogString()
+                    str3 = data.toString().take(DATA_MAX_LENGTH)
+                },
+                { "tile state update: trigger=$str1, state=$str2, data=$str3" }
+            )
+    }
+
+    private fun TileSpec.getLogTag(): String = "${TAG_FORMAT_PREFIX}_${this.spec}"
+
+    private fun TileSpec.getLogBuffer(): LogBuffer =
+        synchronized(logBufferCache) {
+            logBufferCache.getOrPut(this) { defaultLogBufferProvider.get() }
+        }
+
+    private fun StateUpdateTrigger.toLogString(): String =
+        when (this) {
+            is StateUpdateTrigger.ForceUpdate -> "force"
+            is StateUpdateTrigger.InitialRequest -> "init"
+            is StateUpdateTrigger.UserAction<*> -> action.toLogString()
+        }
+
+    private fun QSTileUserAction.toLogString(): String =
+        when (this) {
+            is QSTileUserAction.Click -> "click"
+            is QSTileUserAction.LongClick -> "long click"
+        }
+
+    /* Shortened version of a data class toString() */
+    private fun QSTileState.toLogString(): String =
+        "[label=$label, " +
+            "state=$activationState, " +
+            "s_label=$secondaryLabel, " +
+            "cd=$contentDescription, " +
+            "sd=$stateDescription, " +
+            "svi=$sideViewIcon, " +
+            "enabled=$enabledState, " +
+            "a11y=$expandedAccessibilityClassName" +
+            "]"
+
+    private companion object {
+        const val TAG_FORMAT_PREFIX = "QSLog"
+        const val DATA_MAX_LENGTH = 50
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
index 58a335e..2114751 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
@@ -20,12 +20,15 @@
 import androidx.annotation.VisibleForTesting
 import com.android.internal.util.Preconditions
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataRequest
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
 import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle
 import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
@@ -33,6 +36,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
 import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.kotlin.throttle
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -70,6 +74,9 @@
     private val tileDataInteractor: QSTileDataInteractor<DATA_TYPE>,
     private val mapper: QSTileDataToStateMapper<DATA_TYPE>,
     private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
+    private val falsingManager: FalsingManager,
+    private val qsTileAnalytics: QSTileAnalytics,
+    private val qsTileLogger: QSTileLogger,
     private val backgroundDispatcher: CoroutineDispatcher,
     private val tileScope: CoroutineScope,
 ) : QSTileViewModel {
@@ -81,6 +88,9 @@
         @Assisted tileDataInteractor: QSTileDataInteractor<DATA_TYPE>,
         @Assisted mapper: QSTileDataToStateMapper<DATA_TYPE>,
         disabledByPolicyInteractor: DisabledByPolicyInteractor,
+        falsingManager: FalsingManager,
+        qsTileAnalytics: QSTileAnalytics,
+        qsTileLogger: QSTileLogger,
         @Background backgroundDispatcher: CoroutineDispatcher,
     ) : this(
         config,
@@ -88,6 +98,9 @@
         tileDataInteractor,
         mapper,
         disabledByPolicyInteractor,
+        falsingManager,
+        qsTileAnalytics,
+        qsTileLogger,
         backgroundDispatcher,
         CoroutineScope(SupervisorJob())
     )
@@ -98,8 +111,10 @@
         MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     private val forceUpdates: MutableSharedFlow<Unit> =
         MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+    private val spec
+        get() = config.tileSpec
 
-    private lateinit var tileData: SharedFlow<DATA_TYPE>
+    private lateinit var tileData: SharedFlow<DataWithTrigger<DATA_TYPE>>
 
     override lateinit var state: SharedFlow<QSTileState>
     override val isAvailable: StateFlow<Boolean> =
@@ -128,8 +143,14 @@
 
     @CallSuper
     override fun onActionPerformed(userAction: QSTileUserAction) {
-        Preconditions.checkState(tileData.replayCache.isNotEmpty())
         Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
+
+        qsTileLogger.logUserAction(
+            userAction,
+            spec,
+            tileData.replayCache.isNotEmpty(),
+            state.replayCache.isNotEmpty()
+        )
         userInputs.tryEmit(userAction)
     }
 
@@ -142,7 +163,16 @@
                 state =
                     tileData
                         // TODO(b/299908705): log data and corresponding tile state
-                        .map { mapper.map(config, it) }
+                        .map { dataWithTrigger ->
+                            mapper.map(config, dataWithTrigger.data).also { state ->
+                                qsTileLogger.logStateUpdate(
+                                    spec,
+                                    dataWithTrigger.trigger,
+                                    state,
+                                    dataWithTrigger.data
+                                )
+                            }
+                        }
                         .flowOn(backgroundDispatcher)
                         .shareIn(
                             tileScope,
@@ -158,7 +188,7 @@
         currentLifeState = lifecycle
     }
 
-    private fun createTileDataFlow(): SharedFlow<DATA_TYPE> =
+    private fun createTileDataFlow(): SharedFlow<DataWithTrigger<DATA_TYPE>> =
         userIds
             .flatMapLatest { userId ->
                 merge(
@@ -180,7 +210,7 @@
                         request.trigger.tileData as DATA_TYPE,
                     )
                 }
-                dataFlow
+                dataFlow.map { DataWithTrigger(it, request.trigger) }
             }
             .flowOn(backgroundDispatcher)
             .shareIn(
@@ -193,21 +223,53 @@
         data class StateWithData<T>(val state: QSTileState, val data: T)
 
         return when (config.policy) {
-            is QSTilePolicy.NoRestrictions -> userInputs
-            is QSTilePolicy.Restricted ->
-                userInputs.filter {
-                    val result =
-                        disabledByPolicyInteractor.isDisabled(userId, config.policy.userRestriction)
-                    !disabledByPolicyInteractor.handlePolicyResult(result)
+                is QSTilePolicy.NoRestrictions -> userInputs
+                is QSTilePolicy.Restricted ->
+                    userInputs.filter { action ->
+                        val result =
+                            disabledByPolicyInteractor.isDisabled(
+                                userId,
+                                config.policy.userRestriction
+                            )
+                        !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled ->
+                            if (isDisabled) {
+                                qsTileLogger.logUserActionRejectedByPolicy(action, spec)
+                            }
+                        }
+                    }
+            }
+            .filter { action ->
+                val isFalseAction =
+                    when (action) {
+                        is QSTileUserAction.Click ->
+                            falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
+                        is QSTileUserAction.LongClick ->
+                            falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
+                    }
+                if (isFalseAction) {
+                    qsTileLogger.logUserActionRejectedByFalsing(action, spec)
                 }
-        // Skip the input until there is some data
-        }.sample(state.combine(tileData) { state, data -> StateWithData(state, data) }) {
-            input,
-            stateWithData ->
-            StateUpdateTrigger.UserAction(input, stateWithData.state, stateWithData.data)
-        }
+                !isFalseAction
+            }
+            .throttle(500)
+            // Skip the input until there is some data
+            .sample(state.combine(tileData) { state, data -> StateWithData(state, data) }) {
+                input,
+                stateWithData ->
+                StateUpdateTrigger.UserAction(input, stateWithData.state, stateWithData.data).also {
+                    qsTileLogger.logUserActionPipeline(
+                        spec,
+                        it.action,
+                        stateWithData.state,
+                        stateWithData.data
+                    )
+                    qsTileAnalytics.trackUserAction(config, it.action)
+                }
+            }
     }
 
+    private data class DataWithTrigger<T>(val data: T, val trigger: StateUpdateTrigger)
+
     interface Factory<T> {
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index 1a6cf99..4a3bcae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -26,6 +26,7 @@
     val tileIcon: Icon,
     @StringRes val tileLabelRes: Int,
     val instanceId: InstanceId,
+    val metricsSpec: String = tileSpec.spec,
     val policy: QSTilePolicy = QSTilePolicy.NoRestrictions,
 )
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 249c831..e2de37f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -18,6 +18,8 @@
 
 import com.android.systemui.CoreStartable
 import com.android.systemui.statusbar.core.StatusBarInitializer
+import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepository
+import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepositoryImpl
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepository
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryImpl
 import com.android.systemui.statusbar.phone.LightBarController
@@ -49,6 +51,11 @@
     abstract fun bindStatusBarModeRepositoryStart(impl: StatusBarModeRepositoryImpl): CoreStartable
 
     @Binds
+    abstract fun bindKeyguardStatusBarRepository(
+        impl: KeyguardStatusBarRepositoryImpl
+    ): KeyguardStatusBarRepository
+
+    @Binds
     @IntoMap
     @ClassKey(OngoingCallController::class)
     abstract fun bindOngoingCallController(impl: OngoingCallController): CoreStartable
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt
new file mode 100644
index 0000000..8136de9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.statusbar.data.repository
+
+import android.content.Context
+import com.android.internal.R
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.user.data.repository.UserSwitcherRepository
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/**
+ * Repository for data that's specific to the status bar **on keyguard**. For data that applies to
+ * all status bars, use [StatusBarModeRepository].
+ */
+interface KeyguardStatusBarRepository {
+    /** True if we can show the user switcher on keyguard and false otherwise. */
+    val isKeyguardUserSwitcherEnabled: Flow<Boolean>
+}
+
+@SysUISingleton
+class KeyguardStatusBarRepositoryImpl
+@Inject
+constructor(
+    context: Context,
+    configurationController: ConfigurationController,
+    userSwitcherRepository: UserSwitcherRepository,
+) : KeyguardStatusBarRepository {
+    private val relevantConfigChanges: Flow<Unit> =
+        ConflatedCallbackFlow.conflatedCallbackFlow {
+            val callback =
+                object : ConfigurationController.ConfigurationListener {
+                    override fun onSmallestScreenWidthChanged() {
+                        trySend(Unit)
+                    }
+
+                    override fun onDensityOrFontScaleChanged() {
+                        trySend(Unit)
+                    }
+                }
+            configurationController.addCallback(callback)
+            awaitClose { configurationController.removeCallback(callback) }
+        }
+
+    private val isKeyguardUserSwitcherConfigEnabled: Flow<Boolean> =
+        // The config depends on screen size and user enabled settings, so re-fetch whenever any of
+        // those change.
+        merge(userSwitcherRepository.isEnabled.map {}, relevantConfigChanges).map {
+            context.resources.getBoolean(R.bool.config_keyguardUserSwitcher)
+        }
+
+    /** True if we can show the user switcher on keyguard and false otherwise. */
+    override val isKeyguardUserSwitcherEnabled: Flow<Boolean> =
+        combine(
+            userSwitcherRepository.isEnabled,
+            isKeyguardUserSwitcherConfigEnabled,
+        ) { isEnabled, isKeyguardEnabled ->
+            isEnabled && isKeyguardEnabled
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractor.kt
new file mode 100644
index 0000000..e0c30e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractor.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.statusbar.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class KeyguardStatusBarInteractor
+@Inject
+constructor(
+    keyguardStatusBarRepository: KeyguardStatusBarRepository,
+) {
+    /** True if we can show the user switcher on keyguard and false otherwise. */
+    val isKeyguardUserSwitcherEnabled: Flow<Boolean> =
+        keyguardStatusBarRepository.isKeyguardUserSwitcherEnabled
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 7efa705..58126ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -43,9 +43,9 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.settingslib.Utils;
-import com.android.systemui.res.R;
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
 import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
@@ -367,15 +367,22 @@
         mMultiUserAvatar.setImageDrawable(picture);
     }
 
-    /** Should only be called from {@link KeyguardStatusBarViewController}. */
-    void onBatteryLevelChanged(boolean charging) {
+    /**
+     * Should only be called from {@link KeyguardStatusBarViewController} or
+     * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}.
+     */
+    public void onBatteryChargingChanged(boolean charging) {
         if (mBatteryCharging != charging) {
             mBatteryCharging = charging;
             updateVisibilities();
         }
     }
 
-    void setKeyguardUserSwitcherEnabled(boolean enabled) {
+    /**
+     * Should only be called from {@link KeyguardStatusBarViewController} or
+     * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}.
+     */
+    public void setKeyguardUserSwitcherEnabled(boolean enabled) {
         mKeyguardUserSwitcherEnabled = enabled;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 9cf9714..2960520 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -172,7 +172,7 @@
             new BatteryController.BatteryStateChangeCallback() {
                 @Override
                 public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
-                    mView.onBatteryLevelChanged(charging);
+                    mView.onBatteryChargingChanged(charging);
                 }
             };
 
@@ -430,11 +430,18 @@
 
     /** Sets whether user switcher is enabled. */
     public void setKeyguardUserSwitcherEnabled(boolean enabled) {
+        if (isMigrationEnabled()) {
+            return;
+        }
         mView.setKeyguardUserSwitcherEnabled(enabled);
     }
 
     /** Sets whether this controller should listen to battery updates. */
     public void setBatteryListening(boolean listening) {
+        if (isMigrationEnabled()) {
+            return;
+        }
+
         if (listening == mBatteryListening) {
             return;
         }
@@ -472,6 +479,10 @@
 
     /** Animate the keyguard status bar in. */
     public void animateKeyguardStatusBarIn() {
+        if (isMigrationEnabled()) {
+            return;
+        }
+
         mLogger.log(TAG, LogLevel.DEBUG, "animating status bar in");
         if (mDisableStateTracker.isDisabled()) {
             // If our view is disabled, don't allow us to animate in.
@@ -488,6 +499,10 @@
 
     /** Animate the keyguard status bar out. */
     public void animateKeyguardStatusBarOut(long startDelay, long duration) {
+        if (isMigrationEnabled()) {
+            return;
+        }
+
         mLogger.log(TAG, LogLevel.DEBUG, "animating status bar out");
         ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
         anim.addUpdateListener(mAnimatorUpdateListener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt
index c63ef9e..6988e21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt
@@ -22,6 +22,8 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView
 import com.android.systemui.statusbar.ui.viewmodel.KeyguardStatusBarViewModel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
 
 /** Binds [KeyguardStatusBarViewModel] to [KeyguardStatusBarView]. */
 object KeyguardStatusBarViewBinder {
@@ -32,8 +34,18 @@
     ) {
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.isVisible.collect { isVisible ->
-                    view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
+                launch {
+                    viewModel.isVisible.collect { isVisible ->
+                        view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
+                    }
+                }
+
+                launch { viewModel.isBatteryCharging.collect { view.onBatteryChargingChanged(it) } }
+
+                launch {
+                    viewModel.isKeyguardUserSwitcherEnabled.distinctUntilChanged().collect {
+                        view.setKeyguardUserSwitcherEnabled(it)
+                    }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
index ddfed87..5da01e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
@@ -16,12 +16,18 @@
 
 package com.android.systemui.statusbar.ui.viewmodel
 
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
@@ -41,6 +47,8 @@
 constructor(
     @Application scope: CoroutineScope,
     keyguardInteractor: KeyguardInteractor,
+    keyguardStatusBarInteractor: KeyguardStatusBarInteractor,
+    batteryController: BatteryController,
 ) {
     /** True if this view should be visible and false otherwise. */
     val isVisible: StateFlow<Boolean> =
@@ -51,4 +59,26 @@
                 !isDozing && statusBarState == StatusBarState.KEYGUARD
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    /** True if the device's battery is currently charging and false otherwise. */
+    // Note: Never make this an eagerly-started state flow so that the callback is removed when the
+    // keyguard status bar view isn't attached.
+    val isBatteryCharging: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : BatteryStateChangeCallback {
+                override fun onBatteryLevelChanged(
+                    level: Int,
+                    pluggedIn: Boolean,
+                    charging: Boolean,
+                ) {
+                    trySend(charging)
+                }
+            }
+        batteryController.addCallback(callback)
+        awaitClose { batteryController.removeCallback(callback) }
+    }
+
+    /** True if we can show the user switcher on keyguard and false otherwise. */
+    val isKeyguardUserSwitcherEnabled: Flow<Boolean> =
+        keyguardStatusBarInteractor.isKeyguardUserSwitcherEnabled
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt
index 18ae107..71352ef 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt
@@ -23,4 +23,6 @@
 @Module
 interface UserRepositoryModule {
     @Binds fun bindRepository(impl: UserRepositoryImpl): UserRepository
+
+    @Binds fun userSwitcherRepository(impl: UserSwitcherRepositoryImpl): UserSwitcherRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt
rename to packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
index 5fa75ad..dc7fadd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.footer.data.repository
+package com.android.systemui.user.data.repository
 
 import android.content.Context
 import android.graphics.drawable.Drawable
@@ -22,7 +22,6 @@
 import android.os.UserManager
 import android.provider.Settings.Global.USER_SWITCHER_ENABLED
 import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.res.R
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -30,6 +29,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.qs.SettingObserver
 import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
+import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.UserInfoController
 import com.android.systemui.statusbar.policy.UserSwitcherController
@@ -48,6 +48,9 @@
 interface UserSwitcherRepository {
     /** The current [UserSwitcherStatusModel]. */
     val userSwitcherStatus: Flow<UserSwitcherStatusModel>
+
+    /** Whether the user switcher is currently enabled. */
+    val isEnabled: Flow<Boolean>
 }
 
 @SysUISingleton
@@ -66,8 +69,7 @@
     private val showUserSwitcherForSingleUser =
         context.resources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)
 
-    /** Whether the user switcher is currently enabled. */
-    private val isEnabled: Flow<Boolean> = conflatedCallbackFlow {
+    override val isEnabled: Flow<Boolean> = conflatedCallbackFlow {
         suspend fun updateState() {
             trySendWithFailureLogging(isUserSwitcherEnabled(), TAG)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt
new file mode 100644
index 0000000..2c4e10e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.qs.tiles.base.analytics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.QSEvent
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+class QSTileAnalyticsTest : SysuiTestCase() {
+
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+
+    private lateinit var underTest: QSTileAnalytics
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = QSTileAnalytics(uiEventLogger)
+    }
+
+    @Test
+    fun testClickIsLogged() {
+        underTest.trackUserAction(config, QSTileUserAction.Click(null))
+
+        verify(uiEventLogger)
+            .logWithInstanceId(
+                eq(QSEvent.QS_ACTION_CLICK),
+                eq(0),
+                eq("test_spec"),
+                eq(InstanceId.fakeInstanceId(0))
+            )
+    }
+
+    @Test
+    fun testLongClickIsLogged() {
+        underTest.trackUserAction(config, QSTileUserAction.LongClick(null))
+
+        verify(uiEventLogger)
+            .logWithInstanceId(
+                eq(QSEvent.QS_ACTION_LONG_PRESS),
+                eq(0),
+                eq("test_spec"),
+                eq(InstanceId.fakeInstanceId(0))
+            )
+    }
+
+    private companion object {
+
+        val config = QSTileConfigTestBuilder.build()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
new file mode 100644
index 0000000..4401e0d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.qs.tiles.base.logging
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dump.LogcatEchoTrackerAlways
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+class QSTileLoggerTest : SysuiTestCase() {
+
+    @Mock private lateinit var statusBarController: StatusBarStateController
+
+    private val chattyLogBuffer = LogBuffer("TestChatty", 5, LogcatEchoTrackerAlways())
+    private val logBuffer = LogBuffer("Test", 1, LogcatEchoTrackerAlways())
+
+    private lateinit var underTest: QSTileLogger
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            QSTileLogger(
+                mapOf(TileSpec.create("chatty_tile") to chattyLogBuffer),
+                { logBuffer },
+                statusBarController
+            )
+    }
+
+    @Test
+    fun testChattyLog() {
+        underTest.logUserActionRejectedByFalsing(
+            QSTileUserAction.Click(null),
+            TileSpec.create("chatty_tile"),
+        )
+        underTest.logUserActionRejectedByFalsing(
+            QSTileUserAction.Click(null),
+            TileSpec.create("chatty_tile"),
+        )
+
+        val logs = chattyLogBuffer.getStringBuffer().lines().filter { it.isNotBlank() }
+        assertThat(logs).hasSize(2)
+        logs.forEach { assertThat(it).contains("tile click: rejected by falsing") }
+    }
+
+    @Test
+    fun testLogUserAction() {
+        underTest.logUserAction(
+            QSTileUserAction.Click(null),
+            TileSpec.create("test_spec"),
+            hasData = false,
+            hasTileState = false,
+        )
+
+        assertThat(logBuffer.getStringBuffer())
+            .contains("tile click: statusBarState=SHADE, hasState=false, hasData=false")
+    }
+
+    @Test
+    fun testLogUserActionRejectedByFalsing() {
+        underTest.logUserActionRejectedByFalsing(
+            QSTileUserAction.Click(null),
+            TileSpec.create("test_spec"),
+        )
+
+        assertThat(logBuffer.getStringBuffer()).contains("tile click: rejected by falsing")
+    }
+
+    @Test
+    fun testLogUserActionRejectedByPolicy() {
+        underTest.logUserActionRejectedByPolicy(
+            QSTileUserAction.Click(null),
+            TileSpec.create("test_spec"),
+        )
+
+        assertThat(logBuffer.getStringBuffer()).contains("tile click: rejected by policy")
+    }
+
+    @Test
+    fun testLogUserActionPipeline() {
+        underTest.logUserActionPipeline(
+            TileSpec.create("test_spec"),
+            QSTileUserAction.Click(null),
+            QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {},
+            "test_data",
+        )
+
+        assertThat(logBuffer.getStringBuffer())
+            .contains(
+                "tile click pipeline: " +
+                    "statusBarState=SHADE, " +
+                    "state=[" +
+                    "label=, " +
+                    "state=INACTIVE, " +
+                    "s_label=null, " +
+                    "cd=null, " +
+                    "sd=null, " +
+                    "svi=None, " +
+                    "enabled=ENABLED, " +
+                    "a11y=null" +
+                    "], " +
+                    "data=test_data"
+            )
+    }
+
+    @Test
+    fun testLogStateUpdate() {
+        underTest.logStateUpdate(
+            TileSpec.create("test_spec"),
+            StateUpdateTrigger.ForceUpdate,
+            QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {},
+            "test_data",
+        )
+
+        assertThat(logBuffer.getStringBuffer())
+            .contains(
+                "tile state update: " +
+                    "trigger=force, " +
+                    "state=[" +
+                    "label=, " +
+                    "state=INACTIVE, " +
+                    "s_label=null, " +
+                    "cd=null, " +
+                    "sd=null, " +
+                    "svi=None, " +
+                    "enabled=ENABLED, " +
+                    "a11y=null" +
+                    "], " +
+                    "data=test_data"
+            )
+    }
+
+    private fun LogBuffer.getStringBuffer(): String {
+        val stringWriter = StringWriter()
+        dump(PrintWriter(stringWriter), 0)
+        return stringWriter.buffer.toString()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
index 9024c6c..4760dfa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
@@ -1,21 +1,39 @@
+/*
+ * 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.qs.tiles.viewmodel
 
-import android.graphics.drawable.ShapeDrawable
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.android.internal.logging.InstanceId
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
 import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
 import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataRequest
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
 import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
 import com.android.systemui.qs.tiles.base.viewmodel.BaseQSTileViewModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
@@ -26,6 +44,8 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
 
 // TODO(b/299909368): Add more tests
 @MediumTest
@@ -34,9 +54,13 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() {
 
+    @Mock private lateinit var qsTileLogger: QSTileLogger
+    @Mock private lateinit var qsTileAnalytics: QSTileAnalytics
+
     private val fakeQSTileDataInteractor = FakeQSTileDataInteractor<Any>()
     private val fakeQSTileUserActionInteractor = FakeQSTileUserActionInteractor<Any>()
     private val fakeDisabledByPolicyInteractor = FakeDisabledByPolicyInteractor()
+    private val fakeFalsingManager = FalsingManagerFake()
 
     private val testCoroutineDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testCoroutineDispatcher)
@@ -45,6 +69,7 @@
 
     @Before
     fun setup() {
+        MockitoAnnotations.initMocks(this)
         underTest = createViewModel(testScope)
     }
 
@@ -79,6 +104,9 @@
                     QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}
             },
             fakeDisabledByPolicyInteractor,
+            fakeFalsingManager,
+            qsTileAnalytics,
+            qsTileLogger,
             testCoroutineDispatcher,
             scope.backgroundScope,
         )
@@ -88,7 +116,7 @@
         val TEST_QS_TILE_CONFIG =
             QSTileConfig(
                 TileSpec.create("default"),
-                Icon.Loaded(ShapeDrawable(), null),
+                Icon.Resource(0, null),
                 0,
                 InstanceId.fakeInstanceId(0),
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt
new file mode 100644
index 0000000..f1e6a05
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.statusbar.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeKeyguardStatusBarRepository : KeyguardStatusBarRepository {
+    override val isKeyguardUserSwitcherEnabled = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryImplTest.kt
new file mode 100644
index 0000000..b1c994c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryImplTest.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.statusbar.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.user.data.repository.FakeUserSwitcherRepository
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@SmallTest
+class KeyguardStatusBarRepositoryImplTest : SysuiTestCase() {
+    private val testScope = TestScope()
+    private val configurationController = mock<ConfigurationController>()
+    private val userSwitcherRepository = FakeUserSwitcherRepository()
+
+    val underTest =
+        KeyguardStatusBarRepositoryImpl(
+            context,
+            configurationController,
+            userSwitcherRepository,
+        )
+
+    private val configurationListener: ConfigurationController.ConfigurationListener
+        get() {
+            val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
+            verify(configurationController).addCallback(capture(captor))
+            return captor.value
+        }
+
+    @Test
+    fun isKeyguardUserSwitcherEnabled_switcherNotEnabled_false() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled)
+
+            userSwitcherRepository.isEnabled.value = false
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isKeyguardUserSwitcherEnabled_keyguardConfigNotEnabled_false() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled)
+            userSwitcherRepository.isEnabled.value = true
+
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false)
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isKeyguardUserSwitcherEnabled_switchEnabledAndKeyguardConfigEnabled_true() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled)
+
+            userSwitcherRepository.isEnabled.value = true
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true)
+
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isKeyguardUserSwitcherEnabled_refetchedOnSmallestWidthChanged() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled)
+            userSwitcherRepository.isEnabled.value = true
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true)
+            assertThat(latest).isTrue()
+
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false)
+            configurationListener.onSmallestScreenWidthChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isKeyguardUserSwitcherEnabled_refetchedOnDensityChanged() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled)
+            userSwitcherRepository.isEnabled.value = true
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true)
+            assertThat(latest).isTrue()
+
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false)
+            configurationListener.onDensityOrFontScaleChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isKeyguardUserSwitcherEnabled_refetchedOnEnabledChanged() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled)
+
+            userSwitcherRepository.isEnabled.value = false
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true)
+            assertThat(latest).isFalse()
+
+            // WHEN the switcher becomes enabled but the keyguard switcher becomes disabled
+            context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false)
+            userSwitcherRepository.isEnabled.value = true
+
+            // THEN the value is still false because the keyguard config is refetched
+            assertThat(latest).isFalse()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index c0d248e..6484389 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -64,12 +64,13 @@
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.SceneTestUtils;
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags;
 import com.android.systemui.shade.ShadeViewStateProvider;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.data.repository.FakeKeyguardStatusBarRepository;
+import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -156,7 +157,6 @@
     public void setup() throws Exception {
         mFeatureFlags.set(Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW, false);
         mShadeViewStateProvider = new TestShadeViewStateProvider();
-        mShadeViewStateProvider = new TestShadeViewStateProvider();
 
         MockitoAnnotations.initMocks(this);
 
@@ -176,7 +176,9 @@
         mViewModel =
                 new KeyguardStatusBarViewModel(
                         mTestScope.getBackgroundScope(),
-                        mKeyguardInteractor);
+                        mKeyguardInteractor,
+                        new KeyguardStatusBarInteractor(new FakeKeyguardStatusBarRepository()),
+                        mBatteryController);
 
         allowTestableLooperAsMainThread();
         TestableLooper.get(this).runWithLooper(() -> {
@@ -320,6 +322,15 @@
     }
 
     @Test
+    public void setBatteryListening_true_flagOn_callbackNotAdded() {
+        mFeatureFlags.set(Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW, true);
+
+        mController.setBatteryListening(true);
+
+        verify(mBatteryController, never()).addCallback(any());
+    }
+
+    @Test
     public void updateTopClipping_viewClippingUpdated() {
         int viewTop = 20;
         mKeyguardStatusBarView.setTop(viewTop);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index f4078d5..1bc346d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -29,13 +29,23 @@
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.data.repository.FakeKeyguardStatusBarRepository
+import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
+import org.mockito.Mockito.verify
 
 @SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
 class KeyguardStatusBarViewModelTest : SysuiTestCase() {
     private val testScope = TestScope()
     private val sceneTestUtils = SceneTestUtils(this)
@@ -54,11 +64,18 @@
         ) {
             sceneTestUtils.sceneInteractor()
         }
+    private val keyguardStatusBarInteractor =
+        KeyguardStatusBarInteractor(
+            FakeKeyguardStatusBarRepository(),
+        )
+    private val batteryController = mock<BatteryController>()
 
     private val underTest =
         KeyguardStatusBarViewModel(
             testScope.backgroundScope,
             keyguardInteractor,
+            keyguardStatusBarInteractor,
+            batteryController,
         )
 
     @Test
@@ -102,4 +119,46 @@
 
             assertThat(latest).isTrue()
         }
+
+    @Test
+    fun isBatteryCharging_matchesCallback() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isBatteryCharging)
+            runCurrent()
+
+            val captor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(captor))
+            val callback = captor.value
+
+            callback.onBatteryLevelChanged(
+                /* level= */ 2,
+                /* pluggedIn= */ false,
+                /* charging= */ true,
+            )
+
+            assertThat(latest).isTrue()
+
+            callback.onBatteryLevelChanged(
+                /* level= */ 2,
+                /* pluggedIn= */ true,
+                /* charging= */ false,
+            )
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isBatteryCharging_unregistersWhenNotListening() =
+        testScope.runTest {
+            val job = underTest.isBatteryCharging.launchIn(this)
+            runCurrent()
+
+            val captor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(captor))
+
+            job.cancel()
+            runCurrent()
+
+            verify(batteryController).removeCallback(captor.value)
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/FakeUserSwitcherRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/FakeUserSwitcherRepository.kt
new file mode 100644
index 0000000..758fe93a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/FakeUserSwitcherRepository.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.user.data.repository
+
+import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeUserSwitcherRepository : UserSwitcherRepository {
+    override val isEnabled = MutableStateFlow(false)
+    override val userSwitcherStatus =
+        MutableStateFlow<UserSwitcherStatusModel>(UserSwitcherStatusModel.Disabled)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index cd009df..d6632a3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -38,16 +38,12 @@
 import androidx.test.uiautomator.UiDevice;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.broadcast.FakeBroadcastDispatcher;
 import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger;
-import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 import org.junit.After;
@@ -128,20 +124,8 @@
         // reference and are never sent to the Context. This will also prevent a real
         // BroadcastDispatcher from actually registering receivers.
         mDependency.injectTestDependency(BroadcastDispatcher.class, mFakeBroadcastDispatcher);
-        // A lot of tests get the FalsingManager, often via several layers of indirection.
-        // None of them actually need it.
-        mDependency.injectTestDependency(FalsingManager.class, new FalsingManagerFake());
         mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
 
-        // A lot of tests get the LocalBluetoothManager, often via several layers of indirection.
-        // None of them actually need it.
-        mDependency.injectMockDependency(LocalBluetoothManager.class);
-
-        // Notifications tests are injecting one of these, causing many classes (including
-        // KeyguardUpdateMonitor to be created (injected).
-        // TODO(b/1531701009) Clean up NotificationContentView creation to prevent this
-        mDependency.injectMockDependency(SmartReplyController.class);
-
         // Make sure that all tests on any SystemUIDialog does not crash because this dependency
         // is missing (constructing the actual one would throw).
         // TODO(b/219008720): Remove this.
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
index 1a893f8..bf77b1a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
@@ -36,8 +36,6 @@
 import com.android.systemui.qs.QSSecurityFooterUtils
 import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
 import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl
-import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
-import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl
 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
@@ -51,6 +49,8 @@
 import com.android.systemui.statusbar.policy.SecurityController
 import com.android.systemui.statusbar.policy.UserInfoController
 import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.repository.UserSwitcherRepository
+import com.android.systemui.user.data.repository.UserSwitcherRepositoryImpl
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.FakeSettings
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
new file mode 100644
index 0000000..201926d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.qs.tiles.viewmodel
+
+import androidx.annotation.StringRes
+import com.android.internal.logging.InstanceId
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+object QSTileConfigTestBuilder {
+
+    fun build(configure: BuildingScope.() -> Unit = {}): QSTileConfig =
+        BuildingScope().apply(configure).build()
+
+    class BuildingScope {
+        var tileSpec: TileSpec = TileSpec.create("test_spec")
+        var tileIcon: Icon = Icon.Resource(0, ContentDescription.Resource(0))
+        @StringRes var tileLabel: Int = 0
+        var instanceId: InstanceId = InstanceId.fakeInstanceId(0)
+        var metricsSpec: String = tileSpec.spec
+        var policy: QSTilePolicy = QSTilePolicy.NoRestrictions
+
+        fun build() =
+            QSTileConfig(
+                tileSpec,
+                tileIcon,
+                tileLabel,
+                instanceId,
+                metricsSpec,
+                policy,
+            )
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 9dd0dca..852e36d 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -218,6 +218,9 @@
 
     void setActivityLaunchDefaultAllowed(boolean activityLaunchDefaultAllowed) {
         synchronized (mGenericWindowPolicyControllerLock) {
+            if (mActivityLaunchAllowedByDefault != activityLaunchDefaultAllowed) {
+                mActivityPolicyExemptions.clear();
+            }
             mActivityLaunchAllowedByDefault = activityLaunchDefaultAllowed;
         }
     }
diff --git a/services/core/java/com/android/server/ExplicitHealthCheckController.java b/services/core/java/com/android/server/ExplicitHealthCheckController.java
index 20de40e..3d610d3 100644
--- a/services/core/java/com/android/server/ExplicitHealthCheckController.java
+++ b/services/core/java/com/android/server/ExplicitHealthCheckController.java
@@ -343,7 +343,7 @@
             };
 
             mContext.bindServiceAsUser(intent, mConnection,
-                    Context.BIND_AUTO_CREATE, UserHandle.of(UserHandle.USER_SYSTEM));
+                    Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
             Slog.i(TAG, "Explicit health check service is bound");
         }
     }
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index c094c12..dd54334 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -522,7 +522,8 @@
         Exception res = null;
         final ContentResolver resolver = context.getContentResolver();
         try {
-            Settings.Global.resetToDefaultsAsUser(resolver, null, mode, UserHandle.USER_SYSTEM);
+            Settings.Global.resetToDefaultsAsUser(resolver, null, mode,
+                UserHandle.SYSTEM.getIdentifier());
         } catch (Exception e) {
             res = new RuntimeException("Failed to reset global settings", e);
         }
@@ -779,12 +780,13 @@
     }
 
     private static int[] getAllUserIds() {
-        int[] userIds = { UserHandle.USER_SYSTEM };
+        int systemUserId = UserHandle.SYSTEM.getIdentifier();
+        int[] userIds = { systemUserId };
         try {
             for (File file : FileUtils.listFilesOrEmpty(Environment.getDataSystemDeDirectory())) {
                 try {
                     final int userId = Integer.parseInt(file.getName());
-                    if (userId != UserHandle.USER_SYSTEM) {
+                    if (userId != systemUserId) {
                         userIds = ArrayUtils.appendInt(userIds, userId);
                     }
                 } catch (NumberFormatException ignored) {
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index 6e984bb..b05b397 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -330,7 +330,7 @@
         String describeBlockedStateLocked() {
             final String prefix;
             if (mCurrentMonitor == null) {
-                prefix = "Blocked in handler on ";
+                prefix = "Blocked in handler";
             } else {
                 prefix =  "Blocked in monitor " + mCurrentMonitor.getClass().getName();
             }
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 83a3125..c9528d8 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -707,7 +707,8 @@
     }
 
     private boolean checkCallerHasSystemRoutingPermissions(int pid, int uid) {
-        return checkCallerHasModifyAudioRoutingPermission(pid, uid);
+        return checkCallerHasModifyAudioRoutingPermission(pid, uid)
+                || checkCallerHasBluetoothPermissions(pid, uid);
     }
 
     private boolean checkCallerHasModifyAudioRoutingPermission(int pid, int uid) {
diff --git a/services/tests/servicestests/src/com/android/server/timezone/OWNERS b/services/tests/servicestests/src/com/android/server/timezone/OWNERS
index 6165260..d64cbcd 100644
--- a/services/tests/servicestests/src/com/android/server/timezone/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/timezone/OWNERS
@@ -1,2 +1,2 @@
-# Bug component: 24949
+# Bug component: 847766
 include /services/core/java/com/android/server/timezone/OWNERS
diff --git a/services/usage/java/com/android/server/usage/UsageStatsHandlerThread.java b/services/usage/java/com/android/server/usage/UsageStatsHandlerThread.java
new file mode 100644
index 0000000..6801c94
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UsageStatsHandlerThread.java
@@ -0,0 +1,80 @@
+/*
+ * 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.server.usage;
+
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+import android.os.Trace;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Shared singleton default priority thread for usage stats message handling.
+ *
+ * @see com.android.internal.os.BackgroundThread
+ */
+public final class UsageStatsHandlerThread extends HandlerThread {
+    private static final long SLOW_DISPATCH_THRESHOLD_MS = 10_000;
+    private static final long SLOW_DELIVERY_THRESHOLD_MS = 30_000;
+    private static UsageStatsHandlerThread sInstance;
+    private static Handler sHandler;
+    private static Executor sHandlerExecutor;
+
+    private UsageStatsHandlerThread() {
+        super("usagestats.default", Process.THREAD_PRIORITY_DEFAULT);
+    }
+
+    private static void ensureThreadLocked() {
+        if (sInstance == null) {
+            sInstance = new UsageStatsHandlerThread();
+            sInstance.start();
+            final Looper looper = sInstance.getLooper();
+            looper.setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
+            looper.setSlowLogThresholdMs(
+                    SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS);
+            sHandler = new Handler(sInstance.getLooper());
+            sHandlerExecutor = new HandlerExecutor(sHandler);
+        }
+    }
+
+    /** Returns the UsageStatsHandlerThread singleton */
+    public static UsageStatsHandlerThread get() {
+        synchronized (UsageStatsHandlerThread.class) {
+            ensureThreadLocked();
+            return sInstance;
+        }
+    }
+
+    /** Returns the singleton handler for UsageStatsHandlerThread */
+    public static Handler getHandler() {
+        synchronized (UsageStatsHandlerThread.class) {
+            ensureThreadLocked();
+            return sHandler;
+        }
+    }
+
+    /** Returns the singleton handler executor for UsageStatsHandlerThread */
+    public static Executor getExecutor() {
+        synchronized (UsageStatsHandlerThread.class) {
+            ensureThreadLocked();
+            return sHandlerExecutor;
+        }
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 58b5ae5..18c960e 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -106,6 +106,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
@@ -201,7 +202,8 @@
     static final int MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED = 9;
 
     private final Object mLock = new Object();
-    Handler mHandler;
+    private Handler mHandler;
+    private Handler mIoHandler;
     AppOpsManager mAppOps;
     UserManager mUserManager;
     PackageManager mPackageManager;
@@ -233,7 +235,7 @@
     private final SparseArray<LinkedList<Event>> mReportedEvents = new SparseArray<>();
     final SparseArray<ArraySet<String>> mUsageReporters = new SparseArray();
     final SparseArray<ActivityData> mVisibleActivities = new SparseArray();
-    @GuardedBy("mLock")
+    @GuardedBy("mLaunchTimeAlarmQueues") // Don't hold the main lock
     private final SparseArray<LaunchTimeAlarmQueue> mLaunchTimeAlarmQueues = new SparseArray<>();
     @GuardedBy("mUsageEventListeners") // Don't hold the main lock when calling out
     private final ArraySet<UsageStatsManagerInternal.UsageEventListener> mUsageEventListeners =
@@ -279,6 +281,38 @@
         }
     }
 
+    private final Handler.Callback mIoHandlerCallback = (msg) -> {
+        switch (msg.what) {
+            case MSG_UID_STATE_CHANGED: {
+                final int uid = msg.arg1;
+                final int procState = msg.arg2;
+
+                final int newCounter = (procState <= ActivityManager.PROCESS_STATE_TOP) ? 0 : 1;
+                synchronized (mUidToKernelCounter) {
+                    final int oldCounter = mUidToKernelCounter.get(uid, 0);
+                    if (newCounter != oldCounter) {
+                        mUidToKernelCounter.put(uid, newCounter);
+                        try {
+                            FileUtils.stringToFile(KERNEL_COUNTER_FILE, uid + " " + newCounter);
+                        } catch (IOException e) {
+                            Slog.w(TAG, "Failed to update counter set: " + e);
+                        }
+                    }
+                }
+                return true;
+            }
+            case MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK: {
+                final int userId = msg.arg1;
+                Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+                        "usageStatsHandleEstimatedLaunchTimesOnUser(" + userId + ")");
+                handleEstimatedLaunchTimesOnUserUnlock(userId);
+                Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+                return true;
+            }
+        }
+        return false;
+    };
+
     private final Injector mInjector;
 
     public UsageStatsService(Context context) {
@@ -298,7 +332,9 @@
         mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
         mPackageManager = getContext().getPackageManager();
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
-        mHandler = new H(BackgroundThread.get().getLooper());
+
+        mHandler = new H(UsageStatsHandlerThread.get().getLooper());
+        mIoHandler = new Handler(IoThread.get().getLooper(), mIoHandlerCallback);
 
         mAppStandby = mInjector.getAppStandbyController(getContext());
         mResponseStatsTracker = new BroadcastResponseStatsTracker(mAppStandby, getContext());
@@ -424,6 +460,9 @@
             }
             mUserUnlockedStates.remove(userId);
             mUserState.put(userId, null); // release the service (mainly for GC)
+        }
+
+        synchronized (mLaunchTimeAlarmQueues) {
             LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
             if (alarmQueue != null) {
                 alarmQueue.removeAllAlarms();
@@ -476,11 +515,13 @@
             }
             reportEvent(unlockEvent, userId);
 
-            mHandler.obtainMessage(MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK, userId, 0).sendToTarget();
+            mIoHandler.obtainMessage(MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK,
+                    userId, 0).sendToTarget();
 
             // Remove all the stats stored in memory and in system DE.
             mReportedEvents.remove(userId);
             deleteRecursively(new File(Environment.getDataSystemDeDirectory(userId), "usagestats"));
+
             // Force a flush to disk for the current user to ensure important events are persisted.
             // Note: there is a very very small chance that the system crashes between deleting
             // the stats above from DE and persisting them to CE here in which case we will lose
@@ -599,7 +640,7 @@
     private final IUidObserver mUidObserver = new UidObserver() {
         @Override
         public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
-            mHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget();
+            mIoHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget();
         }
 
         @Override
@@ -671,16 +712,18 @@
                 callingPid, callingUid) == PackageManager.PERMISSION_GRANTED);
     }
 
-    private static void deleteRecursively(File f) {
-        File[] files = f.listFiles();
-        if (files != null) {
-            for (File subFile : files) {
-                deleteRecursively(subFile);
+    private static void deleteRecursively(final File path) {
+        if (path.isDirectory()) {
+            final File[] files = path.listFiles();
+            if (files != null) {
+                for (File subFile : files) {
+                    deleteRecursively(subFile);
+                }
             }
         }
 
-        if (f.exists() && !f.delete()) {
-            Slog.e(TAG, "Failed to delete " + f);
+        if (path.exists() && !path.delete()) {
+            Slog.e(TAG, "Failed to delete " + path);
         }
     }
 
@@ -1241,6 +1284,9 @@
             Slog.i(TAG, "Removing user " + userId + " and all data.");
             mUserState.remove(userId);
             mAppTimeLimit.onUserRemoved(userId);
+        }
+
+        synchronized (mLaunchTimeAlarmQueues) {
             final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
             if (alarmQueue != null) {
                 alarmQueue.removeAllAlarms();
@@ -1271,6 +1317,13 @@
             }
         }
 
+        synchronized (mLaunchTimeAlarmQueues) {
+            final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
+            if (alarmQueue != null) {
+                alarmQueue.removeAlarmForKey(packageName);
+            }
+        }
+
         final int tokenRemoved;
         synchronized (mLock) {
             final long timeRemoved = System.currentTimeMillis();
@@ -1279,10 +1332,7 @@
                 // when the user service is initialized and package manager is queried.
                 return;
             }
-            final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
-            if (alarmQueue != null) {
-                alarmQueue.removeAlarmForKey(packageName);
-            }
+
             final UserUsageStatsService userService = mUserState.get(userId);
             if (userService == null) {
                 return;
@@ -1492,60 +1542,63 @@
             estimatedLaunchTime = calculateEstimatedPackageLaunchTime(userId, packageName);
             mAppStandby.setEstimatedLaunchTime(packageName, userId, estimatedLaunchTime);
 
-            synchronized (mLock) {
-                LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
-                if (alarmQueue == null) {
-                    alarmQueue = new LaunchTimeAlarmQueue(
-                            userId, getContext(), BackgroundThread.get().getLooper());
-                    mLaunchTimeAlarmQueues.put(userId, alarmQueue);
-                }
-                alarmQueue.addAlarm(packageName,
-                        SystemClock.elapsedRealtime() + (estimatedLaunchTime - now));
-            }
+            getOrCreateLaunchTimeAlarmQueue(userId).addAlarm(packageName,
+                    SystemClock.elapsedRealtime() + (estimatedLaunchTime - now));
         }
         return estimatedLaunchTime;
     }
 
+    private LaunchTimeAlarmQueue getOrCreateLaunchTimeAlarmQueue(int userId) {
+        synchronized (mLaunchTimeAlarmQueues) {
+            LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
+            if (alarmQueue == null) {
+                alarmQueue = new LaunchTimeAlarmQueue(
+                    userId, getContext(), BackgroundThread.get().getLooper());
+                mLaunchTimeAlarmQueues.put(userId, alarmQueue);
+            }
+
+            return alarmQueue;
+        }
+    }
+
     @CurrentTimeMillisLong
     private long calculateEstimatedPackageLaunchTime(int userId, String packageName) {
-        synchronized (mLock) {
-            final long endTime = System.currentTimeMillis();
-            final long beginTime = endTime - ONE_WEEK;
-            final long unknownTime = endTime + UNKNOWN_LAUNCH_TIME_DELAY_MS;
-            final UsageEvents events = queryEarliestEventsForPackage(
-                    userId, beginTime, endTime, packageName, Event.ACTIVITY_RESUMED);
-            if (events == null) {
-                if (DEBUG) {
-                    Slog.d(TAG, "No events for " + userId + ":" + packageName);
-                }
-                return unknownTime;
+        final long endTime = System.currentTimeMillis();
+        final long beginTime = endTime - ONE_WEEK;
+        final long unknownTime = endTime + UNKNOWN_LAUNCH_TIME_DELAY_MS;
+        final UsageEvents events = queryEarliestEventsForPackage(
+                userId, beginTime, endTime, packageName, Event.ACTIVITY_RESUMED);
+        if (events == null) {
+            if (DEBUG) {
+                Slog.d(TAG, "No events for " + userId + ":" + packageName);
             }
-            final UsageEvents.Event event = new UsageEvents.Event();
-            final boolean hasMoreThan24HoursOfHistory;
-            if (events.getNextEvent(event)) {
-                hasMoreThan24HoursOfHistory = endTime - event.getTimeStamp() > ONE_DAY;
-                if (DEBUG) {
-                    Slog.d(TAG, userId + ":" + packageName + " history > 24 hours="
-                            + hasMoreThan24HoursOfHistory);
-                }
-            } else {
-                if (DEBUG) {
-                    Slog.d(TAG, userId + ":" + packageName + " has no events");
-                }
-                return unknownTime;
-            }
-            do {
-                if (event.getEventType() == Event.ACTIVITY_RESUMED) {
-                    final long timestamp = event.getTimeStamp();
-                    final long nextLaunch =
-                            calculateNextLaunchTime(hasMoreThan24HoursOfHistory, timestamp);
-                    if (nextLaunch > endTime) {
-                        return nextLaunch;
-                    }
-                }
-            } while (events.getNextEvent(event));
             return unknownTime;
         }
+        final UsageEvents.Event event = new UsageEvents.Event();
+        final boolean hasMoreThan24HoursOfHistory;
+        if (events.getNextEvent(event)) {
+            hasMoreThan24HoursOfHistory = endTime - event.getTimeStamp() > ONE_DAY;
+            if (DEBUG) {
+                Slog.d(TAG, userId + ":" + packageName + " history > 24 hours="
+                        + hasMoreThan24HoursOfHistory);
+            }
+        } else {
+            if (DEBUG) {
+                Slog.d(TAG, userId + ":" + packageName + " has no events");
+            }
+            return unknownTime;
+        }
+        do {
+            if (event.getEventType() == Event.ACTIVITY_RESUMED) {
+                final long timestamp = event.getTimeStamp();
+                final long nextLaunch =
+                        calculateNextLaunchTime(hasMoreThan24HoursOfHistory, timestamp);
+                if (nextLaunch > endTime) {
+                    return nextLaunch;
+                }
+            }
+        } while (events.getNextEvent(event));
+        return unknownTime;
     }
 
     @CurrentTimeMillisLong
@@ -1566,61 +1619,54 @@
     }
 
     private void handleEstimatedLaunchTimesOnUserUnlock(int userId) {
-        synchronized (mLock) {
-            final long nowElapsed = SystemClock.elapsedRealtime();
-            final long now = System.currentTimeMillis();
-            final long beginTime = now - ONE_WEEK;
-            final UsageEvents events = queryEarliestAppEvents(
-                    userId, beginTime, now, Event.ACTIVITY_RESUMED);
-            if (events == null) {
-                return;
+        final long nowElapsed = SystemClock.elapsedRealtime();
+        final long now = System.currentTimeMillis();
+        final long beginTime = now - ONE_WEEK;
+        final UsageEvents events = queryEarliestAppEvents(
+                userId, beginTime, now, Event.ACTIVITY_RESUMED);
+        if (events == null) {
+            return;
+        }
+        final ArrayMap<String, Boolean> hasMoreThan24HoursOfHistory = new ArrayMap<>();
+        final UsageEvents.Event event = new UsageEvents.Event();
+        boolean changedTimes = false;
+        final LaunchTimeAlarmQueue alarmQueue = getOrCreateLaunchTimeAlarmQueue(userId);
+        for (boolean unprocessedEvent = events.getNextEvent(event); unprocessedEvent;
+                unprocessedEvent = events.getNextEvent(event)) {
+            final String packageName = event.getPackageName();
+            if (!hasMoreThan24HoursOfHistory.containsKey(packageName)) {
+                boolean hasHistory = now - event.getTimeStamp() > ONE_DAY;
+                if (DEBUG) {
+                    Slog.d(TAG,
+                            userId + ":" + packageName + " history > 24 hours=" + hasHistory);
+                }
+                hasMoreThan24HoursOfHistory.put(packageName, hasHistory);
             }
-            final ArrayMap<String, Boolean> hasMoreThan24HoursOfHistory = new ArrayMap<>();
-            final UsageEvents.Event event = new UsageEvents.Event();
-            LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
-            if (alarmQueue == null) {
-                alarmQueue = new LaunchTimeAlarmQueue(
-                        userId, getContext(), BackgroundThread.get().getLooper());
-                mLaunchTimeAlarmQueues.put(userId, alarmQueue);
-            }
-            boolean changedTimes = false;
-            for (boolean unprocessedEvent = events.getNextEvent(event); unprocessedEvent;
-                    unprocessedEvent = events.getNextEvent(event)) {
-                final String packageName = event.getPackageName();
-                if (!hasMoreThan24HoursOfHistory.containsKey(packageName)) {
-                    boolean hasHistory = now - event.getTimeStamp() > ONE_DAY;
+            if (event.getEventType() == Event.ACTIVITY_RESUMED) {
+                long estimatedLaunchTime =
+                        mAppStandby.getEstimatedLaunchTime(packageName, userId);
+                if (estimatedLaunchTime < now || estimatedLaunchTime == Long.MAX_VALUE) {
+                    //noinspection ConstantConditions
+                    estimatedLaunchTime = calculateNextLaunchTime(
+                            hasMoreThan24HoursOfHistory.get(packageName), event.getTimeStamp());
+                    mAppStandby.setEstimatedLaunchTime(
+                            packageName, userId, estimatedLaunchTime);
+                }
+                if (estimatedLaunchTime < now + ONE_WEEK) {
+                    // Before a user is unlocked, we don't know when the app will be launched,
+                    // so we give callers the UNKNOWN time. Now that we have a better estimate,
+                    // we should notify them of the change.
                     if (DEBUG) {
-                        Slog.d(TAG,
-                                userId + ":" + packageName + " history > 24 hours=" + hasHistory);
+                        Slog.d(TAG, "User " + userId + " unlock resulting in"
+                                + " estimated launch time change for " + packageName);
                     }
-                    hasMoreThan24HoursOfHistory.put(packageName, hasHistory);
+                    changedTimes |= stageChangedEstimatedLaunchTime(userId, packageName);
                 }
-                if (event.getEventType() == Event.ACTIVITY_RESUMED) {
-                    long estimatedLaunchTime =
-                            mAppStandby.getEstimatedLaunchTime(packageName, userId);
-                    if (estimatedLaunchTime < now || estimatedLaunchTime == Long.MAX_VALUE) {
-                        //noinspection ConstantConditions
-                        estimatedLaunchTime = calculateNextLaunchTime(
-                                hasMoreThan24HoursOfHistory.get(packageName), event.getTimeStamp());
-                        mAppStandby.setEstimatedLaunchTime(
-                                packageName, userId, estimatedLaunchTime);
-                    }
-                    if (estimatedLaunchTime < now + ONE_WEEK) {
-                        // Before a user is unlocked, we don't know when the app will be launched,
-                        // so we give callers the UNKNOWN time. Now that we have a better estimate,
-                        // we should notify them of the change.
-                        if (DEBUG) {
-                            Slog.d(TAG, "User " + userId + " unlock resulting in"
-                                    + " estimated launch time change for " + packageName);
-                        }
-                        changedTimes |= stageChangedEstimatedLaunchTime(userId, packageName);
-                    }
-                    alarmQueue.addAlarm(packageName, nowElapsed + (estimatedLaunchTime - now));
-                }
+                alarmQueue.addAlarm(packageName, nowElapsed + (estimatedLaunchTime - now));
             }
-            if (changedTimes) {
-                mHandler.sendEmptyMessage(MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED);
-            }
+        }
+        if (changedTimes) {
+            mHandler.sendEmptyMessage(MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED);
         }
     }
 
@@ -1989,37 +2035,11 @@
                 case MSG_PACKAGE_REMOVED:
                     onPackageRemoved(msg.arg1, (String) msg.obj);
                     break;
-                case MSG_UID_STATE_CHANGED: {
-                    final int uid = msg.arg1;
-                    final int procState = msg.arg2;
-
-                    final int newCounter = (procState <= ActivityManager.PROCESS_STATE_TOP) ? 0 : 1;
-                    synchronized (mUidToKernelCounter) {
-                        final int oldCounter = mUidToKernelCounter.get(uid, 0);
-                        if (newCounter != oldCounter) {
-                            mUidToKernelCounter.put(uid, newCounter);
-                            try {
-                                FileUtils.stringToFile(KERNEL_COUNTER_FILE, uid + " " + newCounter);
-                            } catch (IOException e) {
-                                Slog.w(TAG, "Failed to update counter set: " + e);
-                            }
-                        }
-                    }
-                    break;
-                }
                 case MSG_ON_START:
                     synchronized (mLock) {
                         loadGlobalComponentUsageLocked();
                     }
                     break;
-                case MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK: {
-                    final int userId = msg.arg1;
-                    Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
-                            "usageStatsHandleEstimatedLaunchTimesOnUser(" + userId + ")");
-                    handleEstimatedLaunchTimesOnUserUnlock(userId);
-                    Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
-                }
-                break;
                 case MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED: {
                     removeMessages(MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED);
 
@@ -2587,11 +2607,12 @@
         @Override
         public void reportChooserSelection(@NonNull String packageName, int userId,
                 @NonNull String contentType, String[] annotations, @NonNull String action) {
-            // A valid package name, content type, and action must be provided for these events
-            Objects.requireNonNull(packageName);
-            Objects.requireNonNull(contentType);
-            Objects.requireNonNull(action);
-            if (contentType.isBlank() || action.isBlank()) {
+            if (packageName == null) {
+                throw new IllegalArgumentException("Package selection must not be null.");
+            }
+            // A valid contentType and action must be provided for chooser selection events.
+            if (contentType == null || contentType.isBlank()
+                    || action == null || action.isBlank()) {
                 return;
             }