Merge "Create QS tile component and modules." into main
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 a65967a..8f26e69 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.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 @@
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 14de5eb..8db6ab2 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.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.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 @@
/**
* 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 @@
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 @@
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 @@
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 @@
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 @@
emit(DataUpdateTrigger.InitialRequest)
qsTileLogger.logInitialRequest(spec)
}
- tileDataInteractor
+ tileDataInteractor()
.tileData(userId, updateTriggers)
.cancellable()
.flowOn(backgroundDispatcher)
@@ -242,23 +190,25 @@
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 @@
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 0000000..71cf228
--- /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 d0809c5..0a6becd 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 @@
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 @@
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/viewmodel/QSTileLifecycle.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
similarity index 68%
copy from packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
index 6d7c576..94b39b6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
@@ -14,9 +14,16 @@
* limitations under the License.
*/
-package com.android.systemui.qs.tiles.viewmodel
+package com.android.systemui.qs.tiles.di
-enum class QSTileLifecycle {
- ALIVE,
- DEAD,
-}
+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 0000000..22c7309
--- /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 0000000..a28a441
--- /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 0000000..f7bec02
--- /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 0000000..6c1c1a3
--- /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 0000000..01df906
--- /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 0000000..ccff8af
--- /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 0000000..558fb64
--- /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 0000000..e33b3e9
--- /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/viewmodel/QSTileLifecycle.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt
index 6d7c576..4a4ba2b 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/CustomTileBoundScope.kt
@@ -14,9 +14,16 @@
* 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.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
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
rename to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt
index 6d7c576..efc7431 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 0000000..a65b2a0
--- /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 0000000..eafbb7d
--- /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 e5cb7ea..debcc5d 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 @@
*/
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 @@
/** 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 33f55ab..72663be 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 @@
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 9b85012..8c1e477 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 @@
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 @@
assertThat(fakeQSTileDataInteractor.dataRequests).isNotEmpty()
assertThat(fakeQSTileDataInteractor.dataRequests.first())
- .isEqualTo(FakeQSTileDataInteractor.DataRequest(1))
+ .isEqualTo(FakeQSTileDataInteractor.DataRequest(0))
}
private fun createViewModel(
@@ -95,12 +92,14 @@
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,