1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.desktopmode 18 19 import android.app.ActivityManager 20 import android.content.Context 21 import android.content.pm.UserInfo 22 import android.os.UserManager 23 import android.util.SparseArray 24 import android.window.DesktopExperienceFlags 25 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM 26 import androidx.core.util.forEach 27 import com.android.internal.protolog.ProtoLog 28 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository 29 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer 30 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 31 import com.android.wm.shell.shared.annotations.ShellMainThread 32 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus 33 import com.android.wm.shell.sysui.ShellController 34 import com.android.wm.shell.sysui.ShellInit 35 import com.android.wm.shell.sysui.UserChangeListener 36 import java.io.PrintWriter 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.launch 39 40 /** Manages per-user DesktopRepository instances. */ 41 class DesktopUserRepositories( 42 context: Context, 43 shellInit: ShellInit, 44 private val shellController: ShellController, 45 private val persistentRepository: DesktopPersistentRepository, 46 private val repositoryInitializer: DesktopRepositoryInitializer, 47 @ShellMainThread private val mainCoroutineScope: CoroutineScope, 48 private val userManager: UserManager, 49 ) : UserChangeListener { 50 private var userId: Int 51 private var userIdToProfileIdsMap: MutableMap<Int, List<Int>> = mutableMapOf() 52 53 // TODO(b/357060209): Add caching for this logic to improve efficiency. 54 val current: DesktopRepository 55 get() = desktopRepoByUserId.getOrCreate(userId) 56 57 private val desktopRepoByUserId = 58 object : SparseArray<DesktopRepository>() { 59 /** Gets [DesktopRepository] for existing [userId] or creates a new one. */ 60 fun getOrCreate(userId: Int): DesktopRepository = 61 this[userId] 62 ?: DesktopRepository(persistentRepository, mainCoroutineScope, userId).also { 63 this[userId] = it 64 } 65 } 66 67 init { 68 userId = ActivityManager.getCurrentUser() 69 if (DesktopModeStatus.canEnterDesktopMode(context)) { 70 shellInit.addInitCallback(::onInit, this) 71 } 72 if ( 73 ENABLE_DESKTOP_WINDOWING_HSUM.isTrue() && 74 !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue 75 ) { 76 userIdToProfileIdsMap[userId] = userManager.getProfiles(userId).map { it.id } 77 } 78 } 79 80 private fun onInit() { 81 repositoryInitializer.initialize(this) 82 shellController.addUserChangeListener(this) 83 if ( 84 ENABLE_DESKTOP_WINDOWING_HSUM.isTrue() && 85 DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue 86 ) { 87 userId = ActivityManager.getCurrentUser() 88 userIdToProfileIdsMap[userId] = userManager.getProfiles(userId).map { it.id } 89 } 90 } 91 92 /** Returns [DesktopRepository] for the parent user id. */ 93 fun getProfile(profileId: Int): DesktopRepository { 94 if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { 95 for ((uid, profileIds) in userIdToProfileIdsMap) { 96 if (profileId in profileIds) { 97 return desktopRepoByUserId.getOrCreate(uid) 98 } 99 } 100 } 101 return desktopRepoByUserId.getOrCreate(profileId) 102 } 103 104 fun getUserIdForProfile(profileId: Int): Int { 105 if (userIdToProfileIdsMap[userId]?.contains(profileId) == true) return userId 106 else return profileId 107 } 108 109 /** Dumps [DesktopRepository] for each user. */ 110 fun dump(pw: PrintWriter, prefix: String) { 111 val innerPrefix = "$prefix " 112 pw.println("${prefix}DesktopUserRepositories:") 113 pw.println("${innerPrefix}currentUserId=$userId") 114 desktopRepoByUserId.forEach { key, value -> value.dump(pw, innerPrefix) } 115 } 116 117 override fun onUserChanged(newUserId: Int, userContext: Context) { 118 logD("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId) 119 userId = newUserId 120 if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { 121 sanitizeUsers() 122 } 123 } 124 125 override fun onUserProfilesChanged(profiles: MutableList<UserInfo>) { 126 logD("onUserProfilesChanged profiles=%s", profiles.toString()) 127 if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { 128 // TODO(b/366397912): Remove all persisted profile data when the profile changes. 129 userIdToProfileIdsMap[userId] = profiles.map { it.id } 130 } 131 } 132 133 private fun sanitizeUsers() { 134 val aliveUserIds = userManager.getAliveUsers().map { it.id } 135 val usersToDelete = userIdToProfileIdsMap.keys.filterNot { it in aliveUserIds } 136 137 usersToDelete.forEach { uid -> 138 userIdToProfileIdsMap.remove(uid) 139 desktopRepoByUserId.remove(uid) 140 } 141 mainCoroutineScope.launch { 142 try { 143 persistentRepository.removeUsers(usersToDelete) 144 } catch (exception: Exception) { 145 logE( 146 "An exception occurred while updating the persistent repository \n%s", 147 exception.stackTrace, 148 ) 149 } 150 } 151 } 152 153 private fun logD(msg: String, vararg arguments: Any?) { 154 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 155 } 156 157 private fun logE(msg: String, vararg arguments: Any?) { 158 ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 159 } 160 161 companion object { 162 private const val TAG = "DesktopUserRepositories" 163 } 164 } 165