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