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