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