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