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