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