diff options
| author | 2023-09-28 18:15:32 +0100 | |
|---|---|---|
| committer | 2023-10-04 14:21:01 +0100 | |
| commit | d8d280f69f06cf9f75d86d97a8a504a405741498 (patch) | |
| tree | 48a12e39db970d6476bcd2300243fbf40407586d | |
| parent | 4fdea46e9be396db4ae77ad82abc9379b121cf01 (diff) | |
Migrate secondary non-critical functionality from QSTileImpl
This includes:
- analytics tracking
- logging
- filtering false touches
Test: atest QSTileAnalyticsTest QSTileLoggerTest
Bug: 299908705
Change-Id: I98fb31c7d91f503782f63ef3cb1975ae1c4933d7
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, + ) + } +} |