1 /* 2 * Copyright (C) 2019 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 package com.android.internal.app; 18 19 import static android.content.Context.ACTIVITY_SERVICE; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.ActivityManager; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.PermissionChecker; 28 import android.content.pm.ActivityInfo; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.LabeledIntent; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.res.Resources; 34 import android.graphics.Bitmap; 35 import android.graphics.ColorMatrix; 36 import android.graphics.ColorMatrixColorFilter; 37 import android.graphics.drawable.BitmapDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.os.AsyncTask; 40 import android.os.RemoteException; 41 import android.os.Trace; 42 import android.os.UserHandle; 43 import android.os.UserManager; 44 import android.text.TextUtils; 45 import android.util.Log; 46 import android.view.LayoutInflater; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.widget.AbsListView; 50 import android.widget.BaseAdapter; 51 import android.widget.ImageView; 52 import android.widget.TextView; 53 54 import com.android.internal.R; 55 import com.android.internal.annotations.VisibleForTesting; 56 import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; 57 import com.android.internal.app.chooser.DisplayResolveInfo; 58 import com.android.internal.app.chooser.TargetInfo; 59 60 import java.util.ArrayList; 61 import java.util.Collection; 62 import java.util.HashMap; 63 import java.util.List; 64 import java.util.Map; 65 66 public class ResolverListAdapter extends BaseAdapter { 67 private static final String TAG = "ResolverListAdapter"; 68 69 private final List<Intent> mIntents; 70 private final Intent[] mInitialIntents; 71 private final List<ResolveInfo> mBaseResolveList; 72 private final PackageManager mPm; 73 protected final Context mContext; 74 private static ColorMatrixColorFilter sSuspendedMatrixColorFilter; 75 private final int mIconDpi; 76 protected ResolveInfo mLastChosen; 77 private DisplayResolveInfo mOtherProfile; 78 ResolverListController mResolverListController; 79 private int mPlaceholderCount; 80 81 protected final LayoutInflater mInflater; 82 83 // This one is the list that the Adapter will actually present. 84 List<DisplayResolveInfo> mDisplayList; 85 private List<ResolvedComponentInfo> mUnfilteredResolveList; 86 87 private int mLastChosenPosition = -1; 88 private boolean mFilterLastUsed; 89 final ResolverListCommunicator mResolverListCommunicator; 90 private Runnable mPostListReadyRunnable; 91 private final boolean mIsAudioCaptureDevice; 92 private boolean mIsTabLoaded; 93 private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); 94 private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>(); 95 ResolverListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, ResolverListCommunicator resolverListCommunicator, boolean isAudioCaptureDevice)96 public ResolverListAdapter(Context context, List<Intent> payloadIntents, 97 Intent[] initialIntents, List<ResolveInfo> rList, 98 boolean filterLastUsed, 99 ResolverListController resolverListController, 100 ResolverListCommunicator resolverListCommunicator, 101 boolean isAudioCaptureDevice) { 102 mContext = context; 103 mIntents = payloadIntents; 104 mInitialIntents = initialIntents; 105 mBaseResolveList = rList; 106 mInflater = LayoutInflater.from(context); 107 mPm = context.getPackageManager(); 108 mDisplayList = new ArrayList<>(); 109 mFilterLastUsed = filterLastUsed; 110 mResolverListController = resolverListController; 111 mResolverListCommunicator = resolverListCommunicator; 112 mIsAudioCaptureDevice = isAudioCaptureDevice; 113 final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE); 114 mIconDpi = am.getLauncherLargeIconDensity(); 115 } 116 handlePackagesChanged()117 public void handlePackagesChanged() { 118 mResolverListCommunicator.onHandlePackagesChanged(this); 119 } 120 setPlaceholderCount(int count)121 public void setPlaceholderCount(int count) { 122 mPlaceholderCount = count; 123 } 124 getPlaceholderCount()125 public int getPlaceholderCount() { 126 return mPlaceholderCount; 127 } 128 129 @Nullable getFilteredItem()130 public DisplayResolveInfo getFilteredItem() { 131 if (mFilterLastUsed && mLastChosenPosition >= 0) { 132 // Not using getItem since it offsets to dodge this position for the list 133 return mDisplayList.get(mLastChosenPosition); 134 } 135 return null; 136 } 137 getOtherProfile()138 public DisplayResolveInfo getOtherProfile() { 139 return mOtherProfile; 140 } 141 getFilteredPosition()142 public int getFilteredPosition() { 143 if (mFilterLastUsed && mLastChosenPosition >= 0) { 144 return mLastChosenPosition; 145 } 146 return AbsListView.INVALID_POSITION; 147 } 148 hasFilteredItem()149 public boolean hasFilteredItem() { 150 return mFilterLastUsed && mLastChosen != null; 151 } 152 getScore(DisplayResolveInfo target)153 public float getScore(DisplayResolveInfo target) { 154 return mResolverListController.getScore(target); 155 } 156 157 /** 158 * Returns the app share score of the given {@code componentName}. 159 */ getScore(ComponentName componentName)160 public float getScore(ComponentName componentName) { 161 return mResolverListController.getScore(componentName); 162 } 163 updateModel(ComponentName componentName)164 public void updateModel(ComponentName componentName) { 165 mResolverListController.updateModel(componentName); 166 } 167 updateChooserCounts(String packageName, String action)168 public void updateChooserCounts(String packageName, String action) { 169 mResolverListController.updateChooserCounts( 170 packageName, getUserHandle().getIdentifier(), action); 171 } 172 getUnfilteredResolveList()173 List<ResolvedComponentInfo> getUnfilteredResolveList() { 174 return mUnfilteredResolveList; 175 } 176 177 /** 178 * Rebuild the list of resolvers. When rebuilding is complete, queue the {@code onPostListReady} 179 * callback on the main handler with {@code rebuildCompleted} true. 180 * 181 * In some cases some parts will need some asynchronous work to complete. Then this will first 182 * immediately queue {@code onPostListReady} (on the main handler) with {@code rebuildCompleted} 183 * false; only when the asynchronous work completes will this then go on to queue another 184 * {@code onPostListReady} callback with {@code rebuildCompleted} true. 185 * 186 * The {@code doPostProcessing} parameter is used to specify whether to update the UI and 187 * load additional targets (e.g. direct share) after the list has been rebuilt. We may choose 188 * to skip that step if we're only loading the inactive profile's resolved apps to know the 189 * number of targets. 190 * 191 * @return Whether the list building was completed synchronously. If not, we'll queue the 192 * {@code onPostListReady} callback first with {@code rebuildCompleted} false, and then again 193 * with {@code rebuildCompleted} true at the end of some newly-launched asynchronous work. 194 * Otherwise the callback is only queued once, with {@code rebuildCompleted} true. 195 */ rebuildList(boolean doPostProcessing)196 protected boolean rebuildList(boolean doPostProcessing) { 197 Trace.beginSection("ResolverListAdapter#rebuildList"); 198 mDisplayList.clear(); 199 mIsTabLoaded = false; 200 mLastChosenPosition = -1; 201 202 List<ResolvedComponentInfo> currentResolveList = getInitialRebuiltResolveList(); 203 204 /* TODO: this seems like unnecessary extra complexity; why do we need to do this "primary" 205 * (i.e. "eligibility") filtering before evaluating the "other profile" special-treatment, 206 * but the "secondary" (i.e. "priority") filtering after? Are there in fact cases where the 207 * eligibility conditions will filter out a result that would've otherwise gotten the "other 208 * profile" treatment? Or, are there cases where the priority conditions *would* filter out 209 * a result, but we *want* that result to get the "other profile" treatment, so we only 210 * filter *after* evaluating the special-treatment conditions? If the answer to either is 211 * "no," then the filtering steps can be consolidated. (And that also makes the "unfiltered 212 * list" bookkeeping a little cleaner.) 213 */ 214 mUnfilteredResolveList = performPrimaryResolveListFiltering(currentResolveList); 215 216 // So far we only support a single other profile at a time. 217 // The first one we see gets special treatment. 218 ResolvedComponentInfo otherProfileInfo = 219 getFirstNonCurrentUserResolvedComponentInfo(currentResolveList); 220 updateOtherProfileTreatment(otherProfileInfo); 221 if (otherProfileInfo != null) { 222 currentResolveList.remove(otherProfileInfo); 223 /* TODO: the previous line removed the "other profile info" item from 224 * mUnfilteredResolveList *ONLY IF* that variable is an alias for the same List instance 225 * as currentResolveList (i.e., if no items were filtered out as the result of the 226 * earlier "primary" filtering). It seems wrong for our behavior to depend on that. 227 * Should we: 228 * A. replicate the above removal to mUnfilteredResolveList (which is idempotent, so we 229 * don't even have to check whether they're aliases); or 230 * B. break the alias relationship by copying currentResolveList to a new 231 * mUnfilteredResolveList instance if necessary before removing otherProfileInfo? 232 * In other words: do we *want* otherProfileInfo in the "unfiltered" results? Either 233 * way, we'll need one of the changes suggested above. 234 */ 235 } 236 237 // If no results have yet been filtered, mUnfilteredResolveList is an alias for the same 238 // List instance as currentResolveList. Then we need to make a copy to store as the 239 // mUnfilteredResolveList if we go on to filter any more items. Otherwise we've already 240 // copied the original unfiltered items to a separate List instance and can now filter 241 // the remainder in-place without any further bookkeeping. 242 boolean needsCopyOfUnfiltered = (mUnfilteredResolveList == currentResolveList); 243 List<ResolvedComponentInfo> originalList = performSecondaryResolveListFiltering( 244 currentResolveList, needsCopyOfUnfiltered); 245 if (originalList != null) { 246 // Only need the originalList value if there was a modification (otherwise it's null 247 // and shouldn't overwrite mUnfilteredResolveList). 248 mUnfilteredResolveList = originalList; 249 } 250 251 boolean result = 252 finishRebuildingListWithFilteredResults(currentResolveList, doPostProcessing); 253 Trace.endSection(); 254 return result; 255 } 256 257 /** 258 * Get the full (unfiltered) set of {@code ResolvedComponentInfo} records for all resolvers 259 * to be considered in a newly-rebuilt list. This list will be filtered and ranked before the 260 * rebuild is complete. 261 */ getInitialRebuiltResolveList()262 List<ResolvedComponentInfo> getInitialRebuiltResolveList() { 263 if (mBaseResolveList != null) { 264 List<ResolvedComponentInfo> currentResolveList = new ArrayList<>(); 265 mResolverListController.addResolveListDedupe(currentResolveList, 266 mResolverListCommunicator.getTargetIntent(), 267 mBaseResolveList); 268 return currentResolveList; 269 } else { 270 return mResolverListController.getResolversForIntent( 271 /* shouldGetResolvedFilter= */ true, 272 mResolverListCommunicator.shouldGetActivityMetadata(), 273 mResolverListCommunicator.shouldGetOnlyDefaultActivities(), 274 mIntents); 275 } 276 } 277 278 /** 279 * Remove ineligible activities from {@code currentResolveList} (if non-null), in-place. More 280 * broadly, filtering logic should apply in the "primary" stage if it should preclude items from 281 * receiving the "other profile" special-treatment described in {@code rebuildList()}. 282 * 283 * @return A copy of the original {@code currentResolveList}, if any items were removed, or a 284 * (possibly null) reference to the original list otherwise. (That is, this always returns a 285 * list of all the unfiltered items, but if no items were filtered, it's just an alias for the 286 * same list that was passed in). 287 */ 288 @Nullable performPrimaryResolveListFiltering( @ullable List<ResolvedComponentInfo> currentResolveList)289 List<ResolvedComponentInfo> performPrimaryResolveListFiltering( 290 @Nullable List<ResolvedComponentInfo> currentResolveList) { 291 /* TODO: mBaseResolveList appears to be(?) some kind of configured mode. Why is it not 292 * subject to filterIneligibleActivities, even though all the other logic still applies 293 * (including "secondary" filtering)? (This also relates to the earlier question; do we 294 * believe there's an item that would be eligible for "other profile" special treatment, 295 * except we want to filter it out as ineligible... but only if we're not in 296 * "mBaseResolveList mode"? */ 297 if ((mBaseResolveList != null) || (currentResolveList == null)) { 298 return currentResolveList; 299 } 300 301 List<ResolvedComponentInfo> originalList = 302 mResolverListController.filterIneligibleActivities(currentResolveList, true); 303 return (originalList == null) ? currentResolveList : originalList; 304 } 305 306 /** 307 * Remove low-priority activities from {@code currentResolveList} (if non-null), in place. More 308 * broadly, filtering logic should apply in the "secondary" stage to prevent items from 309 * appearing in the rebuilt-list results, while still considering those items for the "other 310 * profile" special-treatment described in {@code rebuildList()}. 311 * 312 * @return the same (possibly null) List reference as {@code currentResolveList} if the list is 313 * unmodified as a result of filtering; or, if some item(s) were removed, then either a copy of 314 * the original {@code currentResolveList} (if {@code returnCopyOfOriginalListIfModified} is 315 * true), or null (otherwise). 316 */ 317 @Nullable performSecondaryResolveListFiltering( @ullable List<ResolvedComponentInfo> currentResolveList, boolean returnCopyOfOriginalListIfModified)318 List<ResolvedComponentInfo> performSecondaryResolveListFiltering( 319 @Nullable List<ResolvedComponentInfo> currentResolveList, 320 boolean returnCopyOfOriginalListIfModified) { 321 if ((currentResolveList == null) || currentResolveList.isEmpty()) { 322 return currentResolveList; 323 } 324 return mResolverListController.filterLowPriority( 325 currentResolveList, returnCopyOfOriginalListIfModified); 326 } 327 328 /** 329 * Update the special "other profile" UI treatment based on the components resolved for a 330 * newly-built list. 331 * 332 * @param otherProfileInfo the first {@code ResolvedComponentInfo} specifying a 333 * {@code targetUserId} other than {@code USER_CURRENT}, or null if no such component info was 334 * found in the process of rebuilding the list (or if any such candidates were already removed 335 * due to "primary filtering"). 336 */ updateOtherProfileTreatment(@ullable ResolvedComponentInfo otherProfileInfo)337 void updateOtherProfileTreatment(@Nullable ResolvedComponentInfo otherProfileInfo) { 338 mLastChosen = null; 339 340 if (otherProfileInfo != null) { 341 mOtherProfile = makeOtherProfileDisplayResolveInfo( 342 mContext, otherProfileInfo, mPm, mResolverListCommunicator, mIconDpi); 343 } else { 344 mOtherProfile = null; 345 try { 346 mLastChosen = mResolverListController.getLastChosen(); 347 // TODO: does this also somehow need to update mLastChosenPosition? If so, maybe 348 // the current method should also take responsibility for re-initializing 349 // mLastChosenPosition, where it's currently done at the start of rebuildList()? 350 // (Why is this related to the presence of mOtherProfile in fhe first place?) 351 } catch (RemoteException re) { 352 Log.d(TAG, "Error calling getLastChosenActivity\n" + re); 353 } 354 } 355 } 356 357 /** 358 * Prepare the appropriate placeholders to eventually display the final set of resolved 359 * components in a newly-rebuilt list, and spawn an asynchronous sorting task if necessary. 360 * This eventually results in a {@code onPostListReady} callback with {@code rebuildCompleted} 361 * true; if any asynchronous work is required, that will first be preceded by a separate 362 * occurrence of the callback with {@code rebuildCompleted} false (once there are placeholders 363 * set up to represent the pending asynchronous results). 364 * @return Whether we were able to do all the work to prepare the list for display 365 * synchronously; if false, there will eventually be two separate {@code onPostListReady} 366 * callbacks, first with placeholders to represent pending asynchronous results, then later when 367 * the results are ready for presentation. 368 */ finishRebuildingListWithFilteredResults( @ullable List<ResolvedComponentInfo> filteredResolveList, boolean doPostProcessing)369 boolean finishRebuildingListWithFilteredResults( 370 @Nullable List<ResolvedComponentInfo> filteredResolveList, boolean doPostProcessing) { 371 if (filteredResolveList == null || filteredResolveList.size() < 2) { 372 // No asynchronous work to do. 373 setPlaceholderCount(0); 374 processSortedList(filteredResolveList, doPostProcessing); 375 return true; 376 } 377 378 int placeholderCount = filteredResolveList.size(); 379 if (mResolverListCommunicator.useLayoutWithDefault()) { 380 --placeholderCount; 381 } 382 setPlaceholderCount(placeholderCount); 383 384 // Send an "incomplete" list-ready while the async task is running. 385 postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false); 386 createSortingTask(doPostProcessing).execute(filteredResolveList); 387 return false; 388 } 389 390 AsyncTask<List<ResolvedComponentInfo>, 391 Void, createSortingTask(boolean doPostProcessing)392 List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) { 393 return new AsyncTask<List<ResolvedComponentInfo>, 394 Void, 395 List<ResolvedComponentInfo>>() { 396 @Override 397 protected List<ResolvedComponentInfo> doInBackground( 398 List<ResolvedComponentInfo>... params) { 399 mResolverListController.sort(params[0]); 400 return params[0]; 401 } 402 @Override 403 protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { 404 processSortedList(sortedComponents, doPostProcessing); 405 notifyDataSetChanged(); 406 if (doPostProcessing) { 407 mResolverListCommunicator.updateProfileViewButton(); 408 } 409 } 410 }; 411 } 412 413 protected void processSortedList(List<ResolvedComponentInfo> sortedComponents, 414 boolean doPostProcessing) { 415 final int n = sortedComponents != null ? sortedComponents.size() : 0; 416 Trace.beginSection("ResolverListAdapter#processSortedList:" + n); 417 if (n != 0) { 418 // First put the initial items at the top. 419 if (mInitialIntents != null) { 420 for (int i = 0; i < mInitialIntents.length; i++) { 421 Intent ii = mInitialIntents[i]; 422 if (ii == null) { 423 continue; 424 } 425 // Because of AIDL bug, resolveActivityInfo can't accept subclasses of Intent. 426 final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii); 427 ActivityInfo ai = rii.resolveActivityInfo(mPm, 0); 428 if (ai == null) { 429 Log.w(TAG, "No activity found for " + ii); 430 continue; 431 } 432 ResolveInfo ri = new ResolveInfo(); 433 ri.activityInfo = ai; 434 UserManager userManager = 435 (UserManager) mContext.getSystemService(Context.USER_SERVICE); 436 if (ii instanceof LabeledIntent) { 437 LabeledIntent li = (LabeledIntent) ii; 438 ri.resolvePackageName = li.getSourcePackage(); 439 ri.labelRes = li.getLabelResource(); 440 ri.nonLocalizedLabel = li.getNonLocalizedLabel(); 441 ri.icon = li.getIconResource(); 442 ri.iconResourceId = ri.icon; 443 } 444 if (userManager.isManagedProfile()) { 445 ri.noResourceId = true; 446 ri.icon = 0; 447 } 448 449 addResolveInfo(new DisplayResolveInfo(ii, ri, 450 ri.loadLabel(mPm), null, ii, makePresentationGetter(ri))); 451 } 452 } 453 454 455 for (ResolvedComponentInfo rci : sortedComponents) { 456 final ResolveInfo ri = rci.getResolveInfoAt(0); 457 if (ri != null) { 458 addResolveInfoWithAlternates(rci); 459 } 460 } 461 } 462 463 mResolverListCommunicator.sendVoiceChoicesIfNeeded(); 464 postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); 465 mIsTabLoaded = true; 466 Trace.endSection(); 467 } 468 469 /** 470 * Some necessary methods for creating the list are initiated in onCreate and will also 471 * determine the layout known. We therefore can't update the UI inline and post to the 472 * handler thread to update after the current task is finished. 473 * @param doPostProcessing Whether to update the UI and load additional direct share targets 474 * after the list has been rebuilt 475 * @param rebuildCompleted Whether the list has been completely rebuilt 476 */ 477 void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) { 478 if (mPostListReadyRunnable == null) { 479 mPostListReadyRunnable = new Runnable() { 480 @Override 481 public void run() { 482 mResolverListCommunicator.onPostListReady(ResolverListAdapter.this, 483 doPostProcessing, rebuildCompleted); 484 mPostListReadyRunnable = null; 485 } 486 }; 487 mContext.getMainThreadHandler().post(mPostListReadyRunnable); 488 } 489 } 490 491 private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { 492 final int count = rci.getCount(); 493 final Intent intent = rci.getIntentAt(0); 494 final ResolveInfo add = rci.getResolveInfoAt(0); 495 final Intent replaceIntent = 496 mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent); 497 final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent( 498 add.activityInfo, mResolverListCommunicator.getTargetIntent()); 499 final DisplayResolveInfo 500 dri = new DisplayResolveInfo(intent, add, 501 replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add)); 502 dri.setPinned(rci.isPinned()); 503 if (rci.isPinned()) { 504 Log.i(TAG, "Pinned item: " + rci.name); 505 } 506 addResolveInfo(dri); 507 if (replaceIntent == intent) { 508 // Only add alternates if we didn't get a specific replacement from 509 // the caller. If we have one it trumps potential alternates. 510 for (int i = 1, n = count; i < n; i++) { 511 final Intent altIntent = rci.getIntentAt(i); 512 dri.addAlternateSourceIntent(altIntent); 513 } 514 } 515 updateLastChosenPosition(add); 516 } 517 518 private void updateLastChosenPosition(ResolveInfo info) { 519 // If another profile is present, ignore the last chosen entry. 520 if (mOtherProfile != null) { 521 mLastChosenPosition = -1; 522 return; 523 } 524 if (mLastChosen != null 525 && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) 526 && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { 527 mLastChosenPosition = mDisplayList.size() - 1; 528 } 529 } 530 531 // We assume that at this point we've already filtered out the only intent for a different 532 // targetUserId which we're going to use. 533 private void addResolveInfo(DisplayResolveInfo dri) { 534 if (dri != null && dri.getResolveInfo() != null 535 && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) { 536 if (shouldAddResolveInfo(dri)) { 537 mDisplayList.add(dri); 538 Log.i(TAG, "Add DisplayResolveInfo component: " + dri.getResolvedComponentName() 539 + ", intent component: " + dri.getResolvedIntent().getComponent()); 540 } 541 } 542 } 543 544 // Check whether {@code dri} should be added into mDisplayList. 545 protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) { 546 // Checks if this info is already listed in display. 547 for (DisplayResolveInfo existingInfo : mDisplayList) { 548 if (mResolverListCommunicator 549 .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) { 550 return false; 551 } 552 } 553 return true; 554 } 555 556 @Nullable 557 public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { 558 TargetInfo target = targetInfoForPosition(position, filtered); 559 if (target != null) { 560 return target.getResolveInfo(); 561 } 562 return null; 563 } 564 565 @Nullable 566 public TargetInfo targetInfoForPosition(int position, boolean filtered) { 567 if (filtered) { 568 return getItem(position); 569 } 570 if (mDisplayList.size() > position) { 571 return mDisplayList.get(position); 572 } 573 return null; 574 } 575 576 public int getCount() { 577 int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount : 578 mDisplayList.size(); 579 if (mFilterLastUsed && mLastChosenPosition >= 0) { 580 totalSize--; 581 } 582 return totalSize; 583 } 584 585 public int getUnfilteredCount() { 586 return mDisplayList.size(); 587 } 588 589 @Nullable 590 public TargetInfo getItem(int position) { 591 if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { 592 position++; 593 } 594 if (mDisplayList.size() > position) { 595 return mDisplayList.get(position); 596 } else { 597 return null; 598 } 599 } 600 601 public long getItemId(int position) { 602 return position; 603 } 604 605 public int getDisplayResolveInfoCount() { 606 return mDisplayList.size(); 607 } 608 609 public DisplayResolveInfo getDisplayResolveInfo(int index) { 610 // Used to query services. We only query services for primary targets, not alternates. 611 return mDisplayList.get(index); 612 } 613 614 public final View getView(int position, View convertView, ViewGroup parent) { 615 View view = convertView; 616 if (view == null) { 617 view = createView(parent); 618 } 619 onBindView(view, getItem(position), position); 620 return view; 621 } 622 623 public final View createView(ViewGroup parent) { 624 final View view = onCreateView(parent); 625 final ViewHolder holder = new ViewHolder(view); 626 view.setTag(holder); 627 return view; 628 } 629 630 View onCreateView(ViewGroup parent) { 631 return mInflater.inflate( 632 com.android.internal.R.layout.resolve_list_item, parent, false); 633 } 634 635 public final void bindView(int position, View view) { 636 onBindView(view, getItem(position), position); 637 } 638 639 protected void onBindView(View view, TargetInfo info, int position) { 640 final ViewHolder holder = (ViewHolder) view.getTag(); 641 if (info == null) { 642 holder.icon.setImageDrawable( 643 mContext.getDrawable(R.drawable.resolver_icon_placeholder)); 644 holder.bindLabel("", "", false); 645 return; 646 } 647 648 if (info instanceof DisplayResolveInfo) { 649 DisplayResolveInfo dri = (DisplayResolveInfo) info; 650 if (dri.hasDisplayLabel()) { 651 holder.bindLabel( 652 dri.getDisplayLabel(), 653 dri.getExtendedInfo(), 654 alwaysShowSubLabel()); 655 } else { 656 holder.bindLabel("", "", false); 657 loadLabel(dri); 658 } 659 holder.bindIcon(info); 660 if (!dri.hasDisplayIcon()) { 661 loadIcon(dri); 662 } 663 } 664 } 665 666 protected final void loadIcon(DisplayResolveInfo info) { 667 LoadIconTask task = mIconLoaders.get(info); 668 if (task == null) { 669 task = new LoadIconTask((DisplayResolveInfo) info); 670 mIconLoaders.put(info, task); 671 task.execute(); 672 } 673 } 674 675 private void loadLabel(DisplayResolveInfo info) { 676 LoadLabelTask task = mLabelLoaders.get(info); 677 if (task == null) { 678 task = createLoadLabelTask(info); 679 mLabelLoaders.put(info, task); 680 task.execute(); 681 } 682 } 683 684 protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { 685 return new LoadLabelTask(info); 686 } 687 688 public void onDestroy() { 689 if (mPostListReadyRunnable != null) { 690 mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); 691 mPostListReadyRunnable = null; 692 } 693 if (mResolverListController != null) { 694 mResolverListController.destroy(); 695 } 696 cancelTasks(mIconLoaders.values()); 697 cancelTasks(mLabelLoaders.values()); 698 mIconLoaders.clear(); 699 mLabelLoaders.clear(); 700 } 701 702 private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) { 703 for (T task: tasks) { 704 task.cancel(false); 705 } 706 } 707 708 private static ColorMatrixColorFilter getSuspendedColorMatrix() { 709 if (sSuspendedMatrixColorFilter == null) { 710 711 int grayValue = 127; 712 float scale = 0.5f; // half bright 713 714 ColorMatrix tempBrightnessMatrix = new ColorMatrix(); 715 float[] mat = tempBrightnessMatrix.getArray(); 716 mat[0] = scale; 717 mat[6] = scale; 718 mat[12] = scale; 719 mat[4] = grayValue; 720 mat[9] = grayValue; 721 mat[14] = grayValue; 722 723 ColorMatrix matrix = new ColorMatrix(); 724 matrix.setSaturation(0.0f); 725 matrix.preConcat(tempBrightnessMatrix); 726 sSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix); 727 } 728 return sSuspendedMatrixColorFilter; 729 } 730 731 ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) { 732 return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai); 733 } 734 735 ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) { 736 return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri); 737 } 738 739 Drawable loadIconForResolveInfo(ResolveInfo ri) { 740 // Load icons based on the current process. If in work profile icons should be badged. 741 return makePresentationGetter(ri).getIcon(getUserHandle()); 742 } 743 744 void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { 745 final DisplayResolveInfo iconInfo = getFilteredItem(); 746 if (iconView != null && iconInfo != null) { 747 new AsyncTask<Void, Void, Drawable>() { 748 @Override 749 protected Drawable doInBackground(Void... params) { 750 return loadIconForResolveInfo(iconInfo.getResolveInfo()); 751 } 752 753 @Override 754 protected void onPostExecute(Drawable d) { 755 iconView.setImageDrawable(d); 756 } 757 }.execute(); 758 } 759 } 760 761 @VisibleForTesting 762 public UserHandle getUserHandle() { 763 return mResolverListController.getUserHandle(); 764 } 765 766 protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) { 767 return mResolverListController.getResolversForIntentAsUser(true, 768 mResolverListCommunicator.shouldGetActivityMetadata(), 769 mResolverListCommunicator.shouldGetOnlyDefaultActivities(), 770 mIntents, userHandle); 771 } 772 773 protected List<Intent> getIntents() { 774 return mIntents; 775 } 776 777 protected boolean isTabLoaded() { 778 return mIsTabLoaded; 779 } 780 781 protected void markTabLoaded() { 782 mIsTabLoaded = true; 783 } 784 785 protected boolean alwaysShowSubLabel() { 786 return false; 787 } 788 789 /** 790 * Find the first element in a list of {@code ResolvedComponentInfo} objects whose 791 * {@code ResolveInfo} specifies a {@code targetUserId} other than the current user. 792 * @return the first ResolvedComponentInfo targeting a non-current user, or null if there are 793 * none (or if the list itself is null). 794 */ 795 private static ResolvedComponentInfo getFirstNonCurrentUserResolvedComponentInfo( 796 @Nullable List<ResolvedComponentInfo> resolveList) { 797 if (resolveList == null) { 798 return null; 799 } 800 801 for (ResolvedComponentInfo info : resolveList) { 802 ResolveInfo resolveInfo = info.getResolveInfoAt(0); 803 if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { 804 return info; 805 } 806 } 807 return null; 808 } 809 810 /** 811 * Set up a {@code DisplayResolveInfo} to provide "special treatment" for the first "other" 812 * profile in the resolve list (i.e., the first non-current profile to appear as the target user 813 * of an element in the resolve list). 814 */ 815 private static DisplayResolveInfo makeOtherProfileDisplayResolveInfo( 816 Context context, 817 ResolvedComponentInfo resolvedComponentInfo, 818 PackageManager pm, 819 ResolverListCommunicator resolverListCommunicator, 820 int iconDpi) { 821 ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0); 822 823 Intent pOrigIntent = resolverListCommunicator.getReplacementIntent( 824 resolveInfo.activityInfo, 825 resolvedComponentInfo.getIntentAt(0)); 826 Intent replacementIntent = resolverListCommunicator.getReplacementIntent( 827 resolveInfo.activityInfo, 828 resolverListCommunicator.getTargetIntent()); 829 830 ResolveInfoPresentationGetter presentationGetter = 831 new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo); 832 833 return new DisplayResolveInfo( 834 resolvedComponentInfo.getIntentAt(0), 835 resolveInfo, 836 resolveInfo.loadLabel(pm), 837 resolveInfo.loadLabel(pm), 838 pOrigIntent != null ? pOrigIntent : replacementIntent, 839 presentationGetter); 840 } 841 842 /** 843 * Necessary methods to communicate between {@link ResolverListAdapter} 844 * and {@link ResolverActivity}. 845 */ 846 interface ResolverListCommunicator { 847 848 boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs); 849 850 Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent); 851 852 void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi, 853 boolean rebuildCompleted); 854 855 void sendVoiceChoicesIfNeeded(); 856 857 void updateProfileViewButton(); 858 859 boolean useLayoutWithDefault(); 860 861 boolean shouldGetActivityMetadata(); 862 863 /** 864 * @return true to filter only apps that can handle 865 * {@link android.content.Intent#CATEGORY_DEFAULT} intents 866 */ 867 default boolean shouldGetOnlyDefaultActivities() { return true; }; 868 869 Intent getTargetIntent(); 870 871 void onHandlePackagesChanged(ResolverListAdapter listAdapter); 872 } 873 874 /** 875 * A view holder keeps a reference to a list view and provides functionality for managing its 876 * state. 877 */ 878 @VisibleForTesting 879 public static class ViewHolder { 880 public View itemView; 881 public Drawable defaultItemViewBackground; 882 883 public TextView text; 884 public TextView text2; 885 public ImageView icon; 886 887 @VisibleForTesting 888 public ViewHolder(View view) { 889 itemView = view; 890 defaultItemViewBackground = view.getBackground(); 891 text = (TextView) view.findViewById(com.android.internal.R.id.text1); 892 text2 = (TextView) view.findViewById(com.android.internal.R.id.text2); 893 icon = (ImageView) view.findViewById(R.id.icon); 894 } 895 896 public void bindLabel(CharSequence label, CharSequence subLabel, boolean showSubLabel) { 897 text.setText(label); 898 899 if (TextUtils.equals(label, subLabel)) { 900 subLabel = null; 901 } 902 903 text2.setText(subLabel); 904 if (showSubLabel || subLabel != null) { 905 text2.setVisibility(View.VISIBLE); 906 } else { 907 text2.setVisibility(View.GONE); 908 } 909 910 itemView.setContentDescription(null); 911 } 912 913 public void updateContentDescription(String description) { 914 itemView.setContentDescription(description); 915 } 916 917 public void bindIcon(TargetInfo info) { 918 icon.setImageDrawable(info.getDisplayIcon(itemView.getContext())); 919 if (info.isSuspended()) { 920 icon.setColorFilter(getSuspendedColorMatrix()); 921 } else { 922 icon.setColorFilter(null); 923 } 924 } 925 } 926 927 protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { 928 private final DisplayResolveInfo mDisplayResolveInfo; 929 930 protected LoadLabelTask(DisplayResolveInfo dri) { 931 mDisplayResolveInfo = dri; 932 } 933 934 @Override 935 protected CharSequence[] doInBackground(Void... voids) { 936 ResolveInfoPresentationGetter pg = 937 makePresentationGetter(mDisplayResolveInfo.getResolveInfo()); 938 939 if (mIsAudioCaptureDevice) { 940 // This is an audio capture device, so check record permissions 941 ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo; 942 String packageName = activityInfo.packageName; 943 944 int uid = activityInfo.applicationInfo.uid; 945 boolean hasRecordPermission = 946 PermissionChecker.checkPermissionForPreflight( 947 mContext, 948 android.Manifest.permission.RECORD_AUDIO, -1, uid, 949 packageName) 950 == android.content.pm.PackageManager.PERMISSION_GRANTED; 951 952 if (!hasRecordPermission) { 953 // Doesn't have record permission, so warn the user 954 return new CharSequence[] { 955 pg.getLabel(), 956 mContext.getString(R.string.usb_device_resolve_prompt_warn) 957 }; 958 } 959 } 960 961 return new CharSequence[] { 962 pg.getLabel(), 963 pg.getSubLabel() 964 }; 965 } 966 967 @Override 968 protected void onPostExecute(CharSequence[] result) { 969 if (mDisplayResolveInfo.hasDisplayLabel()) { 970 return; 971 } 972 mDisplayResolveInfo.setDisplayLabel(result[0]); 973 mDisplayResolveInfo.setExtendedInfo(result[1]); 974 notifyDataSetChanged(); 975 } 976 } 977 978 class LoadIconTask extends AsyncTask<Void, Void, Drawable> { 979 protected final DisplayResolveInfo mDisplayResolveInfo; 980 private final ResolveInfo mResolveInfo; 981 982 LoadIconTask(DisplayResolveInfo dri) { 983 mDisplayResolveInfo = dri; 984 mResolveInfo = dri.getResolveInfo(); 985 } 986 987 @Override 988 protected Drawable doInBackground(Void... params) { 989 return loadIconForResolveInfo(mResolveInfo); 990 } 991 992 @Override 993 protected void onPostExecute(Drawable d) { 994 if (getOtherProfile() == mDisplayResolveInfo) { 995 mResolverListCommunicator.updateProfileViewButton(); 996 } else if (!mDisplayResolveInfo.hasDisplayIcon()) { 997 mDisplayResolveInfo.setDisplayIcon(d); 998 notifyDataSetChanged(); 999 } 1000 } 1001 } 1002 1003 /** 1004 * Loads the icon and label for the provided ResolveInfo. 1005 */ 1006 @VisibleForTesting 1007 public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { 1008 private final ResolveInfo mRi; 1009 public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) { 1010 super(ctx, iconDpi, ri.activityInfo); 1011 mRi = ri; 1012 } 1013 1014 @Override 1015 Drawable getIconSubstituteInternal() { 1016 Drawable dr = null; 1017 try { 1018 // Do not use ResolveInfo#getIconResource() as it defaults to the app 1019 if (mRi.resolvePackageName != null && mRi.icon != 0) { 1020 dr = loadIconFromResource( 1021 mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon); 1022 } 1023 } catch (PackageManager.NameNotFoundException e) { 1024 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 1025 + "couldn't find resources for package", e); 1026 } 1027 1028 // Fall back to ActivityInfo if no icon is found via ResolveInfo 1029 if (dr == null) dr = super.getIconSubstituteInternal(); 1030 1031 return dr; 1032 } 1033 1034 @Override 1035 String getAppSubLabelInternal() { 1036 // Will default to app name if no intent filter or activity label set, make sure to 1037 // check if subLabel matches label before final display 1038 return mRi.loadLabel(mPm).toString(); 1039 } 1040 1041 @Override 1042 String getAppLabelForSubstitutePermission() { 1043 // Will default to app name if no activity label set 1044 return mRi.getComponentInfo().loadLabel(mPm).toString(); 1045 } 1046 } 1047 1048 /** 1049 * Loads the icon and label for the provided ActivityInfo. 1050 */ 1051 @VisibleForTesting 1052 public static class ActivityInfoPresentationGetter extends 1053 TargetPresentationGetter { 1054 private final ActivityInfo mActivityInfo; 1055 public ActivityInfoPresentationGetter(Context ctx, int iconDpi, 1056 ActivityInfo activityInfo) { 1057 super(ctx, iconDpi, activityInfo.applicationInfo); 1058 mActivityInfo = activityInfo; 1059 } 1060 1061 @Override 1062 Drawable getIconSubstituteInternal() { 1063 Drawable dr = null; 1064 try { 1065 // Do not use ActivityInfo#getIconResource() as it defaults to the app 1066 if (mActivityInfo.icon != 0) { 1067 dr = loadIconFromResource( 1068 mPm.getResourcesForApplication(mActivityInfo.applicationInfo), 1069 mActivityInfo.icon); 1070 } 1071 } catch (PackageManager.NameNotFoundException e) { 1072 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 1073 + "couldn't find resources for package", e); 1074 } 1075 1076 return dr; 1077 } 1078 1079 @Override 1080 String getAppSubLabelInternal() { 1081 // Will default to app name if no activity label set, make sure to check if subLabel 1082 // matches label before final display 1083 return (String) mActivityInfo.loadLabel(mPm); 1084 } 1085 1086 @Override 1087 String getAppLabelForSubstitutePermission() { 1088 return getAppSubLabelInternal(); 1089 } 1090 } 1091 1092 /** 1093 * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application 1094 * icon and label over any IntentFilter or Activity icon to increase user understanding, with an 1095 * exception for applications that hold the right permission. Always attempts to use available 1096 * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses 1097 * Strings to strip creative formatting. 1098 */ 1099 private abstract static class TargetPresentationGetter { 1100 @Nullable abstract Drawable getIconSubstituteInternal(); 1101 @Nullable abstract String getAppSubLabelInternal(); 1102 @Nullable abstract String getAppLabelForSubstitutePermission(); 1103 1104 private Context mCtx; 1105 private final int mIconDpi; 1106 private final boolean mHasSubstitutePermission; 1107 private final ApplicationInfo mAi; 1108 1109 protected PackageManager mPm; 1110 1111 TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) { 1112 mCtx = ctx; 1113 mPm = ctx.getPackageManager(); 1114 mAi = ai; 1115 mIconDpi = iconDpi; 1116 mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission( 1117 android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, 1118 mAi.packageName); 1119 } 1120 1121 public Drawable getIcon(UserHandle userHandle) { 1122 return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle)); 1123 } 1124 1125 public Bitmap getIconBitmap(@Nullable UserHandle userHandle) { 1126 Drawable dr = null; 1127 if (mHasSubstitutePermission) { 1128 dr = getIconSubstituteInternal(); 1129 } 1130 1131 if (dr == null) { 1132 try { 1133 if (mAi.icon != 0) { 1134 dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon); 1135 } 1136 } catch (PackageManager.NameNotFoundException ignore) { 1137 } 1138 } 1139 1140 // Fall back to ApplicationInfo#loadIcon if nothing has been loaded 1141 if (dr == null) { 1142 dr = mAi.loadIcon(mPm); 1143 } 1144 1145 SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx); 1146 Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle); 1147 sif.recycle(); 1148 1149 return icon; 1150 } 1151 1152 public String getLabel() { 1153 String label = null; 1154 // Apps with the substitute permission will always show the activity label as the 1155 // app label if provided 1156 if (mHasSubstitutePermission) { 1157 label = getAppLabelForSubstitutePermission(); 1158 } 1159 1160 if (label == null) { 1161 label = (String) mAi.loadLabel(mPm); 1162 } 1163 1164 return label; 1165 } 1166 1167 public String getSubLabel() { 1168 // Apps with the substitute permission will always show the resolve info label as the 1169 // sublabel if provided 1170 if (mHasSubstitutePermission){ 1171 String appSubLabel = getAppSubLabelInternal(); 1172 // Use the resolve info label as sublabel if it is set 1173 if(!TextUtils.isEmpty(appSubLabel) 1174 && !TextUtils.equals(appSubLabel, getLabel())){ 1175 return appSubLabel; 1176 } 1177 return null; 1178 } 1179 return getAppSubLabelInternal(); 1180 } 1181 1182 protected String loadLabelFromResource(Resources res, int resId) { 1183 return res.getString(resId); 1184 } 1185 1186 @Nullable 1187 protected Drawable loadIconFromResource(Resources res, int resId) { 1188 return res.getDrawableForDensity(resId, mIconDpi); 1189 } 1190 1191 } 1192 } 1193