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.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.PermissionChecker; 28 import android.content.pm.ActivityInfo; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.LabeledIntent; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.res.Resources; 34 import android.graphics.Bitmap; 35 import android.graphics.ColorMatrix; 36 import android.graphics.ColorMatrixColorFilter; 37 import android.graphics.drawable.BitmapDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.os.AsyncTask; 40 import android.os.RemoteException; 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.List; 61 62 public class ResolverListAdapter extends BaseAdapter { 63 private static final String TAG = "ResolverListAdapter"; 64 65 private final List<Intent> mIntents; 66 private final Intent[] mInitialIntents; 67 private final List<ResolveInfo> mBaseResolveList; 68 private final PackageManager mPm; 69 protected final Context mContext; 70 private static ColorMatrixColorFilter sSuspendedMatrixColorFilter; 71 private final int mIconDpi; 72 protected ResolveInfo mLastChosen; 73 private DisplayResolveInfo mOtherProfile; 74 ResolverListController mResolverListController; 75 private int mPlaceholderCount; 76 77 protected final LayoutInflater mInflater; 78 79 // This one is the list that the Adapter will actually present. 80 List<DisplayResolveInfo> mDisplayList; 81 private List<ResolvedComponentInfo> mUnfilteredResolveList; 82 83 private int mLastChosenPosition = -1; 84 private boolean mFilterLastUsed; 85 final ResolverListCommunicator mResolverListCommunicator; 86 private Runnable mPostListReadyRunnable; 87 private final boolean mIsAudioCaptureDevice; 88 private boolean mIsTabLoaded; 89 ResolverListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, ResolverListCommunicator resolverListCommunicator, boolean isAudioCaptureDevice)90 public ResolverListAdapter(Context context, List<Intent> payloadIntents, 91 Intent[] initialIntents, List<ResolveInfo> rList, 92 boolean filterLastUsed, 93 ResolverListController resolverListController, 94 ResolverListCommunicator resolverListCommunicator, 95 boolean isAudioCaptureDevice) { 96 mContext = context; 97 mIntents = payloadIntents; 98 mInitialIntents = initialIntents; 99 mBaseResolveList = rList; 100 mInflater = LayoutInflater.from(context); 101 mPm = context.getPackageManager(); 102 mDisplayList = new ArrayList<>(); 103 mFilterLastUsed = filterLastUsed; 104 mResolverListController = resolverListController; 105 mResolverListCommunicator = resolverListCommunicator; 106 mIsAudioCaptureDevice = isAudioCaptureDevice; 107 final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE); 108 mIconDpi = am.getLauncherLargeIconDensity(); 109 } 110 handlePackagesChanged()111 public void handlePackagesChanged() { 112 mResolverListCommunicator.onHandlePackagesChanged(this); 113 } 114 setPlaceholderCount(int count)115 public void setPlaceholderCount(int count) { 116 mPlaceholderCount = count; 117 } 118 getPlaceholderCount()119 public int getPlaceholderCount() { 120 return mPlaceholderCount; 121 } 122 123 @Nullable getFilteredItem()124 public DisplayResolveInfo getFilteredItem() { 125 if (mFilterLastUsed && mLastChosenPosition >= 0) { 126 // Not using getItem since it offsets to dodge this position for the list 127 return mDisplayList.get(mLastChosenPosition); 128 } 129 return null; 130 } 131 getOtherProfile()132 public DisplayResolveInfo getOtherProfile() { 133 return mOtherProfile; 134 } 135 getFilteredPosition()136 public int getFilteredPosition() { 137 if (mFilterLastUsed && mLastChosenPosition >= 0) { 138 return mLastChosenPosition; 139 } 140 return AbsListView.INVALID_POSITION; 141 } 142 hasFilteredItem()143 public boolean hasFilteredItem() { 144 return mFilterLastUsed && mLastChosen != null; 145 } 146 getScore(DisplayResolveInfo target)147 public float getScore(DisplayResolveInfo target) { 148 return mResolverListController.getScore(target); 149 } 150 151 /** 152 * Returns the app share score of the given {@code componentName}. 153 */ getScore(ComponentName componentName)154 public float getScore(ComponentName componentName) { 155 return mResolverListController.getScore(componentName); 156 } 157 158 /** 159 * Returns the list of top K component names which have highest 160 * {@link #getScore(DisplayResolveInfo)} 161 */ getTopComponentNames(int topK)162 public List<ComponentName> getTopComponentNames(int topK) { 163 return mResolverListController.getTopComponentNames(topK); 164 } 165 updateModel(ComponentName componentName)166 public void updateModel(ComponentName componentName) { 167 mResolverListController.updateModel(componentName); 168 } 169 updateChooserCounts(String packageName, String action)170 public void updateChooserCounts(String packageName, String action) { 171 mResolverListController.updateChooserCounts( 172 packageName, getUserHandle().getIdentifier(), action); 173 } 174 getUnfilteredResolveList()175 List<ResolvedComponentInfo> getUnfilteredResolveList() { 176 return mUnfilteredResolveList; 177 } 178 179 /** 180 * Rebuild the list of resolvers. In some cases some parts will need some asynchronous work 181 * to complete. 182 * 183 * The {@code doPostProcessing } parameter is used to specify whether to update the UI and 184 * load additional targets (e.g. direct share) after the list has been rebuilt. This is used 185 * in the case where we want to load the inactive profile's resolved apps to know the 186 * number of targets. 187 * 188 * @return Whether or not the list building is completed. 189 */ rebuildList(boolean doPostProcessing)190 protected boolean rebuildList(boolean doPostProcessing) { 191 List<ResolvedComponentInfo> currentResolveList = null; 192 // Clear the value of mOtherProfile from previous call. 193 mOtherProfile = null; 194 mLastChosen = null; 195 mLastChosenPosition = -1; 196 mDisplayList.clear(); 197 mIsTabLoaded = false; 198 199 if (mBaseResolveList != null) { 200 currentResolveList = mUnfilteredResolveList = new ArrayList<>(); 201 mResolverListController.addResolveListDedupe(currentResolveList, 202 mResolverListCommunicator.getTargetIntent(), 203 mBaseResolveList); 204 } else { 205 currentResolveList = mUnfilteredResolveList = 206 mResolverListController.getResolversForIntent( 207 /* shouldGetResolvedFilter= */ true, 208 mResolverListCommunicator.shouldGetActivityMetadata(), 209 mIntents); 210 if (currentResolveList == null) { 211 processSortedList(currentResolveList, doPostProcessing); 212 return true; 213 } 214 List<ResolvedComponentInfo> originalList = 215 mResolverListController.filterIneligibleActivities(currentResolveList, 216 true); 217 if (originalList != null) { 218 mUnfilteredResolveList = originalList; 219 } 220 } 221 222 // So far we only support a single other profile at a time. 223 // The first one we see gets special treatment. 224 for (ResolvedComponentInfo info : currentResolveList) { 225 ResolveInfo resolveInfo = info.getResolveInfoAt(0); 226 if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { 227 Intent pOrigIntent = mResolverListCommunicator.getReplacementIntent( 228 resolveInfo.activityInfo, 229 info.getIntentAt(0)); 230 Intent replacementIntent = mResolverListCommunicator.getReplacementIntent( 231 resolveInfo.activityInfo, 232 mResolverListCommunicator.getTargetIntent()); 233 mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0), 234 resolveInfo, 235 resolveInfo.loadLabel(mPm), 236 resolveInfo.loadLabel(mPm), 237 pOrigIntent != null ? pOrigIntent : replacementIntent, 238 makePresentationGetter(resolveInfo)); 239 currentResolveList.remove(info); 240 break; 241 } 242 } 243 244 if (mOtherProfile == null) { 245 try { 246 mLastChosen = mResolverListController.getLastChosen(); 247 } catch (RemoteException re) { 248 Log.d(TAG, "Error calling getLastChosenActivity\n" + re); 249 } 250 } 251 252 setPlaceholderCount(0); 253 int n; 254 if ((currentResolveList != null) && ((n = currentResolveList.size()) > 0)) { 255 // We only care about fixing the unfilteredList if the current resolve list and 256 // current resolve list are currently the same. 257 List<ResolvedComponentInfo> originalList = 258 mResolverListController.filterLowPriority(currentResolveList, 259 mUnfilteredResolveList == currentResolveList); 260 if (originalList != null) { 261 mUnfilteredResolveList = originalList; 262 } 263 264 if (currentResolveList.size() > 1) { 265 int placeholderCount = currentResolveList.size(); 266 if (mResolverListCommunicator.useLayoutWithDefault()) { 267 --placeholderCount; 268 } 269 setPlaceholderCount(placeholderCount); 270 createSortingTask(doPostProcessing).execute(currentResolveList); 271 postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false); 272 return false; 273 } else { 274 processSortedList(currentResolveList, doPostProcessing); 275 return true; 276 } 277 } else { 278 processSortedList(currentResolveList, doPostProcessing); 279 return true; 280 } 281 } 282 283 AsyncTask<List<ResolvedComponentInfo>, 284 Void, createSortingTask(boolean doPostProcessing)285 List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) { 286 return new AsyncTask<List<ResolvedComponentInfo>, 287 Void, 288 List<ResolvedComponentInfo>>() { 289 @Override 290 protected List<ResolvedComponentInfo> doInBackground( 291 List<ResolvedComponentInfo>... params) { 292 mResolverListController.sort(params[0]); 293 return params[0]; 294 } 295 @Override 296 protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { 297 processSortedList(sortedComponents, doPostProcessing); 298 notifyDataSetChanged(); 299 if (doPostProcessing) { 300 mResolverListCommunicator.updateProfileViewButton(); 301 } 302 } 303 }; 304 } 305 306 protected void processSortedList(List<ResolvedComponentInfo> sortedComponents, 307 boolean doPostProcessing) { 308 int n; 309 if (sortedComponents != null && (n = sortedComponents.size()) != 0) { 310 // First put the initial items at the top. 311 if (mInitialIntents != null) { 312 for (int i = 0; i < mInitialIntents.length; i++) { 313 Intent ii = mInitialIntents[i]; 314 if (ii == null) { 315 continue; 316 } 317 ActivityInfo ai = ii.resolveActivityInfo( 318 mPm, 0); 319 if (ai == null) { 320 Log.w(TAG, "No activity found for " + ii); 321 continue; 322 } 323 ResolveInfo ri = new ResolveInfo(); 324 ri.activityInfo = ai; 325 UserManager userManager = 326 (UserManager) mContext.getSystemService(Context.USER_SERVICE); 327 if (ii instanceof LabeledIntent) { 328 LabeledIntent li = (LabeledIntent) ii; 329 ri.resolvePackageName = li.getSourcePackage(); 330 ri.labelRes = li.getLabelResource(); 331 ri.nonLocalizedLabel = li.getNonLocalizedLabel(); 332 ri.icon = li.getIconResource(); 333 ri.iconResourceId = ri.icon; 334 } 335 if (userManager.isManagedProfile()) { 336 ri.noResourceId = true; 337 ri.icon = 0; 338 } 339 340 addResolveInfo(new DisplayResolveInfo(ii, ri, 341 ri.loadLabel(mPm), null, ii, makePresentationGetter(ri))); 342 } 343 } 344 345 346 for (ResolvedComponentInfo rci : sortedComponents) { 347 final ResolveInfo ri = rci.getResolveInfoAt(0); 348 if (ri != null) { 349 addResolveInfoWithAlternates(rci); 350 } 351 } 352 } 353 354 mResolverListCommunicator.sendVoiceChoicesIfNeeded(); 355 postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); 356 mIsTabLoaded = true; 357 } 358 359 /** 360 * Some necessary methods for creating the list are initiated in onCreate and will also 361 * determine the layout known. We therefore can't update the UI inline and post to the 362 * handler thread to update after the current task is finished. 363 * @param doPostProcessing Whether to update the UI and load additional direct share targets 364 * after the list has been rebuilt 365 * @param rebuildCompleted Whether the list has been completely rebuilt 366 */ 367 void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) { 368 if (mPostListReadyRunnable == null) { 369 mPostListReadyRunnable = new Runnable() { 370 @Override 371 public void run() { 372 mResolverListCommunicator.onPostListReady(ResolverListAdapter.this, 373 doPostProcessing, rebuildCompleted); 374 mPostListReadyRunnable = null; 375 } 376 }; 377 mContext.getMainThreadHandler().post(mPostListReadyRunnable); 378 } 379 } 380 381 private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { 382 final int count = rci.getCount(); 383 final Intent intent = rci.getIntentAt(0); 384 final ResolveInfo add = rci.getResolveInfoAt(0); 385 final Intent replaceIntent = 386 mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent); 387 final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent( 388 add.activityInfo, mResolverListCommunicator.getTargetIntent()); 389 final DisplayResolveInfo 390 dri = new DisplayResolveInfo(intent, add, 391 replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add)); 392 dri.setPinned(rci.isPinned()); 393 if (rci.isPinned()) { 394 Log.i(TAG, "Pinned item: " + rci.name); 395 } 396 addResolveInfo(dri); 397 if (replaceIntent == intent) { 398 // Only add alternates if we didn't get a specific replacement from 399 // the caller. If we have one it trumps potential alternates. 400 for (int i = 1, n = count; i < n; i++) { 401 final Intent altIntent = rci.getIntentAt(i); 402 dri.addAlternateSourceIntent(altIntent); 403 } 404 } 405 updateLastChosenPosition(add); 406 } 407 408 private void updateLastChosenPosition(ResolveInfo info) { 409 // If another profile is present, ignore the last chosen entry. 410 if (mOtherProfile != null) { 411 mLastChosenPosition = -1; 412 return; 413 } 414 if (mLastChosen != null 415 && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) 416 && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { 417 mLastChosenPosition = mDisplayList.size() - 1; 418 } 419 } 420 421 // We assume that at this point we've already filtered out the only intent for a different 422 // targetUserId which we're going to use. 423 private void addResolveInfo(DisplayResolveInfo dri) { 424 if (dri != null && dri.getResolveInfo() != null 425 && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) { 426 if (shouldAddResolveInfo(dri)) { 427 mDisplayList.add(dri); 428 Log.i(TAG, "Add DisplayResolveInfo component: " + dri.getResolvedComponentName() 429 + ", intent component: " + dri.getResolvedIntent().getComponent()); 430 } 431 } 432 } 433 434 // Check whether {@code dri} should be added into mDisplayList. 435 protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) { 436 // Checks if this info is already listed in display. 437 for (DisplayResolveInfo existingInfo : mDisplayList) { 438 if (mResolverListCommunicator 439 .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) { 440 return false; 441 } 442 } 443 return true; 444 } 445 446 @Nullable 447 public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { 448 TargetInfo target = targetInfoForPosition(position, filtered); 449 if (target != null) { 450 return target.getResolveInfo(); 451 } 452 return null; 453 } 454 455 @Nullable 456 public TargetInfo targetInfoForPosition(int position, boolean filtered) { 457 if (filtered) { 458 return getItem(position); 459 } 460 if (mDisplayList.size() > position) { 461 return mDisplayList.get(position); 462 } 463 return null; 464 } 465 466 public int getCount() { 467 int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount : 468 mDisplayList.size(); 469 if (mFilterLastUsed && mLastChosenPosition >= 0) { 470 totalSize--; 471 } 472 return totalSize; 473 } 474 475 public int getUnfilteredCount() { 476 return mDisplayList.size(); 477 } 478 479 @Nullable 480 public TargetInfo getItem(int position) { 481 if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { 482 position++; 483 } 484 if (mDisplayList.size() > position) { 485 return mDisplayList.get(position); 486 } else { 487 return null; 488 } 489 } 490 491 public long getItemId(int position) { 492 return position; 493 } 494 495 public int getDisplayResolveInfoCount() { 496 return mDisplayList.size(); 497 } 498 499 public DisplayResolveInfo getDisplayResolveInfo(int index) { 500 // Used to query services. We only query services for primary targets, not alternates. 501 return mDisplayList.get(index); 502 } 503 504 public final View getView(int position, View convertView, ViewGroup parent) { 505 View view = convertView; 506 if (view == null) { 507 view = createView(parent); 508 } 509 onBindView(view, getItem(position), position); 510 return view; 511 } 512 513 public final View createView(ViewGroup parent) { 514 final View view = onCreateView(parent); 515 final ViewHolder holder = new ViewHolder(view); 516 view.setTag(holder); 517 return view; 518 } 519 520 View onCreateView(ViewGroup parent) { 521 return mInflater.inflate( 522 com.android.internal.R.layout.resolve_list_item, parent, false); 523 } 524 525 public final void bindView(int position, View view) { 526 onBindView(view, getItem(position), position); 527 } 528 529 protected void onBindView(View view, TargetInfo info, int position) { 530 final ViewHolder holder = (ViewHolder) view.getTag(); 531 if (info == null) { 532 holder.icon.setImageDrawable( 533 mContext.getDrawable(R.drawable.resolver_icon_placeholder)); 534 return; 535 } 536 537 if (info instanceof DisplayResolveInfo 538 && !((DisplayResolveInfo) info).hasDisplayLabel()) { 539 getLoadLabelTask((DisplayResolveInfo) info, holder).execute(); 540 } else { 541 holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); 542 } 543 544 if (info instanceof DisplayResolveInfo 545 && !((DisplayResolveInfo) info).hasDisplayIcon()) { 546 new LoadIconTask((DisplayResolveInfo) info, holder).execute(); 547 } else { 548 holder.bindIcon(info); 549 } 550 } 551 552 protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { 553 return new LoadLabelTask(info, holder); 554 } 555 556 public void onDestroy() { 557 if (mPostListReadyRunnable != null) { 558 mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); 559 mPostListReadyRunnable = null; 560 } 561 if (mResolverListController != null) { 562 mResolverListController.destroy(); 563 } 564 } 565 566 private static ColorMatrixColorFilter getSuspendedColorMatrix() { 567 if (sSuspendedMatrixColorFilter == null) { 568 569 int grayValue = 127; 570 float scale = 0.5f; // half bright 571 572 ColorMatrix tempBrightnessMatrix = new ColorMatrix(); 573 float[] mat = tempBrightnessMatrix.getArray(); 574 mat[0] = scale; 575 mat[6] = scale; 576 mat[12] = scale; 577 mat[4] = grayValue; 578 mat[9] = grayValue; 579 mat[14] = grayValue; 580 581 ColorMatrix matrix = new ColorMatrix(); 582 matrix.setSaturation(0.0f); 583 matrix.preConcat(tempBrightnessMatrix); 584 sSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix); 585 } 586 return sSuspendedMatrixColorFilter; 587 } 588 589 ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) { 590 return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai); 591 } 592 593 ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) { 594 return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri); 595 } 596 597 Drawable loadIconForResolveInfo(ResolveInfo ri) { 598 // Load icons based on the current process. If in work profile icons should be badged. 599 return makePresentationGetter(ri).getIcon(getUserHandle()); 600 } 601 602 void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { 603 final DisplayResolveInfo iconInfo = getFilteredItem(); 604 if (iconView != null && iconInfo != null) { 605 new AsyncTask<Void, Void, Drawable>() { 606 @Override 607 protected Drawable doInBackground(Void... params) { 608 return loadIconForResolveInfo(iconInfo.getResolveInfo()); 609 } 610 611 @Override 612 protected void onPostExecute(Drawable d) { 613 iconView.setImageDrawable(d); 614 } 615 }.execute(); 616 } 617 } 618 619 @VisibleForTesting 620 public UserHandle getUserHandle() { 621 return mResolverListController.getUserHandle(); 622 } 623 624 protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) { 625 return mResolverListController.getResolversForIntentAsUser(true, 626 mResolverListCommunicator.shouldGetActivityMetadata(), 627 mIntents, userHandle); 628 } 629 630 protected List<Intent> getIntents() { 631 return mIntents; 632 } 633 634 protected boolean isTabLoaded() { 635 return mIsTabLoaded; 636 } 637 638 protected void markTabLoaded() { 639 mIsTabLoaded = true; 640 } 641 642 protected boolean alwaysShowSubLabel() { 643 return false; 644 } 645 646 /** 647 * Necessary methods to communicate between {@link ResolverListAdapter} 648 * and {@link ResolverActivity}. 649 */ 650 interface ResolverListCommunicator { 651 652 boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs); 653 654 Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent); 655 656 void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi, 657 boolean rebuildCompleted); 658 659 void sendVoiceChoicesIfNeeded(); 660 661 void updateProfileViewButton(); 662 663 boolean useLayoutWithDefault(); 664 665 boolean shouldGetActivityMetadata(); 666 667 Intent getTargetIntent(); 668 669 void onHandlePackagesChanged(ResolverListAdapter listAdapter); 670 } 671 672 static class ViewHolder { 673 public View itemView; 674 public Drawable defaultItemViewBackground; 675 676 public TextView text; 677 public TextView text2; 678 public ImageView icon; 679 680 ViewHolder(View view) { 681 itemView = view; 682 defaultItemViewBackground = view.getBackground(); 683 text = (TextView) view.findViewById(com.android.internal.R.id.text1); 684 text2 = (TextView) view.findViewById(com.android.internal.R.id.text2); 685 icon = (ImageView) view.findViewById(R.id.icon); 686 } 687 688 public void bindLabel(CharSequence label, CharSequence subLabel, boolean showSubLabel) { 689 text.setText(label); 690 691 if (TextUtils.equals(label, subLabel)) { 692 subLabel = null; 693 } 694 695 text2.setText(subLabel); 696 if (showSubLabel || subLabel != null) { 697 text2.setVisibility(View.VISIBLE); 698 } else { 699 text2.setVisibility(View.GONE); 700 } 701 702 itemView.setContentDescription(null); 703 } 704 705 public void updateContentDescription(String description) { 706 itemView.setContentDescription(description); 707 } 708 709 public void bindIcon(TargetInfo info) { 710 icon.setImageDrawable(info.getDisplayIcon(itemView.getContext())); 711 if (info.isSuspended()) { 712 icon.setColorFilter(getSuspendedColorMatrix()); 713 } else { 714 icon.setColorFilter(null); 715 } 716 } 717 } 718 719 protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { 720 private final DisplayResolveInfo mDisplayResolveInfo; 721 private final ViewHolder mHolder; 722 723 protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) { 724 mDisplayResolveInfo = dri; 725 mHolder = holder; 726 } 727 728 @Override 729 protected CharSequence[] doInBackground(Void... voids) { 730 ResolveInfoPresentationGetter pg = 731 makePresentationGetter(mDisplayResolveInfo.getResolveInfo()); 732 733 if (mIsAudioCaptureDevice) { 734 // This is an audio capture device, so check record permissions 735 ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo; 736 String packageName = activityInfo.packageName; 737 738 int uid = activityInfo.applicationInfo.uid; 739 boolean hasRecordPermission = 740 PermissionChecker.checkPermissionForPreflight( 741 mContext, 742 android.Manifest.permission.RECORD_AUDIO, -1, uid, 743 packageName) 744 == android.content.pm.PackageManager.PERMISSION_GRANTED; 745 746 if (!hasRecordPermission) { 747 // Doesn't have record permission, so warn the user 748 return new CharSequence[] { 749 pg.getLabel(), 750 mContext.getString(R.string.usb_device_resolve_prompt_warn) 751 }; 752 } 753 } 754 755 return new CharSequence[] { 756 pg.getLabel(), 757 pg.getSubLabel() 758 }; 759 } 760 761 @Override 762 protected void onPostExecute(CharSequence[] result) { 763 mDisplayResolveInfo.setDisplayLabel(result[0]); 764 mDisplayResolveInfo.setExtendedInfo(result[1]); 765 mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel()); 766 } 767 } 768 769 class LoadIconTask extends AsyncTask<Void, Void, Drawable> { 770 protected final DisplayResolveInfo mDisplayResolveInfo; 771 private final ResolveInfo mResolveInfo; 772 private ViewHolder mHolder; 773 774 LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) { 775 mDisplayResolveInfo = dri; 776 mResolveInfo = dri.getResolveInfo(); 777 mHolder = holder; 778 } 779 780 @Override 781 protected Drawable doInBackground(Void... params) { 782 return loadIconForResolveInfo(mResolveInfo); 783 } 784 785 @Override 786 protected void onPostExecute(Drawable d) { 787 if (getOtherProfile() == mDisplayResolveInfo) { 788 mResolverListCommunicator.updateProfileViewButton(); 789 } else { 790 mDisplayResolveInfo.setDisplayIcon(d); 791 mHolder.bindIcon(mDisplayResolveInfo); 792 } 793 } 794 795 public void setViewHolder(ViewHolder holder) { 796 mHolder = holder; 797 mHolder.bindIcon(mDisplayResolveInfo); 798 } 799 } 800 801 /** 802 * Loads the icon and label for the provided ResolveInfo. 803 */ 804 @VisibleForTesting 805 public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { 806 private final ResolveInfo mRi; 807 public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) { 808 super(ctx, iconDpi, ri.activityInfo); 809 mRi = ri; 810 } 811 812 @Override 813 Drawable getIconSubstituteInternal() { 814 Drawable dr = null; 815 try { 816 // Do not use ResolveInfo#getIconResource() as it defaults to the app 817 if (mRi.resolvePackageName != null && mRi.icon != 0) { 818 dr = loadIconFromResource( 819 mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon); 820 } 821 } catch (PackageManager.NameNotFoundException e) { 822 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 823 + "couldn't find resources for package", e); 824 } 825 826 // Fall back to ActivityInfo if no icon is found via ResolveInfo 827 if (dr == null) dr = super.getIconSubstituteInternal(); 828 829 return dr; 830 } 831 832 @Override 833 String getAppSubLabelInternal() { 834 // Will default to app name if no intent filter or activity label set, make sure to 835 // check if subLabel matches label before final display 836 return mRi.loadLabel(mPm).toString(); 837 } 838 } 839 840 /** 841 * Loads the icon and label for the provided ActivityInfo. 842 */ 843 @VisibleForTesting 844 public static class ActivityInfoPresentationGetter extends 845 TargetPresentationGetter { 846 private final ActivityInfo mActivityInfo; 847 public ActivityInfoPresentationGetter(Context ctx, int iconDpi, 848 ActivityInfo activityInfo) { 849 super(ctx, iconDpi, activityInfo.applicationInfo); 850 mActivityInfo = activityInfo; 851 } 852 853 @Override 854 Drawable getIconSubstituteInternal() { 855 Drawable dr = null; 856 try { 857 // Do not use ActivityInfo#getIconResource() as it defaults to the app 858 if (mActivityInfo.icon != 0) { 859 dr = loadIconFromResource( 860 mPm.getResourcesForApplication(mActivityInfo.applicationInfo), 861 mActivityInfo.icon); 862 } 863 } catch (PackageManager.NameNotFoundException e) { 864 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 865 + "couldn't find resources for package", e); 866 } 867 868 return dr; 869 } 870 871 @Override 872 String getAppSubLabelInternal() { 873 // Will default to app name if no activity label set, make sure to check if subLabel 874 // matches label before final display 875 return (String) mActivityInfo.loadLabel(mPm); 876 } 877 } 878 879 /** 880 * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application 881 * icon and label over any IntentFilter or Activity icon to increase user understanding, with an 882 * exception for applications that hold the right permission. Always attempts to use available 883 * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses 884 * Strings to strip creative formatting. 885 */ 886 private abstract static class TargetPresentationGetter { 887 @Nullable abstract Drawable getIconSubstituteInternal(); 888 @Nullable abstract String getAppSubLabelInternal(); 889 890 private Context mCtx; 891 private final int mIconDpi; 892 private final boolean mHasSubstitutePermission; 893 private final ApplicationInfo mAi; 894 895 protected PackageManager mPm; 896 897 TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) { 898 mCtx = ctx; 899 mPm = ctx.getPackageManager(); 900 mAi = ai; 901 mIconDpi = iconDpi; 902 mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission( 903 android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, 904 mAi.packageName); 905 } 906 907 public Drawable getIcon(UserHandle userHandle) { 908 return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle)); 909 } 910 911 public Bitmap getIconBitmap(UserHandle userHandle) { 912 Drawable dr = null; 913 if (mHasSubstitutePermission) { 914 dr = getIconSubstituteInternal(); 915 } 916 917 if (dr == null) { 918 try { 919 if (mAi.icon != 0) { 920 dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon); 921 } 922 } catch (PackageManager.NameNotFoundException ignore) { 923 } 924 } 925 926 // Fall back to ApplicationInfo#loadIcon if nothing has been loaded 927 if (dr == null) { 928 dr = mAi.loadIcon(mPm); 929 } 930 931 SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx); 932 Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle); 933 sif.recycle(); 934 935 return icon; 936 } 937 938 public String getLabel() { 939 String label = null; 940 // Apps with the substitute permission will always show the sublabel as their label 941 if (mHasSubstitutePermission) { 942 label = getAppSubLabelInternal(); 943 } 944 945 if (label == null) { 946 label = (String) mAi.loadLabel(mPm); 947 } 948 949 return label; 950 } 951 952 public String getSubLabel() { 953 // Apps with the substitute permission will never have a sublabel 954 if (mHasSubstitutePermission) return null; 955 return getAppSubLabelInternal(); 956 } 957 958 protected String loadLabelFromResource(Resources res, int resId) { 959 return res.getString(resId); 960 } 961 962 @Nullable 963 protected Drawable loadIconFromResource(Resources res, int resId) { 964 return res.getDrawableForDensity(resId, mIconDpi); 965 } 966 967 } 968 } 969