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