Merge changes I696de567,I98fb31c7 into main

* changes:
  Move CustomTile specific stuff behind the interface in TileServices
  Migrate secondary non-critical functionality from QSTileImpl
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/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/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/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/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,
+            )
+    }
+}