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