diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt index c6b3d6ac00..553a620fd8 100644 --- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt @@ -18,7 +18,6 @@ package com.android.quickstep.recents.di import android.content.Context import android.util.Log -import android.view.View import com.android.launcher3.util.coroutines.DispatcherProvider import com.android.launcher3.util.coroutines.ProductionDispatchers import com.android.quickstep.RecentsModel @@ -32,8 +31,6 @@ import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase import com.android.quickstep.recents.viewmodel.RecentsViewData -import com.android.quickstep.task.viewmodel.TaskOverlayViewModel -import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.utilities.PreviewPositionHelper import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope @@ -49,10 +46,12 @@ class RecentsDependencies private constructor(appContext: Context) { } /** - * This function initialised the default scope with RecentsView dependencies. These dependencies - * are used multiple times and should be a singleton to share across Recents classes. + * This function initialises the default scope with RecentsView dependencies. Some dependencies + * are global while others are per-RecentsView. The scope is used to differentiate between + * RecentsViews. */ private fun startDefaultScope(appContext: Context) { + Log.d(TAG, "startDefaultScope") createScope(DEFAULT_SCOPE_ID).apply { set(RecentsViewData::class.java.simpleName, RecentsViewData()) val dispatcherProvider: DispatcherProvider = ProductionDispatchers @@ -86,6 +85,32 @@ class RecentsDependencies private constructor(appContext: Context) { } } + /** + * This function initialises a scope associated with the dependencies of a single RecentsView. + * + * @param viewContext the Context associated with a RecentsView. + * @return the scope id associated with the new RecentsDependenciesScope. + */ + fun createRecentsViewScope(viewContext: Context): String { + val scopeId = viewContext.hashCode().toString() + Log.d(TAG, "createRecentsViewScope $scopeId") + val scope = + createScope(scopeId).apply { + set(RecentsViewData::class.java.simpleName, RecentsViewData()) + val dispatcherProvider: DispatcherProvider = + get(DEFAULT_SCOPE_ID) + val recentsCoroutineScope = + CoroutineScope( + SupervisorJob() + + dispatcherProvider.unconfined + + CoroutineName("RecentsView$scopeId") + ) + set(CoroutineScope::class.java.simpleName, recentsCoroutineScope) + } + scope.linkTo(getScope(DEFAULT_SCOPE_ID)) + return scopeId + } + inline fun inject( scopeId: RecentsScopeId = "", extras: RecentsDependenciesExtras = RecentsDependenciesExtras(), @@ -170,25 +195,14 @@ class RecentsDependencies private constructor(appContext: Context) { log("linked scopes: ${getScope(scopeId).scopeIdsLinked}") val instance: Any = when (modelClass) { - RecentsViewData::class.java -> RecentsViewData() - TaskOverlayViewModel::class.java -> { - val task = extras["Task"] as Task - TaskOverlayViewModel( - task = task, - recentsViewData = inject(), - recentTasksRepository = inject(), - getThumbnailPositionUseCase = inject(), - dispatcherProvider = inject(), - ) - } IsThumbnailValidUseCase::class.java -> - IsThumbnailValidUseCase(rotationStateRepository = inject()) - GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject()) + IsThumbnailValidUseCase(rotationStateRepository = inject(scopeId)) + GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject(scopeId)) GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase() GetThumbnailPositionUseCase::class.java -> GetThumbnailPositionUseCase( - deviceProfileRepository = inject(), - rotationStateRepository = inject(), + deviceProfileRepository = inject(scopeId), + rotationStateRepository = inject(scopeId), previewPositionHelper = PreviewPositionHelper(), ) OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase() @@ -222,58 +236,58 @@ class RecentsDependencies private constructor(appContext: Context) { } companion object { - private const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope" + const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope" private const val TAG = "RecentsDependencies" private const val DEBUG = false - private var activeRecentsCount = 0 - @Volatile private lateinit var instance: RecentsDependencies + @Volatile private var instance: RecentsDependencies? = null - fun initialize(view: View): RecentsDependencies = initialize(view.context) - - fun initialize(context: Context): RecentsDependencies { + private fun initialize(context: Context): RecentsDependencies { Log.d(TAG, "initializing") synchronized(this) { - activeRecentsCount++ - instance = RecentsDependencies(context.applicationContext) + val newInstance = RecentsDependencies(context.applicationContext) + instance = newInstance + return newInstance } - return instance + } + + fun maybeInitialize(context: Context): RecentsDependencies { + return instance ?: initialize(context) } fun getInstance(): RecentsDependencies { - if (!Companion::instance.isInitialized) { - throw UninitializedPropertyAccessException( - "Recents dependencies are not initialized. " + - "Call `RecentsDependencies.initialize` before using this container." - ) - } return instance + ?: throw UninitializedPropertyAccessException( + "Recents dependencies are not initialized. " + + "Call `RecentsDependencies.maybeInitialize` before using this container." + ) } @JvmStatic - fun destroy() { - // When Launcher Activity restarts, the old view's RecentsView.onDetachedFromWindow - // happens after the new view's creation. This means that destroy can be called after a - // new initialisation. This check prevents a newly initialised tree from being - // destroyed. Ideally we would have 1 instance of the dependency tree for each - // RecentsView. - // - // This check is sufficient to avoid a leak of the dependency tree after the Activity is - // destroyed while also allowing Launcher auto-restarts (production behaviour) to easily - // reinitialise the dependency tree. - // - // TODO(b/353917593): Better lifecycle decisions will be implemented in this bug or when - // replacing with Dagger (b/371370483). - activeRecentsCount-- - if (activeRecentsCount == 0) { - instance.scopes.clear() - Log.d(TAG, "destroyed", Exception("Printing stack trace")) - } else { - Log.d( - TAG, - "RecentsDependencies was not destroyed. " + - "There is still an active RecentsView instance.", - ) + fun destroy(viewContext: Context) { + synchronized(this) { + val localInstance = instance ?: return + val scopeId = viewContext.hashCode().toString() + val scope = localInstance.scopes[scopeId] + if (scope == null) { + Log.e( + TAG, + "Trying to destroy an unknown scope. Scopes: ${localInstance.scopes.size}", + ) + return + } + scope.close() + localInstance.scopes.remove(scopeId) + if (DEBUG) { + Log.d(TAG, "destroyed $scopeId", Exception("Printing stack trace")) + } else { + Log.d(TAG, "destroyed $scopeId") + } + if (localInstance.scopes.size == 1) { + // Only the default scope left - destroy this too. + instance = null + Log.d(TAG, "also destroyed default scope") + } } } } diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt index f51660bd70..d4f567e9d7 100644 --- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt +++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt @@ -43,8 +43,9 @@ import kotlinx.coroutines.launch * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM. */ class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) { - private val recentsCoroutineScope: CoroutineScope = RecentsDependencies.get() - private val dispatcherProvider: DispatcherProvider = RecentsDependencies.get() + private val scope = overlay.taskView.context + private val recentsCoroutineScope: CoroutineScope = RecentsDependencies.get(scope) + private val dispatcherProvider: DispatcherProvider = RecentsDependencies.get(scope) private lateinit var overlayInitializedScope: CoroutineScope private var uiState: TaskOverlayUiState = Disabled @@ -75,10 +76,10 @@ class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOver viewModel = TaskOverlayViewModel( task = task, - recentsViewData = RecentsDependencies.get(), - getThumbnailPositionUseCase = RecentsDependencies.get(), - recentTasksRepository = RecentsDependencies.get(), - dispatcherProvider = RecentsDependencies.get(), + recentsViewData = RecentsDependencies.get(scope), + getThumbnailPositionUseCase = RecentsDependencies.get(scope), + recentTasksRepository = RecentsDependencies.get(scope), + dispatcherProvider = RecentsDependencies.get(scope), ) viewModel.overlayState .dropWhile { it == Disabled } diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt index 75f3b69140..e0ea518c09 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -336,7 +336,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu if (enableRefactorTaskThumbnail()) { viewModel = - DesktopTaskViewModel(organizeDesktopTasksUseCase = RecentsDependencies.get()) + DesktopTaskViewModel(organizeDesktopTasksUseCase = RecentsDependencies.get(context)) } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 89726aa791..2c84e52614 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -861,7 +861,6 @@ public abstract class RecentsView< private final Matrix mTmpMatrix = new Matrix(); private int mTaskViewCount = 0; - @Nullable public TaskView getFirstTaskView() { return mUtils.getFirstTaskView(); @@ -881,22 +880,23 @@ public abstract class RecentsView< // Start Recents Dependency graph if (enableRefactorTaskThumbnail()) { - RecentsDependencies recentsDependencies = RecentsDependencies.Companion.initialize( - this); + RecentsDependencies recentsDependencies = RecentsDependencies.Companion.maybeInitialize( + context); + String scopeId = recentsDependencies.createRecentsViewScope(context); mRecentsViewModel = new RecentsViewModel( - recentsDependencies.inject(RecentTasksRepository.class), - recentsDependencies.inject(RecentsViewData.class) + recentsDependencies.inject(RecentTasksRepository.class, scopeId), + recentsDependencies.inject(RecentsViewData.class, scopeId) ); mHelper = new RecentsViewModelHelper( mRecentsViewModel, - recentsDependencies.inject(CoroutineScope.class), - recentsDependencies.inject(DispatcherProvider.class) + recentsDependencies.inject(CoroutineScope.class, scopeId), + recentsDependencies.inject(DispatcherProvider.class, scopeId) ); - recentsDependencies.provide(RecentsRotationStateRepository.class, + recentsDependencies.provide(RecentsRotationStateRepository.class, scopeId, () -> new RecentsRotationStateRepositoryImpl(mOrientationState)); - recentsDependencies.provide(RecentsDeviceProfileRepository.class, + recentsDependencies.provide(RecentsDeviceProfileRepository.class, scopeId, () -> new RecentsDeviceProfileRepositoryImpl(mContainer)); } else { mRecentsViewModel = null; @@ -1283,7 +1283,7 @@ public abstract class RecentsView< Log.e(TAG, "Ongoing initializations could not be killed", e); } mHelper.onDestroy(); - RecentsDependencies.destroy(); + RecentsDependencies.destroy(getContext()); } } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index ab785924dd..86e1305c0a 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -840,6 +840,7 @@ constructor( taskOverlayFactory: TaskOverlayFactory, ) { cancelPendingLoadTasks() + this.orientedState = orientedState // Needed for dependencies taskContainers = listOf( createTaskContainer( @@ -857,15 +858,17 @@ constructor( protected open fun onBind(orientedState: RecentsOrientedState) { if (enableRefactorTaskThumbnail()) { + val scopeId = context + Log.d(TAG, "onBind $scopeId ${orientedState.containerInterface}") viewModel = TaskViewModel( taskViewType = type, - recentsViewData = RecentsDependencies.get(), - getTaskUseCase = RecentsDependencies.get(), - getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(), - isThumbnailValidUseCase = RecentsDependencies.get(), - getThumbnailPositionUseCase = RecentsDependencies.get(), - dispatcherProvider = RecentsDependencies.get(), + recentsViewData = RecentsDependencies.get(scopeId), + getTaskUseCase = RecentsDependencies.get(scopeId), + getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(scopeId), + isThumbnailValidUseCase = RecentsDependencies.get(scopeId), + getThumbnailPositionUseCase = RecentsDependencies.get(scopeId), + dispatcherProvider = RecentsDependencies.get(scopeId), ) .apply { bind(*taskIds) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt index 8d20ba8e7f..42adfec82c 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt @@ -76,12 +76,12 @@ class TaskViewItemInfoTest { context.initDaggerComponent( DaggerTaskViewItemInfoTest_TestComponent.builder().bindUserCache(userCache) ) - RecentsDependencies.initialize(context) + RecentsDependencies.maybeInitialize(context) } @After fun tearDown() { - RecentsDependencies.destroy() + RecentsDependencies.destroy(context) } @Test