diff options
20 files changed, 618 insertions, 155 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index a65967a0349b..8f26e694a067 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -31,6 +31,7 @@ import com.android.systemui.qs.ReduceBrightColorsController; import com.android.systemui.qs.external.QSExternalModule; import com.android.systemui.qs.pipeline.dagger.QSPipelineModule; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.di.QSTilesModule; import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.ManagedProfileController; @@ -60,17 +61,22 @@ import javax.inject.Named; QSFlagsModule.class, QSHostModule.class, QSPipelineModule.class, + QSTilesModule.class, } ) public interface QSModule { - /** A map of internal QS tiles. Ensures that this can be injected even if - * it is empty */ + /** + * A map of internal QS tiles. Ensures that this can be injected even if + * it is empty + */ @Multibinds Map<String, QSTileImpl<?>> tileMap(); - /** A map of internal QS tile ViewModels. Ensures that this can be injected even if - * it is empty */ + /** + * A map of internal QS tile ViewModels. Ensures that this can be injected even if + * it is empty + */ @Multibinds Map<String, QSTileViewModel> tileViewModelMap(); 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 14de5eb8be7f..8db6ab2d9459 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 @@ -17,9 +17,6 @@ package com.android.systemui.qs.tiles.base.viewmodel 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.DataUpdateTrigger @@ -30,7 +27,6 @@ import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor 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 import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction @@ -38,13 +34,11 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.throttle import com.android.systemui.util.time.SystemClock -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -66,19 +60,17 @@ import kotlinx.coroutines.flow.stateIn /** * Provides a hassle-free way to implement new tiles according to current System UI architecture - * standards. THis ViewModel is cheap to instantiate and does nothing until it's moved to - * [QSTileLifecycle.ALIVE] state. + * standards. This ViewModel is cheap to instantiate and does nothing until its [state] is listened. * - * Inject [BaseQSTileViewModel.Factory] to create a new instance of this class. + * Don't use this constructor directly. Instead, inject [QSViewModelFactory] to create a new + * instance of this class. */ @OptIn(ExperimentalCoroutinesApi::class) -class BaseQSTileViewModel<DATA_TYPE> -@VisibleForTesting -constructor( - override val config: QSTileConfig, - private val userActionInteractor: QSTileUserActionInteractor<DATA_TYPE>, - private val tileDataInteractor: QSTileDataInteractor<DATA_TYPE>, - private val mapper: QSTileDataToStateMapper<DATA_TYPE>, +class BaseQSTileViewModel<DATA_TYPE>( + val tileConfig: () -> QSTileConfig, + private val userActionInteractor: () -> QSTileUserActionInteractor<DATA_TYPE>, + private val tileDataInteractor: () -> QSTileDataInteractor<DATA_TYPE>, + private val mapper: () -> QSTileDataToStateMapper<DATA_TYPE>, private val disabledByPolicyInteractor: DisabledByPolicyInteractor, userRepository: UserRepository, private val falsingManager: FalsingManager, @@ -86,37 +78,9 @@ constructor( private val qsTileLogger: QSTileLogger, private val systemClock: SystemClock, private val backgroundDispatcher: CoroutineDispatcher, - private val tileScope: CoroutineScope, + private val tileScope: CoroutineScope = CoroutineScope(SupervisorJob()), ) : QSTileViewModel { - @AssistedInject - constructor( - @Assisted config: QSTileConfig, - @Assisted userActionInteractor: QSTileUserActionInteractor<DATA_TYPE>, - @Assisted tileDataInteractor: QSTileDataInteractor<DATA_TYPE>, - @Assisted mapper: QSTileDataToStateMapper<DATA_TYPE>, - disabledByPolicyInteractor: DisabledByPolicyInteractor, - userRepository: UserRepository, - falsingManager: FalsingManager, - qsTileAnalytics: QSTileAnalytics, - qsTileLogger: QSTileLogger, - systemClock: SystemClock, - @Background backgroundDispatcher: CoroutineDispatcher, - ) : this( - config, - userActionInteractor, - tileDataInteractor, - mapper, - disabledByPolicyInteractor, - userRepository, - falsingManager, - qsTileAnalytics, - qsTileLogger, - systemClock, - backgroundDispatcher, - CoroutineScope(SupervisorJob()) - ) - private val userIds: MutableStateFlow<Int> = MutableStateFlow(userRepository.getSelectedUserInfo().id) private val userInputs: MutableSharedFlow<QSTileUserAction> = @@ -126,12 +90,26 @@ constructor( private val spec get() = config.tileSpec - private lateinit var tileData: SharedFlow<DATA_TYPE> + private val tileData: SharedFlow<DATA_TYPE> = createTileDataFlow() - override lateinit var state: SharedFlow<QSTileState> + override val config + get() = tileConfig() + override val state: SharedFlow<QSTileState> = + tileData + .map { data -> + mapper().map(config, data).also { state -> + qsTileLogger.logStateUpdate(spec, state, data) + } + } + .flowOn(backgroundDispatcher) + .shareIn( + tileScope, + SharingStarted.WhileSubscribed(), + replay = 1, + ) override val isAvailable: StateFlow<Boolean> = userIds - .flatMapLatest { tileDataInteractor.availability(it) } + .flatMapLatest { tileDataInteractor().availability(it) } .flowOn(backgroundDispatcher) .stateIn( tileScope, @@ -139,24 +117,18 @@ constructor( true, ) - private var currentLifeState: QSTileLifecycle = QSTileLifecycle.DEAD - @CallSuper override fun forceUpdate() { - Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE) forceUpdates.tryEmit(Unit) } @CallSuper override fun onUserIdChanged(userId: Int) { - Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE) userIds.tryEmit(userId) } @CallSuper override fun onActionPerformed(userAction: QSTileUserAction) { - Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE) - qsTileLogger.logUserAction( userAction, spec, @@ -166,32 +138,8 @@ constructor( userInputs.tryEmit(userAction) } - @CallSuper - override fun onLifecycle(lifecycle: QSTileLifecycle) { - when (lifecycle) { - QSTileLifecycle.ALIVE -> { - Preconditions.checkState(currentLifeState == QSTileLifecycle.DEAD) - tileData = createTileDataFlow() - state = - tileData - .map { data -> - mapper.map(config, data).also { state -> - qsTileLogger.logStateUpdate(spec, state, data) - } - } - .flowOn(backgroundDispatcher) - .shareIn( - tileScope, - SharingStarted.WhileSubscribed(), - replay = 1, - ) - } - QSTileLifecycle.DEAD -> { - Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE) - tileScope.coroutineContext.cancelChildren() - } - } - currentLifeState = lifecycle + override fun destroy() { + tileScope.cancel() } private fun createTileDataFlow(): SharedFlow<DATA_TYPE> = @@ -208,7 +156,7 @@ constructor( emit(DataUpdateTrigger.InitialRequest) qsTileLogger.logInitialRequest(spec) } - tileDataInteractor + tileDataInteractor() .tileData(userId, updateTriggers) .cancellable() .flowOn(backgroundDispatcher) @@ -242,23 +190,25 @@ constructor( DataUpdateTrigger.UserInput(QSTileInput(userId, action, data)) } - .onEach { userActionInteractor.handleInput(it.input) } + .onEach { userActionInteractor().handleInput(it.input) } .flowOn(backgroundDispatcher) } private fun Flow<QSTileUserAction>.filterByPolicy(userId: Int): Flow<QSTileUserAction> = - when (config.policy) { - is QSTilePolicy.NoRestrictions -> this - is QSTilePolicy.Restricted -> - filter { action -> - val result = - disabledByPolicyInteractor.isDisabled(userId, config.policy.userRestriction) - !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled -> - if (isDisabled) { - qsTileLogger.logUserActionRejectedByPolicy(action, spec) + config.policy.let { policy -> + when (policy) { + is QSTilePolicy.NoRestrictions -> this@filterByPolicy + is QSTilePolicy.Restricted -> + filter { action -> + val result = + disabledByPolicyInteractor.isDisabled(userId, policy.userRestriction) + !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled -> + if (isDisabled) { + qsTileLogger.logUserActionRejectedByPolicy(action, spec) + } } } - } + } } private fun Flow<QSTileUserAction>.filterFalseActions(): Flow<QSTileUserAction> = @@ -279,30 +229,4 @@ constructor( private companion object { const val CLICK_THROTTLE_DURATION = 200L } - - /** - * Factory interface for assisted inject. Dagger has bad time supporting generics in assisted - * injection factories now. That's why you need to create an interface implementing this one and - * annotate it with [dagger.assisted.AssistedFactory]. - * - * ex: @AssistedFactory interface FooFactory : BaseQSTileViewModel.Factory<FooData> - */ - interface Factory<T> { - - /** - * @param config contains all the static information (like TileSpec) about the tile. - * @param userActionInteractor encapsulates user input processing logic. Use it to start - * activities, show dialogs or otherwise update the tile state. - * @param tileDataInteractor provides [DATA_TYPE] and its availability. - * @param mapper maps [DATA_TYPE] to the [QSTileState] that is then displayed by the View - * layer. It's called in [backgroundDispatcher], so it's safe to perform long running - * operations there. - */ - fun create( - config: QSTileConfig, - userActionInteractor: QSTileUserActionInteractor<T>, - tileDataInteractor: QSTileDataInteractor<T>, - mapper: QSTileDataToStateMapper<T>, - ): BaseQSTileViewModel<T> - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSViewModelFactory.kt new file mode 100644 index 000000000000..71cf228481ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSViewModelFactory.kt @@ -0,0 +1,125 @@ +/* + * 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.viewmodel + +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.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.base.logging.QSTileLogger +import com.android.systemui.qs.tiles.impl.di.QSTileComponent +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher + +/** + * Factory to create an appropriate [BaseQSTileViewModel] instance depending on your circumstances. + * + * @see [QSViewModelFactory.Component] + * @see [QSViewModelFactory.Static] + */ +sealed interface QSViewModelFactory<T> { + + /** + * This factory allows you to pass an instance of [QSTileComponent] to a view model effectively + * binding them together. This achieves a DI scope that lives along the instance of + * [BaseQSTileViewModel]. + */ + class Component<T> + @Inject + constructor( + private val disabledByPolicyInteractor: DisabledByPolicyInteractor, + private val userRepository: UserRepository, + private val falsingManager: FalsingManager, + private val qsTileAnalytics: QSTileAnalytics, + private val qsTileLogger: QSTileLogger, + private val systemClock: SystemClock, + @Background private val backgroundDispatcher: CoroutineDispatcher, + ) : QSViewModelFactory<T> { + + /** + * Creates [BaseQSTileViewModel] based on the interactors obtained from [component]. + * Reference of that [component] is then stored along the view model. + */ + fun create(component: QSTileComponent<T>): BaseQSTileViewModel<T> = + BaseQSTileViewModel( + component::config, + component::userActionInteractor, + component::dataInteractor, + component::dataToStateMapper, + disabledByPolicyInteractor, + userRepository, + falsingManager, + qsTileAnalytics, + qsTileLogger, + systemClock, + backgroundDispatcher, + ) + } + + /** + * This factory passes by necessary implementations to the [BaseQSTileViewModel]. This is a + * default choice for most of the tiles. + */ + class Static<T> + @Inject + constructor( + private val disabledByPolicyInteractor: DisabledByPolicyInteractor, + private val userRepository: UserRepository, + private val falsingManager: FalsingManager, + private val qsTileAnalytics: QSTileAnalytics, + private val qsTileLogger: QSTileLogger, + private val systemClock: SystemClock, + @Background private val backgroundDispatcher: CoroutineDispatcher, + ) : QSViewModelFactory<T> { + + /** + * @param config contains all the static information (like TileSpec) about the tile. + * @param userActionInteractor encapsulates user input processing logic. Use it to start + * activities, show dialogs or otherwise update the tile state. + * @param tileDataInteractor provides [DATA_TYPE] and its availability. + * @param mapper maps [DATA_TYPE] to the [QSTileState] that is then displayed by the View + * layer. It's called in [backgroundDispatcher], so it's safe to perform long running + * operations there. + */ + fun create( + config: QSTileConfig, + userActionInteractor: QSTileUserActionInteractor<T>, + tileDataInteractor: QSTileDataInteractor<T>, + mapper: QSTileDataToStateMapper<T>, + ): BaseQSTileViewModel<T> = + BaseQSTileViewModel( + { config }, + { userActionInteractor }, + { tileDataInteractor }, + { mapper }, + disabledByPolicyInteractor, + userRepository, + falsingManager, + qsTileAnalytics, + qsTileLogger, + systemClock, + backgroundDispatcher, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt index d0809c52acd9..0a6becd6e4ca 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt @@ -19,7 +19,6 @@ package com.android.systemui.qs.tiles.di import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.qs.QSFactory import com.android.systemui.plugins.qs.QSTile -import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel import com.android.systemui.qs.tiles.viewmodel.QSTileViewModelAdapter import javax.inject.Inject @@ -38,7 +37,6 @@ constructor( override fun createTile(tileSpec: String): QSTile? = tileMap[tileSpec]?.let { val tile = it.get() - tile.onLifecycle(QSTileLifecycle.ALIVE) adapterFactory.create(tile) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt new file mode 100644 index 000000000000..94b39b6db9d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt @@ -0,0 +1,29 @@ +/* + * 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.di + +import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent +import dagger.Module + +/** Module listing subcomponents */ +@Module( + subcomponents = + [ + CustomTileComponent::class, + ] +) +interface QSTilesModule diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt new file mode 100644 index 000000000000..22c7309d45a6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt @@ -0,0 +1,35 @@ +/* + * 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.impl.custom + +import android.content.ComponentName +import android.graphics.drawable.Icon +import android.service.quicksettings.Tile +import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent + +data class CustomTileData( + val userId: Int, + val componentName: ComponentName, + val tile: Tile, + val callingAppUid: Int, + val isActive: Boolean, + val hasPendingBind: Boolean, + val shouldShowChevron: Boolean, + val defaultTileLabel: CharSequence?, + val defaultTileIcon: Icon?, + val component: CustomTileBoundComponent, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt new file mode 100644 index 000000000000..a28a441ca7fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt @@ -0,0 +1,35 @@ +/* + * 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.impl.custom + +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.di.QSTileScope +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +@QSTileScope +class CustomTileInteractor @Inject constructor() : QSTileDataInteractor<CustomTileData> { + + override fun tileData(userId: Int, triggers: Flow<DataUpdateTrigger>): Flow<CustomTileData> { + TODO("Not yet implemented") + } + + override fun availability(userId: Int): Flow<Boolean> { + TODO("Not yet implemented") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt new file mode 100644 index 000000000000..f7bec024b7bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt @@ -0,0 +1,31 @@ +/* + * 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.impl.custom + +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.di.QSTileScope +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import javax.inject.Inject + +@QSTileScope +class CustomTileMapper @Inject constructor() : QSTileDataToStateMapper<CustomTileData> { + + override fun map(config: QSTileConfig, data: CustomTileData): QSTileState { + TODO("Not yet implemented") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt new file mode 100644 index 000000000000..6c1c1a34abc0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt @@ -0,0 +1,31 @@ +/* + * 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.impl.custom + +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.di.QSTileScope +import javax.inject.Inject + +@QSTileScope +class CustomTileUserActionInteractor @Inject constructor() : + QSTileUserActionInteractor<CustomTileData> { + + override suspend fun handleInput(input: QSTileInput<CustomTileData>) { + TODO("Not yet implemented") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt new file mode 100644 index 000000000000..01df90662579 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt @@ -0,0 +1,34 @@ +/* + * 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.impl.custom.di + +import com.android.systemui.qs.tiles.impl.di.QSTileComponent +import com.android.systemui.qs.tiles.impl.di.QSTileScope +import dagger.Subcomponent + +@QSTileScope +@Subcomponent(modules = [QSTileConfigModule::class, CustomTileModule::class]) +interface CustomTileComponent : QSTileComponent<Any> { + + @Subcomponent.Builder + interface Builder { + + fun qsTileConfigModule(module: QSTileConfigModule): Builder + + fun build(): CustomTileComponent + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt new file mode 100644 index 000000000000..ccff8afe7628 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt @@ -0,0 +1,46 @@ +/* + * 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.impl.custom.di + +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.custom.CustomTileData +import com.android.systemui.qs.tiles.impl.custom.CustomTileInteractor +import com.android.systemui.qs.tiles.impl.custom.CustomTileMapper +import com.android.systemui.qs.tiles.impl.custom.CustomTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent +import dagger.Binds +import dagger.Module + +/** Provides bindings for QSTile interfaces */ +@Module(subcomponents = [CustomTileBoundComponent::class]) +interface CustomTileModule { + + @Binds + fun bindDataInteractor( + dataInteractor: CustomTileInteractor + ): QSTileDataInteractor<CustomTileData> + + @Binds + fun bindUserActionInteractor( + userActionInteractor: CustomTileUserActionInteractor + ): QSTileUserActionInteractor<CustomTileData> + + @Binds + fun bindMapper(customTileMapper: CustomTileMapper): QSTileDataToStateMapper<CustomTileData> +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/QSTileConfigModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/QSTileConfigModule.kt new file mode 100644 index 000000000000..558fb64e3ef8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/QSTileConfigModule.kt @@ -0,0 +1,43 @@ +/* + * 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.impl.custom.di + +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import dagger.Module +import dagger.Provides + +/** + * Provides [QSTileConfig] and [TileSpec]. To be used along in a QS tile scoped component + * implementing [com.android.systemui.qs.tiles.impl.di.QSTileComponent]. In that case it makes it + * possible to inject config and tile spec associated with the current tile + */ +@Module +class QSTileConfigModule(private val config: QSTileConfig) { + + @Provides fun provideConfig(): QSTileConfig = config + + @Provides fun provideTileSpec(): TileSpec = config.tileSpec + + @Provides + fun provideCustomTileSpec(): TileSpec.CustomTileSpec = + config.tileSpec as TileSpec.CustomTileSpec + + @Provides + fun providePlatformTileSpec(): TileSpec.PlatformTileSpec = + config.tileSpec as TileSpec.PlatformTileSpec +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt new file mode 100644 index 000000000000..e33b3e917629 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt @@ -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.qs.tiles.impl.custom.di.bound + +import android.os.UserHandle +import dagger.BindsInstance +import dagger.Subcomponent +import kotlinx.coroutines.CoroutineScope + +/** @see CustomTileBoundScope */ +@CustomTileBoundScope +@Subcomponent +interface CustomTileBoundComponent { + + @Subcomponent.Builder + interface Builder { + @BindsInstance fun user(@CustomTileUser user: UserHandle): Builder + @BindsInstance fun coroutineScope(@CustomTileBoundScope scope: CoroutineScope): Builder + + fun build(): CustomTileBoundComponent + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt new file mode 100644 index 000000000000..4a4ba2bf7ef9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt @@ -0,0 +1,29 @@ +/* + * 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.impl.custom.di.bound + +import javax.inject.Scope + +/** + * Scope annotation for bound custom tile scope. This scope lives when a particular + * [com.android.systemui.qs.external.CustomTile] is listening and bound to the + * [android.service.quicksettings.TileService]. + */ +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Scope +annotation class CustomTileBoundScope diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt index 6d7c57605bec..efc743127418 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt @@ -14,9 +14,12 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.viewmodel +package com.android.systemui.qs.tiles.impl.custom.di.bound -enum class QSTileLifecycle { - ALIVE, - DEAD, -} +import javax.inject.Qualifier + +/** User associated with current custom tile binding. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class CustomTileUser diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt new file mode 100644 index 000000000000..a65b2a063e98 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt @@ -0,0 +1,38 @@ +/* + * 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.impl.di + +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig + +/** + * Base QS tile component. It should be used with [QSTileScope] to create a custom tile scoped + * component. Pass this component to + * [com.android.systemui.qs.tiles.base.viewmodel.QSViewModelFactory.Component]. + */ +interface QSTileComponent<T> { + + fun dataInteractor(): QSTileDataInteractor<T> + + fun userActionInteractor(): QSTileUserActionInteractor<T> + + fun config(): QSTileConfig + + fun dataToStateMapper(): QSTileDataToStateMapper<T> +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileScope.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileScope.kt new file mode 100644 index 000000000000..eafbb7d3523e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileScope.kt @@ -0,0 +1,27 @@ +/* + * 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.impl.di + +import javax.inject.Scope + +/** + * Scope annotation for QS tiles. This scope is created for each tile and is disposed when the tile + * is no longer needed (ex. it's removed from QS). So, it lives along the instance of + * [com.android.systemui.qs.tiles.base.viewmodel.BaseQSTileViewModel]. This doesn't align with tile + * visibility. For example, the tile scope survives shade open/close. + */ +@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class QSTileScope diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt index e5cb7ea3e098..debcc5df11ad 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt @@ -29,27 +29,15 @@ import kotlinx.coroutines.flow.StateFlow */ interface QSTileViewModel { - /** - * State of the tile to be shown by the view. It's guaranteed that it's only accessed between - * [QSTileLifecycle.ALIVE] and [QSTileLifecycle.DEAD]. - */ + /** State of the tile to be shown by the view. */ val state: SharedFlow<QSTileState> val config: QSTileConfig - /** - * Specifies whether this device currently supports this tile. This might be called outside of - * [QSTileLifecycle.ALIVE] and [QSTileLifecycle.DEAD] bounds (for example in Edit Mode). - */ + /** Specifies whether this device currently supports this tile. */ val isAvailable: StateFlow<Boolean> /** - * Handles ViewModel lifecycle. Implementations should be inactive outside of - * [QSTileLifecycle.ALIVE] and [QSTileLifecycle.DEAD] bounds. - */ - fun onLifecycle(lifecycle: QSTileLifecycle) - - /** * Notifies about the user change. Implementations should avoid using 3rd party userId sources * and use this value instead. This is to maintain consistent and concurrency-free behaviour * across different parts of QS. @@ -61,6 +49,12 @@ interface QSTileViewModel { /** Notifies underlying logic about user input. */ fun onActionPerformed(userAction: QSTileUserAction) + + /** + * Frees the resources held by this view model. Call it when you no longer need the instance, + * because there is no guarantee it will work as expected beyond this point. + */ + fun destroy() } /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index 33f55ab53233..72663be6708f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -180,7 +180,7 @@ constructor( override fun destroy() { stateJob?.cancel() availabilityJob?.cancel() - qsTileViewModel.onLifecycle(QSTileLifecycle.DEAD) + qsTileViewModel.destroy() } override fun getState(): QSTile.State? = 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 9b85012b29a9..8c1e4771c299 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 @@ -77,9 +77,6 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { testScope.runTest { assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty() - underTest.onLifecycle(QSTileLifecycle.ALIVE) - underTest.onUserIdChanged(1) - assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty() underTest.state.launchIn(backgroundScope) @@ -87,7 +84,7 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { assertThat(fakeQSTileDataInteractor.dataRequests).isNotEmpty() assertThat(fakeQSTileDataInteractor.dataRequests.first()) - .isEqualTo(FakeQSTileDataInteractor.DataRequest(1)) + .isEqualTo(FakeQSTileDataInteractor.DataRequest(0)) } private fun createViewModel( @@ -95,12 +92,14 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { config: QSTileConfig = TEST_QS_TILE_CONFIG, ): QSTileViewModel = BaseQSTileViewModel( - config, - fakeQSTileUserActionInteractor, - fakeQSTileDataInteractor, - object : QSTileDataToStateMapper<Any> { - override fun map(config: QSTileConfig, data: Any): QSTileState = - QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {} + { config }, + { fakeQSTileUserActionInteractor }, + { fakeQSTileDataInteractor }, + { + object : QSTileDataToStateMapper<Any> { + override fun map(config: QSTileConfig, data: Any): QSTileState = + QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {} + } }, fakeDisabledByPolicyInteractor, fakeUserRepository, |