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.UserHandle 30 import android.os.UserManager 31 import android.service.chooser.ChooserTarget 32 import android.text.TextUtils 33 import android.util.Log 34 import androidx.annotation.MainThread 35 import androidx.annotation.OpenForTesting 36 import androidx.annotation.VisibleForTesting 37 import androidx.annotation.WorkerThread 38 import com.android.intentresolver.Flags.fixShortcutLoaderJobLeak 39 import com.android.intentresolver.Flags.fixShortcutsFlashing 40 import com.android.intentresolver.chooser.DisplayResolveInfo 41 import com.android.intentresolver.measurements.Tracer 42 import com.android.intentresolver.measurements.runTracing 43 import java.util.concurrent.Executor 44 import java.util.concurrent.atomic.AtomicReference 45 import java.util.function.Consumer 46 import kotlinx.coroutines.CoroutineDispatcher 47 import kotlinx.coroutines.CoroutineScope 48 import kotlinx.coroutines.CoroutineStart 49 import kotlinx.coroutines.Dispatchers 50 import kotlinx.coroutines.Job 51 import kotlinx.coroutines.asExecutor 52 import kotlinx.coroutines.cancel 53 import kotlinx.coroutines.channels.BufferOverflow 54 import kotlinx.coroutines.delay 55 import kotlinx.coroutines.flow.MutableSharedFlow 56 import kotlinx.coroutines.flow.combine 57 import kotlinx.coroutines.flow.filter 58 import kotlinx.coroutines.flow.flowOn 59 import kotlinx.coroutines.isActive 60 import kotlinx.coroutines.launch 61 62 /** 63 * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager. 64 * 65 * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut 66 * updates. The shortcut loading is triggered in the constructor or by the [reset] method, the 67 * processing happens on the [dispatcher] and the result is delivered through the [callback] on the 68 * default [scope]'s dispatcher, the main thread. 69 */ 70 @OpenForTesting 71 open class ShortcutLoader 72 @VisibleForTesting 73 constructor( 74 private val context: Context, 75 parentScope: CoroutineScope, 76 private val appPredictor: AppPredictorProxy?, 77 private val userHandle: UserHandle, 78 private val isPersonalProfile: Boolean, 79 private val targetIntentFilter: IntentFilter?, 80 private val dispatcher: CoroutineDispatcher, 81 private val callback: Consumer<Result>, 82 ) { 83 private val scope = 84 if (fixShortcutLoaderJobLeak()) parentScope.createChildScope() else parentScope 85 private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter() 86 private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager 87 private val appPredictorWatchdog = AtomicReference<Job?>(null) 88 private val appPredictorCallback = 89 ScopedAppTargetListCallback(scope) { onAppPredictorCallback(it) }.toAppPredictorCallback() 90 91 private val appTargetSource = 92 MutableSharedFlow<Array<DisplayResolveInfo>?>( 93 replay = 1, 94 onBufferOverflow = BufferOverflow.DROP_OLDEST, 95 ) 96 private val shortcutSource = 97 MutableSharedFlow<ShortcutData?>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) 98 private val isDestroyed 99 get() = !scope.isActive 100 101 private val id 102 get() = System.identityHashCode(this).toString(Character.MAX_RADIX) 103 104 @MainThread 105 constructor( 106 context: Context, 107 scope: CoroutineScope, 108 appPredictor: AppPredictor?, 109 userHandle: UserHandle, 110 targetIntentFilter: IntentFilter?, 111 callback: Consumer<Result>, 112 ) : this( 113 context, 114 scope, 115 appPredictor?.let { AppPredictorProxy(it) }, 116 userHandle, 117 userHandle == UserHandle.of(ActivityManager.getCurrentUser()), 118 targetIntentFilter, 119 Dispatchers.IO, 120 callback, 121 ) 122 123 init { 124 appPredictor?.registerPredictionUpdates(dispatcher.asExecutor(), appPredictorCallback) 125 scope 126 .launch { 127 appTargetSource 128 .combine(shortcutSource) { appTargets, shortcutData -> 129 if (appTargets == null || shortcutData == null) { 130 null 131 } else { 132 runTracing("filter-shortcuts-${userHandle.identifier}") { 133 filterShortcuts( 134 appTargets, 135 shortcutData.shortcuts, 136 shortcutData.isFromAppPredictor, 137 shortcutData.appPredictorTargets, 138 ) 139 } 140 } 141 } 142 .filter { it != null } 143 .flowOn(dispatcher) 144 .collect { callback.accept(it ?: error("can not be null")) } 145 } 146 .invokeOnCompletion { 147 runCatching { appPredictor?.unregisterPredictionUpdates(appPredictorCallback) } 148 Log.d(TAG, "[$id] destroyed, user: $userHandle") 149 } 150 reset() 151 } 152 153 /** Clear application targets (see [updateAppTargets] and initiate shortcuts loading. */ 154 @OpenForTesting 155 open fun reset() { 156 Log.d(TAG, "[$id] reset shortcut loader for user $userHandle") 157 appTargetSource.tryEmit(null) 158 shortcutSource.tryEmit(null) 159 scope.launch(dispatcher) { loadShortcuts() } 160 } 161 162 /** 163 * Update resolved application targets; as soon as shortcuts are loaded, they will be filtered 164 * against the targets and the is delivered to the client through the [callback]. 165 */ 166 @OpenForTesting 167 open fun updateAppTargets(appTargets: Array<DisplayResolveInfo>) { 168 appTargetSource.tryEmit(appTargets) 169 } 170 171 @OpenForTesting 172 open fun destroy() { 173 if (fixShortcutLoaderJobLeak()) { 174 scope.cancel() 175 } 176 } 177 178 @WorkerThread 179 private fun loadShortcuts() { 180 // no need to query direct share for work profile when its locked or disabled 181 if (!shouldQueryDirectShareTargets()) { 182 Log.d(TAG, "[$id] skip shortcuts loading for user $userHandle") 183 return 184 } 185 Log.d(TAG, "[$id] querying direct share targets for user $userHandle") 186 queryDirectShareTargets(false) 187 } 188 189 @WorkerThread 190 private fun queryDirectShareTargets(skipAppPredictionService: Boolean) { 191 if (!skipAppPredictionService && appPredictor != null) { 192 try { 193 Log.d(TAG, "[$id] query AppPredictor for user $userHandle") 194 195 val watchdogJob = 196 if (fixShortcutsFlashing()) { 197 scope 198 .launch(start = CoroutineStart.LAZY) { 199 delay(APP_PREDICTOR_RESPONSE_TIMEOUT_MS) 200 Log.w(TAG, "AppPredictor response timeout for user: $userHandle") 201 appPredictorCallback.onTargetsAvailable(emptyList()) 202 } 203 .also { job -> 204 appPredictorWatchdog.getAndSet(job)?.cancel() 205 job.invokeOnCompletion { 206 appPredictorWatchdog.compareAndSet(job, null) 207 } 208 } 209 } else { 210 null 211 } 212 213 Tracer.beginAppPredictorQueryTrace(userHandle) 214 appPredictor.requestPredictionUpdate() 215 216 watchdogJob?.start() 217 return 218 } catch (e: Throwable) { 219 endAppPredictorQueryTrace(userHandle) 220 // we might have been destroyed concurrently, nothing left to do 221 if (isDestroyed) { 222 return 223 } 224 Log.e(TAG, "[$id] failed to query AppPredictor for user $userHandle", e) 225 } 226 } 227 // Default to just querying ShortcutManager if AppPredictor not present. 228 if (targetIntentFilter == null) { 229 Log.d(TAG, "[$id] skip querying ShortcutManager for $userHandle") 230 sendShareShortcutInfoList( 231 emptyList(), 232 isFromAppPredictor = false, 233 appPredictorTargets = null, 234 ) 235 return 236 } 237 Log.d(TAG, "[$id] query ShortcutManager for user $userHandle") 238 val shortcuts = 239 runTracing("shortcut-mngr-${userHandle.identifier}") { 240 queryShortcutManager(targetIntentFilter) 241 } 242 Log.d(TAG, "[$id] receive shortcuts from ShortcutManager for user $userHandle") 243 sendShareShortcutInfoList(shortcuts, false, null) 244 } 245 246 @WorkerThread 247 private fun queryShortcutManager(targetIntentFilter: IntentFilter): List<ShareShortcutInfo> { 248 val selectedProfileContext = context.createContextAsUser(userHandle, 0 /* flags */) 249 val sm = 250 selectedProfileContext.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager? 251 val pm = context.createContextAsUser(userHandle, 0 /* flags */).packageManager 252 return sm?.getShareTargets(targetIntentFilter)?.filter { 253 pm.isPackageEnabled(it.targetComponent.packageName) 254 } ?: emptyList() 255 } 256 257 @WorkerThread 258 private fun onAppPredictorCallback(appPredictorTargets: List<AppTarget>) { 259 appPredictorWatchdog.get()?.cancel() 260 endAppPredictorQueryTrace(userHandle) 261 Log.d(TAG, "[$id] receive app targets from AppPredictor") 262 if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) { 263 // APS may be disabled, so try querying targets ourselves. 264 queryDirectShareTargets(true) 265 return 266 } 267 val pm = context.createContextAsUser(userHandle, 0).packageManager 268 val pair = appPredictorTargets.toShortcuts(pm) 269 sendShareShortcutInfoList(pair.shortcuts, true, pair.appTargets) 270 } 271 272 @WorkerThread 273 private fun List<AppTarget>.toShortcuts(pm: PackageManager): ShortcutsAppTargetsPair = 274 fold(ShortcutsAppTargetsPair(ArrayList(size), ArrayList(size))) { acc, appTarget -> 275 val shortcutInfo = appTarget.shortcutInfo 276 val packageName = appTarget.packageName 277 val className = appTarget.className 278 if (shortcutInfo != null && className != null && pm.isPackageEnabled(packageName)) { 279 (acc.shortcuts as ArrayList<ShareShortcutInfo>).add( 280 ShareShortcutInfo(shortcutInfo, ComponentName(packageName, className)) 281 ) 282 (acc.appTargets as ArrayList<AppTarget>).add(appTarget) 283 } 284 acc 285 } 286 287 @WorkerThread 288 private fun sendShareShortcutInfoList( 289 shortcuts: List<ShareShortcutInfo>, 290 isFromAppPredictor: Boolean, 291 appPredictorTargets: List<AppTarget>?, 292 ) { 293 shortcutSource.tryEmit(ShortcutData(shortcuts, isFromAppPredictor, appPredictorTargets)) 294 } 295 296 private fun filterShortcuts( 297 appTargets: Array<DisplayResolveInfo>, 298 shortcuts: List<ShareShortcutInfo>, 299 isFromAppPredictor: Boolean, 300 appPredictorTargets: List<AppTarget>?, 301 ): Result { 302 if (appPredictorTargets != null && appPredictorTargets.size != shortcuts.size) { 303 throw RuntimeException( 304 "resultList and appTargets must have the same size." + 305 " resultList.size()=" + 306 shortcuts.size + 307 " appTargets.size()=" + 308 appPredictorTargets.size 309 ) 310 } 311 val directShareAppTargetCache = HashMap<ChooserTarget, AppTarget>() 312 val directShareShortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>() 313 // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path 314 // for direct share targets. After ShareSheet is refactored we should use the 315 // ShareShortcutInfos directly. 316 val resultRecords: MutableList<ShortcutResultInfo> = ArrayList() 317 for (displayResolveInfo in appTargets) { 318 val matchingShortcuts = 319 shortcuts.filter { it.targetComponent == displayResolveInfo.resolvedComponentName } 320 if (matchingShortcuts.isEmpty()) continue 321 val chooserTargets = 322 shortcutToChooserTargetConverter.convertToChooserTarget( 323 matchingShortcuts, 324 shortcuts, 325 appPredictorTargets, 326 directShareAppTargetCache, 327 directShareShortcutInfoCache, 328 ) 329 val resultRecord = ShortcutResultInfo(displayResolveInfo, chooserTargets) 330 resultRecords.add(resultRecord) 331 } 332 return Result( 333 isFromAppPredictor, 334 appTargets, 335 resultRecords.toTypedArray(), 336 directShareAppTargetCache, 337 directShareShortcutInfoCache, 338 ) 339 } 340 341 /** 342 * Returns `false` if `userHandle` is the work profile and it's either in quiet mode or not 343 * running. 344 */ 345 private fun shouldQueryDirectShareTargets(): Boolean = isPersonalProfile || isProfileActive 346 347 @get:VisibleForTesting 348 protected val isProfileActive: Boolean 349 get() = 350 userManager.isUserRunning(userHandle) && 351 userManager.isUserUnlocked(userHandle) && 352 !userManager.isQuietModeEnabled(userHandle) 353 354 private class ShortcutData( 355 val shortcuts: List<ShareShortcutInfo>, 356 val isFromAppPredictor: Boolean, 357 val appPredictorTargets: List<AppTarget>?, 358 ) 359 360 /** Resolved shortcuts with corresponding app targets. */ 361 class Result( 362 val isFromAppPredictor: Boolean, 363 /** 364 * Input app targets (see [ShortcutLoader.updateAppTargets] the shortcuts were process 365 * against. 366 */ 367 val appTargets: Array<DisplayResolveInfo>, 368 /** Shortcuts grouped by app target. */ 369 val shortcutsByApp: Array<ShortcutResultInfo>, 370 val directShareAppTargetCache: Map<ChooserTarget, AppTarget>, 371 val directShareShortcutInfoCache: Map<ChooserTarget, ShortcutInfo>, 372 ) 373 374 private fun endAppPredictorQueryTrace(userHandle: UserHandle) { 375 val duration = Tracer.endAppPredictorQueryTrace(userHandle) 376 Log.d(TAG, "[$id] AppPredictor query duration for user $userHandle: $duration ms") 377 } 378 379 /** Shortcuts grouped by app. */ 380 class ShortcutResultInfo( 381 val appTarget: DisplayResolveInfo, 382 val shortcuts: List<ChooserTarget?>, 383 ) 384 385 private class ShortcutsAppTargetsPair( 386 val shortcuts: List<ShareShortcutInfo>, 387 val appTargets: List<AppTarget>?, 388 ) 389 390 /** A wrapper around AppPredictor to facilitate unit-testing. */ 391 @VisibleForTesting 392 open class AppPredictorProxy internal constructor(private val mAppPredictor: AppPredictor) { 393 /** [AppPredictor.registerPredictionUpdates] */ 394 open fun registerPredictionUpdates( 395 callbackExecutor: Executor, 396 callback: AppPredictor.Callback, 397 ) = mAppPredictor.registerPredictionUpdates(callbackExecutor, callback) 398 399 /** [AppPredictor.unregisterPredictionUpdates] */ 400 open fun unregisterPredictionUpdates(callback: AppPredictor.Callback) = 401 mAppPredictor.unregisterPredictionUpdates(callback) 402 403 /** [AppPredictor.requestPredictionUpdate] */ 404 open fun requestPredictionUpdate() = mAppPredictor.requestPredictionUpdate() 405 } 406 407 companion object { 408 @VisibleForTesting const val APP_PREDICTOR_RESPONSE_TIMEOUT_MS = 2_000L 409 private const val TAG = "ShortcutLoader" 410 411 private fun PackageManager.isPackageEnabled(packageName: String): Boolean { 412 if (TextUtils.isEmpty(packageName)) { 413 return false 414 } 415 return runCatching { 416 val appInfo = 417 getApplicationInfo( 418 packageName, 419 PackageManager.ApplicationInfoFlags.of( 420 PackageManager.GET_META_DATA.toLong() 421 ), 422 ) 423 appInfo.enabled && (appInfo.flags and ApplicationInfo.FLAG_SUSPENDED) == 0 424 } 425 .getOrDefault(false) 426 } 427 428 /** 429 * Creates a new coroutine scope and makes its job a child of the given, `this`, coroutine 430 * scope's job. This ensures that the new scope will be canceled when the parent scope is 431 * canceled (but not vice versa). 432 */ 433 private fun CoroutineScope.createChildScope() = 434 CoroutineScope(coroutineContext + Job(parent = coroutineContext[Job])) 435 } 436 } 437