• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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