1 /*
2 * 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 package com.android.wm.shell.windowdecor.common
17
18 import android.annotation.DimenRes
19 import android.app.ActivityManager.RunningTaskInfo
20 import android.content.Context
21 import android.content.pm.ActivityInfo
22 import android.content.pm.PackageManager
23 import android.graphics.Bitmap
24 import android.os.LocaleList
25 import android.os.UserHandle
26 import androidx.tracing.Trace
27 import com.android.internal.annotations.VisibleForTesting
28 import com.android.launcher3.icons.BaseIconFactory
29 import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT
30 import com.android.launcher3.icons.IconProvider
31 import com.android.wm.shell.R
32 import com.android.wm.shell.common.UserProfileContexts
33 import com.android.wm.shell.shared.annotations.ShellBackgroundThread
34 import com.android.wm.shell.sysui.ShellCommandHandler
35 import com.android.wm.shell.sysui.ShellController
36 import com.android.wm.shell.sysui.ShellInit
37 import com.android.wm.shell.sysui.UserChangeListener
38 import java.io.PrintWriter
39 import java.util.concurrent.ConcurrentHashMap
40
41 /**
42 * A utility and cache for window decoration UI resources.
43 */
44 class WindowDecorTaskResourceLoader(
45 shellInit: ShellInit,
46 private val shellController: ShellController,
47 private val shellCommandHandler: ShellCommandHandler,
48 private val userProfilesContexts: UserProfileContexts,
49 private val iconProvider: IconProvider,
50 private val headerIconFactory: BaseIconFactory,
51 private val veilIconFactory: BaseIconFactory,
52 ) {
53 constructor(
54 context: Context,
55 shellInit: ShellInit,
56 shellController: ShellController,
57 shellCommandHandler: ShellCommandHandler,
58 userProfileContexts: UserProfileContexts,
59 ) : this(
60 shellInit,
61 shellController,
62 shellCommandHandler,
63 userProfileContexts,
64 IconProvider(context),
65 headerIconFactory = context.createIconFactory(R.dimen.desktop_mode_caption_icon_radius),
66 veilIconFactory = context.createIconFactory(R.dimen.desktop_mode_resize_veil_icon_size),
67 )
68
69 /**
70 * A map of task -> resources to prevent unnecessary binder calls and resource loading
71 * when multiple window decorations need the same resources, for example, the app name or icon
72 * used in the header and menu.
73 */
74 @VisibleForTesting
75 val taskToResourceCache = ConcurrentHashMap<Int, AppResources>()
76 /**
77 * Keeps track of existing tasks with a window decoration. Useful to verify that requests to
78 * get resources occur within the lifecycle of a window decoration, otherwise it'd be possible
79 * to load a tasks resources into memory without a future signal to clean up the resource.
80 * See [onWindowDecorClosed].
81 */
82 private val existingTasks = mutableSetOf<Int>()
83
84 /**
85 * A map of task -> localeList to keep track of the language of app name that's currently
86 * cached in |taskToResourceCache|.
87 */
88 @VisibleForTesting
89 val localeListOnCache = ConcurrentHashMap<Int, LocaleList>()
90
91 init {
92 shellInit.addInitCallback(this::onInit, this)
93 }
94
onInitnull95 private fun onInit() {
96 shellCommandHandler.addDumpCallback(this::dump, this)
97 shellController.addUserChangeListener(object : UserChangeListener {
98 override fun onUserChanged(newUserId: Int, userContext: Context) {
99 // No need to hold on to resources for tasks of another profile.
100 taskToResourceCache.clear()
101 }
102 })
103 }
104
105 /** Returns the user readable name for this task. */
106 @ShellBackgroundThread
getNamenull107 fun getName(taskInfo: RunningTaskInfo): CharSequence {
108 checkWindowDecorExists(taskInfo)
109 val cachedResources = taskToResourceCache[taskInfo.taskId]
110 val localeListActiveOnCacheTime = localeListOnCache[taskInfo.taskId]
111 if (cachedResources != null &&
112 taskInfo.getConfiguration().getLocales().equals(localeListActiveOnCacheTime)) {
113 return cachedResources.appName
114 }
115 val resources = loadAppResources(taskInfo)
116 taskToResourceCache[taskInfo.taskId] = resources
117 localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
118 return resources.appName
119 }
120
121 /** Returns the icon for use by the app header and menus for this task. */
122 @ShellBackgroundThread
getHeaderIconnull123 fun getHeaderIcon(taskInfo: RunningTaskInfo): Bitmap {
124 checkWindowDecorExists(taskInfo)
125 val cachedResources = taskToResourceCache[taskInfo.taskId]
126 if (cachedResources != null) {
127 return cachedResources.appIcon
128 }
129 val resources = loadAppResources(taskInfo)
130 taskToResourceCache[taskInfo.taskId] = resources
131 localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
132 return resources.appIcon
133 }
134
135 /** Returns the icon for use by the resize veil for this task. */
136 @ShellBackgroundThread
getVeilIconnull137 fun getVeilIcon(taskInfo: RunningTaskInfo): Bitmap {
138 checkWindowDecorExists(taskInfo)
139 val cachedResources = taskToResourceCache[taskInfo.taskId]
140 if (cachedResources != null) {
141 return cachedResources.veilIcon
142 }
143 val resources = loadAppResources(taskInfo)
144 taskToResourceCache[taskInfo.taskId] = resources
145 localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
146 return resources.veilIcon
147 }
148
149 /** Called when a window decoration for this task is created. */
onWindowDecorCreatednull150 fun onWindowDecorCreated(taskInfo: RunningTaskInfo) {
151 existingTasks.add(taskInfo.taskId)
152 }
153
154 /** Called when a window decoration for this task is closed. */
onWindowDecorClosednull155 fun onWindowDecorClosed(taskInfo: RunningTaskInfo) {
156 existingTasks.remove(taskInfo.taskId)
157 taskToResourceCache.remove(taskInfo.taskId)
158 localeListOnCache.remove(taskInfo.taskId)
159 }
160
checkWindowDecorExistsnull161 private fun checkWindowDecorExists(taskInfo: RunningTaskInfo) {
162 check(existingTasks.contains(taskInfo.taskId)) {
163 "Attempt to obtain resource for non-existent decoration"
164 }
165 }
166
loadAppResourcesnull167 private fun loadAppResources(taskInfo: RunningTaskInfo): AppResources {
168 Trace.beginSection("$TAG#loadAppResources")
169 try {
170 val pm = userProfilesContexts.getOrCreate(taskInfo.userId).packageManager
171 val activityInfo = getActivityInfo(taskInfo, pm)
172 val appName = pm.getApplicationLabel(activityInfo.applicationInfo)
173 val appIconDrawable = iconProvider.getIcon(activityInfo)
174 val badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, taskInfo.userHandle())
175 val appIcon = headerIconFactory.createIconBitmap(badgedAppIconDrawable, /* scale= */ 1f)
176 val veilIcon = veilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)
177 return AppResources(appName = appName, appIcon = appIcon, veilIcon = veilIcon)
178 } finally {
179 Trace.endSection()
180 }
181 }
182
getActivityInfonull183 private fun getActivityInfo(taskInfo: RunningTaskInfo, pm: PackageManager): ActivityInfo {
184 return pm.getActivityInfo(taskInfo.component(), /* flags= */ 0)
185 }
186
componentnull187 private fun RunningTaskInfo.component() = baseIntent.component!!
188
189 private fun RunningTaskInfo.userHandle() = UserHandle.of(userId)
190
191 data class AppResources(val appName: CharSequence, val appIcon: Bitmap, val veilIcon: Bitmap)
192
193 private fun dump(pw: PrintWriter, prefix: String) {
194 val innerPrefix = "$prefix "
195 pw.println("${prefix}$TAG")
196 pw.println(innerPrefix + "appResourceCache=$taskToResourceCache")
197 pw.println(innerPrefix + "existingTasks=$existingTasks")
198 }
199
200 companion object {
201 private const val TAG = "AppResourceProvider"
202 }
203 }
204
205 /** Creates an icon factory with the provided [dimensions]. */
Contextnull206 fun Context.createIconFactory(@DimenRes dimensions: Int): BaseIconFactory {
207 val densityDpi = resources.displayMetrics.densityDpi
208 val iconSize = resources.getDimensionPixelSize(dimensions)
209 return BaseIconFactory(this, densityDpi, iconSize)
210 }
211