Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 426f7436 authored by Jordan Silva's avatar Jordan Silva
Browse files

Introducing Manual DI for Overview

This CL adds the RecentsDependencyContainer to maintain singletons and dependencies related to RecentsView. RecentsDependencies is a singleton that requires the application context to be initialized. Unlike regular singletons, this class has an initialize function where it gets initialized and can be retrieved with getInstance without providing the appContext each time.

- We've updated the refactored classes behind the -enable_refactor_task_thumbnail flag to use this DI solution rather than relying on .parent or recreating dependencies.

- To inject dependencies, you can use RecentsDependencies.inject for lazy initialization or RecentsDependencies.get for eager initialization.

- At the moment, we don't have a singleton or factory definition. All dependencies created by RecentsDependencies will be stored in a specific scope, making the instance a singleton within that scope.

- You can create or retrieve a dependency in a particular scope by calling RecentsDependencies.inject(scopeId).

- If you don't need the dependency to be stored in RecentsDependencies, you can create it manually in your code and inject the necessary parameters from RecentsDependencies (see the viewModel in TaskOverlayHelper).

- Handling the cleaning/resetting of dependencies will be addressed in b/353917593. RecentsView lifecycle is more complex and doesn't get recreated every time. We need to determine which dependencies or scopes can be destroyed and recreated.

Fix: 349055024
Test: NONE
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Change-Id: I27b92e3038f1cce0fd53b637dba5054c05b40283
parent 2495e980
Loading
Loading
Loading
Loading
+251 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.quickstep.recents.di

import android.content.Context
import android.util.Log
import android.view.View
import com.android.quickstep.RecentsModel
import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.data.TasksRepository
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.quickstep.task.viewmodel.TaskViewModel
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
import java.util.logging.Level

internal typealias RecentsScopeId = String

class RecentsDependencies private constructor(private val appContext: Context) {
    private val scopes = mutableMapOf<RecentsScopeId, RecentsDependenciesScope>()

    init {
        startDefaultScope(appContext)
    }

    /**
     * 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.
     */
    private fun startDefaultScope(appContext: Context) {
        createScope(DEFAULT_SCOPE_ID).apply {
            set(RecentsViewData::class.java.simpleName, RecentsViewData())

            // Create RecentsTaskRepository singleton
            val recentTasksRepository: RecentTasksRepository =
                with(RecentsModel.INSTANCE.get(appContext)) {
                    TasksRepository(this, thumbnailCache, iconCache)
                }
            set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
        }
    }

    inline fun <reified T> inject(
        scopeId: RecentsScopeId = "",
        extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
        noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
    ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory)

    @Suppress("UNCHECKED_CAST")
    @JvmOverloads
    fun <T> inject(
        modelClass: Class<T>,
        scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
        extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
        factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
    ): T {
        val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID }
        val scope = scopes[currentScopeId] ?: createScope(currentScopeId)

        log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO)
        var instance: T?
        synchronized(this) {
            instance = getDependency(scope, modelClass)
            log("found instance? $instance", Log.INFO)
            if (instance == null) {
                instance =
                    factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras)
                scope[modelClass.simpleName] = instance!!
            }
        }
        return instance!!
    }

    inline fun <reified T> provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T =
        provide(T::class.java, scopeId = scopeId, factory = factory)

    @JvmOverloads
    fun <T> provide(
        modelClass: Class<T>,
        scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
        factory: () -> T,
    ) = inject(modelClass, scopeId, factory = { factory.invoke() })

    private fun <T> getDependency(scope: RecentsDependenciesScope, modelClass: Class<T>): T? {
        var instance: T? = scope[modelClass.simpleName] as T?
        if (instance == null) {
            instance =
                scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId ->
                    getScope(scopeId)[modelClass.simpleName]
                } as T?
        }
        if (instance != null) log("Found dependency: $instance", Log.INFO)
        return instance
    }

    fun getScope(scope: Any): RecentsDependenciesScope {
        val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
        return getScope(scopeId)
    }

    fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope =
        scopes[scopeId] ?: createScope(scopeId)

    // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely.
    //  Each class should be responsible for providing a factory function to create a new instance.
    @Suppress("UNCHECKED_CAST")
    private fun <T> createDependency(
        modelClass: Class<T>,
        scopeId: RecentsScopeId,
        extras: RecentsDependenciesExtras,
    ): T {
        log("createDependency ${modelClass.simpleName} with $scopeId and $extras", Log.WARN)
        val instance: Any =
            when (modelClass) {
                RecentTasksRepository::class.java -> {
                    with(RecentsModel.INSTANCE.get(appContext)) {
                        TasksRepository(this, thumbnailCache, iconCache)
                    }
                }
                RecentsViewData::class.java -> RecentsViewData()
                TaskViewModel::class.java -> TaskViewModel(taskViewData = inject(scopeId, extras))
                TaskViewData::class.java -> {
                    val taskViewType = extras["TaskViewType"] as TaskViewType
                    TaskViewData(taskViewType)
                }
                TaskContainerData::class.java -> TaskContainerData()
                TaskThumbnailViewModel::class.java ->
                    TaskThumbnailViewModel(
                        recentsViewData = inject(),
                        taskViewData = inject(scopeId, extras),
                        taskContainerData = inject(),
                        getThumbnailPositionUseCase = inject(),
                        tasksRepository = inject()
                    )
                TaskOverlayViewModel::class.java -> {
                    val task = extras["Task"] as Task
                    TaskOverlayViewModel(
                        task = task,
                        recentsViewData = inject(),
                        recentTasksRepository = inject(),
                        getThumbnailPositionUseCase = inject()
                    )
                }
                GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
                GetThumbnailPositionUseCase::class.java ->
                    GetThumbnailPositionUseCase(
                        deviceProfileRepository = inject(),
                        rotationStateRepository = inject(),
                        tasksRepository = inject()
                    )
                else -> {
                    log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
                    error("Factory for ${modelClass.simpleName} not defined!")
                }
            }
        return instance as T
    }

    private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
        return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it }
    }

    private fun log(message: String, @Log.Level level: Int = Log.DEBUG) {
        if (DEBUG) {
            when (level) {
                Log.WARN -> Log.w(TAG, message)
                Log.VERBOSE -> Log.v(TAG, message)
                Log.INFO -> Log.i(TAG, message)
                Log.ERROR -> Log.e(TAG, message)
                else -> Log.d(TAG, message)
            }
        }
    }

    companion object {
        private const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope"
        private const val TAG = "RecentsDependencies"
        private const val DEBUG = false

        @Volatile private lateinit var instance: RecentsDependencies

        fun initialize(view: View): RecentsDependencies = initialize(view.context)

        fun initialize(context: Context): RecentsDependencies {
            synchronized(this) {
                if (!Companion::instance.isInitialized) {
                    instance = RecentsDependencies(context.applicationContext)
                }
            }
            return instance
        }

        fun getInstance(): RecentsDependencies {
            if (!Companion::instance.isInitialized) {
                throw UninitializedPropertyAccessException(
                    "Recents dependencies are not initialized. " +
                        "Call `RecentsDependencies.initialize` before using this container."
                )
            }
            return instance
        }

        fun destroy() {
            instance.scopes.clear()
            instance.startDefaultScope(instance.appContext)
        }
    }
}

inline fun <reified T> RecentsDependencies.Companion.inject(
    scope: Any = "",
    vararg extras: Pair<String, Any>,
    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
): Lazy<T> = lazy { get(scope, RecentsDependenciesExtras(extras), factory) }

inline fun <reified T> RecentsDependencies.Companion.get(
    scope: Any = "",
    extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
): T {
    val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
    return getInstance().inject(scopeId, extras, factory)
}

inline fun <reified T> RecentsDependencies.Companion.get(
    scope: Any = "",
    vararg extras: Pair<String, Any>,
    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
): T = get(scope, RecentsDependenciesExtras(extras), factory)

fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId)
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.quickstep.recents.di

data class RecentsDependenciesExtras(private val data: MutableMap<String, Any> = mutableMapOf()) {
    constructor(value: Array<out Pair<String, Any>>) : this(value.toMap().toMutableMap())

    operator fun get(key: String) = data[key]

    operator fun set(key: String, value: Any) {
        data[key] = value
    }
}
+71 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.quickstep.recents.di

import android.util.Log

class RecentsDependenciesScope(
    val scopeId: RecentsScopeId,
    private val dependencies: MutableMap<String, Any> = mutableMapOf(),
    private val scopeIds: MutableList<RecentsScopeId> = mutableListOf()
) {
    val scopeIdsLinked: List<RecentsScopeId>
        get() = scopeIds.toList()

    operator fun get(identifier: String): Any? {
        log("get $identifier")
        return dependencies[identifier]
    }

    operator fun set(key: String, value: Any) {
        synchronized(this) {
            log("set $key")
            dependencies[key] = value
        }
    }

    fun remove(key: String): Any? {
        synchronized(this) {
            log("remove $key")
            return dependencies.remove(key)
        }
    }

    fun linkTo(scope: RecentsDependenciesScope) {
        log("linking to ${scope.scopeId}")
        scopeIds += scope.scopeId
    }

    fun close() {
        log("reset")
        synchronized(this) { dependencies.clear() }
    }

    private fun log(message: String) {
        if (DEBUG) Log.d(TAG, "[scopeId=$scopeId] $message")
    }

    override fun toString(): String =
        "scopeId: $scopeId" +
            "\n dependencies: ${dependencies.map { "${it.key}=${it.value}" }.joinToString(", ")}" +
            "\n linked to: ${scopeIds.joinToString(", ")}"

    private companion object {
        private const val TAG = "RecentsDependenciesScope"
        private const val DEBUG = false
    }
}
+48 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.quickstep.recents.viewmodel

import com.android.quickstep.recents.data.RecentTasksRepository

class RecentsViewModel(
    private val recentsTasksRepository: RecentTasksRepository,
    private val recentsViewData: RecentsViewData
) {
    fun refreshAllTaskData() {
        recentsTasksRepository.getAllTaskData(true)
    }

    fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
        recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
    }

    fun updateScale(scale: Float) {
        recentsViewData.scale.value = scale
    }

    fun updateFullscreenProgress(fullscreenProgress: Float) {
        recentsViewData.fullscreenProgress.value = fullscreenProgress
    }

    fun updateTasksFullyVisible(taskIds: Set<Int>) {
        recentsViewData.settledFullyVisibleTaskIds.value = taskIds
    }

    fun setOverlayEnabled(isOverlayEnabled: Boolean) {
        recentsViewData.overlayEnabled.value = isOverlayEnabled
    }
}
+5 −23
Original line number Diff line number Diff line
@@ -31,15 +31,14 @@ import androidx.core.view.isVisible
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.util.ViewPool
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.inject
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.util.TaskCornerRadius
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.RecentsViewContainer
import com.android.quickstep.views.TaskView
import com.android.systemui.shared.system.QuickStepContract
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -50,25 +49,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
    // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
    //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
    //  This is using a lazy for now because the dependencies cannot be obtained without DI.
    val viewModel by lazy {
        val recentsView =
            RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
                .getOverviewPanel<RecentsView<*, *>>()
        TaskThumbnailViewModel(
            recentsView.mRecentsViewData!!,
            (parent as TaskView).taskViewData,
            (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
            recentsView.mTasksRepository!!,
            GetThumbnailPositionUseCase(
                recentsView.mDeviceProfileRepository!!,
                recentsView.mOrientedStateRepository!!,
                recentsView.mTasksRepository!!
            )
        )
    }

    private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)

    private lateinit var viewAttachedScope: CoroutineScope

Loading