summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java40
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesDefaultLog.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesLogBuffers.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesVerboseLog.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt189
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt96
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt81
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt172
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt32
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt48
12 files changed, 782 insertions, 23 deletions
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 1943b340b9b1..67531ad9926a 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.LogcatEchoTrackerProd;
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 com.android.systemui.util.wakelock.WakeLockLog;
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 @@ public class LogModule {
@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 @@ public class LogModule {
/**
* 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 @@ public class LogModule {
/**
* 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 000000000000..6575cdd69c93
--- /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 000000000000..62d49fefeb6a
--- /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 000000000000..b0c2f8c59deb
--- /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/tiles/base/analytics/QSTileAnalytics.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt
new file mode 100644
index 000000000000..0d15a5b6b4d4
--- /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 000000000000..70a683b81f75
--- /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 58a335e462a1..2114751ef57b 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.CallSuper
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.QSTileState
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 @@ constructor(
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 @@ constructor(
@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 @@ constructor(
tileDataInteractor,
mapper,
disabledByPolicyInteractor,
+ falsingManager,
+ qsTileAnalytics,
+ qsTileLogger,
backgroundDispatcher,
CoroutineScope(SupervisorJob())
)
@@ -98,8 +111,10 @@ constructor(
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 @@ constructor(
@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 @@ constructor(
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 @@ constructor(
currentLifeState = lifecycle
}
- private fun createTileDataFlow(): SharedFlow<DATA_TYPE> =
+ private fun createTileDataFlow(): SharedFlow<DataWithTrigger<DATA_TYPE>> =
userIds
.flatMapLatest { userId ->
merge(
@@ -180,7 +210,7 @@ constructor(
request.trigger.tileData as DATA_TYPE,
)
}
- dataFlow
+ dataFlow.map { DataWithTrigger(it, request.trigger) }
}
.flowOn(backgroundDispatcher)
.shareIn(
@@ -193,21 +223,53 @@ constructor(
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 1a6cf99ab810..4a3bcae17fd0 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 @@ data class QSTileConfig(
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 000000000000..2c4e10eadfc9
--- /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 000000000000..4401e0d60da6
--- /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 9024c6c5576b..4760dfa3561d 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 kotlinx.coroutines.test.runTest
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 @@ import org.junit.runner.RunWith
@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 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() {
@Before
fun setup() {
+ MockitoAnnotations.initMocks(this)
underTest = createViewModel(testScope)
}
@@ -79,6 +104,9 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() {
QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}
},
fakeDisabledByPolicyInteractor,
+ fakeFalsingManager,
+ qsTileAnalytics,
+ qsTileLogger,
testCoroutineDispatcher,
scope.backgroundScope,
)
@@ -88,7 +116,7 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() {
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 000000000000..201926df5a5c
--- /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,
+ )
+ }
+}