• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 
17 
18 package com.android.intentresolver;
19 
20 import android.annotation.WorkerThread;
21 import android.app.ActivityManager;
22 import android.app.AppGlobals;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.pm.ActivityInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.os.RemoteException;
31 import android.os.UserHandle;
32 import android.util.Log;
33 
34 import com.android.intentresolver.chooser.DisplayResolveInfo;
35 import com.android.intentresolver.model.AbstractResolverComparator;
36 import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
37 import com.android.internal.annotations.VisibleForTesting;
38 
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.PriorityQueue;
43 import java.util.concurrent.CountDownLatch;
44 
45 /**
46  * A helper for the ResolverActivity that exposes methods to retrieve, filter and sort its list of
47  * resolvers.
48  */
49 public class ResolverListController {
50 
51     private final Context mContext;
52     private final PackageManager mpm;
53     private final int mLaunchedFromUid;
54 
55     // Needed for sorting resolvers.
56     private final Intent mTargetIntent;
57     private final String mReferrerPackage;
58 
59     private static final String TAG = "ResolverListController";
60     private static final boolean DEBUG = false;
61 
62     private AbstractResolverComparator mResolverComparator;
63     private boolean isComputed = false;
64 
ResolverListController( Context context, PackageManager pm, Intent targetIntent, String referrerPackage, int launchedFromUid)65     public ResolverListController(
66             Context context,
67             PackageManager pm,
68             Intent targetIntent,
69             String referrerPackage,
70             int launchedFromUid) {
71         this(context, pm, targetIntent, referrerPackage, launchedFromUid,
72                     new ResolverRankerServiceResolverComparator(
73                         context, targetIntent, referrerPackage, null, null));
74     }
75 
ResolverListController( Context context, PackageManager pm, Intent targetIntent, String referrerPackage, int launchedFromUid, AbstractResolverComparator resolverComparator)76     public ResolverListController(
77             Context context,
78             PackageManager pm,
79             Intent targetIntent,
80             String referrerPackage,
81             int launchedFromUid,
82             AbstractResolverComparator resolverComparator) {
83         mContext = context;
84         mpm = pm;
85         mLaunchedFromUid = launchedFromUid;
86         mTargetIntent = targetIntent;
87         mReferrerPackage = referrerPackage;
88         mResolverComparator = resolverComparator;
89     }
90 
91     @VisibleForTesting
getLastChosen()92     public ResolveInfo getLastChosen() throws RemoteException {
93         return AppGlobals.getPackageManager().getLastChosenActivity(
94                 mTargetIntent, mTargetIntent.resolveTypeIfNeeded(mContext.getContentResolver()),
95                 PackageManager.MATCH_DEFAULT_ONLY);
96     }
97 
98     @VisibleForTesting
setLastChosen(Intent intent, IntentFilter filter, int match)99     public void setLastChosen(Intent intent, IntentFilter filter, int match)
100             throws RemoteException {
101         AppGlobals.getPackageManager().setLastChosenActivity(intent,
102                 intent.resolveType(mContext.getContentResolver()),
103                 PackageManager.MATCH_DEFAULT_ONLY,
104                 filter, match, intent.getComponent());
105     }
106 
107     /**
108      * Get data about all the ways the user with the specified handle can resolve any of the
109      * provided {@code intents}.
110      */
getResolversForIntentAsUser( boolean shouldGetResolvedFilter, boolean shouldGetActivityMetadata, boolean shouldGetOnlyDefaultActivities, List<Intent> intents, UserHandle userHandle)111     public List<ResolvedComponentInfo> getResolversForIntentAsUser(
112             boolean shouldGetResolvedFilter,
113             boolean shouldGetActivityMetadata,
114             boolean shouldGetOnlyDefaultActivities,
115             List<Intent> intents,
116             UserHandle userHandle) {
117         int baseFlags = (shouldGetOnlyDefaultActivities ? PackageManager.MATCH_DEFAULT_ONLY : 0)
118                 | PackageManager.MATCH_DIRECT_BOOT_AWARE
119                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
120                 | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
121                 | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0);
122         return getResolversForIntentAsUserInternal(intents, userHandle, baseFlags);
123     }
124 
getResolversForIntentAsUserInternal( List<Intent> intents, UserHandle userHandle, int baseFlags)125     private List<ResolvedComponentInfo> getResolversForIntentAsUserInternal(
126             List<Intent> intents, UserHandle userHandle, int baseFlags) {
127         List<ResolvedComponentInfo> resolvedComponents = null;
128         for (int i = 0, N = intents.size(); i < N; i++) {
129             Intent intent = intents.get(i);
130             int flags = baseFlags;
131             if (intent.isWebIntent()
132                         || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
133                 flags |= PackageManager.MATCH_INSTANT;
134             }
135             // Because of AIDL bug, queryIntentActivitiesAsUser can't accept subclasses of Intent.
136             intent = (intent.getClass() == Intent.class) ? intent : new Intent(
137                     intent);
138             final List<ResolveInfo> infos = mpm.queryIntentActivitiesAsUser(intent, flags,
139                     userHandle);
140             if (infos != null) {
141                 if (resolvedComponents == null) {
142                     resolvedComponents = new ArrayList<>();
143                 }
144                 addResolveListDedupe(resolvedComponents, intent, infos);
145             }
146         }
147         return resolvedComponents;
148     }
149 
150     @VisibleForTesting
addResolveListDedupe( List<ResolvedComponentInfo> into, Intent intent, List<ResolveInfo> from)151     public void addResolveListDedupe(
152             List<ResolvedComponentInfo> into, Intent intent, List<ResolveInfo> from) {
153         final int fromCount = from.size();
154         final int intoCount = into.size();
155         for (int i = 0; i < fromCount; i++) {
156             final ResolveInfo newInfo = from.get(i);
157             boolean found = false;
158             // Only loop to the end of into as it was before we started; no dupes in from.
159             for (int j = 0; j < intoCount; j++) {
160                 final ResolvedComponentInfo rci = into.get(j);
161                 if (isSameResolvedComponent(newInfo, rci)) {
162                     found = true;
163                     rci.add(intent, newInfo);
164                     break;
165                 }
166             }
167             if (!found) {
168                 final ComponentName name = new ComponentName(
169                         newInfo.activityInfo.packageName, newInfo.activityInfo.name);
170                 final ResolvedComponentInfo rci = new ResolvedComponentInfo(name, intent, newInfo);
171                 rci.setPinned(isComponentPinned(name));
172                 into.add(rci);
173             }
174         }
175     }
176 
177 
178     /**
179      * Whether this component is pinned by the user. Always false for resolver; overridden in
180      * Chooser.
181      */
isComponentPinned(ComponentName name)182     public boolean isComponentPinned(ComponentName name) {
183         return false;
184     }
185 
186     // Filter out any activities that the launched uid does not have permission for.
187     // To preserve the inputList, optionally will return the original list if any modification has
188     // been made.
189     @VisibleForTesting
filterIneligibleActivities( List<ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified)190     public ArrayList<ResolvedComponentInfo> filterIneligibleActivities(
191             List<ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified) {
192         ArrayList<ResolvedComponentInfo> listToReturn = null;
193         for (int i = inputList.size()-1; i >= 0; i--) {
194             ActivityInfo ai = inputList.get(i)
195                     .getResolveInfoAt(0).activityInfo;
196             int granted = ActivityManager.checkComponentPermission(
197                     ai.permission, mLaunchedFromUid,
198                     ai.applicationInfo.uid, ai.exported);
199 
200             if (granted != PackageManager.PERMISSION_GRANTED
201                     || isComponentFiltered(ai.getComponentName())) {
202                 // Access not allowed! We're about to filter an item,
203                 // so modify the unfiltered version if it hasn't already been modified.
204                 if (returnCopyOfOriginalListIfModified && listToReturn == null) {
205                     listToReturn = new ArrayList<>(inputList);
206                 }
207                 inputList.remove(i);
208             }
209         }
210         return listToReturn;
211     }
212 
213     // Filter out any low priority items.
214     //
215     // To preserve the inputList, optionally will return the original list if any modification has
216     // been made.
217     @VisibleForTesting
filterLowPriority( List<ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified)218     public ArrayList<ResolvedComponentInfo> filterLowPriority(
219             List<ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified) {
220         ArrayList<ResolvedComponentInfo> listToReturn = null;
221         // Only display the first matches that are either of equal
222         // priority or have asked to be default options.
223         ResolvedComponentInfo rci0 = inputList.get(0);
224         ResolveInfo r0 = rci0.getResolveInfoAt(0);
225         int N = inputList.size();
226         for (int i = 1; i < N; i++) {
227             ResolveInfo ri = inputList.get(i).getResolveInfoAt(0);
228             if (DEBUG) Log.v(
229                     TAG,
230                     r0.activityInfo.name + "=" +
231                             r0.priority + "/" + r0.isDefault + " vs " +
232                             ri.activityInfo.name + "=" +
233                             ri.priority + "/" + ri.isDefault);
234             if (r0.priority != ri.priority ||
235                     r0.isDefault != ri.isDefault) {
236                 while (i < N) {
237                     if (returnCopyOfOriginalListIfModified && listToReturn == null) {
238                         listToReturn = new ArrayList<>(inputList);
239                     }
240                     inputList.remove(i);
241                     N--;
242                 }
243             }
244         }
245         return listToReturn;
246     }
247 
compute(List<ResolvedComponentInfo> inputList)248     private void compute(List<ResolvedComponentInfo> inputList) throws InterruptedException {
249         if (mResolverComparator == null) {
250             Log.d(TAG, "Comparator has already been destroyed; skipped.");
251             return;
252         }
253         final CountDownLatch finishComputeSignal = new CountDownLatch(1);
254         mResolverComparator.setCallBack(() -> finishComputeSignal.countDown());
255         mResolverComparator.compute(inputList);
256         finishComputeSignal.await();
257         isComputed = true;
258     }
259 
260     @VisibleForTesting
261     @WorkerThread
sort(List<ResolvedComponentInfo> inputList)262     public void sort(List<ResolvedComponentInfo> inputList) {
263         try {
264             long beforeRank = System.currentTimeMillis();
265             if (!isComputed) {
266                 compute(inputList);
267             }
268             Collections.sort(inputList, mResolverComparator);
269 
270             long afterRank = System.currentTimeMillis();
271             if (DEBUG) {
272                 Log.d(TAG, "Time Cost: " + Long.toString(afterRank - beforeRank));
273             }
274         } catch (InterruptedException e) {
275             Log.e(TAG, "Compute & Sort was interrupted: " + e);
276         }
277     }
278 
279     @VisibleForTesting
280     @WorkerThread
topK(List<ResolvedComponentInfo> inputList, int k)281     public void topK(List<ResolvedComponentInfo> inputList, int k) {
282         if (inputList == null || inputList.isEmpty() || k <= 0) {
283             return;
284         }
285         if (inputList.size() <= k) {
286             // Fall into normal sort when number of ranked elements
287             // needed is not smaller than size of input list.
288             sort(inputList);
289             return;
290         }
291         try {
292             long beforeRank = System.currentTimeMillis();
293             if (!isComputed) {
294                 compute(inputList);
295             }
296 
297             // Top of this heap has lowest rank.
298             PriorityQueue<ResolvedComponentInfo> minHeap = new PriorityQueue<>(k,
299                     (o1, o2) -> -mResolverComparator.compare(o1, o2));
300             final int size = inputList.size();
301             // Use this pointer to keep track of the position of next element
302             // to update in input list, starting from the last position.
303             int pointer = size - 1;
304             minHeap.addAll(inputList.subList(size - k, size));
305             for (int i = size - k - 1; i >= 0; --i) {
306                 ResolvedComponentInfo ci = inputList.get(i);
307                 if (-mResolverComparator.compare(ci, minHeap.peek()) > 0) {
308                     // When ranked higher than top of heap, remove top of heap,
309                     // update input list with it, add this new element to heap.
310                     inputList.set(pointer--, minHeap.poll());
311                     minHeap.add(ci);
312                 } else {
313                     // When ranked no higher than top of heap, update input list
314                     // with this new element.
315                     inputList.set(pointer--, ci);
316                 }
317             }
318 
319             // Now we have top k elements in heap, update first
320             // k positions of input list with them.
321             while (!minHeap.isEmpty()) {
322                 inputList.set(pointer--, minHeap.poll());
323             }
324 
325             long afterRank = System.currentTimeMillis();
326             if (DEBUG) {
327                 Log.d(TAG, "Time Cost for top " + k + " targets: "
328                         + Long.toString(afterRank - beforeRank));
329             }
330         } catch (InterruptedException e) {
331             Log.e(TAG, "Compute & greatestOf was interrupted: " + e);
332         }
333     }
334 
isSameResolvedComponent(ResolveInfo a, ResolvedComponentInfo b)335     private static boolean isSameResolvedComponent(ResolveInfo a, ResolvedComponentInfo b) {
336         final ActivityInfo ai = a.activityInfo;
337         return ai.packageName.equals(b.name.getPackageName())
338                 && ai.name.equals(b.name.getClassName());
339     }
340 
isComponentFiltered(ComponentName componentName)341     boolean isComponentFiltered(ComponentName componentName) {
342         return false;
343     }
344 
345     @VisibleForTesting
getScore(DisplayResolveInfo target)346     public float getScore(DisplayResolveInfo target) {
347         return mResolverComparator.getScore(target.getResolvedComponentName());
348     }
349 
350     /**
351      * Returns the app share score of the given {@code componentName}.
352      */
getScore(ComponentName componentName)353     public float getScore(ComponentName componentName) {
354         return mResolverComparator.getScore(componentName);
355     }
356 
updateModel(ComponentName componentName)357     public void updateModel(ComponentName componentName) {
358         mResolverComparator.updateModel(componentName);
359     }
360 
updateChooserCounts(String packageName, int userId, String action)361     public void updateChooserCounts(String packageName, int userId, String action) {
362         mResolverComparator.updateChooserCounts(packageName, userId, action);
363     }
364 
destroy()365     public void destroy() {
366         mResolverComparator.destroy();
367     }
368 }
369