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