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