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.internal.app;
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.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.os.RemoteException;
32 import android.util.Log;
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.lang.InterruptedException;
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.concurrent.CountDownLatch;
39 import java.util.List;
40 
41 /**
42  * A helper for the ResolverActivity that exposes methods to retrieve, filter and sort its list of
43  * resolvers.
44  */
45 public class ResolverListController {
46 
47     private final Context mContext;
48     private final PackageManager mpm;
49     private final int mLaunchedFromUid;
50 
51     // Needed for sorting resolvers.
52     private final Intent mTargetIntent;
53     private final String mReferrerPackage;
54 
55     private static final String TAG = "ResolverListController";
56     private static final boolean DEBUG = false;
57 
58     private ResolverComparator mResolverComparator;
59 
ResolverListController( Context context, PackageManager pm, Intent targetIntent, String referrerPackage, int launchedFromUid)60     public ResolverListController(
61             Context context,
62             PackageManager pm,
63             Intent targetIntent,
64             String referrerPackage,
65             int launchedFromUid) {
66         mContext = context;
67         mpm = pm;
68         mLaunchedFromUid = launchedFromUid;
69         mTargetIntent = targetIntent;
70         mReferrerPackage = referrerPackage;
71     }
72 
73     @VisibleForTesting
getLastChosen()74     public ResolveInfo getLastChosen() throws RemoteException {
75         return AppGlobals.getPackageManager().getLastChosenActivity(
76                 mTargetIntent, mTargetIntent.resolveTypeIfNeeded(mContext.getContentResolver()),
77                 PackageManager.MATCH_DEFAULT_ONLY);
78     }
79 
80     @VisibleForTesting
setLastChosen(Intent intent, IntentFilter filter, int match)81     public void setLastChosen(Intent intent, IntentFilter filter, int match)
82             throws RemoteException {
83         AppGlobals.getPackageManager().setLastChosenActivity(intent,
84                 intent.resolveType(mContext.getContentResolver()),
85                 PackageManager.MATCH_DEFAULT_ONLY,
86                 filter, match, intent.getComponent());
87     }
88 
89     @VisibleForTesting
getResolversForIntent( boolean shouldGetResolvedFilter, boolean shouldGetActivityMetadata, List<Intent> intents)90     public List<ResolverActivity.ResolvedComponentInfo> getResolversForIntent(
91             boolean shouldGetResolvedFilter,
92             boolean shouldGetActivityMetadata,
93             List<Intent> intents) {
94         List<ResolverActivity.ResolvedComponentInfo> resolvedComponents = null;
95         for (int i = 0, N = intents.size(); i < N; i++) {
96             final Intent intent = intents.get(i);
97             final List<ResolveInfo> infos = mpm.queryIntentActivities(intent,
98                     PackageManager.MATCH_DEFAULT_ONLY
99                             | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
100                             | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0)
101                             | PackageManager.MATCH_INSTANT);
102             // Remove any activities that are not exported.
103             int totalSize = infos.size();
104             for (int j = totalSize - 1; j >= 0 ; j--) {
105                 ResolveInfo info = infos.get(j);
106                 if (info.activityInfo != null && !info.activityInfo.exported) {
107                     infos.remove(j);
108                 }
109             }
110             if (infos != null) {
111                 if (resolvedComponents == null) {
112                     resolvedComponents = new ArrayList<>();
113                 }
114                 addResolveListDedupe(resolvedComponents, intent, infos);
115             }
116         }
117         return resolvedComponents;
118     }
119 
120     @VisibleForTesting
addResolveListDedupe(List<ResolverActivity.ResolvedComponentInfo> into, Intent intent, List<ResolveInfo> from)121     public void addResolveListDedupe(List<ResolverActivity.ResolvedComponentInfo> into,
122             Intent intent,
123             List<ResolveInfo> from) {
124         final int fromCount = from.size();
125         final int intoCount = into.size();
126         for (int i = 0; i < fromCount; i++) {
127             final ResolveInfo newInfo = from.get(i);
128             boolean found = false;
129             // Only loop to the end of into as it was before we started; no dupes in from.
130             for (int j = 0; j < intoCount; j++) {
131                 final ResolverActivity.ResolvedComponentInfo rci = into.get(j);
132                 if (isSameResolvedComponent(newInfo, rci)) {
133                     found = true;
134                     rci.add(intent, newInfo);
135                     break;
136                 }
137             }
138             if (!found) {
139                 final ComponentName name = new ComponentName(
140                         newInfo.activityInfo.packageName, newInfo.activityInfo.name);
141                 final ResolverActivity.ResolvedComponentInfo rci =
142                         new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
143                 rci.setPinned(isComponentPinned(name));
144                 into.add(rci);
145             }
146         }
147     }
148 
149     // Filter out any activities that the launched uid does not have permission for.
150     //
151     // Also filter out those that are suspended because they couldn't be started. We don't do this
152     // when we have an explicit list of resolved activities, because that only happens when
153     // we are being subclassed, so we can safely launch whatever they gave us.
154     //
155     // To preserve the inputList, optionally will return the original list if any modification has
156     // been made.
157     @VisibleForTesting
filterIneligibleActivities( List<ResolverActivity.ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified)158     public ArrayList<ResolverActivity.ResolvedComponentInfo> filterIneligibleActivities(
159             List<ResolverActivity.ResolvedComponentInfo> inputList,
160             boolean returnCopyOfOriginalListIfModified) {
161         ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
162         for (int i = inputList.size()-1; i >= 0; i--) {
163             ActivityInfo ai = inputList.get(i)
164                     .getResolveInfoAt(0).activityInfo;
165             int granted = ActivityManager.checkComponentPermission(
166                     ai.permission, mLaunchedFromUid,
167                     ai.applicationInfo.uid, ai.exported);
168             boolean suspended = (ai.applicationInfo.flags
169                     & ApplicationInfo.FLAG_SUSPENDED) != 0;
170             if (granted != PackageManager.PERMISSION_GRANTED || suspended
171                     || isComponentFiltered(ai.getComponentName())) {
172                 // Access not allowed! We're about to filter an item,
173                 // so modify the unfiltered version if it hasn't already been modified.
174                 if (returnCopyOfOriginalListIfModified && listToReturn == null) {
175                     listToReturn = new ArrayList<>(inputList);
176                 }
177                 inputList.remove(i);
178             }
179         }
180         return listToReturn;
181     }
182 
183     // Filter out any low priority items.
184     //
185     // To preserve the inputList, optionally will return the original list if any modification has
186     // been made.
187     @VisibleForTesting
filterLowPriority( List<ResolverActivity.ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified)188     public ArrayList<ResolverActivity.ResolvedComponentInfo> filterLowPriority(
189             List<ResolverActivity.ResolvedComponentInfo> inputList,
190             boolean returnCopyOfOriginalListIfModified) {
191         ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
192         // Only display the first matches that are either of equal
193         // priority or have asked to be default options.
194         ResolverActivity.ResolvedComponentInfo rci0 = inputList.get(0);
195         ResolveInfo r0 = rci0.getResolveInfoAt(0);
196         int N = inputList.size();
197         for (int i = 1; i < N; i++) {
198             ResolveInfo ri = inputList.get(i).getResolveInfoAt(0);
199             if (DEBUG) Log.v(
200                     TAG,
201                     r0.activityInfo.name + "=" +
202                             r0.priority + "/" + r0.isDefault + " vs " +
203                             ri.activityInfo.name + "=" +
204                             ri.priority + "/" + ri.isDefault);
205             if (r0.priority != ri.priority ||
206                     r0.isDefault != ri.isDefault) {
207                 while (i < N) {
208                     if (returnCopyOfOriginalListIfModified && listToReturn == null) {
209                         listToReturn = new ArrayList<>(inputList);
210                     }
211                     inputList.remove(i);
212                     N--;
213                 }
214             }
215         }
216         return listToReturn;
217     }
218 
219     private class ComputeCallback implements ResolverComparator.AfterCompute {
220 
221         private CountDownLatch mFinishComputeSignal;
222 
ComputeCallback(CountDownLatch finishComputeSignal)223         public ComputeCallback(CountDownLatch finishComputeSignal) {
224             mFinishComputeSignal = finishComputeSignal;
225         }
226 
afterCompute()227         public void afterCompute () {
228             mFinishComputeSignal.countDown();
229         }
230     }
231 
232     @VisibleForTesting
233     @WorkerThread
sort(List<ResolverActivity.ResolvedComponentInfo> inputList)234     public void sort(List<ResolverActivity.ResolvedComponentInfo> inputList) {
235         final CountDownLatch finishComputeSignal = new CountDownLatch(1);
236         ComputeCallback callback = new ComputeCallback(finishComputeSignal);
237         if (mResolverComparator == null) {
238             mResolverComparator =
239                     new ResolverComparator(mContext, mTargetIntent, mReferrerPackage, callback);
240         } else {
241             mResolverComparator.setCallBack(callback);
242         }
243         try {
244             long beforeRank = System.currentTimeMillis();
245             mResolverComparator.compute(inputList);
246             finishComputeSignal.await();
247             Collections.sort(inputList, mResolverComparator);
248             long afterRank = System.currentTimeMillis();
249             if (DEBUG) {
250                 Log.d(TAG, "Time Cost: " + Long.toString(afterRank - beforeRank));
251             }
252         } catch (InterruptedException e) {
253             Log.e(TAG, "Compute & Sort was interrupted: " + e);
254         }
255     }
256 
isSameResolvedComponent(ResolveInfo a, ResolverActivity.ResolvedComponentInfo b)257     private static boolean isSameResolvedComponent(ResolveInfo a,
258             ResolverActivity.ResolvedComponentInfo b) {
259         final ActivityInfo ai = a.activityInfo;
260         return ai.packageName.equals(b.name.getPackageName())
261                 && ai.name.equals(b.name.getClassName());
262     }
263 
isComponentPinned(ComponentName name)264     boolean isComponentPinned(ComponentName name) {
265         return false;
266     }
267 
isComponentFiltered(ComponentName componentName)268     boolean isComponentFiltered(ComponentName componentName) {
269         return false;
270     }
271 
272     @VisibleForTesting
getScore(ResolverActivity.DisplayResolveInfo target)273     public float getScore(ResolverActivity.DisplayResolveInfo target) {
274         if (mResolverComparator == null) {
275             return 0.0f;
276         }
277         return mResolverComparator.getScore(target.getResolvedComponentName());
278     }
279 
updateModel(ComponentName componentName)280     public void updateModel(ComponentName componentName) {
281         if (mResolverComparator != null) {
282             mResolverComparator.updateModel(componentName);
283         }
284     }
285 
updateChooserCounts(String packageName, int userId, String action)286     public void updateChooserCounts(String packageName, int userId, String action) {
287         if (mResolverComparator != null) {
288             mResolverComparator.updateChooserCounts(packageName, userId, action);
289         }
290     }
291 
destroy()292     public void destroy() {
293         if (mResolverComparator != null) {
294             mResolverComparator.destroy();
295         }
296     }
297 }
298