1 /* 2 * Copyright (C) 2008 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.Intent.FLAG_ACTIVITY_NEW_TASK; 20 21 import android.annotation.Nullable; 22 import android.annotation.StringRes; 23 import android.annotation.UiThread; 24 import android.app.Activity; 25 import android.app.ActivityManager; 26 import android.app.ActivityTaskManager; 27 import android.app.ActivityThread; 28 import android.app.VoiceInteractor.PickOptionRequest; 29 import android.app.VoiceInteractor.PickOptionRequest.Option; 30 import android.app.VoiceInteractor.Prompt; 31 import android.compat.annotation.UnsupportedAppUsage; 32 import android.content.ComponentName; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.IntentFilter; 36 import android.content.pm.ActivityInfo; 37 import android.content.pm.ApplicationInfo; 38 import android.content.pm.LabeledIntent; 39 import android.content.pm.PackageManager; 40 import android.content.pm.PackageManager.NameNotFoundException; 41 import android.content.pm.ResolveInfo; 42 import android.content.pm.UserInfo; 43 import android.content.res.Configuration; 44 import android.content.res.Resources; 45 import android.graphics.Bitmap; 46 import android.graphics.ColorMatrix; 47 import android.graphics.ColorMatrixColorFilter; 48 import android.graphics.Insets; 49 import android.graphics.drawable.BitmapDrawable; 50 import android.graphics.drawable.Drawable; 51 import android.net.Uri; 52 import android.os.AsyncTask; 53 import android.os.Build; 54 import android.os.Bundle; 55 import android.os.IBinder; 56 import android.os.PatternMatcher; 57 import android.os.Process; 58 import android.os.RemoteException; 59 import android.os.StrictMode; 60 import android.os.UserHandle; 61 import android.os.UserManager; 62 import android.provider.MediaStore; 63 import android.provider.Settings; 64 import android.text.TextUtils; 65 import android.util.Log; 66 import android.util.Slog; 67 import android.view.LayoutInflater; 68 import android.view.View; 69 import android.view.ViewGroup; 70 import android.view.ViewGroup.LayoutParams; 71 import android.view.WindowInsets; 72 import android.widget.AbsListView; 73 import android.widget.AdapterView; 74 import android.widget.BaseAdapter; 75 import android.widget.Button; 76 import android.widget.ImageView; 77 import android.widget.ListView; 78 import android.widget.Space; 79 import android.widget.TextView; 80 import android.widget.Toast; 81 82 import com.android.internal.R; 83 import com.android.internal.annotations.VisibleForTesting; 84 import com.android.internal.content.PackageMonitor; 85 import com.android.internal.logging.MetricsLogger; 86 import com.android.internal.logging.nano.MetricsProto; 87 import com.android.internal.widget.ResolverDrawerLayout; 88 89 import java.util.ArrayList; 90 import java.util.Arrays; 91 import java.util.Iterator; 92 import java.util.List; 93 import java.util.Objects; 94 import java.util.Set; 95 96 /** 97 * This activity is displayed when the system attempts to start an Intent for 98 * which there is more than one matching activity, allowing the user to decide 99 * which to go to. It is not normally used directly by application developers. 100 */ 101 @UiThread 102 public class ResolverActivity extends Activity { 103 104 @UnsupportedAppUsage ResolverActivity()105 public ResolverActivity() { 106 } 107 108 // Temporary flag for new chooser delegate behavior. 109 boolean mEnableChooserDelegate = true; 110 111 @UnsupportedAppUsage 112 protected ResolveListAdapter mAdapter; 113 private boolean mSafeForwardingMode; 114 protected AbsListView mAdapterView; 115 private Button mAlwaysButton; 116 private Button mOnceButton; 117 protected View mProfileView; 118 private int mIconDpi; 119 private int mLastSelected = AbsListView.INVALID_POSITION; 120 private boolean mResolvingHome = false; 121 private int mProfileSwitchMessageId = -1; 122 private int mLayoutId; 123 private final ArrayList<Intent> mIntents = new ArrayList<>(); 124 private PickTargetOptionRequest mPickOptionRequest; 125 private String mReferrerPackage; 126 private CharSequence mTitle; 127 private int mDefaultTitleResId; 128 private boolean mUseLayoutForBrowsables; 129 130 // Whether or not this activity supports choosing a default handler for the intent. 131 private boolean mSupportsAlwaysUseOption; 132 protected ResolverDrawerLayout mResolverDrawerLayout; 133 @UnsupportedAppUsage 134 protected PackageManager mPm; 135 protected int mLaunchedFromUid; 136 137 private static final String TAG = "ResolverActivity"; 138 private static final boolean DEBUG = false; 139 private Runnable mPostListReadyRunnable; 140 141 private boolean mRegistered; 142 143 private ColorMatrixColorFilter mSuspendedMatrixColorFilter; 144 145 protected Insets mSystemWindowInsets = null; 146 private Space mFooterSpacer = null; 147 148 /** See {@link #setRetainInOnStop}. */ 149 private boolean mRetainInOnStop; 150 151 private static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"; 152 private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; 153 private static final String OPEN_LINKS_COMPONENT_KEY = "app_link_state"; 154 155 private final PackageMonitor mPackageMonitor = createPackageMonitor(); 156 157 /** 158 * Get the string resource to be used as a label for the link to the resolver activity for an 159 * action. 160 * 161 * @param action The action to resolve 162 * 163 * @return The string resource to be used as a label 164 */ getLabelRes(String action)165 public static @StringRes int getLabelRes(String action) { 166 return ActionTitle.forAction(action).labelRes; 167 } 168 169 private enum ActionTitle { 170 VIEW(Intent.ACTION_VIEW, 171 com.android.internal.R.string.whichViewApplication, 172 com.android.internal.R.string.whichViewApplicationNamed, 173 com.android.internal.R.string.whichViewApplicationLabel), 174 EDIT(Intent.ACTION_EDIT, 175 com.android.internal.R.string.whichEditApplication, 176 com.android.internal.R.string.whichEditApplicationNamed, 177 com.android.internal.R.string.whichEditApplicationLabel), 178 SEND(Intent.ACTION_SEND, 179 com.android.internal.R.string.whichSendApplication, 180 com.android.internal.R.string.whichSendApplicationNamed, 181 com.android.internal.R.string.whichSendApplicationLabel), 182 SENDTO(Intent.ACTION_SENDTO, 183 com.android.internal.R.string.whichSendToApplication, 184 com.android.internal.R.string.whichSendToApplicationNamed, 185 com.android.internal.R.string.whichSendToApplicationLabel), 186 SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, 187 com.android.internal.R.string.whichSendApplication, 188 com.android.internal.R.string.whichSendApplicationNamed, 189 com.android.internal.R.string.whichSendApplicationLabel), 190 CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE, 191 com.android.internal.R.string.whichImageCaptureApplication, 192 com.android.internal.R.string.whichImageCaptureApplicationNamed, 193 com.android.internal.R.string.whichImageCaptureApplicationLabel), 194 DEFAULT(null, 195 com.android.internal.R.string.whichApplication, 196 com.android.internal.R.string.whichApplicationNamed, 197 com.android.internal.R.string.whichApplicationLabel), 198 HOME(Intent.ACTION_MAIN, 199 com.android.internal.R.string.whichHomeApplication, 200 com.android.internal.R.string.whichHomeApplicationNamed, 201 com.android.internal.R.string.whichHomeApplicationLabel); 202 203 // titles for layout that deals with http(s) intents 204 public static final int BROWSABLE_TITLE_RES = 205 com.android.internal.R.string.whichOpenLinksWith; 206 public static final int BROWSABLE_HOST_TITLE_RES = 207 com.android.internal.R.string.whichOpenHostLinksWith; 208 public static final int BROWSABLE_HOST_APP_TITLE_RES = 209 com.android.internal.R.string.whichOpenHostLinksWithApp; 210 public static final int BROWSABLE_APP_TITLE_RES = 211 com.android.internal.R.string.whichOpenLinksWithApp; 212 213 public final String action; 214 public final int titleRes; 215 public final int namedTitleRes; 216 public final @StringRes int labelRes; 217 ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes)218 ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) { 219 this.action = action; 220 this.titleRes = titleRes; 221 this.namedTitleRes = namedTitleRes; 222 this.labelRes = labelRes; 223 } 224 forAction(String action)225 public static ActionTitle forAction(String action) { 226 for (ActionTitle title : values()) { 227 if (title != HOME && action != null && action.equals(title.action)) { 228 return title; 229 } 230 } 231 return DEFAULT; 232 } 233 } 234 createPackageMonitor()235 protected PackageMonitor createPackageMonitor() { 236 return new PackageMonitor() { 237 @Override 238 public void onSomePackagesChanged() { 239 mAdapter.handlePackagesChanged(); 240 bindProfileView(); 241 } 242 243 @Override 244 public boolean onPackageChanged(String packageName, int uid, String[] components) { 245 // We care about all package changes, not just the whole package itself which is 246 // default behavior. 247 return true; 248 } 249 }; 250 } 251 252 private Intent makeMyIntent() { 253 Intent intent = new Intent(getIntent()); 254 intent.setComponent(null); 255 // The resolver activity is set to be hidden from recent tasks. 256 // we don't want this attribute to be propagated to the next activity 257 // being launched. Note that if the original Intent also had this 258 // flag set, we are now losing it. That should be a very rare case 259 // and we can live with this. 260 intent.setFlags(intent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 261 return intent; 262 } 263 264 @Override 265 protected void onCreate(Bundle savedInstanceState) { 266 // Use a specialized prompt when we're handling the 'Home' app startActivity() 267 final Intent intent = makeMyIntent(); 268 final Set<String> categories = intent.getCategories(); 269 if (Intent.ACTION_MAIN.equals(intent.getAction()) 270 && categories != null 271 && categories.size() == 1 272 && categories.contains(Intent.CATEGORY_HOME)) { 273 // Note: this field is not set to true in the compatibility version. 274 mResolvingHome = true; 275 } 276 277 setSafeForwardingMode(true); 278 279 onCreate(savedInstanceState, intent, null, 0, null, null, true); 280 } 281 282 /** 283 * Compatibility version for other bundled services that use this overload without 284 * a default title resource 285 */ 286 @UnsupportedAppUsage 287 protected void onCreate(Bundle savedInstanceState, Intent intent, 288 CharSequence title, Intent[] initialIntents, 289 List<ResolveInfo> rList, boolean supportsAlwaysUseOption) { 290 onCreate(savedInstanceState, intent, title, 0, initialIntents, rList, 291 supportsAlwaysUseOption); 292 } 293 294 protected void onCreate(Bundle savedInstanceState, Intent intent, 295 CharSequence title, int defaultTitleRes, Intent[] initialIntents, 296 List<ResolveInfo> rList, boolean supportsAlwaysUseOption) { 297 setTheme(R.style.Theme_DeviceDefault_Resolver); 298 super.onCreate(savedInstanceState); 299 300 // Determine whether we should show that intent is forwarded 301 // from managed profile to owner or other way around. 302 setProfileSwitchMessageId(intent.getContentUserHint()); 303 304 try { 305 mLaunchedFromUid = ActivityTaskManager.getService().getLaunchedFromUid( 306 getActivityToken()); 307 } catch (RemoteException e) { 308 mLaunchedFromUid = -1; 309 } 310 311 if (mLaunchedFromUid < 0 || UserHandle.isIsolated(mLaunchedFromUid)) { 312 // Gulp! 313 finish(); 314 return; 315 } 316 317 mPm = getPackageManager(); 318 319 mPackageMonitor.register(this, getMainLooper(), false); 320 mRegistered = true; 321 mReferrerPackage = getReferrerPackageName(); 322 323 final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); 324 mIconDpi = am.getLauncherLargeIconDensity(); 325 326 // Add our initial intent as the first item, regardless of what else has already been added. 327 mIntents.add(0, new Intent(intent)); 328 mTitle = title; 329 mDefaultTitleResId = defaultTitleRes; 330 331 mUseLayoutForBrowsables = getTargetIntent() == null 332 ? false 333 : isHttpSchemeAndViewAction(getTargetIntent()); 334 335 mSupportsAlwaysUseOption = supportsAlwaysUseOption; 336 337 if (configureContentView(mIntents, initialIntents, rList)) { 338 return; 339 } 340 341 final ResolverDrawerLayout rdl = findViewById(R.id.contentPanel); 342 if (rdl != null) { 343 rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() { 344 @Override 345 public void onDismissed() { 346 finish(); 347 } 348 }); 349 if (isVoiceInteraction()) { 350 rdl.setCollapsed(false); 351 } 352 353 rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 354 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 355 rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets); 356 357 mResolverDrawerLayout = rdl; 358 } 359 360 mProfileView = findViewById(R.id.profile_button); 361 if (mProfileView != null) { 362 mProfileView.setOnClickListener(this::onProfileClick); 363 bindProfileView(); 364 } 365 366 initSuspendedColorMatrix(); 367 368 final Set<String> categories = intent.getCategories(); 369 MetricsLogger.action(this, mAdapter.hasFilteredItem() 370 ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED 371 : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED, 372 intent.getAction() + ":" + intent.getType() + ":" 373 + (categories != null ? Arrays.toString(categories.toArray()) : "")); 374 } 375 376 protected void onProfileClick(View v) { 377 final DisplayResolveInfo dri = mAdapter.getOtherProfile(); 378 if (dri == null) { 379 return; 380 } 381 382 // Do not show the profile switch message anymore. 383 mProfileSwitchMessageId = -1; 384 385 onTargetSelected(dri, false); 386 finish(); 387 } 388 389 /** 390 * Numerous layouts are supported, each with optional ViewGroups. 391 * Make sure the inset gets added to the correct View, using 392 * a footer for Lists so it can properly scroll under the navbar. 393 */ 394 protected boolean shouldAddFooterView() { 395 if (useLayoutWithDefault()) return true; 396 397 View buttonBar = findViewById(R.id.button_bar); 398 if (buttonBar == null || buttonBar.getVisibility() == View.GONE) return true; 399 400 return false; 401 } 402 403 protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { 404 mSystemWindowInsets = insets.getSystemWindowInsets(); 405 406 mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, 407 mSystemWindowInsets.right, 0); 408 409 resetButtonBar(); 410 411 // Need extra padding so the list can fully scroll up 412 if (shouldAddFooterView()) { 413 if (mFooterSpacer == null) { 414 mFooterSpacer = new Space(getApplicationContext()); 415 } else { 416 ((ListView) mAdapterView).removeFooterView(mFooterSpacer); 417 } 418 mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, 419 mSystemWindowInsets.bottom)); 420 ((ListView) mAdapterView).addFooterView(mFooterSpacer); 421 } 422 423 View emptyView = findViewById(R.id.empty); 424 if (emptyView != null) { 425 emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom 426 + getResources().getDimensionPixelSize( 427 R.dimen.chooser_edge_margin_normal) * 2); 428 } 429 430 return insets.consumeSystemWindowInsets(); 431 } 432 433 @Override 434 public void onConfigurationChanged(Configuration newConfig) { 435 super.onConfigurationChanged(newConfig); 436 mAdapter.handlePackagesChanged(); 437 438 if (mSystemWindowInsets != null) { 439 mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, 440 mSystemWindowInsets.right, 0); 441 } 442 } 443 444 private void initSuspendedColorMatrix() { 445 int grayValue = 127; 446 float scale = 0.5f; // half bright 447 448 ColorMatrix tempBrightnessMatrix = new ColorMatrix(); 449 float[] mat = tempBrightnessMatrix.getArray(); 450 mat[0] = scale; 451 mat[6] = scale; 452 mat[12] = scale; 453 mat[4] = grayValue; 454 mat[9] = grayValue; 455 mat[14] = grayValue; 456 457 ColorMatrix matrix = new ColorMatrix(); 458 matrix.setSaturation(0.0f); 459 matrix.preConcat(tempBrightnessMatrix); 460 mSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix); 461 } 462 463 public void sendVoiceChoicesIfNeeded() { 464 if (!isVoiceInteraction()) { 465 // Clearly not needed. 466 return; 467 } 468 469 final Option[] options = new Option[mAdapter.getCount()]; 470 for (int i = 0, N = options.length; i < N; i++) { 471 TargetInfo target = mAdapter.getItem(i); 472 if (target == null) { 473 // If this occurs, a new set of targets is being loaded. Let that complete, 474 // and have the next call to send voice choices proceed instead. 475 return; 476 } 477 options[i] = optionForChooserTarget(target, i); 478 } 479 480 mPickOptionRequest = new PickTargetOptionRequest( 481 new Prompt(getTitle()), options, null); 482 getVoiceInteractor().submitRequest(mPickOptionRequest); 483 } 484 485 Option optionForChooserTarget(TargetInfo target, int index) { 486 return new Option(target.getDisplayLabel(), index); 487 } 488 489 protected final void setAdditionalTargets(Intent[] intents) { 490 if (intents != null) { 491 for (Intent intent : intents) { 492 mIntents.add(intent); 493 } 494 } 495 } 496 497 public Intent getTargetIntent() { 498 return mIntents.isEmpty() ? null : mIntents.get(0); 499 } 500 501 protected String getReferrerPackageName() { 502 final Uri referrer = getReferrer(); 503 if (referrer != null && "android-app".equals(referrer.getScheme())) { 504 return referrer.getHost(); 505 } 506 return null; 507 } 508 509 public int getLayoutResource() { 510 return R.layout.resolver_list; 511 } 512 513 protected void bindProfileView() { 514 if (mProfileView == null) { 515 return; 516 } 517 518 final DisplayResolveInfo dri = mAdapter.getOtherProfile(); 519 if (dri != null) { 520 mProfileView.setVisibility(View.VISIBLE); 521 View text = mProfileView.findViewById(R.id.profile_button); 522 if (!(text instanceof TextView)) { 523 text = mProfileView.findViewById(R.id.text1); 524 } 525 ((TextView) text).setText(dri.getDisplayLabel()); 526 } else { 527 mProfileView.setVisibility(View.GONE); 528 } 529 } 530 531 private void setProfileSwitchMessageId(int contentUserHint) { 532 if (contentUserHint != UserHandle.USER_CURRENT && 533 contentUserHint != UserHandle.myUserId()) { 534 UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); 535 UserInfo originUserInfo = userManager.getUserInfo(contentUserHint); 536 boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile() 537 : false; 538 boolean targetIsManaged = userManager.isManagedProfile(); 539 if (originIsManaged && !targetIsManaged) { 540 mProfileSwitchMessageId = com.android.internal.R.string.forward_intent_to_owner; 541 } else if (!originIsManaged && targetIsManaged) { 542 mProfileSwitchMessageId = com.android.internal.R.string.forward_intent_to_work; 543 } 544 } 545 } 546 547 /** 548 * Turn on launch mode that is safe to use when forwarding intents received from 549 * applications and running in system processes. This mode uses Activity.startActivityAsCaller 550 * instead of the normal Activity.startActivity for launching the activity selected 551 * by the user. 552 * 553 * <p>This mode is set to true by default if the activity is initialized through 554 * {@link #onCreate(android.os.Bundle)}. If a subclass calls one of the other onCreate 555 * methods, it is set to false by default. You must set it before calling one of the 556 * more detailed onCreate methods, so that it will be set correctly in the case where 557 * there is only one intent to resolve and it is thus started immediately.</p> 558 */ 559 public void setSafeForwardingMode(boolean safeForwarding) { 560 mSafeForwardingMode = safeForwarding; 561 } 562 563 protected CharSequence getTitleForAction(Intent intent, int defaultTitleRes) { 564 final ActionTitle title = mResolvingHome 565 ? ActionTitle.HOME 566 : ActionTitle.forAction(intent.getAction()); 567 568 // While there may already be a filtered item, we can only use it in the title if the list 569 // is already sorted and all information relevant to it is already in the list. 570 final boolean named = mAdapter.getFilteredPosition() >= 0; 571 if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) { 572 return getString(defaultTitleRes); 573 } else if (isHttpSchemeAndViewAction(intent)) { 574 // If the Intent's scheme is http(s) then we need to warn the user that 575 // they're giving access for the activity to open URLs from this specific host 576 String dialogTitle = null; 577 if (named && !mUseLayoutForBrowsables) { 578 dialogTitle = getString(ActionTitle.BROWSABLE_APP_TITLE_RES, 579 mAdapter.getFilteredItem().getDisplayLabel()); 580 } else if (named && mUseLayoutForBrowsables) { 581 dialogTitle = getString(ActionTitle.BROWSABLE_HOST_APP_TITLE_RES, 582 intent.getData().getHost(), 583 mAdapter.getFilteredItem().getDisplayLabel()); 584 } else if (mAdapter.areAllTargetsBrowsers()) { 585 dialogTitle = getString(ActionTitle.BROWSABLE_TITLE_RES); 586 } else { 587 dialogTitle = getString(ActionTitle.BROWSABLE_HOST_TITLE_RES, 588 intent.getData().getHost()); 589 } 590 return dialogTitle; 591 } else { 592 return named 593 ? getString(title.namedTitleRes, mAdapter.getFilteredItem().getDisplayLabel()) 594 : getString(title.titleRes); 595 } 596 } 597 598 void dismiss() { 599 if (!isFinishing()) { 600 finish(); 601 } 602 } 603 604 605 /** 606 * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application 607 * icon and label over any IntentFilter or Activity icon to increase user understanding, with an 608 * exception for applications that hold the right permission. Always attempts to use available 609 * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses 610 * Strings to strip creative formatting. 611 */ 612 private abstract static class TargetPresentationGetter { 613 @Nullable abstract Drawable getIconSubstituteInternal(); 614 @Nullable abstract String getAppSubLabelInternal(); 615 616 private Context mCtx; 617 private final int mIconDpi; 618 private final boolean mHasSubstitutePermission; 619 private final ApplicationInfo mAi; 620 621 protected PackageManager mPm; 622 623 TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) { 624 mCtx = ctx; 625 mPm = ctx.getPackageManager(); 626 mAi = ai; 627 mIconDpi = iconDpi; 628 mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission( 629 android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, 630 mAi.packageName); 631 } 632 633 public Drawable getIcon(UserHandle userHandle) { 634 return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle)); 635 } 636 637 public Bitmap getIconBitmap(UserHandle userHandle) { 638 Drawable dr = null; 639 if (mHasSubstitutePermission) { 640 dr = getIconSubstituteInternal(); 641 } 642 643 if (dr == null) { 644 try { 645 if (mAi.icon != 0) { 646 dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon); 647 } 648 } catch (NameNotFoundException ignore) { 649 } 650 } 651 652 // Fall back to ApplicationInfo#loadIcon if nothing has been loaded 653 if (dr == null) { 654 dr = mAi.loadIcon(mPm); 655 } 656 657 SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx); 658 Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle); 659 sif.recycle(); 660 661 return icon; 662 } 663 664 public String getLabel() { 665 String label = null; 666 // Apps with the substitute permission will always show the sublabel as their label 667 if (mHasSubstitutePermission) { 668 label = getAppSubLabelInternal(); 669 } 670 671 if (label == null) { 672 label = (String) mAi.loadLabel(mPm); 673 } 674 675 return label; 676 } 677 678 public String getSubLabel() { 679 // Apps with the substitute permission will never have a sublabel 680 if (mHasSubstitutePermission) return null; 681 return getAppSubLabelInternal(); 682 } 683 684 protected String loadLabelFromResource(Resources res, int resId) { 685 return res.getString(resId); 686 } 687 688 @Nullable 689 protected Drawable loadIconFromResource(Resources res, int resId) { 690 return res.getDrawableForDensity(resId, mIconDpi); 691 } 692 693 } 694 695 /** 696 * Loads the icon and label for the provided ResolveInfo. 697 */ 698 @VisibleForTesting 699 public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { 700 private final ResolveInfo mRi; 701 public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) { 702 super(ctx, iconDpi, ri.activityInfo); 703 mRi = ri; 704 } 705 706 @Override 707 Drawable getIconSubstituteInternal() { 708 Drawable dr = null; 709 try { 710 // Do not use ResolveInfo#getIconResource() as it defaults to the app 711 if (mRi.resolvePackageName != null && mRi.icon != 0) { 712 dr = loadIconFromResource( 713 mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon); 714 } 715 } catch (NameNotFoundException e) { 716 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 717 + "couldn't find resources for package", e); 718 } 719 720 // Fall back to ActivityInfo if no icon is found via ResolveInfo 721 if (dr == null) dr = super.getIconSubstituteInternal(); 722 723 return dr; 724 } 725 726 @Override 727 String getAppSubLabelInternal() { 728 // Will default to app name if no intent filter or activity label set, make sure to 729 // check if subLabel matches label before final display 730 return (String) mRi.loadLabel(mPm); 731 } 732 } 733 734 ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) { 735 return new ResolveInfoPresentationGetter(this, mIconDpi, ri); 736 } 737 738 /** 739 * Loads the icon and label for the provided ActivityInfo. 740 */ 741 @VisibleForTesting 742 public static class ActivityInfoPresentationGetter extends TargetPresentationGetter { 743 private final ActivityInfo mActivityInfo; 744 public ActivityInfoPresentationGetter(Context ctx, int iconDpi, 745 ActivityInfo activityInfo) { 746 super(ctx, iconDpi, activityInfo.applicationInfo); 747 mActivityInfo = activityInfo; 748 } 749 750 @Override 751 Drawable getIconSubstituteInternal() { 752 Drawable dr = null; 753 try { 754 // Do not use ActivityInfo#getIconResource() as it defaults to the app 755 if (mActivityInfo.icon != 0) { 756 dr = loadIconFromResource( 757 mPm.getResourcesForApplication(mActivityInfo.applicationInfo), 758 mActivityInfo.icon); 759 } 760 } catch (NameNotFoundException e) { 761 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 762 + "couldn't find resources for package", e); 763 } 764 765 return dr; 766 } 767 768 @Override 769 String getAppSubLabelInternal() { 770 // Will default to app name if no activity label set, make sure to check if subLabel 771 // matches label before final display 772 return (String) mActivityInfo.loadLabel(mPm); 773 } 774 } 775 776 protected ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) { 777 return new ActivityInfoPresentationGetter(this, mIconDpi, ai); 778 } 779 780 Drawable loadIconForResolveInfo(ResolveInfo ri) { 781 // Load icons based on the current process. If in work profile icons should be badged. 782 return makePresentationGetter(ri).getIcon(Process.myUserHandle()); 783 } 784 785 @Override 786 protected void onRestart() { 787 super.onRestart(); 788 if (!mRegistered) { 789 mPackageMonitor.register(this, getMainLooper(), false); 790 mRegistered = true; 791 } 792 mAdapter.handlePackagesChanged(); 793 bindProfileView(); 794 } 795 796 @Override 797 protected void onStop() { 798 super.onStop(); 799 if (mRegistered) { 800 mPackageMonitor.unregister(); 801 mRegistered = false; 802 } 803 final Intent intent = getIntent(); 804 if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction() 805 && !mResolvingHome && !mRetainInOnStop) { 806 // This resolver is in the unusual situation where it has been 807 // launched at the top of a new task. We don't let it be added 808 // to the recent tasks shown to the user, and we need to make sure 809 // that each time we are launched we get the correct launching 810 // uid (not re-using the same resolver from an old launching uid), 811 // so we will now finish ourself since being no longer visible, 812 // the user probably can't get back to us. 813 if (!isChangingConfigurations()) { 814 finish(); 815 } 816 } 817 } 818 819 @Override 820 protected void onDestroy() { 821 super.onDestroy(); 822 if (!isChangingConfigurations() && mPickOptionRequest != null) { 823 mPickOptionRequest.cancel(); 824 } 825 if (mPostListReadyRunnable != null) { 826 getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); 827 mPostListReadyRunnable = null; 828 } 829 if (mAdapter != null && mAdapter.mResolverListController != null) { 830 mAdapter.mResolverListController.destroy(); 831 } 832 } 833 834 @Override 835 protected void onRestoreInstanceState(Bundle savedInstanceState) { 836 super.onRestoreInstanceState(savedInstanceState); 837 resetButtonBar(); 838 } 839 840 private boolean isHttpSchemeAndViewAction(Intent intent) { 841 return (IntentFilter.SCHEME_HTTP.equals(intent.getScheme()) 842 || IntentFilter.SCHEME_HTTPS.equals(intent.getScheme())) 843 && Intent.ACTION_VIEW.equals(intent.getAction()); 844 } 845 846 private boolean hasManagedProfile() { 847 UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); 848 if (userManager == null) { 849 return false; 850 } 851 852 try { 853 List<UserInfo> profiles = userManager.getProfiles(getUserId()); 854 for (UserInfo userInfo : profiles) { 855 if (userInfo != null && userInfo.isManagedProfile()) { 856 return true; 857 } 858 } 859 } catch (SecurityException e) { 860 return false; 861 } 862 return false; 863 } 864 865 private boolean supportsManagedProfiles(ResolveInfo resolveInfo) { 866 try { 867 ApplicationInfo appInfo = getPackageManager().getApplicationInfo( 868 resolveInfo.activityInfo.packageName, 0 /* default flags */); 869 return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP; 870 } catch (NameNotFoundException e) { 871 return false; 872 } 873 } 874 875 private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos, 876 boolean filtered) { 877 boolean enabled = false; 878 if (hasValidSelection) { 879 ResolveInfo ri = mAdapter.resolveInfoForPosition(checkedPos, filtered); 880 if (ri == null) { 881 Log.e(TAG, "Invalid position supplied to setAlwaysButtonEnabled"); 882 return; 883 } else if (ri.targetUserId != UserHandle.USER_CURRENT) { 884 Log.e(TAG, "Attempted to set selection to resolve info for another user"); 885 return; 886 } else { 887 enabled = true; 888 } 889 if (mUseLayoutForBrowsables && !ri.handleAllWebDataURI) { 890 mAlwaysButton.setText(getResources() 891 .getString(R.string.activity_resolver_set_always)); 892 } else { 893 mAlwaysButton.setText(getResources() 894 .getString(R.string.activity_resolver_use_always)); 895 } 896 } 897 mAlwaysButton.setEnabled(enabled); 898 } 899 900 public void onButtonClick(View v) { 901 final int id = v.getId(); 902 int which = mAdapter.hasFilteredItem() 903 ? mAdapter.getFilteredPosition() 904 : mAdapterView.getCheckedItemPosition(); 905 boolean hasIndexBeenFiltered = !mAdapter.hasFilteredItem(); 906 ResolveInfo ri = mAdapter.resolveInfoForPosition(which, hasIndexBeenFiltered); 907 if (mUseLayoutForBrowsables 908 && !ri.handleAllWebDataURI && id == R.id.button_always) { 909 showSettingsForSelected(ri); 910 } else { 911 startSelected(which, id == R.id.button_always, hasIndexBeenFiltered); 912 } 913 } 914 915 private void showSettingsForSelected(ResolveInfo ri) { 916 Intent intent = new Intent(); 917 918 final String packageName = ri.activityInfo.packageName; 919 Bundle showFragmentArgs = new Bundle(); 920 showFragmentArgs.putString(EXTRA_FRAGMENT_ARG_KEY, OPEN_LINKS_COMPONENT_KEY); 921 showFragmentArgs.putString("package", packageName); 922 923 // For regular apps, we open the Open by Default page 924 intent.setAction(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS) 925 .setData(Uri.fromParts("package", packageName, null)) 926 .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 927 .putExtra(EXTRA_FRAGMENT_ARG_KEY, OPEN_LINKS_COMPONENT_KEY) 928 .putExtra(EXTRA_SHOW_FRAGMENT_ARGS, showFragmentArgs); 929 930 startActivity(intent); 931 } 932 933 public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) { 934 if (isFinishing()) { 935 return; 936 } 937 ResolveInfo ri = mAdapter.resolveInfoForPosition(which, hasIndexBeenFiltered); 938 if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) { 939 Toast.makeText(this, String.format(getResources().getString( 940 com.android.internal.R.string.activity_resolver_work_profiles_support), 941 ri.activityInfo.loadLabel(getPackageManager()).toString()), 942 Toast.LENGTH_LONG).show(); 943 return; 944 } 945 946 TargetInfo target = mAdapter.targetInfoForPosition(which, hasIndexBeenFiltered); 947 if (target == null) { 948 return; 949 } 950 if (onTargetSelected(target, always)) { 951 if (always && mSupportsAlwaysUseOption) { 952 MetricsLogger.action( 953 this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS); 954 } else if (mSupportsAlwaysUseOption) { 955 MetricsLogger.action( 956 this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE); 957 } else { 958 MetricsLogger.action( 959 this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP); 960 } 961 MetricsLogger.action(this, mAdapter.hasFilteredItem() 962 ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED 963 : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED); 964 finish(); 965 } 966 } 967 968 /** 969 * Replace me in subclasses! 970 */ 971 public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { 972 return defIntent; 973 } 974 975 protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { 976 final ResolveInfo ri = target.getResolveInfo(); 977 final Intent intent = target != null ? target.getResolvedIntent() : null; 978 979 if (intent != null && (mSupportsAlwaysUseOption || mAdapter.hasFilteredItem()) 980 && mAdapter.mUnfilteredResolveList != null) { 981 // Build a reasonable intent filter, based on what matched. 982 IntentFilter filter = new IntentFilter(); 983 Intent filterIntent; 984 985 if (intent.getSelector() != null) { 986 filterIntent = intent.getSelector(); 987 } else { 988 filterIntent = intent; 989 } 990 991 String action = filterIntent.getAction(); 992 if (action != null) { 993 filter.addAction(action); 994 } 995 Set<String> categories = filterIntent.getCategories(); 996 if (categories != null) { 997 for (String cat : categories) { 998 filter.addCategory(cat); 999 } 1000 } 1001 filter.addCategory(Intent.CATEGORY_DEFAULT); 1002 1003 int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK; 1004 Uri data = filterIntent.getData(); 1005 if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { 1006 String mimeType = filterIntent.resolveType(this); 1007 if (mimeType != null) { 1008 try { 1009 filter.addDataType(mimeType); 1010 } catch (IntentFilter.MalformedMimeTypeException e) { 1011 Log.w("ResolverActivity", e); 1012 filter = null; 1013 } 1014 } 1015 } 1016 if (data != null && data.getScheme() != null) { 1017 // We need the data specification if there was no type, 1018 // OR if the scheme is not one of our magical "file:" 1019 // or "content:" schemes (see IntentFilter for the reason). 1020 if (cat != IntentFilter.MATCH_CATEGORY_TYPE 1021 || (!"file".equals(data.getScheme()) 1022 && !"content".equals(data.getScheme()))) { 1023 filter.addDataScheme(data.getScheme()); 1024 1025 // Look through the resolved filter to determine which part 1026 // of it matched the original Intent. 1027 Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator(); 1028 if (pIt != null) { 1029 String ssp = data.getSchemeSpecificPart(); 1030 while (ssp != null && pIt.hasNext()) { 1031 PatternMatcher p = pIt.next(); 1032 if (p.match(ssp)) { 1033 filter.addDataSchemeSpecificPart(p.getPath(), p.getType()); 1034 break; 1035 } 1036 } 1037 } 1038 Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator(); 1039 if (aIt != null) { 1040 while (aIt.hasNext()) { 1041 IntentFilter.AuthorityEntry a = aIt.next(); 1042 if (a.match(data) >= 0) { 1043 int port = a.getPort(); 1044 filter.addDataAuthority(a.getHost(), 1045 port >= 0 ? Integer.toString(port) : null); 1046 break; 1047 } 1048 } 1049 } 1050 pIt = ri.filter.pathsIterator(); 1051 if (pIt != null) { 1052 String path = data.getPath(); 1053 while (path != null && pIt.hasNext()) { 1054 PatternMatcher p = pIt.next(); 1055 if (p.match(path)) { 1056 filter.addDataPath(p.getPath(), p.getType()); 1057 break; 1058 } 1059 } 1060 } 1061 } 1062 } 1063 1064 if (filter != null) { 1065 final int N = mAdapter.mUnfilteredResolveList.size(); 1066 ComponentName[] set; 1067 // If we don't add back in the component for forwarding the intent to a managed 1068 // profile, the preferred activity may not be updated correctly (as the set of 1069 // components we tell it we knew about will have changed). 1070 final boolean needToAddBackProfileForwardingComponent 1071 = mAdapter.mOtherProfile != null; 1072 if (!needToAddBackProfileForwardingComponent) { 1073 set = new ComponentName[N]; 1074 } else { 1075 set = new ComponentName[N + 1]; 1076 } 1077 1078 int bestMatch = 0; 1079 for (int i=0; i<N; i++) { 1080 ResolveInfo r = mAdapter.mUnfilteredResolveList.get(i).getResolveInfoAt(0); 1081 set[i] = new ComponentName(r.activityInfo.packageName, 1082 r.activityInfo.name); 1083 if (r.match > bestMatch) bestMatch = r.match; 1084 } 1085 1086 if (needToAddBackProfileForwardingComponent) { 1087 set[N] = mAdapter.mOtherProfile.getResolvedComponentName(); 1088 final int otherProfileMatch = mAdapter.mOtherProfile.getResolveInfo().match; 1089 if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch; 1090 } 1091 1092 if (alwaysCheck) { 1093 final int userId = getUserId(); 1094 final PackageManager pm = getPackageManager(); 1095 1096 // Set the preferred Activity 1097 pm.addPreferredActivity(filter, bestMatch, set, intent.getComponent()); 1098 1099 if (ri.handleAllWebDataURI) { 1100 // Set default Browser if needed 1101 final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId); 1102 if (TextUtils.isEmpty(packageName)) { 1103 pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId); 1104 } 1105 } else { 1106 // Update Domain Verification status 1107 ComponentName cn = intent.getComponent(); 1108 String packageName = cn.getPackageName(); 1109 String dataScheme = (data != null) ? data.getScheme() : null; 1110 1111 boolean isHttpOrHttps = (dataScheme != null) && 1112 (dataScheme.equals(IntentFilter.SCHEME_HTTP) || 1113 dataScheme.equals(IntentFilter.SCHEME_HTTPS)); 1114 1115 boolean isViewAction = (action != null) && action.equals(Intent.ACTION_VIEW); 1116 boolean hasCategoryBrowsable = (categories != null) && 1117 categories.contains(Intent.CATEGORY_BROWSABLE); 1118 1119 if (isHttpOrHttps && isViewAction && hasCategoryBrowsable) { 1120 pm.updateIntentVerificationStatusAsUser(packageName, 1121 PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS, 1122 userId); 1123 } 1124 } 1125 } else { 1126 try { 1127 mAdapter.mResolverListController.setLastChosen(intent, filter, bestMatch); 1128 } catch (RemoteException re) { 1129 Log.d(TAG, "Error calling setLastChosenActivity\n" + re); 1130 } 1131 } 1132 } 1133 } 1134 1135 if (target != null) { 1136 safelyStartActivity(target); 1137 1138 // Rely on the ActivityManager to pop up a dialog regarding app suspension 1139 // and return false 1140 if (target.isSuspended()) { 1141 return false; 1142 } 1143 } 1144 1145 return true; 1146 } 1147 1148 public void safelyStartActivity(TargetInfo cti) { 1149 // We're dispatching intents that might be coming from legacy apps, so 1150 // don't kill ourselves. 1151 StrictMode.disableDeathOnFileUriExposure(); 1152 try { 1153 safelyStartActivityInternal(cti); 1154 } finally { 1155 StrictMode.enableDeathOnFileUriExposure(); 1156 } 1157 } 1158 1159 private void safelyStartActivityInternal(TargetInfo cti) { 1160 // If needed, show that intent is forwarded 1161 // from managed profile to owner or other way around. 1162 if (mProfileSwitchMessageId != -1) { 1163 Toast.makeText(this, getString(mProfileSwitchMessageId), Toast.LENGTH_LONG).show(); 1164 } 1165 if (!mSafeForwardingMode) { 1166 if (cti.start(this, null)) { 1167 onActivityStarted(cti); 1168 } 1169 return; 1170 } 1171 try { 1172 if (cti.startAsCaller(this, null, UserHandle.USER_NULL)) { 1173 onActivityStarted(cti); 1174 } 1175 } catch (RuntimeException e) { 1176 String launchedFromPackage; 1177 try { 1178 launchedFromPackage = ActivityTaskManager.getService().getLaunchedFromPackage( 1179 getActivityToken()); 1180 } catch (RemoteException e2) { 1181 launchedFromPackage = "??"; 1182 } 1183 Slog.wtf(TAG, "Unable to launch as uid " + mLaunchedFromUid 1184 + " package " + launchedFromPackage + ", while running in " 1185 + ActivityThread.currentProcessName(), e); 1186 } 1187 } 1188 1189 1190 boolean startAsCallerImpl(Intent intent, Bundle options, boolean ignoreTargetSecurity, 1191 int userId) { 1192 // Pass intent to delegate chooser activity with permission token. 1193 // TODO: This should move to a trampoline Activity in the system when the ChooserActivity 1194 // moves into systemui 1195 try { 1196 // TODO: Once this is a small springboard activity, it can move off the UI process 1197 // and we can move the request method to ActivityManagerInternal. 1198 IBinder permissionToken = ActivityTaskManager.getService() 1199 .requestStartActivityPermissionToken(getActivityToken()); 1200 final Intent chooserIntent = new Intent(); 1201 final ComponentName delegateActivity = ComponentName.unflattenFromString( 1202 Resources.getSystem().getString(R.string.config_chooserActivity)); 1203 chooserIntent.setClassName(delegateActivity.getPackageName(), 1204 delegateActivity.getClassName()); 1205 chooserIntent.putExtra(ActivityTaskManager.EXTRA_PERMISSION_TOKEN, permissionToken); 1206 1207 // TODO: These extras will change as chooser activity moves into systemui 1208 chooserIntent.putExtra(Intent.EXTRA_INTENT, intent); 1209 chooserIntent.putExtra(ActivityTaskManager.EXTRA_OPTIONS, options); 1210 chooserIntent.putExtra(ActivityTaskManager.EXTRA_IGNORE_TARGET_SECURITY, 1211 ignoreTargetSecurity); 1212 chooserIntent.putExtra(Intent.EXTRA_USER_ID, userId); 1213 chooserIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT 1214 | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 1215 startActivity(chooserIntent); 1216 } catch (RemoteException e) { 1217 Log.e(TAG, e.toString()); 1218 } 1219 return true; 1220 } 1221 1222 public void onActivityStarted(TargetInfo cti) { 1223 // Do nothing 1224 } 1225 1226 public boolean shouldGetActivityMetadata() { 1227 return false; 1228 } 1229 1230 public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { 1231 return !target.isSuspended(); 1232 } 1233 1234 public void showTargetDetails(ResolveInfo ri) { 1235 Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 1236 .setData(Uri.fromParts("package", ri.activityInfo.packageName, null)) 1237 .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 1238 startActivity(in); 1239 } 1240 1241 public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents, 1242 Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid, 1243 boolean filterLastUsed) { 1244 return new ResolveListAdapter(context, payloadIntents, initialIntents, rList, 1245 launchedFromUid, filterLastUsed, createListController()); 1246 } 1247 1248 @VisibleForTesting 1249 protected ResolverListController createListController() { 1250 return new ResolverListController( 1251 this, 1252 mPm, 1253 getTargetIntent(), 1254 getReferrerPackageName(), 1255 mLaunchedFromUid); 1256 } 1257 1258 /** 1259 * Returns true if the activity is finishing and creation should halt 1260 */ 1261 public boolean configureContentView(List<Intent> payloadIntents, Intent[] initialIntents, 1262 List<ResolveInfo> rList) { 1263 // The last argument of createAdapter is whether to do special handling 1264 // of the last used choice to highlight it in the list. We need to always 1265 // turn this off when running under voice interaction, since it results in 1266 // a more complicated UI that the current voice interaction flow is not able 1267 // to handle. 1268 mAdapter = createAdapter(this, payloadIntents, initialIntents, rList, 1269 mLaunchedFromUid, mSupportsAlwaysUseOption && !isVoiceInteraction()); 1270 boolean rebuildCompleted = mAdapter.rebuildList(); 1271 1272 if (useLayoutWithDefault()) { 1273 mLayoutId = R.layout.resolver_list_with_default; 1274 } else { 1275 mLayoutId = getLayoutResource(); 1276 } 1277 setContentView(mLayoutId); 1278 1279 int count = mAdapter.getUnfilteredCount(); 1280 1281 // We only rebuild asynchronously when we have multiple elements to sort. In the case where 1282 // we're already done, we can check if we should auto-launch immediately. 1283 if (rebuildCompleted) { 1284 if (count == 1 && mAdapter.getOtherProfile() == null) { 1285 // Only one target, so we're a candidate to auto-launch! 1286 final TargetInfo target = mAdapter.targetInfoForPosition(0, false); 1287 if (shouldAutoLaunchSingleChoice(target)) { 1288 safelyStartActivity(target); 1289 mPackageMonitor.unregister(); 1290 mRegistered = false; 1291 finish(); 1292 return true; 1293 } 1294 } 1295 } 1296 1297 1298 mAdapterView = findViewById(R.id.resolver_list); 1299 1300 if (count == 0 && mAdapter.mPlaceholderCount == 0) { 1301 final TextView emptyView = findViewById(R.id.empty); 1302 emptyView.setVisibility(View.VISIBLE); 1303 mAdapterView.setVisibility(View.GONE); 1304 } else { 1305 mAdapterView.setVisibility(View.VISIBLE); 1306 onPrepareAdapterView(mAdapterView, mAdapter); 1307 } 1308 return false; 1309 } 1310 1311 public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) { 1312 final boolean useHeader = adapter.hasFilteredItem(); 1313 final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; 1314 1315 adapterView.setAdapter(mAdapter); 1316 1317 final ItemClickListener listener = new ItemClickListener(); 1318 adapterView.setOnItemClickListener(listener); 1319 adapterView.setOnItemLongClickListener(listener); 1320 1321 if (mSupportsAlwaysUseOption || mUseLayoutForBrowsables) { 1322 listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); 1323 } 1324 1325 // In case this method is called again (due to activity recreation), avoid adding a new 1326 // header if one is already present. 1327 if (useHeader && listView != null && listView.getHeaderViewsCount() == 0) { 1328 listView.setHeaderDividersEnabled(true); 1329 listView.addHeaderView(LayoutInflater.from(this).inflate( 1330 R.layout.resolver_different_item_header, listView, false)); 1331 } 1332 } 1333 1334 /** 1335 * Configure the area above the app selection list (title, content preview, etc). 1336 */ 1337 public void setHeader() { 1338 if (mAdapter.getCount() == 0 && mAdapter.mPlaceholderCount == 0) { 1339 final TextView titleView = findViewById(R.id.title); 1340 if (titleView != null) { 1341 titleView.setVisibility(View.GONE); 1342 } 1343 } 1344 1345 CharSequence title = mTitle != null 1346 ? mTitle 1347 : getTitleForAction(getTargetIntent(), mDefaultTitleResId); 1348 1349 if (!TextUtils.isEmpty(title)) { 1350 final TextView titleView = findViewById(R.id.title); 1351 if (titleView != null) { 1352 titleView.setText(title); 1353 } 1354 setTitle(title); 1355 } 1356 1357 final ImageView iconView = findViewById(R.id.icon); 1358 final DisplayResolveInfo iconInfo = mAdapter.getFilteredItem(); 1359 if (iconView != null && iconInfo != null) { 1360 new LoadIconTask(iconInfo, iconView).execute(); 1361 } 1362 } 1363 1364 private void resetButtonBar() { 1365 if (!mSupportsAlwaysUseOption && !mUseLayoutForBrowsables) { 1366 return; 1367 } 1368 final ViewGroup buttonLayout = findViewById(R.id.button_bar); 1369 if (buttonLayout != null) { 1370 buttonLayout.setVisibility(View.VISIBLE); 1371 1372 if (!useLayoutWithDefault()) { 1373 int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; 1374 buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(), 1375 buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize( 1376 R.dimen.resolver_button_bar_spacing) + inset); 1377 } 1378 mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once); 1379 mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always); 1380 1381 resetAlwaysOrOnceButtonBar(); 1382 } else { 1383 Log.e(TAG, "Layout unexpectedly does not have a button bar"); 1384 } 1385 } 1386 1387 private void resetAlwaysOrOnceButtonBar() { 1388 if (useLayoutWithDefault() 1389 && mAdapter.getFilteredPosition() != ListView.INVALID_POSITION) { 1390 setAlwaysButtonEnabled(true, mAdapter.getFilteredPosition(), false); 1391 mOnceButton.setEnabled(true); 1392 return; 1393 } 1394 1395 // When the items load in, if an item was already selected, enable the buttons 1396 if (mAdapterView != null 1397 && mAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) { 1398 setAlwaysButtonEnabled(true, mAdapterView.getCheckedItemPosition(), true); 1399 mOnceButton.setEnabled(true); 1400 } 1401 } 1402 1403 private boolean useLayoutWithDefault() { 1404 return mSupportsAlwaysUseOption && mAdapter.hasFilteredItem(); 1405 } 1406 1407 /** 1408 * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets 1409 * called and we are launched in a new task. 1410 */ 1411 protected void setRetainInOnStop(boolean retainInOnStop) { 1412 mRetainInOnStop = retainInOnStop; 1413 } 1414 1415 /** 1416 * Check a simple match for the component of two ResolveInfos. 1417 */ 1418 static boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) { 1419 return lhs == null ? rhs == null 1420 : lhs.activityInfo == null ? rhs.activityInfo == null 1421 : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name) 1422 && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName); 1423 } 1424 1425 public final class DisplayResolveInfo implements TargetInfo { 1426 private final ResolveInfo mResolveInfo; 1427 private final CharSequence mDisplayLabel; 1428 private Drawable mDisplayIcon; 1429 private Drawable mBadge; 1430 private final CharSequence mExtendedInfo; 1431 private final Intent mResolvedIntent; 1432 private final List<Intent> mSourceIntents = new ArrayList<>(); 1433 private boolean mIsSuspended; 1434 private boolean mPinned = false; 1435 1436 public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, 1437 CharSequence pInfo, Intent pOrigIntent) { 1438 mSourceIntents.add(originalIntent); 1439 mResolveInfo = pri; 1440 mDisplayLabel = pLabel; 1441 mExtendedInfo = pInfo; 1442 1443 final Intent intent = new Intent(pOrigIntent != null ? pOrigIntent : 1444 getReplacementIntent(pri.activityInfo, getTargetIntent())); 1445 intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT 1446 | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 1447 final ActivityInfo ai = mResolveInfo.activityInfo; 1448 intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name)); 1449 1450 mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; 1451 1452 mResolvedIntent = intent; 1453 } 1454 1455 private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags) { 1456 mSourceIntents.addAll(other.getAllSourceIntents()); 1457 mResolveInfo = other.mResolveInfo; 1458 mDisplayLabel = other.mDisplayLabel; 1459 mDisplayIcon = other.mDisplayIcon; 1460 mExtendedInfo = other.mExtendedInfo; 1461 mResolvedIntent = new Intent(other.mResolvedIntent); 1462 mResolvedIntent.fillIn(fillInIntent, flags); 1463 } 1464 1465 public ResolveInfo getResolveInfo() { 1466 return mResolveInfo; 1467 } 1468 1469 public CharSequence getDisplayLabel() { 1470 return mDisplayLabel; 1471 } 1472 1473 public Drawable getDisplayIcon() { 1474 return mDisplayIcon; 1475 } 1476 1477 @Override 1478 public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { 1479 return new DisplayResolveInfo(this, fillInIntent, flags); 1480 } 1481 1482 @Override 1483 public List<Intent> getAllSourceIntents() { 1484 return mSourceIntents; 1485 } 1486 1487 public void addAlternateSourceIntent(Intent alt) { 1488 mSourceIntents.add(alt); 1489 } 1490 1491 public void setDisplayIcon(Drawable icon) { 1492 mDisplayIcon = icon; 1493 } 1494 1495 public boolean hasDisplayIcon() { 1496 return mDisplayIcon != null; 1497 } 1498 1499 public CharSequence getExtendedInfo() { 1500 return mExtendedInfo; 1501 } 1502 1503 public Intent getResolvedIntent() { 1504 return mResolvedIntent; 1505 } 1506 1507 @Override 1508 public ComponentName getResolvedComponentName() { 1509 return new ComponentName(mResolveInfo.activityInfo.packageName, 1510 mResolveInfo.activityInfo.name); 1511 } 1512 1513 @Override 1514 public boolean start(Activity activity, Bundle options) { 1515 activity.startActivity(mResolvedIntent, options); 1516 return true; 1517 } 1518 1519 @Override 1520 public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { 1521 if (mEnableChooserDelegate) { 1522 return activity.startAsCallerImpl(mResolvedIntent, options, false, userId); 1523 } else { 1524 activity.startActivityAsCaller(mResolvedIntent, options, null, false, userId); 1525 return true; 1526 } 1527 } 1528 1529 @Override 1530 public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { 1531 activity.startActivityAsUser(mResolvedIntent, options, user); 1532 return false; 1533 } 1534 1535 public boolean isSuspended() { 1536 return mIsSuspended; 1537 } 1538 1539 @Override 1540 public boolean isPinned() { 1541 return mPinned; 1542 } 1543 1544 public void setPinned(boolean pinned) { 1545 mPinned = pinned; 1546 } 1547 } 1548 1549 List<DisplayResolveInfo> getDisplayList() { 1550 return mAdapter.mDisplayList; 1551 } 1552 1553 /** 1554 * A single target as represented in the chooser. 1555 */ 1556 public interface TargetInfo { 1557 /** 1558 * Get the resolved intent that represents this target. Note that this may not be the 1559 * intent that will be launched by calling one of the <code>start</code> methods provided; 1560 * this is the intent that will be credited with the launch. 1561 * 1562 * @return the resolved intent for this target 1563 */ 1564 Intent getResolvedIntent(); 1565 1566 /** 1567 * Get the resolved component name that represents this target. Note that this may not 1568 * be the component that will be directly launched by calling one of the <code>start</code> 1569 * methods provided; this is the component that will be credited with the launch. 1570 * 1571 * @return the resolved ComponentName for this target 1572 */ 1573 ComponentName getResolvedComponentName(); 1574 1575 /** 1576 * Start the activity referenced by this target. 1577 * 1578 * @param activity calling Activity performing the launch 1579 * @param options ActivityOptions bundle 1580 * @return true if the start completed successfully 1581 */ 1582 boolean start(Activity activity, Bundle options); 1583 1584 /** 1585 * Start the activity referenced by this target as if the ResolverActivity's caller 1586 * was performing the start operation. 1587 * 1588 * @param activity calling Activity (actually) performing the launch 1589 * @param options ActivityOptions bundle 1590 * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller 1591 * @return true if the start completed successfully 1592 */ 1593 boolean startAsCaller(ResolverActivity activity, Bundle options, int userId); 1594 1595 /** 1596 * Start the activity referenced by this target as a given user. 1597 * 1598 * @param activity calling activity performing the launch 1599 * @param options ActivityOptions bundle 1600 * @param user handle for the user to start the activity as 1601 * @return true if the start completed successfully 1602 */ 1603 boolean startAsUser(Activity activity, Bundle options, UserHandle user); 1604 1605 /** 1606 * Return the ResolveInfo about how and why this target matched the original query 1607 * for available targets. 1608 * 1609 * @return ResolveInfo representing this target's match 1610 */ 1611 ResolveInfo getResolveInfo(); 1612 1613 /** 1614 * Return the human-readable text label for this target. 1615 * 1616 * @return user-visible target label 1617 */ 1618 CharSequence getDisplayLabel(); 1619 1620 /** 1621 * Return any extended info for this target. This may be used to disambiguate 1622 * otherwise identical targets. 1623 * 1624 * @return human-readable disambig string or null if none present 1625 */ 1626 CharSequence getExtendedInfo(); 1627 1628 /** 1629 * @return The drawable that should be used to represent this target including badge 1630 */ 1631 Drawable getDisplayIcon(); 1632 1633 /** 1634 * Clone this target with the given fill-in information. 1635 */ 1636 TargetInfo cloneFilledIn(Intent fillInIntent, int flags); 1637 1638 /** 1639 * @return the list of supported source intents deduped against this single target 1640 */ 1641 List<Intent> getAllSourceIntents(); 1642 1643 /** 1644 * @return true if this target can be selected by the user 1645 */ 1646 boolean isSuspended(); 1647 1648 /** 1649 * @return true if this target should be pinned to the front by the request of the user 1650 */ 1651 boolean isPinned(); 1652 } 1653 1654 public class ResolveListAdapter extends BaseAdapter { 1655 private final List<Intent> mIntents; 1656 private final Intent[] mInitialIntents; 1657 private final List<ResolveInfo> mBaseResolveList; 1658 protected ResolveInfo mLastChosen; 1659 private DisplayResolveInfo mOtherProfile; 1660 private ResolverListController mResolverListController; 1661 private int mPlaceholderCount; 1662 private boolean mAllTargetsAreBrowsers = false; 1663 1664 protected final LayoutInflater mInflater; 1665 1666 // This one is the list that the Adapter will actually present. 1667 List<DisplayResolveInfo> mDisplayList; 1668 List<ResolvedComponentInfo> mUnfilteredResolveList; 1669 1670 private int mLastChosenPosition = -1; 1671 private boolean mFilterLastUsed; 1672 1673 public ResolveListAdapter(Context context, List<Intent> payloadIntents, 1674 Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid, 1675 boolean filterLastUsed, 1676 ResolverListController resolverListController) { 1677 mIntents = payloadIntents; 1678 mInitialIntents = initialIntents; 1679 mBaseResolveList = rList; 1680 mLaunchedFromUid = launchedFromUid; 1681 mInflater = LayoutInflater.from(context); 1682 mDisplayList = new ArrayList<>(); 1683 mFilterLastUsed = filterLastUsed; 1684 mResolverListController = resolverListController; 1685 } 1686 1687 public void handlePackagesChanged() { 1688 rebuildList(); 1689 if (getCount() == 0) { 1690 // We no longer have any items... just finish the activity. 1691 finish(); 1692 } 1693 } 1694 1695 public void setPlaceholderCount(int count) { 1696 mPlaceholderCount = count; 1697 } 1698 1699 public int getPlaceholderCount() { return mPlaceholderCount; } 1700 1701 @Nullable 1702 public DisplayResolveInfo getFilteredItem() { 1703 if (mFilterLastUsed && mLastChosenPosition >= 0) { 1704 // Not using getItem since it offsets to dodge this position for the list 1705 return mDisplayList.get(mLastChosenPosition); 1706 } 1707 return null; 1708 } 1709 1710 public DisplayResolveInfo getOtherProfile() { 1711 return mOtherProfile; 1712 } 1713 1714 public int getFilteredPosition() { 1715 if (mFilterLastUsed && mLastChosenPosition >= 0) { 1716 return mLastChosenPosition; 1717 } 1718 return AbsListView.INVALID_POSITION; 1719 } 1720 1721 public boolean hasFilteredItem() { 1722 return mFilterLastUsed && mLastChosen != null; 1723 } 1724 1725 public float getScore(DisplayResolveInfo target) { 1726 return mResolverListController.getScore(target); 1727 } 1728 1729 public void updateModel(ComponentName componentName) { 1730 mResolverListController.updateModel(componentName); 1731 } 1732 1733 public void updateChooserCounts(String packageName, int userId, String action) { 1734 mResolverListController.updateChooserCounts(packageName, userId, action); 1735 } 1736 1737 /** 1738 * @return true if all items in the display list are defined as browsers by 1739 * ResolveInfo.handleAllWebDataURI 1740 */ 1741 public boolean areAllTargetsBrowsers() { 1742 return mAllTargetsAreBrowsers; 1743 } 1744 1745 /** 1746 * Rebuild the list of resolvers. In some cases some parts will need some asynchronous work 1747 * to complete. 1748 * 1749 * @return Whether or not the list building is completed. 1750 */ 1751 protected boolean rebuildList() { 1752 List<ResolvedComponentInfo> currentResolveList = null; 1753 // Clear the value of mOtherProfile from previous call. 1754 mOtherProfile = null; 1755 mLastChosen = null; 1756 mLastChosenPosition = -1; 1757 mAllTargetsAreBrowsers = false; 1758 mDisplayList.clear(); 1759 if (mBaseResolveList != null) { 1760 currentResolveList = mUnfilteredResolveList = new ArrayList<>(); 1761 mResolverListController.addResolveListDedupe(currentResolveList, 1762 getTargetIntent(), 1763 mBaseResolveList); 1764 } else { 1765 currentResolveList = mUnfilteredResolveList = 1766 mResolverListController.getResolversForIntent(shouldGetResolvedFilter(), 1767 shouldGetActivityMetadata(), 1768 mIntents); 1769 if (currentResolveList == null) { 1770 processSortedList(currentResolveList); 1771 return true; 1772 } 1773 List<ResolvedComponentInfo> originalList = 1774 mResolverListController.filterIneligibleActivities(currentResolveList, 1775 true); 1776 if (originalList != null) { 1777 mUnfilteredResolveList = originalList; 1778 } 1779 } 1780 1781 // So far we only support a single other profile at a time. 1782 // The first one we see gets special treatment. 1783 for (ResolvedComponentInfo info : currentResolveList) { 1784 if (info.getResolveInfoAt(0).targetUserId != UserHandle.USER_CURRENT) { 1785 mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0), 1786 info.getResolveInfoAt(0), 1787 info.getResolveInfoAt(0).loadLabel(mPm), 1788 info.getResolveInfoAt(0).loadLabel(mPm), 1789 getReplacementIntent(info.getResolveInfoAt(0).activityInfo, 1790 info.getIntentAt(0))); 1791 currentResolveList.remove(info); 1792 break; 1793 } 1794 } 1795 1796 if (mOtherProfile == null) { 1797 try { 1798 mLastChosen = mResolverListController.getLastChosen(); 1799 } catch (RemoteException re) { 1800 Log.d(TAG, "Error calling getLastChosenActivity\n" + re); 1801 } 1802 } 1803 1804 int N; 1805 if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) { 1806 // We only care about fixing the unfilteredList if the current resolve list and 1807 // current resolve list are currently the same. 1808 List<ResolvedComponentInfo> originalList = 1809 mResolverListController.filterLowPriority(currentResolveList, 1810 mUnfilteredResolveList == currentResolveList); 1811 if (originalList != null) { 1812 mUnfilteredResolveList = originalList; 1813 } 1814 1815 if (currentResolveList.size() > 1) { 1816 int placeholderCount = currentResolveList.size(); 1817 if (useLayoutWithDefault()) { 1818 --placeholderCount; 1819 } 1820 setPlaceholderCount(placeholderCount); 1821 AsyncTask<List<ResolvedComponentInfo>, 1822 Void, 1823 List<ResolvedComponentInfo>> sortingTask = 1824 new AsyncTask<List<ResolvedComponentInfo>, 1825 Void, 1826 List<ResolvedComponentInfo>>() { 1827 @Override 1828 protected List<ResolvedComponentInfo> doInBackground( 1829 List<ResolvedComponentInfo>... params) { 1830 mResolverListController.sort(params[0]); 1831 return params[0]; 1832 } 1833 1834 @Override 1835 protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { 1836 processSortedList(sortedComponents); 1837 bindProfileView(); 1838 notifyDataSetChanged(); 1839 } 1840 }; 1841 sortingTask.execute(currentResolveList); 1842 postListReadyRunnable(); 1843 return false; 1844 } else { 1845 processSortedList(currentResolveList); 1846 return true; 1847 } 1848 } else { 1849 processSortedList(currentResolveList); 1850 return true; 1851 } 1852 } 1853 1854 1855 private void processSortedList(List<ResolvedComponentInfo> sortedComponents) { 1856 int N; 1857 if (sortedComponents != null && (N = sortedComponents.size()) != 0) { 1858 mAllTargetsAreBrowsers = mUseLayoutForBrowsables; 1859 1860 // First put the initial items at the top. 1861 if (mInitialIntents != null) { 1862 for (int i = 0; i < mInitialIntents.length; i++) { 1863 Intent ii = mInitialIntents[i]; 1864 if (ii == null) { 1865 continue; 1866 } 1867 ActivityInfo ai = ii.resolveActivityInfo( 1868 getPackageManager(), 0); 1869 if (ai == null) { 1870 Log.w(TAG, "No activity found for " + ii); 1871 continue; 1872 } 1873 ResolveInfo ri = new ResolveInfo(); 1874 ri.activityInfo = ai; 1875 UserManager userManager = 1876 (UserManager) getSystemService(Context.USER_SERVICE); 1877 if (ii instanceof LabeledIntent) { 1878 LabeledIntent li = (LabeledIntent) ii; 1879 ri.resolvePackageName = li.getSourcePackage(); 1880 ri.labelRes = li.getLabelResource(); 1881 ri.nonLocalizedLabel = li.getNonLocalizedLabel(); 1882 ri.icon = li.getIconResource(); 1883 ri.iconResourceId = ri.icon; 1884 } 1885 if (userManager.isManagedProfile()) { 1886 ri.noResourceId = true; 1887 ri.icon = 0; 1888 } 1889 mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; 1890 1891 addResolveInfo(new DisplayResolveInfo(ii, ri, 1892 ri.loadLabel(getPackageManager()), null, ii)); 1893 } 1894 } 1895 1896 1897 for (ResolvedComponentInfo rci : sortedComponents) { 1898 final ResolveInfo ri = rci.getResolveInfoAt(0); 1899 if (ri != null) { 1900 mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; 1901 1902 ResolveInfoPresentationGetter pg = makePresentationGetter(ri); 1903 addResolveInfoWithAlternates(rci, pg.getSubLabel(), pg.getLabel()); 1904 } 1905 } 1906 } 1907 1908 sendVoiceChoicesIfNeeded(); 1909 postListReadyRunnable(); 1910 } 1911 1912 1913 1914 /** 1915 * Some necessary methods for creating the list are initiated in onCreate and will also 1916 * determine the layout known. We therefore can't update the UI inline and post to the 1917 * handler thread to update after the current task is finished. 1918 */ 1919 private void postListReadyRunnable() { 1920 if (mPostListReadyRunnable == null) { 1921 mPostListReadyRunnable = new Runnable() { 1922 @Override 1923 public void run() { 1924 setHeader(); 1925 resetButtonBar(); 1926 onListRebuilt(); 1927 mPostListReadyRunnable = null; 1928 } 1929 }; 1930 getMainThreadHandler().post(mPostListReadyRunnable); 1931 } 1932 } 1933 1934 public void onListRebuilt() { 1935 int count = getUnfilteredCount(); 1936 if (count == 1 && getOtherProfile() == null) { 1937 // Only one target, so we're a candidate to auto-launch! 1938 final TargetInfo target = targetInfoForPosition(0, false); 1939 if (shouldAutoLaunchSingleChoice(target)) { 1940 safelyStartActivity(target); 1941 finish(); 1942 } 1943 } 1944 } 1945 1946 public boolean shouldGetResolvedFilter() { 1947 return mFilterLastUsed; 1948 } 1949 1950 private void addResolveInfoWithAlternates(ResolvedComponentInfo rci, 1951 CharSequence extraInfo, CharSequence roLabel) { 1952 final int count = rci.getCount(); 1953 final Intent intent = rci.getIntentAt(0); 1954 final ResolveInfo add = rci.getResolveInfoAt(0); 1955 final Intent replaceIntent = getReplacementIntent(add.activityInfo, intent); 1956 final DisplayResolveInfo dri = new DisplayResolveInfo(intent, add, roLabel, 1957 extraInfo, replaceIntent); 1958 dri.setPinned(rci.isPinned()); 1959 if (rci.isPinned()) { 1960 Log.i(TAG, "Pinned item: " + rci.name); 1961 } 1962 addResolveInfo(dri); 1963 if (replaceIntent == intent) { 1964 // Only add alternates if we didn't get a specific replacement from 1965 // the caller. If we have one it trumps potential alternates. 1966 for (int i = 1, N = count; i < N; i++) { 1967 final Intent altIntent = rci.getIntentAt(i); 1968 dri.addAlternateSourceIntent(altIntent); 1969 } 1970 } 1971 updateLastChosenPosition(add); 1972 } 1973 1974 private void updateLastChosenPosition(ResolveInfo info) { 1975 // If another profile is present, ignore the last chosen entry. 1976 if (mOtherProfile != null) { 1977 mLastChosenPosition = -1; 1978 return; 1979 } 1980 if (mLastChosen != null 1981 && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) 1982 && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { 1983 mLastChosenPosition = mDisplayList.size() - 1; 1984 } 1985 } 1986 1987 // We assume that at this point we've already filtered out the only intent for a different 1988 // targetUserId which we're going to use. 1989 private void addResolveInfo(DisplayResolveInfo dri) { 1990 if (dri != null && dri.mResolveInfo != null 1991 && dri.mResolveInfo.targetUserId == UserHandle.USER_CURRENT) { 1992 // Checks if this info is already listed in display. 1993 for (DisplayResolveInfo existingInfo : mDisplayList) { 1994 if (resolveInfoMatch(dri.mResolveInfo, existingInfo.mResolveInfo)) { 1995 return; 1996 } 1997 } 1998 mDisplayList.add(dri); 1999 } 2000 } 2001 2002 @Nullable 2003 public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { 2004 TargetInfo target = targetInfoForPosition(position, filtered); 2005 if (target != null) { 2006 return target.getResolveInfo(); 2007 } 2008 return null; 2009 } 2010 2011 @Nullable 2012 public TargetInfo targetInfoForPosition(int position, boolean filtered) { 2013 if (filtered) { 2014 return getItem(position); 2015 } 2016 if (mDisplayList.size() > position) { 2017 return mDisplayList.get(position); 2018 } 2019 return null; 2020 } 2021 2022 public int getCount() { 2023 int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount : 2024 mDisplayList.size(); 2025 if (mFilterLastUsed && mLastChosenPosition >= 0) { 2026 totalSize--; 2027 } 2028 return totalSize; 2029 } 2030 2031 public int getUnfilteredCount() { 2032 return mDisplayList.size(); 2033 } 2034 2035 @Nullable 2036 public TargetInfo getItem(int position) { 2037 if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { 2038 position++; 2039 } 2040 if (mDisplayList.size() > position) { 2041 return mDisplayList.get(position); 2042 } else { 2043 return null; 2044 } 2045 } 2046 2047 public long getItemId(int position) { 2048 return position; 2049 } 2050 2051 public int getDisplayResolveInfoCount() { 2052 return mDisplayList.size(); 2053 } 2054 2055 public DisplayResolveInfo getDisplayResolveInfo(int index) { 2056 // Used to query services. We only query services for primary targets, not alternates. 2057 return mDisplayList.get(index); 2058 } 2059 2060 public final View getView(int position, View convertView, ViewGroup parent) { 2061 View view = convertView; 2062 if (view == null) { 2063 view = createView(parent); 2064 } 2065 onBindView(view, getItem(position)); 2066 return view; 2067 } 2068 2069 public final View createView(ViewGroup parent) { 2070 final View view = onCreateView(parent); 2071 final ViewHolder holder = new ViewHolder(view); 2072 view.setTag(holder); 2073 return view; 2074 } 2075 2076 public View onCreateView(ViewGroup parent) { 2077 return mInflater.inflate( 2078 com.android.internal.R.layout.resolve_list_item, parent, false); 2079 } 2080 2081 public final void bindView(int position, View view) { 2082 onBindView(view, getItem(position)); 2083 } 2084 2085 protected void onBindView(View view, TargetInfo info) { 2086 final ViewHolder holder = (ViewHolder) view.getTag(); 2087 if (info == null) { 2088 holder.icon.setImageDrawable( 2089 getDrawable(R.drawable.resolver_icon_placeholder)); 2090 return; 2091 } 2092 2093 final CharSequence label = info.getDisplayLabel(); 2094 if (!TextUtils.equals(holder.text.getText(), label)) { 2095 holder.text.setText(info.getDisplayLabel()); 2096 } 2097 2098 // Always show a subLabel for visual consistency across list items. Show an empty 2099 // subLabel if the subLabel is the same as the label 2100 CharSequence subLabel = info.getExtendedInfo(); 2101 if (TextUtils.equals(label, subLabel)) subLabel = null; 2102 2103 if (!TextUtils.equals(holder.text2.getText(), subLabel) 2104 && !TextUtils.isEmpty(subLabel)) { 2105 holder.text2.setVisibility(View.VISIBLE); 2106 holder.text2.setText(subLabel); 2107 } 2108 2109 if (info.isSuspended()) { 2110 holder.icon.setColorFilter(mSuspendedMatrixColorFilter); 2111 } else { 2112 holder.icon.setColorFilter(null); 2113 } 2114 2115 if (info instanceof DisplayResolveInfo 2116 && !((DisplayResolveInfo) info).hasDisplayIcon()) { 2117 new LoadIconTask((DisplayResolveInfo) info, holder.icon).execute(); 2118 } else { 2119 holder.icon.setImageDrawable(info.getDisplayIcon()); 2120 } 2121 } 2122 } 2123 2124 2125 @VisibleForTesting 2126 public static final class ResolvedComponentInfo { 2127 public final ComponentName name; 2128 private final List<Intent> mIntents = new ArrayList<>(); 2129 private final List<ResolveInfo> mResolveInfos = new ArrayList<>(); 2130 private boolean mPinned; 2131 2132 public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) { 2133 this.name = name; 2134 add(intent, info); 2135 } 2136 2137 public void add(Intent intent, ResolveInfo info) { 2138 mIntents.add(intent); 2139 mResolveInfos.add(info); 2140 } 2141 2142 public int getCount() { 2143 return mIntents.size(); 2144 } 2145 2146 public Intent getIntentAt(int index) { 2147 return index >= 0 ? mIntents.get(index) : null; 2148 } 2149 2150 public ResolveInfo getResolveInfoAt(int index) { 2151 return index >= 0 ? mResolveInfos.get(index) : null; 2152 } 2153 2154 public int findIntent(Intent intent) { 2155 for (int i = 0, N = mIntents.size(); i < N; i++) { 2156 if (intent.equals(mIntents.get(i))) { 2157 return i; 2158 } 2159 } 2160 return -1; 2161 } 2162 2163 public int findResolveInfo(ResolveInfo info) { 2164 for (int i = 0, N = mResolveInfos.size(); i < N; i++) { 2165 if (info.equals(mResolveInfos.get(i))) { 2166 return i; 2167 } 2168 } 2169 return -1; 2170 } 2171 2172 public boolean isPinned() { 2173 return mPinned; 2174 } 2175 2176 public void setPinned(boolean pinned) { 2177 mPinned = pinned; 2178 } 2179 2180 } 2181 2182 static class ViewHolder { 2183 public View itemView; 2184 public Drawable defaultItemViewBackground; 2185 2186 public TextView text; 2187 public TextView text2; 2188 public ImageView icon; 2189 2190 public ViewHolder(View view) { 2191 itemView = view; 2192 defaultItemViewBackground = view.getBackground(); 2193 text = (TextView) view.findViewById(com.android.internal.R.id.text1); 2194 text2 = (TextView) view.findViewById(com.android.internal.R.id.text2); 2195 icon = (ImageView) view.findViewById(R.id.icon); 2196 } 2197 } 2198 2199 class ItemClickListener implements AdapterView.OnItemClickListener, 2200 AdapterView.OnItemLongClickListener { 2201 @Override 2202 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 2203 final ListView listView = parent instanceof ListView ? (ListView) parent : null; 2204 if (listView != null) { 2205 position -= listView.getHeaderViewsCount(); 2206 } 2207 if (position < 0) { 2208 // Header views don't count. 2209 return; 2210 } 2211 // If we're still loading, we can't yet enable the buttons. 2212 if (mAdapter.resolveInfoForPosition(position, true) == null) { 2213 return; 2214 } 2215 2216 final int checkedPos = mAdapterView.getCheckedItemPosition(); 2217 final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; 2218 if (!useLayoutWithDefault() 2219 && (!hasValidSelection || mLastSelected != checkedPos) 2220 && mAlwaysButton != null) { 2221 setAlwaysButtonEnabled(hasValidSelection, checkedPos, true); 2222 mOnceButton.setEnabled(hasValidSelection); 2223 if (hasValidSelection) { 2224 mAdapterView.smoothScrollToPosition(checkedPos); 2225 } 2226 mLastSelected = checkedPos; 2227 } else { 2228 startSelected(position, false, true); 2229 } 2230 } 2231 2232 @Override 2233 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 2234 final ListView listView = parent instanceof ListView ? (ListView) parent : null; 2235 if (listView != null) { 2236 position -= listView.getHeaderViewsCount(); 2237 } 2238 if (position < 0) { 2239 // Header views don't count. 2240 return false; 2241 } 2242 ResolveInfo ri = mAdapter.resolveInfoForPosition(position, true); 2243 showTargetDetails(ri); 2244 return true; 2245 } 2246 2247 } 2248 2249 class LoadIconTask extends AsyncTask<Void, Void, Drawable> { 2250 protected final DisplayResolveInfo mDisplayResolveInfo; 2251 private final ResolveInfo mResolveInfo; 2252 private final ImageView mTargetView; 2253 2254 LoadIconTask(DisplayResolveInfo dri, ImageView target) { 2255 mDisplayResolveInfo = dri; 2256 mResolveInfo = dri.getResolveInfo(); 2257 mTargetView = target; 2258 } 2259 2260 @Override 2261 protected Drawable doInBackground(Void... params) { 2262 return loadIconForResolveInfo(mResolveInfo); 2263 } 2264 2265 @Override 2266 protected void onPostExecute(Drawable d) { 2267 if (mAdapter.getOtherProfile() == mDisplayResolveInfo) { 2268 bindProfileView(); 2269 } else { 2270 mDisplayResolveInfo.setDisplayIcon(d); 2271 mTargetView.setImageDrawable(d); 2272 } 2273 } 2274 } 2275 2276 static final boolean isSpecificUriMatch(int match) { 2277 match = match&IntentFilter.MATCH_CATEGORY_MASK; 2278 return match >= IntentFilter.MATCH_CATEGORY_HOST 2279 && match <= IntentFilter.MATCH_CATEGORY_PATH; 2280 } 2281 2282 static class PickTargetOptionRequest extends PickOptionRequest { 2283 public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options, 2284 @Nullable Bundle extras) { 2285 super(prompt, options, extras); 2286 } 2287 2288 @Override 2289 public void onCancel() { 2290 super.onCancel(); 2291 final ResolverActivity ra = (ResolverActivity) getActivity(); 2292 if (ra != null) { 2293 ra.mPickOptionRequest = null; 2294 ra.finish(); 2295 } 2296 } 2297 2298 @Override 2299 public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) { 2300 super.onPickOptionResult(finished, selections, result); 2301 if (selections.length != 1) { 2302 // TODO In a better world we would filter the UI presented here and let the 2303 // user refine. Maybe later. 2304 return; 2305 } 2306 2307 final ResolverActivity ra = (ResolverActivity) getActivity(); 2308 if (ra != null) { 2309 final TargetInfo ti = ra.mAdapter.getItem(selections[0].getIndex()); 2310 if (ra.onTargetSelected(ti, false)) { 2311 ra.mPickOptionRequest = null; 2312 ra.finish(); 2313 } 2314 } 2315 } 2316 } 2317 } 2318