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,
+ )
+ }
+}