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