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

Commit b8605b77 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Load resources from the correct profile" into main

parents b03d4e6f 83cde0df
Loading
Loading
Loading
Loading
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.common

import android.app.ActivityManager
import android.content.Context
import android.content.pm.UserInfo
import android.os.UserHandle
import android.os.UserManager
import android.util.SparseArray
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener

/** Creates and manages contexts for all the profiles of the current user. */
class UserProfileContexts(
    private val baseContext: Context,
    private val shellController: ShellController,
    shellInit: ShellInit,
) {
    // Contexts for all the profiles of the current user.
    private val currentProfilesContext = SparseArray<Context>()

    lateinit var userContext: Context
        private set

    init {
        shellInit.addInitCallback(this::onInit, this)
    }

    private fun onInit() {
        shellController.addUserChangeListener(
            object : UserChangeListener {
                override fun onUserChanged(newUserId: Int, userContext: Context) {
                    currentProfilesContext.clear()
                    this@UserProfileContexts.userContext = userContext
                    currentProfilesContext.put(newUserId, userContext)
                }

                override fun onUserProfilesChanged(profiles: List<UserInfo>) {
                    updateProfilesContexts(profiles)
                }
            }
        )
        val defaultUserId = ActivityManager.getCurrentUser()
        val userManager = baseContext.getSystemService(UserManager::class.java)
        userContext = baseContext.createContextAsUser(UserHandle.of(defaultUserId), /* flags= */ 0)
        updateProfilesContexts(userManager.getProfiles(defaultUserId))
    }

    private fun updateProfilesContexts(profiles: List<UserInfo>) {
        for (profile in profiles) {
            if (profile.id in currentProfilesContext) continue
            val profileContext = baseContext.createContextAsUser(profile.userHandle, /* flags= */ 0)
            currentProfilesContext.put(profile.id, profileContext)
        }
        val profilesToRemove = buildList<Int> {
            for (i in 0..<currentProfilesContext.size()) {
                val userId = currentProfilesContext.keyAt(i)
                if (profiles.none { it.id == userId }) {
                    add(userId)
                }
            }
        }
        profilesToRemove.forEach { currentProfilesContext.remove(it) }
    }

    operator fun get(userId: Int): Context? = currentProfilesContext.get(userId)
}
+14 −2
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.UserProfileContexts;
import com.android.wm.shell.common.split.SplitState;
import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler;
import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver;
@@ -991,9 +992,10 @@ public abstract class WMShellModule {
    static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader(
            @NonNull Context context, @NonNull ShellInit shellInit,
            @NonNull ShellController shellController,
            @NonNull ShellCommandHandler shellCommandHandler) {
            @NonNull ShellCommandHandler shellCommandHandler,
            @NonNull UserProfileContexts userProfileContexts) {
        return new WindowDecorTaskResourceLoader(context, shellInit, shellController,
                shellCommandHandler);
                shellCommandHandler, userProfileContexts);
    }

    @WMSingleton
@@ -1423,4 +1425,14 @@ public abstract class WMShellModule {
            Transitions transitions, ShellInit shellInit) {
        return new OverviewToDesktopTransitionObserver(transitions, shellInit);
    }

    @WMSingleton
    @Provides
    static UserProfileContexts provideUserProfilesContexts(
            Context context,
            ShellController shellController,
            ShellInit shellInit) {
        return new UserProfileContexts(context, shellController, shellInit);
    }

}
+18 −19
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@
package com.android.wm.shell.windowdecor.common

import android.annotation.DimenRes
import android.app.ActivityManager
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
import android.content.pm.ActivityInfo
@@ -29,6 +28,7 @@ import com.android.launcher3.icons.BaseIconFactory
import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT
import com.android.launcher3.icons.IconProvider
import com.android.wm.shell.R
import com.android.wm.shell.common.UserProfileContexts
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
@@ -41,10 +41,10 @@ import java.util.concurrent.ConcurrentHashMap
 * A utility and cache for window decoration UI resources.
 */
class WindowDecorTaskResourceLoader(
    private val context: Context,
    shellInit: ShellInit,
    private val shellController: ShellController,
    private val shellCommandHandler: ShellCommandHandler,
    private val userProfilesContexts: UserProfileContexts,
    private val iconProvider: IconProvider,
    private val headerIconFactory: BaseIconFactory,
    private val veilIconFactory: BaseIconFactory,
@@ -54,11 +54,12 @@ class WindowDecorTaskResourceLoader(
        shellInit: ShellInit,
        shellController: ShellController,
        shellCommandHandler: ShellCommandHandler,
        userProfileContexts: UserProfileContexts,
    ) : this(
        context,
        shellInit,
        shellController,
        shellCommandHandler,
        userProfileContexts,
        IconProvider(context),
        headerIconFactory = context.createIconFactory(R.dimen.desktop_mode_caption_icon_radius),
        veilIconFactory = context.createIconFactory(R.dimen.desktop_mode_resize_veil_icon_size),
@@ -79,9 +80,6 @@ class WindowDecorTaskResourceLoader(
     */
    private val existingTasks = mutableSetOf<Int>()

    @VisibleForTesting
    lateinit var currentUserContext: Context

    init {
        shellInit.addInitCallback(this::onInit, this)
    }
@@ -90,14 +88,10 @@ class WindowDecorTaskResourceLoader(
        shellCommandHandler.addDumpCallback(this::dump, this)
        shellController.addUserChangeListener(object : UserChangeListener {
            override fun onUserChanged(newUserId: Int, userContext: Context) {
                currentUserContext = userContext
                // No need to hold on to resources for tasks of another profile.
                taskToResourceCache.clear()
            }
        })
        currentUserContext = context.createContextAsUser(
            UserHandle.of(ActivityManager.getCurrentUser()), /* flags= */ 0
        )
    }

    /** Returns the user readable name for this task. */
@@ -158,15 +152,20 @@ class WindowDecorTaskResourceLoader(

    private fun loadAppResources(taskInfo: RunningTaskInfo): AppResources {
        Trace.beginSection("$TAG#loadAppResources")
        val pm = currentUserContext.packageManager
        try {
            val pm = checkNotNull(userProfilesContexts[taskInfo.userId]?.packageManager) {
                "Could not get context for user ${taskInfo.userId}"
            }
            val activityInfo = getActivityInfo(taskInfo, pm)
            val appName = pm.getApplicationLabel(activityInfo.applicationInfo)
            val appIconDrawable = iconProvider.getIcon(activityInfo)
            val badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, taskInfo.userHandle())
            val appIcon = headerIconFactory.createIconBitmap(badgedAppIconDrawable, /* scale= */ 1f)
            val veilIcon = veilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)
        Trace.endSection()
            return AppResources(appName = appName, appIcon = appIcon, veilIcon = veilIcon)
        } finally {
            Trace.endSection()
        }
    }

    private fun getActivityInfo(taskInfo: RunningTaskInfo, pm: PackageManager): ActivityInfo {
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.common

import android.app.ActivityManager
import android.content.Context
import android.content.pm.UserInfo
import android.os.UserHandle
import android.os.UserManager
import android.testing.AndroidTestingRunner
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyInt
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

/**
 * Tests for [UserProfileContexts].
 */
@RunWith(AndroidTestingRunner::class)
class UserProfileContextsTest : ShellTestCase() {

    private val testExecutor = TestShellExecutor()
    private val shellInit = ShellInit(testExecutor)
    private val activityManager = mock<ActivityManager>()
    private val userManager = mock<UserManager>()
    private val shellController = mock<ShellController>()
    private val baseContext = mock<Context>()

    private lateinit var userProfilesContexts: UserProfileContexts

    @Before
    fun setUp() {
        doReturn(activityManager)
            .whenever(baseContext)
            .getSystemService(eq(ActivityManager::class.java))
        doReturn(userManager).whenever(baseContext).getSystemService(eq(UserManager::class.java))
        doAnswer { invocation ->
                val userHandle = invocation.getArgument<UserHandle>(0)
                createContextForUser(userHandle.identifier)
            }
            .whenever(baseContext)
            .createContextAsUser(any<UserHandle>(), anyInt())
        // Define users and profiles
        val currentUser = ActivityManager.getCurrentUser()
        whenever(userManager.getProfiles(eq(currentUser)))
            .thenReturn(
                listOf(UserInfo(currentUser, "Current", 0), UserInfo(MAIN_PROFILE, "Work", 0))
            )
        whenever(userManager.getProfiles(eq(SECOND_USER))).thenReturn(SECOND_PROFILES)
        userProfilesContexts = UserProfileContexts(baseContext, shellController, shellInit)
        shellInit.init()
    }

    @Test
    fun onInit_registerUserChangeAndInit() {
        val currentUser = ActivityManager.getCurrentUser()

        verify(shellController, times(1)).addUserChangeListener(any())
        assertThat(userProfilesContexts.userContext.userId).isEqualTo(currentUser)
        assertThat(userProfilesContexts[currentUser]?.userId).isEqualTo(currentUser)
        assertThat(userProfilesContexts[MAIN_PROFILE]?.userId).isEqualTo(MAIN_PROFILE)
        assertThat(userProfilesContexts[SECOND_USER]).isNull()
    }

    @Test
    fun onUserChanged_updateUserContext() {
        val userChangeListener = retrieveUserChangeListener()
        val newUserContext = createContextForUser(SECOND_USER)

        userChangeListener.onUserChanged(SECOND_USER, newUserContext)

        assertThat(userProfilesContexts.userContext).isEqualTo(newUserContext)
        assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newUserContext)
    }

    @Test
    fun onUserProfilesChanged_updateAllContexts() {
        val userChangeListener = retrieveUserChangeListener()
        val newUserContext = createContextForUser(SECOND_USER)
        userChangeListener.onUserChanged(SECOND_USER, newUserContext)

        userChangeListener.onUserProfilesChanged(SECOND_PROFILES)

        assertThat(userProfilesContexts.userContext).isEqualTo(newUserContext)
        assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newUserContext)
        assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE)
        assertThat(userProfilesContexts[SECOND_PROFILE_2]?.userId).isEqualTo(SECOND_PROFILE_2)
    }

    @Test
    fun onUserProfilesChanged_keepOnlyNewProfiles() {
        val userChangeListener = retrieveUserChangeListener()
        val newUserContext = createContextForUser(SECOND_USER)
        userChangeListener.onUserChanged(SECOND_USER, newUserContext)
        userChangeListener.onUserProfilesChanged(SECOND_PROFILES)
        val newProfiles = listOf(
            UserInfo(SECOND_USER, "Second", 0),
            UserInfo(SECOND_PROFILE, "Second Profile", 0),
            UserInfo(MAIN_PROFILE, "Main profile", 0),
        )

        userChangeListener.onUserProfilesChanged(newProfiles)

        assertThat(userProfilesContexts[SECOND_PROFILE_2]).isNull()
        assertThat(userProfilesContexts[MAIN_PROFILE]?.userId).isEqualTo(MAIN_PROFILE)
        assertThat(userProfilesContexts[SECOND_USER]?.userId).isEqualTo(SECOND_USER)
        assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE)
    }

    private fun retrieveUserChangeListener(): UserChangeListener {
        val captor = argumentCaptor<UserChangeListener>()

        verify(shellController, times(1)).addUserChangeListener(captor.capture())

        return captor.firstValue
    }

    private fun createContextForUser(userId: Int): Context {
        val newContext = mock<Context>()
        whenever(newContext.userId).thenReturn(userId)
        return newContext
    }

    private companion object {
        const val SECOND_USER = 3
        const val MAIN_PROFILE = 11
        const val SECOND_PROFILE = 15
        const val SECOND_PROFILE_2 = 17

        val SECOND_PROFILES =
            listOf(
                UserInfo(SECOND_USER, "Second", 0),
                UserInfo(SECOND_PROFILE, "Second Profile", 0),
                UserInfo(SECOND_PROFILE_2, "Second Profile 2", 0),
            )
    }
}
+4 −11
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.launcher3.icons.IconProvider
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.common.UserProfileContexts
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener
@@ -69,6 +70,7 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
    private val mockIconProvider = mock<IconProvider>()
    private val mockHeaderIconFactory = mock<BaseIconFactory>()
    private val mockVeilIconFactory = mock<BaseIconFactory>()
    private val mMockUserProfileContexts = mock<UserProfileContexts>()

    private lateinit var spyContext: TestableContext
    private lateinit var loader: WindowDecorTaskResourceLoader
@@ -83,12 +85,13 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
        spyContext = spy(mContext)
        spyContext.setMockPackageManager(mockPackageManager)
        doReturn(spyContext).whenever(spyContext).createContextAsUser(any(), anyInt())
        doReturn(spyContext).whenever(mMockUserProfileContexts)[anyInt()]
        loader =
            WindowDecorTaskResourceLoader(
                context = spyContext,
                shellInit = shellInit,
                shellController = mockShellController,
                shellCommandHandler = mock(),
                userProfilesContexts = mMockUserProfileContexts,
                iconProvider = mockIconProvider,
                headerIconFactory = mockHeaderIconFactory,
                veilIconFactory = mockVeilIconFactory,
@@ -169,16 +172,6 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
        verifyZeroInteractions(mockPackageManager, mockIconProvider, mockVeilIconFactory)
    }

    @Test
    fun testUserChange_updatesContext() {
        val newUser = 5000
        val newContext = mock<Context>()

        userChangeListener.onUserChanged(newUser, newContext)

        assertThat(loader.currentUserContext).isEqualTo(newContext)
    }

    @Test
    fun testUserChange_clearsCache() {
        val newUser = 5000