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