• 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.AsyncTask
30 import android.os.UserHandle
31 import android.os.UserManager
32 import android.service.chooser.ChooserTarget
33 import android.text.TextUtils
34 import android.util.Log
35 import androidx.annotation.MainThread
36 import androidx.annotation.OpenForTesting
37 import androidx.annotation.VisibleForTesting
38 import androidx.annotation.WorkerThread
39 import com.android.intentresolver.chooser.DisplayResolveInfo
40 import java.lang.RuntimeException
41 import java.util.ArrayList
42 import java.util.HashMap
43 import java.util.concurrent.Executor
44 import java.util.concurrent.atomic.AtomicReference
45 import java.util.function.Consumer
46 
47 /**
48  * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager.
49  *
50  *
51  * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut
52  * updates. The shortcut loading is triggered by the [queryShortcuts],
53  * the processing will happen on the [backgroundExecutor] and the result is delivered
54  * through the [callback] on the [callbackExecutor], the main thread.
55  *
56  *
57  * The current version does not improve on the legacy in a way that it does not guarantee that
58  * each invocation of the [queryShortcuts] will be matched by an
59  * invocation of the callback (there are early terminations of the flow). Also, the fetched
60  * shortcuts would be matched against the last known input, i.e. two invocations of
61  * [queryShortcuts] may result in two callbacks where shortcuts are
62  * processed against the latest input.
63  *
64  */
65 @OpenForTesting
66 open class ShortcutLoader @VisibleForTesting constructor(
67     private val context: Context,
68     private val appPredictor: AppPredictorProxy?,
69     private val userHandle: UserHandle,
70     private val isPersonalProfile: Boolean,
71     private val targetIntentFilter: IntentFilter?,
72     private val backgroundExecutor: Executor,
73     private val callbackExecutor: Executor,
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 activeRequest = AtomicReference(NO_REQUEST)
79     private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) }
80     private var isDestroyed = false
81 
82     @MainThread
83     constructor(
84         context: Context,
85         appPredictor: AppPredictor?,
86         userHandle: UserHandle,
87         targetIntentFilter: IntentFilter?,
88         callback: Consumer<Result>
89     ) : this(
90         context,
91         appPredictor?.let { AppPredictorProxy(it) },
92         userHandle, userHandle == UserHandle.of(ActivityManager.getCurrentUser()),
93         targetIntentFilter,
94         AsyncTask.SERIAL_EXECUTOR,
95         context.mainExecutor,
96         callback
97     )
98 
99     init {
100         appPredictor?.registerPredictionUpdates(callbackExecutor, appPredictorCallback)
101     }
102 
103     /**
104      * Unsubscribe from app predictor if one was provided.
105      */
106     @OpenForTesting
107     @MainThread
108     open fun destroy() {
109         isDestroyed = true
110         appPredictor?.unregisterPredictionUpdates(appPredictorCallback)
111     }
112 
113     /**
114      * Set new resolved targets. This will trigger shortcut loading.
115      * @param appTargets a collection of application targets a loaded set of shortcuts will be
116      * grouped against
117      */
118     @OpenForTesting
119     @MainThread
120     open fun queryShortcuts(appTargets: Array<DisplayResolveInfo>) {
121         if (isDestroyed) return
122         activeRequest.set(Request(appTargets))
123         backgroundExecutor.execute { loadShortcuts() }
124     }
125 
126     @WorkerThread
127     private fun loadShortcuts() {
128         // no need to query direct share for work profile when its locked or disabled
129         if (!shouldQueryDirectShareTargets()) return
130         Log.d(TAG, "querying direct share targets")
131         queryDirectShareTargets(false)
132     }
133 
134     @WorkerThread
135     private fun queryDirectShareTargets(skipAppPredictionService: Boolean) {
136         if (!skipAppPredictionService && appPredictor != null) {
137             appPredictor.requestPredictionUpdate()
138             return
139         }
140         // Default to just querying ShortcutManager if AppPredictor not present.
141         if (targetIntentFilter == null) return
142         val shortcuts = queryShortcutManager(targetIntentFilter)
143         sendShareShortcutInfoList(shortcuts, false, null)
144     }
145 
146     @WorkerThread
147     private fun queryShortcutManager(targetIntentFilter: IntentFilter): List<ShareShortcutInfo> {
148         val selectedProfileContext = context.createContextAsUser(userHandle, 0 /* flags */)
149         val sm = selectedProfileContext
150             .getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager?
151         val pm = context.createContextAsUser(userHandle, 0 /* flags */).packageManager
152         return sm?.getShareTargets(targetIntentFilter)
153             ?.filter { pm.isPackageEnabled(it.targetComponent.packageName) }
154             ?: emptyList()
155     }
156 
157     @WorkerThread
158     private fun onAppPredictorCallback(appPredictorTargets: List<AppTarget>) {
159         if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) {
160             // APS may be disabled, so try querying targets ourselves.
161             queryDirectShareTargets(true)
162             return
163         }
164         val pm = context.createContextAsUser(userHandle, 0).packageManager
165         val pair = appPredictorTargets.toShortcuts(pm)
166         sendShareShortcutInfoList(pair.shortcuts, true, pair.appTargets)
167     }
168 
169     @WorkerThread
170     private fun List<AppTarget>.toShortcuts(pm: PackageManager): ShortcutsAppTargetsPair =
171         fold(
172             ShortcutsAppTargetsPair(ArrayList(size), ArrayList(size))
173         ) { acc, appTarget ->
174             val shortcutInfo = appTarget.shortcutInfo
175             val packageName = appTarget.packageName
176             val className = appTarget.className
177             if (shortcutInfo != null && className != null && pm.isPackageEnabled(packageName)) {
178                 (acc.shortcuts as ArrayList<ShareShortcutInfo>).add(
179                     ShareShortcutInfo(shortcutInfo, ComponentName(packageName, className))
180                 )
181                 (acc.appTargets as ArrayList<AppTarget>).add(appTarget)
182             }
183             acc
184         }
185 
186     @WorkerThread
187     private fun sendShareShortcutInfoList(
188         shortcuts: List<ShareShortcutInfo>,
189         isFromAppPredictor: Boolean,
190         appPredictorTargets: List<AppTarget>?
191     ) {
192         if (appPredictorTargets != null && appPredictorTargets.size != shortcuts.size) {
193             throw RuntimeException(
194                 "resultList and appTargets must have the same size."
195                         + " resultList.size()=" + shortcuts.size
196                         + " appTargets.size()=" + appPredictorTargets.size
197             )
198         }
199         val directShareAppTargetCache = HashMap<ChooserTarget, AppTarget>()
200         val directShareShortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>()
201         // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
202         // for direct share targets. After ShareSheet is refactored we should use the
203         // ShareShortcutInfos directly.
204         val appTargets = activeRequest.get().appTargets
205         val resultRecords: MutableList<ShortcutResultInfo> = ArrayList()
206         for (displayResolveInfo in appTargets) {
207             val matchingShortcuts = shortcuts.filter {
208                 it.targetComponent == displayResolveInfo.resolvedComponentName
209             }
210             if (matchingShortcuts.isEmpty()) continue
211             val chooserTargets = shortcutToChooserTargetConverter.convertToChooserTarget(
212                 matchingShortcuts,
213                 shortcuts,
214                 appPredictorTargets,
215                 directShareAppTargetCache,
216                 directShareShortcutInfoCache
217             )
218             val resultRecord = ShortcutResultInfo(displayResolveInfo, chooserTargets)
219             resultRecords.add(resultRecord)
220         }
221         postReport(
222             Result(
223                 isFromAppPredictor,
224                 appTargets,
225                 resultRecords.toTypedArray(),
226                 directShareAppTargetCache,
227                 directShareShortcutInfoCache
228             )
229         )
230     }
231 
232     private fun postReport(result: Result) = callbackExecutor.execute { report(result) }
233 
234     @MainThread
235     private fun report(result: Result) {
236         if (isDestroyed) return
237         callback.accept(result)
238     }
239 
240     /**
241      * Returns `false` if `userHandle` is the work profile and it's either
242      * in quiet mode or not running.
243      */
244     private fun shouldQueryDirectShareTargets(): Boolean = isPersonalProfile || isProfileActive
245 
246     @get:VisibleForTesting
247     protected val isProfileActive: Boolean
248         get() = userManager.isUserRunning(userHandle)
249             && userManager.isUserUnlocked(userHandle)
250             && !userManager.isQuietModeEnabled(userHandle)
251 
252     private class Request(val appTargets: Array<DisplayResolveInfo>)
253 
254     /**
255      * Resolved shortcuts with corresponding app targets.
256      */
257     class Result(
258         val isFromAppPredictor: Boolean,
259         /**
260          * Input app targets (see [ShortcutLoader.queryShortcuts] the
261          * shortcuts were process against.
262          */
263         val appTargets: Array<DisplayResolveInfo>,
264         /**
265          * Shortcuts grouped by app target.
266          */
267         val shortcutsByApp: Array<ShortcutResultInfo>,
268         val directShareAppTargetCache: Map<ChooserTarget, AppTarget>,
269         val directShareShortcutInfoCache: Map<ChooserTarget, ShortcutInfo>
270     )
271 
272     /**
273      * Shortcuts grouped by app.
274      */
275     class ShortcutResultInfo(
276         val appTarget: DisplayResolveInfo,
277         val shortcuts: List<ChooserTarget?>
278     )
279 
280     private class ShortcutsAppTargetsPair(
281         val shortcuts: List<ShareShortcutInfo>,
282         val appTargets: List<AppTarget>?
283     )
284 
285     /**
286      * A wrapper around AppPredictor to facilitate unit-testing.
287      */
288     @VisibleForTesting
289     open class AppPredictorProxy internal constructor(private val mAppPredictor: AppPredictor) {
290         /**
291          * [AppPredictor.registerPredictionUpdates]
292          */
293         open fun registerPredictionUpdates(
294             callbackExecutor: Executor, callback: AppPredictor.Callback
295         ) = mAppPredictor.registerPredictionUpdates(callbackExecutor, callback)
296 
297         /**
298          * [AppPredictor.unregisterPredictionUpdates]
299          */
300         open fun unregisterPredictionUpdates(callback: AppPredictor.Callback) =
301             mAppPredictor.unregisterPredictionUpdates(callback)
302 
303         /**
304          * [AppPredictor.requestPredictionUpdate]
305          */
306         open fun requestPredictionUpdate() = mAppPredictor.requestPredictionUpdate()
307     }
308 
309     companion object {
310         private const val TAG = "ShortcutLoader"
311         private val NO_REQUEST = Request(arrayOf())
312 
313         private fun PackageManager.isPackageEnabled(packageName: String): Boolean {
314             if (TextUtils.isEmpty(packageName)) {
315                 return false
316             }
317             return runCatching {
318                 val appInfo = getApplicationInfo(
319                     packageName,
320                     PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())
321                 )
322                 appInfo.enabled && (appInfo.flags and ApplicationInfo.FLAG_SUSPENDED) == 0
323             }.getOrDefault(false)
324         }
325     }
326 }
327