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 package com.android.wm.shell.common 17 18 import android.annotation.UserIdInt 19 import android.app.PendingIntent 20 import android.content.ComponentName 21 import android.content.Context 22 import android.content.pm.LauncherApps 23 import android.content.pm.PackageManager 24 import android.content.pm.PackageManager.Property 25 import android.os.UserHandle 26 import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI 27 import com.android.internal.protolog.ProtoLog 28 import com.android.wm.shell.R 29 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL 30 import com.android.wm.shell.sysui.ShellCommandHandler 31 import com.android.wm.shell.sysui.ShellInit 32 import java.io.PrintWriter 33 import java.util.Arrays 34 35 /** 36 * Helper for multi-instance related checks. 37 */ 38 class MultiInstanceHelper @JvmOverloads constructor( 39 private val context: Context, 40 private val packageManager: PackageManager, 41 private val staticAppsSupportingMultiInstance: Array<String> = context.resources 42 .getStringArray(R.array.config_appsSupportMultiInstancesSplit), 43 shellInit: ShellInit, 44 private val shellCommandHandler: ShellCommandHandler, 45 private val supportsMultiInstanceProperty: Boolean 46 ) : ShellCommandHandler.ShellCommandActionHandler { 47 48 init { 49 shellInit.addInitCallback(this::onInit, this) 50 } 51 52 private fun onInit() { 53 shellCommandHandler.addCommandCallback("multi-instance", this, this) 54 } 55 56 /** 57 * Returns whether a specific component desires to be launched in multiple instances. 58 */ 59 fun supportsMultiInstanceSplit(componentName: ComponentName?, @UserIdInt userId: Int): Boolean { 60 if (componentName == null || componentName.packageName == null) { 61 // TODO(b/262864589): Handle empty component case 62 return false 63 } 64 65 // Check the pre-defined allow list 66 val packageName = componentName.packageName 67 for (pkg in staticAppsSupportingMultiInstance) { 68 if (pkg == packageName) { 69 ProtoLog.v(WM_SHELL, "application=%s in allowlist supports multi-instance", 70 packageName) 71 return true 72 } 73 } 74 75 if (!supportsMultiInstanceProperty) { 76 // If not checking the multi-instance properties, then return early 77 return false 78 } 79 80 // Check the activity property first 81 try { 82 val activityProp = packageManager.getPropertyAsUser( 83 PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName.packageName, 84 componentName.className, userId) 85 // If the above call doesn't throw a NameNotFoundException, then the activity property 86 // should override the application property value 87 if (activityProp.isBoolean) { 88 ProtoLog.v(WM_SHELL, "activity=%s supports multi-instance", componentName) 89 return activityProp.boolean 90 } else { 91 ProtoLog.w(WM_SHELL, "Warning: property=%s for activity=%s has non-bool type=%d", 92 PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, activityProp.type) 93 } 94 } catch (nnfe: PackageManager.NameNotFoundException) { 95 // Not specified in the activity, fall through 96 } 97 98 // Check the application property otherwise 99 try { 100 val appProp = packageManager.getPropertyAsUser( 101 PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, null /* className */, 102 userId) 103 if (appProp.isBoolean) { 104 ProtoLog.v(WM_SHELL, "application=%s supports multi-instance", packageName) 105 return appProp.boolean 106 } else { 107 ProtoLog.w(WM_SHELL, 108 "Warning: property=%s for application=%s has non-bool type=%d", 109 PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, appProp.type) 110 } 111 } catch (nnfe: PackageManager.NameNotFoundException) { 112 // Not specified in either application or activity 113 } 114 return false 115 } 116 117 override fun onShellCommand(args: Array<out String>?, pw: PrintWriter?): Boolean { 118 if (pw == null || args == null || args.isEmpty()) { 119 return false 120 } 121 when (args[0]) { 122 "list" -> return dumpSupportedApps(pw) 123 } 124 return false 125 } 126 127 override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { 128 pw.println("${prefix}list") 129 pw.println("$prefix Lists all the packages that support the multiinstance property") 130 } 131 132 /** 133 * Dumps the static allowlist and list of apps that have the declared property in the manifest. 134 */ 135 private fun dumpSupportedApps(pw: PrintWriter): Boolean { 136 pw.println("Static allow list (for all users):") 137 staticAppsSupportingMultiInstance.forEach { pkg -> 138 pw.println(" $pkg") 139 } 140 141 // TODO(b/391693747): Dump this per-user once PM allows us to query properties 142 // for non-calling users 143 val apps = packageManager.queryApplicationProperty( 144 PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI) 145 val activities = packageManager.queryActivityProperty( 146 PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI) 147 val appsWithProperty = (apps + activities) 148 .sortedWith(object : Comparator<Property?> { 149 override fun compare(o1: Property?, o2: Property?): Int { 150 if (o1?.packageName != o2?.packageName) { 151 return o1?.packageName!!.compareTo(o2?.packageName!!) 152 } else { 153 if (o1?.className != null) { 154 return o1.className!!.compareTo(o2?.className!!) 155 } else if (o2?.className != null) { 156 return -o2.className!!.compareTo(o1?.className!!) 157 } 158 return 0 159 } 160 } 161 }) 162 if (appsWithProperty.isNotEmpty()) { 163 pw.println("Apps (User ${context.userId}):") 164 appsWithProperty.forEach { prop -> 165 if (prop.isBoolean && prop.boolean) { 166 if (prop.className != null) { 167 pw.println(" ${prop.packageName}/${prop.className}") 168 } else { 169 pw.println(" ${prop.packageName}") 170 } 171 } 172 } 173 } 174 return true 175 } 176 177 companion object { 178 /** Returns the component from a PendingIntent */ 179 @JvmStatic 180 fun getComponent(pendingIntent: PendingIntent?): ComponentName? { 181 return pendingIntent?.intent?.component 182 } 183 184 /** Returns the component from a shortcut */ 185 @JvmStatic 186 fun getShortcutComponent(packageName: String, shortcutId: String, 187 user: UserHandle, launcherApps: LauncherApps): ComponentName? { 188 val query = LauncherApps.ShortcutQuery() 189 query.setPackage(packageName) 190 query.setShortcutIds(Arrays.asList(shortcutId)) 191 query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED) 192 val shortcuts = launcherApps.getShortcuts(query, user) 193 val info = if (shortcuts != null && shortcuts.size > 0) shortcuts[0] else null 194 return info?.activity 195 } 196 197 /** Returns true if package names and user ids match. */ 198 @JvmStatic 199 fun samePackage(packageName1: String?, packageName2: String?, 200 userId1: Int, userId2: Int): Boolean { 201 return (packageName1 != null && packageName1 == packageName2) && (userId1 == userId2) 202 } 203 } 204 } 205