1 /* <lambda>null2 * Copyright (C) 2022 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.intentresolver.shortcuts 17 18 import android.app.ActivityManager 19 import android.app.prediction.AppPredictor 20 import android.app.prediction.AppTarget 21 import android.content.ComponentName 22 import android.content.Context 23 import android.content.IntentFilter 24 import android.content.pm.ApplicationInfo 25 import android.content.pm.PackageManager 26 import android.content.pm.ShortcutInfo 27 import android.content.pm.ShortcutManager 28 import android.content.pm.ShortcutManager.ShareShortcutInfo 29 import android.os.AsyncTask 30 import android.os.UserHandle 31 import android.os.UserManager 32 import android.service.chooser.ChooserTarget 33 import android.text.TextUtils 34 import android.util.Log 35 import androidx.annotation.MainThread 36 import androidx.annotation.OpenForTesting 37 import androidx.annotation.VisibleForTesting 38 import androidx.annotation.WorkerThread 39 import com.android.intentresolver.chooser.DisplayResolveInfo 40 import java.lang.RuntimeException 41 import java.util.ArrayList 42 import java.util.HashMap 43 import java.util.concurrent.Executor 44 import java.util.concurrent.atomic.AtomicReference 45 import java.util.function.Consumer 46 47 /** 48 * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager. 49 * 50 * 51 * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut 52 * updates. The shortcut loading is triggered by the [queryShortcuts], 53 * the processing will happen on the [backgroundExecutor] and the result is delivered 54 * through the [callback] on the [callbackExecutor], the main thread. 55 * 56 * 57 * The current version does not improve on the legacy in a way that it does not guarantee that 58 * each invocation of the [queryShortcuts] will be matched by an 59 * invocation of the callback (there are early terminations of the flow). Also, the fetched 60 * shortcuts would be matched against the last known input, i.e. two invocations of 61 * [queryShortcuts] may result in two callbacks where shortcuts are 62 * processed against the latest input. 63 * 64 */ 65 @OpenForTesting 66 open class ShortcutLoader @VisibleForTesting constructor( 67 private val context: Context, 68 private val appPredictor: AppPredictorProxy?, 69 private val userHandle: UserHandle, 70 private val isPersonalProfile: Boolean, 71 private val targetIntentFilter: IntentFilter?, 72 private val backgroundExecutor: Executor, 73 private val callbackExecutor: Executor, 74 private val callback: Consumer<Result> 75 ) { 76 private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter() 77 private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager 78 private val activeRequest = AtomicReference(NO_REQUEST) 79 private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) } 80 private var isDestroyed = false 81 82 @MainThread 83 constructor( 84 context: Context, 85 appPredictor: AppPredictor?, 86 userHandle: UserHandle, 87 targetIntentFilter: IntentFilter?, 88 callback: Consumer<Result> 89 ) : this( 90 context, 91 appPredictor?.let { AppPredictorProxy(it) }, 92 userHandle, userHandle == UserHandle.of(ActivityManager.getCurrentUser()), 93 targetIntentFilter, 94 AsyncTask.SERIAL_EXECUTOR, 95 context.mainExecutor, 96 callback 97 ) 98 99 init { 100 appPredictor?.registerPredictionUpdates(callbackExecutor, appPredictorCallback) 101 } 102 103 /** 104 * Unsubscribe from app predictor if one was provided. 105 */ 106 @OpenForTesting 107 @MainThread 108 open fun destroy() { 109 isDestroyed = true 110 appPredictor?.unregisterPredictionUpdates(appPredictorCallback) 111 } 112 113 /** 114 * Set new resolved targets. This will trigger shortcut loading. 115 * @param appTargets a collection of application targets a loaded set of shortcuts will be 116 * grouped against 117 */ 118 @OpenForTesting 119 @MainThread 120 open fun queryShortcuts(appTargets: Array<DisplayResolveInfo>) { 121 if (isDestroyed) return 122 activeRequest.set(Request(appTargets)) 123 backgroundExecutor.execute { loadShortcuts() } 124 } 125 126 @WorkerThread 127 private fun loadShortcuts() { 128 // no need to query direct share for work profile when its locked or disabled 129 if (!shouldQueryDirectShareTargets()) return 130 Log.d(TAG, "querying direct share targets") 131 queryDirectShareTargets(false) 132 } 133 134 @WorkerThread 135 private fun queryDirectShareTargets(skipAppPredictionService: Boolean) { 136 if (!skipAppPredictionService && appPredictor != null) { 137 appPredictor.requestPredictionUpdate() 138 return 139 } 140 // Default to just querying ShortcutManager if AppPredictor not present. 141 if (targetIntentFilter == null) return 142 val shortcuts = queryShortcutManager(targetIntentFilter) 143 sendShareShortcutInfoList(shortcuts, false, null) 144 } 145 146 @WorkerThread 147 private fun queryShortcutManager(targetIntentFilter: IntentFilter): List<ShareShortcutInfo> { 148 val selectedProfileContext = context.createContextAsUser(userHandle, 0 /* flags */) 149 val sm = selectedProfileContext 150 .getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager? 151 val pm = context.createContextAsUser(userHandle, 0 /* flags */).packageManager 152 return sm?.getShareTargets(targetIntentFilter) 153 ?.filter { pm.isPackageEnabled(it.targetComponent.packageName) } 154 ?: emptyList() 155 } 156 157 @WorkerThread 158 private fun onAppPredictorCallback(appPredictorTargets: List<AppTarget>) { 159 if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) { 160 // APS may be disabled, so try querying targets ourselves. 161 queryDirectShareTargets(true) 162 return 163 } 164 val pm = context.createContextAsUser(userHandle, 0).packageManager 165 val pair = appPredictorTargets.toShortcuts(pm) 166 sendShareShortcutInfoList(pair.shortcuts, true, pair.appTargets) 167 } 168 169 @WorkerThread 170 private fun List<AppTarget>.toShortcuts(pm: PackageManager): ShortcutsAppTargetsPair = 171 fold( 172 ShortcutsAppTargetsPair(ArrayList(size), ArrayList(size)) 173 ) { acc, appTarget -> 174 val shortcutInfo = appTarget.shortcutInfo 175 val packageName = appTarget.packageName 176 val className = appTarget.className 177 if (shortcutInfo != null && className != null && pm.isPackageEnabled(packageName)) { 178 (acc.shortcuts as ArrayList<ShareShortcutInfo>).add( 179 ShareShortcutInfo(shortcutInfo, ComponentName(packageName, className)) 180 ) 181 (acc.appTargets as ArrayList<AppTarget>).add(appTarget) 182 } 183 acc 184 } 185 186 @WorkerThread 187 private fun sendShareShortcutInfoList( 188 shortcuts: List<ShareShortcutInfo>, 189 isFromAppPredictor: Boolean, 190 appPredictorTargets: List<AppTarget>? 191 ) { 192 if (appPredictorTargets != null && appPredictorTargets.size != shortcuts.size) { 193 throw RuntimeException( 194 "resultList and appTargets must have the same size." 195 + " resultList.size()=" + shortcuts.size 196 + " appTargets.size()=" + appPredictorTargets.size 197 ) 198 } 199 val directShareAppTargetCache = HashMap<ChooserTarget, AppTarget>() 200 val directShareShortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>() 201 // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path 202 // for direct share targets. After ShareSheet is refactored we should use the 203 // ShareShortcutInfos directly. 204 val appTargets = activeRequest.get().appTargets 205 val resultRecords: MutableList<ShortcutResultInfo> = ArrayList() 206 for (displayResolveInfo in appTargets) { 207 val matchingShortcuts = shortcuts.filter { 208 it.targetComponent == displayResolveInfo.resolvedComponentName 209 } 210 if (matchingShortcuts.isEmpty()) continue 211 val chooserTargets = shortcutToChooserTargetConverter.convertToChooserTarget( 212 matchingShortcuts, 213 shortcuts, 214 appPredictorTargets, 215 directShareAppTargetCache, 216 directShareShortcutInfoCache 217 ) 218 val resultRecord = ShortcutResultInfo(displayResolveInfo, chooserTargets) 219 resultRecords.add(resultRecord) 220 } 221 postReport( 222 Result( 223 isFromAppPredictor, 224 appTargets, 225 resultRecords.toTypedArray(), 226 directShareAppTargetCache, 227 directShareShortcutInfoCache 228 ) 229 ) 230 } 231 232 private fun postReport(result: Result) = callbackExecutor.execute { report(result) } 233 234 @MainThread 235 private fun report(result: Result) { 236 if (isDestroyed) return 237 callback.accept(result) 238 } 239 240 /** 241 * Returns `false` if `userHandle` is the work profile and it's either 242 * in quiet mode or not running. 243 */ 244 private fun shouldQueryDirectShareTargets(): Boolean = isPersonalProfile || isProfileActive 245 246 @get:VisibleForTesting 247 protected val isProfileActive: Boolean 248 get() = userManager.isUserRunning(userHandle) 249 && userManager.isUserUnlocked(userHandle) 250 && !userManager.isQuietModeEnabled(userHandle) 251 252 private class Request(val appTargets: Array<DisplayResolveInfo>) 253 254 /** 255 * Resolved shortcuts with corresponding app targets. 256 */ 257 class Result( 258 val isFromAppPredictor: Boolean, 259 /** 260 * Input app targets (see [ShortcutLoader.queryShortcuts] the 261 * shortcuts were process against. 262 */ 263 val appTargets: Array<DisplayResolveInfo>, 264 /** 265 * Shortcuts grouped by app target. 266 */ 267 val shortcutsByApp: Array<ShortcutResultInfo>, 268 val directShareAppTargetCache: Map<ChooserTarget, AppTarget>, 269 val directShareShortcutInfoCache: Map<ChooserTarget, ShortcutInfo> 270 ) 271 272 /** 273 * Shortcuts grouped by app. 274 */ 275 class ShortcutResultInfo( 276 val appTarget: DisplayResolveInfo, 277 val shortcuts: List<ChooserTarget?> 278 ) 279 280 private class ShortcutsAppTargetsPair( 281 val shortcuts: List<ShareShortcutInfo>, 282 val appTargets: List<AppTarget>? 283 ) 284 285 /** 286 * A wrapper around AppPredictor to facilitate unit-testing. 287 */ 288 @VisibleForTesting 289 open class AppPredictorProxy internal constructor(private val mAppPredictor: AppPredictor) { 290 /** 291 * [AppPredictor.registerPredictionUpdates] 292 */ 293 open fun registerPredictionUpdates( 294 callbackExecutor: Executor, callback: AppPredictor.Callback 295 ) = mAppPredictor.registerPredictionUpdates(callbackExecutor, callback) 296 297 /** 298 * [AppPredictor.unregisterPredictionUpdates] 299 */ 300 open fun unregisterPredictionUpdates(callback: AppPredictor.Callback) = 301 mAppPredictor.unregisterPredictionUpdates(callback) 302 303 /** 304 * [AppPredictor.requestPredictionUpdate] 305 */ 306 open fun requestPredictionUpdate() = mAppPredictor.requestPredictionUpdate() 307 } 308 309 companion object { 310 private const val TAG = "ShortcutLoader" 311 private val NO_REQUEST = Request(arrayOf()) 312 313 private fun PackageManager.isPackageEnabled(packageName: String): Boolean { 314 if (TextUtils.isEmpty(packageName)) { 315 return false 316 } 317 return runCatching { 318 val appInfo = getApplicationInfo( 319 packageName, 320 PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) 321 ) 322 appInfo.enabled && (appInfo.flags and ApplicationInfo.FLAG_SUSPENDED) == 0 323 }.getOrDefault(false) 324 } 325 } 326 } 327