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