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