1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.quickstep.recents.di
18
19 import android.content.Context
20 import android.util.Log
21 import com.android.launcher3.util.coroutines.DispatcherProvider
22 import com.android.launcher3.util.coroutines.ProductionDispatchers
23 import com.android.quickstep.RecentsModel
24 import com.android.quickstep.recents.data.RecentTasksRepository
25 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate
26 import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl
27 import com.android.quickstep.recents.data.TasksRepository
28 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
29 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
30 import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
31 import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
32 import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
33 import com.android.quickstep.recents.viewmodel.RecentsViewData
34 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper.PreviewPositionHelperFactory
35 import kotlinx.coroutines.CoroutineName
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.SupervisorJob
38
39 internal typealias RecentsScopeId = String
40
41 class RecentsDependencies private constructor(appContext: Context) {
42 private val scopes = mutableMapOf<RecentsScopeId, RecentsDependenciesScope>()
43
44 init {
45 startDefaultScope(appContext)
46 }
47
48 /**
49 * This function initialises the default scope with RecentsView dependencies. Some dependencies
50 * are global while others are per-RecentsView. The scope is used to differentiate between
51 * RecentsViews.
52 */
53 private fun startDefaultScope(appContext: Context) {
54 Log.d(TAG, "startDefaultScope")
55 createScope(DEFAULT_SCOPE_ID).apply {
56 set(RecentsViewData::class.java.simpleName, RecentsViewData())
57 val dispatcherProvider: DispatcherProvider = ProductionDispatchers
58 val recentsCoroutineScope =
59 CoroutineScope(
60 SupervisorJob() + dispatcherProvider.unconfined + CoroutineName("RecentsView")
61 )
62 set(CoroutineScope::class.java.simpleName, recentsCoroutineScope)
63 set(DispatcherProvider::class.java.simpleName, dispatcherProvider)
64 val recentsModel = RecentsModel.INSTANCE.get(appContext)
65 val taskVisualsChangedDelegate =
66 TaskVisualsChangedDelegateImpl(
67 recentsModel,
68 recentsModel.thumbnailCache.highResLoadingState,
69 )
70 set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate)
71
72 // Create RecentsTaskRepository singleton
73 val recentTasksRepository: RecentTasksRepository =
74 with(recentsModel) {
75 TasksRepository(
76 this,
77 thumbnailCache,
78 iconCache,
79 taskVisualsChangedDelegate,
80 recentsCoroutineScope,
81 ProductionDispatchers,
82 )
83 }
84 set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
85 }
86 }
87
88 /**
89 * This function initialises a scope associated with the dependencies of a single RecentsView.
90 *
91 * @param viewContext the Context associated with a RecentsView.
92 * @return the scope id associated with the new RecentsDependenciesScope.
93 */
94 fun createRecentsViewScope(viewContext: Context): String {
95 val scopeId = viewContext.hashCode().toString()
96 Log.d(TAG, "createRecentsViewScope $scopeId")
97 val scope =
98 createScope(scopeId).apply {
99 set(RecentsViewData::class.java.simpleName, RecentsViewData())
100 val dispatcherProvider: DispatcherProvider =
101 get<DispatcherProvider>(DEFAULT_SCOPE_ID)
102 val recentsCoroutineScope =
103 CoroutineScope(
104 SupervisorJob() +
105 dispatcherProvider.unconfined +
106 CoroutineName("RecentsView$scopeId")
107 )
108 set(CoroutineScope::class.java.simpleName, recentsCoroutineScope)
109 }
110 scope.linkTo(getScope(DEFAULT_SCOPE_ID))
111 return scopeId
112 }
113
114 inline fun <reified T> inject(
115 scopeId: RecentsScopeId = "",
116 extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
117 noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
118 ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory)
119
120 @Suppress("UNCHECKED_CAST")
121 @JvmOverloads
122 fun <T> inject(
123 modelClass: Class<T>,
124 scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
125 extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
126 factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
127 ): T {
128 val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID }
129 val scope = scopes[currentScopeId] ?: createScope(currentScopeId)
130
131 log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO)
132 var instance: T?
133 synchronized(this) {
134 instance = getDependency(scope, modelClass)
135 log("found instance? $instance", Log.INFO)
136 if (instance == null) {
137 instance =
138 factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras)
139 scope[modelClass.simpleName] = instance!!
140 log(
141 "instance of $modelClass" +
142 " (${instance.hashCode()}) added to scope ${scope.scopeId}"
143 )
144 }
145 }
146 return instance!!
147 }
148
149 inline fun <reified T> provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T =
150 provide(T::class.java, scopeId = scopeId, factory = factory)
151
152 @JvmOverloads
153 fun <T> provide(
154 modelClass: Class<T>,
155 scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
156 factory: () -> T,
157 ) = inject(modelClass, scopeId, factory = { factory.invoke() })
158
159 private fun <T> getDependency(scope: RecentsDependenciesScope, modelClass: Class<T>): T? {
160 var instance: T? = scope[modelClass.simpleName] as T?
161 if (instance == null) {
162 instance =
163 scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId ->
164 getScope(scopeId)[modelClass.simpleName]
165 } as T?
166 }
167 if (instance != null) log("Found dependency: $instance", Log.INFO)
168 return instance
169 }
170
171 fun getScope(scope: Any): RecentsDependenciesScope {
172 val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
173 return getScope(scopeId)
174 }
175
176 fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope =
177 scopes[scopeId] ?: createScope(scopeId)
178
179 fun removeScope(scope: Any) {
180 val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
181 scopes[scopeId]?.close()
182 scopes.remove(scopeId)
183 log("Scope $scopeId removed")
184 }
185
186 // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely.
187 // Each class should be responsible for providing a factory function to create a new instance.
188 @Suppress("UNCHECKED_CAST")
189 private fun <T> createDependency(
190 modelClass: Class<T>,
191 scopeId: RecentsScopeId,
192 extras: RecentsDependenciesExtras,
193 ): T {
194 log("createDependency ${modelClass.simpleName} with $scopeId and $extras started", Log.WARN)
195 log("linked scopes: ${getScope(scopeId).scopeIdsLinked}")
196 val instance: Any =
197 when (modelClass) {
198 IsThumbnailValidUseCase::class.java ->
199 IsThumbnailValidUseCase(rotationStateRepository = inject(scopeId))
200 GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject(scopeId))
201 GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase()
202 GetThumbnailPositionUseCase::class.java ->
203 GetThumbnailPositionUseCase(
204 deviceProfileRepository = inject(scopeId),
205 rotationStateRepository = inject(scopeId),
206 previewPositionHelperFactory = PreviewPositionHelperFactory(),
207 )
208 OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
209 else -> {
210 log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
211 error("Factory for ${modelClass.simpleName} not defined!")
212 }
213 }
214 return (instance as T).also {
215 log(
216 "createDependency ${modelClass.simpleName} with $scopeId and $extras completed",
217 Log.WARN,
218 )
219 }
220 }
221
222 private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
223 return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it }
224 }
225
226 private fun log(message: String, @Log.Level level: Int = Log.DEBUG) {
227 if (DEBUG) {
228 when (level) {
229 Log.WARN -> Log.w(TAG, message)
230 Log.VERBOSE -> Log.v(TAG, message)
231 Log.INFO -> Log.i(TAG, message)
232 Log.ERROR -> Log.e(TAG, message)
233 else -> Log.d(TAG, message)
234 }
235 }
236 }
237
238 companion object {
239 const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope"
240 private const val TAG = "RecentsDependencies"
241 private const val DEBUG = false
242
243 @Volatile private var instance: RecentsDependencies? = null
244
245 private fun initialize(context: Context): RecentsDependencies {
246 Log.d(TAG, "initializing")
247 synchronized(this) {
248 val newInstance = RecentsDependencies(context.applicationContext)
249 instance = newInstance
250 return newInstance
251 }
252 }
253
254 fun maybeInitialize(context: Context): RecentsDependencies {
255 return instance ?: initialize(context)
256 }
257
258 fun getInstance(): RecentsDependencies {
259 return instance
260 ?: throw UninitializedPropertyAccessException(
261 "Recents dependencies are not initialized. " +
262 "Call `RecentsDependencies.maybeInitialize` before using this container."
263 )
264 }
265
266 @JvmStatic
267 fun destroy(viewContext: Context) {
268 synchronized(this) {
269 val localInstance = instance ?: return
270 val scopeId = viewContext.hashCode().toString()
271 val scope = localInstance.scopes[scopeId]
272 if (scope == null) {
273 Log.e(
274 TAG,
275 "Trying to destroy an unknown scope. Scopes: ${localInstance.scopes.size}",
276 )
277 return
278 }
279 scope.close()
280 localInstance.scopes.remove(scopeId)
281 if (DEBUG) {
282 Log.d(TAG, "destroyed $scopeId", Exception("Printing stack trace"))
283 } else {
284 Log.d(TAG, "destroyed $scopeId")
285 }
286 if (localInstance.scopes.size == 1) {
287 // Only the default scope left - destroy this too.
288 instance = null
289 Log.d(TAG, "also destroyed default scope")
290 }
291 }
292 }
293 }
294 }
295
injectnull296 inline fun <reified T> RecentsDependencies.Companion.inject(
297 scope: Any = "",
298 vararg extras: Pair<String, Any>,
299 noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
300 ): Lazy<T> = lazy { get(scope, RecentsDependenciesExtras(extras), factory) }
301
getnull302 inline fun <reified T> RecentsDependencies.Companion.get(
303 scope: Any = "",
304 extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
305 noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
306 ): T {
307 val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
308 return getInstance().inject(scopeId, extras, factory)
309 }
310
getnull311 inline fun <reified T> RecentsDependencies.Companion.get(
312 scope: Any = "",
313 vararg extras: Pair<String, Any>,
314 noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
315 ): T = get(scope, RecentsDependenciesExtras(extras), factory)
316
317 fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId)
318