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,