Add a AutoAddInteractor

This is meant to replace the old AutoTileManager. It works with a
collection of AutoAddable, so adding new rules for tiles that need to be
added is easier to do.

Fixes: 278577795
Test: atest com.android.systemui.qs.pipeline
Flag: QS_PIPELINE_AUTO_ADD
Change-Id: Iee7e70f90034c5bb773a515660f88548d5a0aa86
This commit is contained in:
Fabián Kozynski
2023-06-16 10:48:55 -04:00
parent 2c770a9438
commit bd1e439025
11 changed files with 463 additions and 3 deletions

View File

@@ -161,7 +161,9 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
// finishes before creating any tiles. // finishes before creating any tiles.
tunerService.addTunable(this, TILES_SETTING); tunerService.addTunable(this, TILES_SETTING);
// AutoTileManager can modify mTiles so make sure mTiles has already been initialized. // AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
mAutoTiles = autoTiles.get(); if (!mFeatureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) {
mAutoTiles = autoTiles.get();
}
}); });
} }

View File

@@ -0,0 +1,22 @@
/*
* 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.pipeline.dagger
import javax.inject.Qualifier
/** A [LogBuffer] for the QS pipeline to track auto-added tiles */
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class QSAutoAddLog

View File

@@ -16,13 +16,40 @@
package com.android.systemui.qs.pipeline.dagger package com.android.systemui.qs.pipeline.dagger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
import com.android.systemui.qs.pipeline.data.repository.AutoAddSettingRepository import com.android.systemui.qs.pipeline.data.repository.AutoAddSettingRepository
import com.android.systemui.qs.pipeline.domain.model.AutoAddable
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides
import dagger.multibindings.Multibinds
@Module @Module(
includes =
[
BaseAutoAddableModule::class,
]
)
abstract class QSAutoAddModule { abstract class QSAutoAddModule {
@Binds abstract fun bindAutoAddRepository(impl: AutoAddSettingRepository): AutoAddRepository @Binds abstract fun bindAutoAddRepository(impl: AutoAddSettingRepository): AutoAddRepository
@Multibinds abstract fun providesAutoAddableSet(): Set<AutoAddable>
companion object {
/**
* Provides a logging buffer for all logs related to the new Quick Settings pipeline to log
* auto added tiles.
*/
@Provides
@SysUISingleton
@QSAutoAddLog
fun provideQSAutoAddLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create(QSPipelineLogger.AUTO_ADD_TAG, maxSize = 100, systrace = false)
}
}
} }

View File

@@ -19,5 +19,5 @@ import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy import java.lang.annotation.RetentionPolicy
import javax.inject.Qualifier import javax.inject.Qualifier
/** A {@link LogBuffer} for the new QS Pipeline for logging changes to the set of current tiles. */ /** A [LogBuffer] for the new QS Pipeline for logging changes to the set of current tiles. */
@Qualifier @MustBeDocumented @Retention(RetentionPolicy.RUNTIME) annotation class QSTileListLog @Qualifier @MustBeDocumented @Retention(RetentionPolicy.RUNTIME) annotation class QSTileListLog

View File

@@ -0,0 +1,123 @@
/*
* 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.pipeline.domain.interactor
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
import com.android.systemui.qs.pipeline.domain.model.AutoAddable
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.util.asIndenting
import com.android.systemui.util.indentIfPossible
import java.io.PrintWriter
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
/**
* Collects the signals coming from all registered [AutoAddable] and adds/removes tiles accordingly.
*/
@SysUISingleton
class AutoAddInteractor
@Inject
constructor(
private val autoAddables: Set<@JvmSuppressWildcards AutoAddable>,
private val repository: AutoAddRepository,
private val dumpManager: DumpManager,
private val qsPipelineLogger: QSPipelineLogger,
@Application private val scope: CoroutineScope,
) : Dumpable {
private val initialized = AtomicBoolean(false)
/** Start collection of signals following the user from [currentTilesInteractor]. */
fun init(currentTilesInteractor: CurrentTilesInteractor) {
if (!initialized.compareAndSet(false, true)) {
return
}
dumpManager.registerNormalDumpable(TAG, this)
scope.launch {
currentTilesInteractor.userId.collectLatest { userId ->
coroutineScope {
val previouslyAdded = repository.autoAddedTiles(userId).stateIn(this)
autoAddables
.map { addable ->
val autoAddSignal = addable.autoAddSignal(userId)
when (val lifecycle = addable.autoAddTracking) {
is AutoAddTracking.Always -> autoAddSignal
is AutoAddTracking.Disabled -> emptyFlow()
is AutoAddTracking.IfNotAdded -> {
if (lifecycle.spec !in previouslyAdded.value) {
autoAddSignal.filterIsInstance<AutoAddSignal.Add>().take(1)
} else {
emptyFlow()
}
}
}
}
.merge()
.collect { signal ->
when (signal) {
is AutoAddSignal.Add -> {
if (signal.spec !in previouslyAdded.value) {
currentTilesInteractor.addTile(signal.spec, signal.position)
qsPipelineLogger.logTileAutoAdded(
userId,
signal.spec,
signal.position
)
repository.markTileAdded(userId, signal.spec)
}
}
is AutoAddSignal.Remove -> {
currentTilesInteractor.removeTiles(setOf(signal.spec))
qsPipelineLogger.logTileAutoRemoved(userId, signal.spec)
repository.unmarkTileAdded(userId, signal.spec)
}
}
}
}
}
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
with(pw.asIndenting()) {
println("AutoAddables:")
indentIfPossible { autoAddables.forEach { println(it.description) } }
}
}
companion object {
private const val TAG = "AutoAddInteractor"
}
}

View File

@@ -125,6 +125,7 @@ interface CurrentTilesInteractor : ProtoDumpable {
class CurrentTilesInteractorImpl class CurrentTilesInteractorImpl
@Inject @Inject
constructor( constructor(
autoAddInteractor: AutoAddInteractor,
private val tileSpecRepository: TileSpecRepository, private val tileSpecRepository: TileSpecRepository,
private val installedTilesComponentRepository: InstalledTilesComponentRepository, private val installedTilesComponentRepository: InstalledTilesComponentRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
@@ -172,6 +173,10 @@ constructor(
if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
startTileCollection() startTileCollection()
} }
if (featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) {
autoAddInteractor.init(this)
}
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)

View File

@@ -19,6 +19,7 @@ package com.android.systemui.qs.pipeline.shared.logging
import android.annotation.UserIdInt import android.annotation.UserIdInt
import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel import com.android.systemui.log.LogLevel
import com.android.systemui.qs.pipeline.dagger.QSAutoAddLog
import com.android.systemui.qs.pipeline.dagger.QSTileListLog import com.android.systemui.qs.pipeline.dagger.QSTileListLog
import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.TileSpec
import javax.inject.Inject import javax.inject.Inject
@@ -32,10 +33,12 @@ class QSPipelineLogger
@Inject @Inject
constructor( constructor(
@QSTileListLog private val tileListLogBuffer: LogBuffer, @QSTileListLog private val tileListLogBuffer: LogBuffer,
@QSAutoAddLog private val tileAutoAddLogBuffer: LogBuffer,
) { ) {
companion object { companion object {
const val TILE_LIST_TAG = "QSTileListLog" const val TILE_LIST_TAG = "QSTileListLog"
const val AUTO_ADD_TAG = "QSAutoAddableLog"
} }
/** /**
@@ -136,6 +139,31 @@ constructor(
) )
} }
fun logTileAutoAdded(userId: Int, spec: TileSpec, position: Int) {
tileAutoAddLogBuffer.log(
AUTO_ADD_TAG,
LogLevel.DEBUG,
{
int1 = userId
int2 = position
str1 = spec.toString()
},
{ "Tile $str1 auto added for user $int1 at position $int2" }
)
}
fun logTileAutoRemoved(userId: Int, spec: TileSpec) {
tileAutoAddLogBuffer.log(
AUTO_ADD_TAG,
LogLevel.DEBUG,
{
int1 = userId
str1 = spec.toString()
},
{ "Tile $str1 auto removed for user $int1" }
)
}
/** Reasons for destroying an existing tile. */ /** Reasons for destroying an existing tile. */
enum class TileDestroyedReason(val readable: String) { enum class TileDestroyedReason(val readable: String) {
TILE_REMOVED("Tile removed from current set"), TILE_REMOVED("Tile removed from current set"),

View File

@@ -140,6 +140,7 @@ public class QSTileHostTest extends SysuiTestCase {
mFeatureFlags = new FakeFeatureFlags(); mFeatureFlags = new FakeFeatureFlags();
mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false); mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false);
mFeatureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, false);
mMainExecutor = new FakeExecutor(new FakeSystemClock()); mMainExecutor = new FakeExecutor(new FakeSystemClock());

View File

@@ -0,0 +1,201 @@
/*
* 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.pipeline.domain.interactor
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.DumpManager
import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
import com.android.systemui.qs.pipeline.domain.autoaddable.FakeAutoAddable
import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
import com.android.systemui.qs.pipeline.domain.model.AutoAddable
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class AutoAddInteractorTest : SysuiTestCase() {
private val testScope = TestScope()
private val autoAddRepository = FakeAutoAddRepository()
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var currentTilesInteractor: CurrentTilesInteractor
@Mock private lateinit var logger: QSPipelineLogger
private lateinit var underTest: AutoAddInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(currentTilesInteractor.userId).thenReturn(MutableStateFlow(USER))
}
@Test
fun autoAddable_alwaysTrack_addSignal_tileAddedAndMarked() =
testScope.runTest {
val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
underTest = createInteractor(setOf(fakeAutoAddable))
val position = 3
fakeAutoAddable.sendAddSignal(USER, position)
runCurrent()
verify(currentTilesInteractor).addTile(SPEC, position)
assertThat(autoAddedTiles).contains(SPEC)
}
@Test
fun autoAddable_alwaysTrack_addThenRemoveSignal_tileAddedAndRemoved() =
testScope.runTest {
val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
underTest = createInteractor(setOf(fakeAutoAddable))
val position = 3
fakeAutoAddable.sendAddSignal(USER, position)
runCurrent()
fakeAutoAddable.sendRemoveSignal(USER)
runCurrent()
val inOrder = inOrder(currentTilesInteractor)
inOrder.verify(currentTilesInteractor).addTile(SPEC, position)
inOrder.verify(currentTilesInteractor).removeTiles(setOf(SPEC))
assertThat(autoAddedTiles).doesNotContain(SPEC)
}
@Test
fun autoAddable_alwaysTrack_addSignalWhenAddedPreviously_noop() =
testScope.runTest {
val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
autoAddRepository.markTileAdded(USER, SPEC)
runCurrent()
underTest = createInteractor(setOf(fakeAutoAddable))
val position = 3
fakeAutoAddable.sendAddSignal(USER, position)
runCurrent()
verify(currentTilesInteractor, never()).addTile(SPEC, position)
}
@Test
fun autoAddable_disabled_noInteractionsWithCurrentTilesInteractor() =
testScope.runTest {
val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Disabled)
val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
underTest = createInteractor(setOf(fakeAutoAddable))
val position = 3
fakeAutoAddable.sendAddSignal(USER, position)
runCurrent()
fakeAutoAddable.sendRemoveSignal(USER)
runCurrent()
verify(currentTilesInteractor, never()).addTile(any(), anyInt())
verify(currentTilesInteractor, never()).removeTiles(any())
assertThat(autoAddedTiles).doesNotContain(SPEC)
}
@Test
fun autoAddable_trackIfNotAdded_removeSignal_noop() =
testScope.runTest {
val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
runCurrent()
underTest = createInteractor(setOf(fakeAutoAddable))
fakeAutoAddable.sendRemoveSignal(USER)
runCurrent()
verify(currentTilesInteractor, never()).addTile(any(), anyInt())
verify(currentTilesInteractor, never()).removeTiles(any())
}
@Test
fun autoAddable_trackIfNotAdded_addSignalWhenPreviouslyAdded_noop() =
testScope.runTest {
val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
autoAddRepository.markTileAdded(USER, SPEC)
runCurrent()
underTest = createInteractor(setOf(fakeAutoAddable))
fakeAutoAddable.sendAddSignal(USER)
runCurrent()
verify(currentTilesInteractor, never()).addTile(any(), anyInt())
verify(currentTilesInteractor, never()).removeTiles(any())
}
@Test
fun autoAddable_trackIfNotAdded_addSignal_addedAndMarked() =
testScope.runTest {
val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
underTest = createInteractor(setOf(fakeAutoAddable))
val position = 3
fakeAutoAddable.sendAddSignal(USER, position)
runCurrent()
verify(currentTilesInteractor).addTile(SPEC, position)
assertThat(autoAddedTiles).contains(SPEC)
}
private fun createInteractor(autoAddables: Set<AutoAddable>): AutoAddInteractor {
return AutoAddInteractor(
autoAddables,
autoAddRepository,
dumpManager,
logger,
testScope.backgroundScope
)
.apply { init(currentTilesInteractor) }
}
companion object {
private val SPEC = TileSpec.create("spec")
private val USER = 10
}
}

View File

@@ -83,6 +83,8 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
@Mock private lateinit var customTileStatePersister: CustomTileStatePersister @Mock private lateinit var customTileStatePersister: CustomTileStatePersister
@Mock private lateinit var autoAddInteractor: AutoAddInteractor
@Mock private lateinit var userTracker: UserTracker @Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var logger: QSPipelineLogger @Mock private lateinit var logger: QSPipelineLogger
@@ -100,6 +102,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true) featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
featureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, true)
userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1)) userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
@@ -107,6 +110,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
underTest = underTest =
CurrentTilesInteractorImpl( CurrentTilesInteractorImpl(
autoAddInteractor = autoAddInteractor,
tileSpecRepository = tileSpecRepository, tileSpecRepository = tileSpecRepository,
installedTilesComponentRepository = installedTilesPackageRepository, installedTilesComponentRepository = installedTilesPackageRepository,
userRepository = userRepository, userRepository = userRepository,
@@ -647,6 +651,11 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC) assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC)
} }
@Test
fun autoAddInteractor_initted() {
verify(autoAddInteractor).init(underTest)
}
private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) { private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
this.state = state this.state = state
this.label = label this.label = label

View File

@@ -0,0 +1,42 @@
/*
* 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.pipeline.data.repository
import com.android.systemui.qs.pipeline.shared.TileSpec
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeAutoAddRepository : AutoAddRepository {
private val autoAddedTilesPerUser = mutableMapOf<Int, MutableStateFlow<Set<TileSpec>>>()
override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
return getFlow(userId)
}
override suspend fun markTileAdded(userId: Int, spec: TileSpec) {
if (spec == TileSpec.Invalid) return
with(getFlow(userId)) { value = value.toMutableSet().apply { add(spec) } }
}
override suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) {
with(getFlow(userId)) { value = value.toMutableSet().apply { remove(spec) } }
}
private fun getFlow(userId: Int): MutableStateFlow<Set<TileSpec>> =
autoAddedTilesPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
}