Make separate dependency scopes and graphs for different RecentsViews

Test: locally tested on Tangor
Flag: EXEMPT - bugfix
Bug: 399364544

Change-Id: I788c5644f198ead206bb7512ec96eab838e51cdb
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index c6b3d6a..553a620 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 @@
 
 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.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 @@
     }
 
     /**
-     * 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 @@
         }
     }
 
+    /**
+     * 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<DispatcherProvider>(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 <reified T> inject(
         scopeId: RecentsScopeId = "",
         extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
@@ -170,25 +195,14 @@
         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 @@
     }
 
     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 f51660b..d4f567e 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 @@
  * 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 @@
         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 75f3b69..e0ea518 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -336,7 +336,7 @@
 
         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 44bf82c..e0b1466 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -865,7 +865,6 @@
     private final Matrix mTmpMatrix = new Matrix();
 
     private int mTaskViewCount = 0;
-
     @Nullable
     public TaskView getFirstTaskView() {
         return mUtils.getFirstTaskView();
@@ -885,22 +884,23 @@
 
         // 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;
@@ -1288,7 +1288,7 @@
                 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 276318c..f1d6c7c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -835,6 +835,7 @@
         taskOverlayFactory: TaskOverlayFactory,
     ) {
         cancelPendingLoadTasks()
+        this.orientedState = orientedState // Needed for dependencies
         taskContainers =
             listOf(
                 createTaskContainer(
@@ -852,15 +853,17 @@
 
     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 8d20ba8..42adfec 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 @@
         context.initDaggerComponent(
             DaggerTaskViewItemInfoTest_TestComponent.builder().bindUserCache(userCache)
         )
-        RecentsDependencies.initialize(context)
+        RecentsDependencies.maybeInitialize(context)
     }
 
     @After
     fun tearDown() {
-        RecentsDependencies.destroy()
+        RecentsDependencies.destroy(context)
     }
 
     @Test