1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver; 18 19 import static android.app.VoiceInteractor.PickOptionRequest.Option; 20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 21 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 22 23 import static androidx.lifecycle.LifecycleKt.getCoroutineScope; 24 25 import static com.android.intentresolver.ChooserActionFactory.EDIT_SOURCE; 26 import static com.android.intentresolver.Flags.delayDrawerOffsetCalculation; 27 import static com.android.intentresolver.Flags.fixShortcutsFlashingFixed; 28 import static com.android.intentresolver.Flags.interactiveSession; 29 import static com.android.intentresolver.Flags.keyboardNavigationFix; 30 import static com.android.intentresolver.Flags.rebuildAdaptersOnTargetPinning; 31 import static com.android.intentresolver.Flags.refineSystemActions; 32 import static com.android.intentresolver.Flags.shareouselUpdateExcludeComponentsExtra; 33 import static com.android.intentresolver.Flags.unselectFinalItem; 34 import static com.android.intentresolver.ext.CreationExtrasExtKt.replaceDefaultArgs; 35 import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL; 36 import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK; 37 import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; 38 39 import static java.util.Objects.requireNonNull; 40 41 import android.app.ActivityManager; 42 import android.app.ActivityOptions; 43 import android.app.ActivityThread; 44 import android.app.VoiceInteractor; 45 import android.app.admin.DevicePolicyEventLogger; 46 import android.app.prediction.AppPredictor; 47 import android.app.prediction.AppTarget; 48 import android.app.prediction.AppTargetEvent; 49 import android.app.prediction.AppTargetId; 50 import android.content.ClipboardManager; 51 import android.content.ComponentName; 52 import android.content.ContentResolver; 53 import android.content.Context; 54 import android.content.Intent; 55 import android.content.IntentFilter; 56 import android.content.IntentSender; 57 import android.content.SharedPreferences; 58 import android.content.pm.ActivityInfo; 59 import android.content.pm.PackageManager; 60 import android.content.pm.ResolveInfo; 61 import android.content.pm.ShortcutInfo; 62 import android.content.res.Configuration; 63 import android.database.Cursor; 64 import android.graphics.Insets; 65 import android.graphics.Rect; 66 import android.net.Uri; 67 import android.os.Bundle; 68 import android.os.StrictMode; 69 import android.os.SystemClock; 70 import android.os.Trace; 71 import android.os.UserHandle; 72 import android.service.chooser.ChooserTarget; 73 import android.stats.devicepolicy.DevicePolicyEnums; 74 import android.text.TextUtils; 75 import android.util.Log; 76 import android.util.Slog; 77 import android.view.Gravity; 78 import android.view.LayoutInflater; 79 import android.view.View; 80 import android.view.ViewGroup; 81 import android.view.ViewGroup.LayoutParams; 82 import android.view.ViewTreeObserver; 83 import android.view.Window; 84 import android.view.WindowInsets; 85 import android.view.WindowManager; 86 import android.widget.FrameLayout; 87 import android.widget.ImageView; 88 import android.widget.TabHost; 89 import android.widget.TabWidget; 90 import android.widget.TextView; 91 import android.widget.Toast; 92 93 import androidx.annotation.MainThread; 94 import androidx.annotation.NonNull; 95 import androidx.annotation.Nullable; 96 import androidx.fragment.app.FragmentActivity; 97 import androidx.lifecycle.ViewModelProvider; 98 import androidx.lifecycle.viewmodel.CreationExtras; 99 import androidx.recyclerview.widget.GridLayoutManager; 100 import androidx.recyclerview.widget.RecyclerView; 101 import androidx.viewpager.widget.ViewPager; 102 103 import com.android.intentresolver.ChooserRefinementManager.RefinementType; 104 import com.android.intentresolver.chooser.DisplayResolveInfo; 105 import com.android.intentresolver.chooser.MultiDisplayResolveInfo; 106 import com.android.intentresolver.chooser.TargetInfo; 107 import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; 108 import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; 109 import com.android.intentresolver.data.model.ChooserRequest; 110 import com.android.intentresolver.data.repository.ActivityModelRepository; 111 import com.android.intentresolver.data.repository.DevicePolicyResources; 112 import com.android.intentresolver.domain.interactor.UserInteractor; 113 import com.android.intentresolver.emptystate.CompositeEmptyStateProvider; 114 import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; 115 import com.android.intentresolver.emptystate.EmptyStateProvider; 116 import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider; 117 import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider; 118 import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider; 119 import com.android.intentresolver.grid.ChooserGridAdapter; 120 import com.android.intentresolver.icons.Caching; 121 import com.android.intentresolver.icons.TargetDataLoader; 122 import com.android.intentresolver.inject.Background; 123 import com.android.intentresolver.logging.EventLog; 124 import com.android.intentresolver.measurements.Tracer; 125 import com.android.intentresolver.model.AbstractResolverComparator; 126 import com.android.intentresolver.model.AppPredictionServiceResolverComparator; 127 import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; 128 import com.android.intentresolver.platform.AppPredictionAvailable; 129 import com.android.intentresolver.platform.ImageEditor; 130 import com.android.intentresolver.platform.NearbyShare; 131 import com.android.intentresolver.profiles.ChooserMultiProfilePagerAdapter; 132 import com.android.intentresolver.profiles.MultiProfilePagerAdapter.ProfileType; 133 import com.android.intentresolver.profiles.OnProfileSelectedListener; 134 import com.android.intentresolver.profiles.OnSwitchOnWorkSelectedListener; 135 import com.android.intentresolver.profiles.TabConfig; 136 import com.android.intentresolver.shared.model.ActivityModel; 137 import com.android.intentresolver.shared.model.Profile; 138 import com.android.intentresolver.shortcuts.AppPredictorFactory; 139 import com.android.intentresolver.shortcuts.ShortcutLoader; 140 import com.android.intentresolver.ui.ActionTitle; 141 import com.android.intentresolver.ui.ProfilePagerResources; 142 import com.android.intentresolver.ui.ShareResultSender; 143 import com.android.intentresolver.ui.ShareResultSenderFactory; 144 import com.android.intentresolver.ui.viewmodel.ChooserViewModel; 145 import com.android.intentresolver.widget.ActionRow; 146 import com.android.intentresolver.widget.ChooserNestedScrollView; 147 import com.android.intentresolver.widget.ImagePreviewView; 148 import com.android.intentresolver.widget.ResolverDrawerLayout; 149 import com.android.intentresolver.widget.ResolverDrawerLayoutExt; 150 import com.android.internal.annotations.VisibleForTesting; 151 import com.android.internal.content.PackageMonitor; 152 import com.android.internal.logging.MetricsLogger; 153 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 154 import com.android.internal.util.LatencyTracker; 155 156 import com.google.common.collect.ImmutableList; 157 158 import dagger.hilt.android.AndroidEntryPoint; 159 160 import kotlinx.coroutines.CoroutineDispatcher; 161 162 import java.util.ArrayList; 163 import java.util.Arrays; 164 import java.util.Collection; 165 import java.util.Collections; 166 import java.util.HashMap; 167 import java.util.LinkedHashMap; 168 import java.util.List; 169 import java.util.Map; 170 import java.util.Objects; 171 import java.util.Optional; 172 import java.util.Set; 173 import java.util.concurrent.ExecutorService; 174 import java.util.concurrent.Executors; 175 import java.util.concurrent.atomic.AtomicLong; 176 import java.util.function.Consumer; 177 import java.util.function.Supplier; 178 179 import javax.inject.Inject; 180 181 /** 182 * The Chooser Activity handles intent resolution specifically for sharing intents - 183 * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}. 184 * 185 */ 186 @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 187 @AndroidEntryPoint(FragmentActivity.class) 188 public class ChooserActivity extends Hilt_ChooserActivity implements 189 ResolverListAdapter.ResolverListCommunicator, PackagesChangedListener, StartsSelectedItem { 190 private static final String TAG = "ChooserActivity"; 191 192 /** 193 * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself 194 * in onStop when launched in a new task. If this extra is set to true, we do not finish 195 * ourselves when onStop gets called. 196 */ 197 public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP 198 = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP"; 199 200 /** 201 * Transition name for the first image preview. 202 * To be used for shared element transition into this activity. 203 */ 204 public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image"; 205 206 private static final boolean DEBUG = true; 207 208 public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; 209 private static final String SHORTCUT_TARGET = "shortcut_target"; 210 211 ////////////////////////////////////////////////////////////////////////////////////////////// 212 // Inherited properties. 213 ////////////////////////////////////////////////////////////////////////////////////////////// 214 private static final String TAB_TAG_PERSONAL = "personal"; 215 private static final String TAB_TAG_WORK = "work"; 216 217 private static final String LAST_SHOWN_PROFILE = "last_shown_tab_key"; 218 public static final String METRICS_CATEGORY_CHOOSER = "intent_chooser"; 219 220 private int mLayoutId; 221 private UserHandle mHeaderCreatorUser; 222 private boolean mRegistered; 223 private PackageMonitor mPersonalPackageMonitor; 224 private PackageMonitor mWorkPackageMonitor; 225 226 protected ResolverDrawerLayout mResolverDrawerLayout; 227 private TabHost mTabHost; 228 private ResolverViewPager mViewPager; 229 protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter; 230 protected final LatencyTracker mLatencyTracker = getLatencyTracker(); 231 232 /** See {@link #setRetainInOnStop}. */ 233 private boolean mRetainInOnStop; 234 protected Insets mSystemWindowInsets = null; 235 private ResolverActivity.PickTargetOptionRequest mPickOptionRequest; 236 237 @Nullable 238 private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; 239 240 ////////////////////////////////////////////////////////////////////////////////////////////// 241 ////////////////////////////////////////////////////////////////////////////////////////////// 242 243 244 // TODO: these data structures are for one-time use in shuttling data from where they're 245 // populated in `ShortcutToChooserTargetConverter` to where they're consumed in 246 // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`. 247 // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their 248 // intermediate data, and then these members can be removed. 249 private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>(); 250 private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>(); 251 252 static final int TARGET_TYPE_DEFAULT = 0; 253 static final int TARGET_TYPE_CHOOSER_TARGET = 1; 254 static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2; 255 static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3; 256 257 private static final int SCROLL_STATUS_IDLE = 0; 258 private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1; 259 private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; 260 261 @Inject public UserInteractor mUserInteractor; 262 @Inject @Background public CoroutineDispatcher mBackgroundDispatcher; 263 @Inject public ChooserHelper mChooserHelper; 264 @Inject public EventLog mEventLog; 265 @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable; 266 @Inject @ImageEditor public Optional<ComponentName> mImageEditor; 267 @Inject @NearbyShare public Optional<ComponentName> mNearbyShare; 268 @Inject 269 @Caching 270 public TargetDataLoader mTargetDataLoader; 271 @Inject public DevicePolicyResources mDevicePolicyResources; 272 @Inject public ProfilePagerResources mProfilePagerResources; 273 @Inject public PackageManager mPackageManager; 274 @Inject public ClipboardManager mClipboardManager; 275 @Inject public IntentForwarding mIntentForwarding; 276 @Inject public ShareResultSenderFactory mShareResultSenderFactory; 277 @Inject public ActivityModelRepository mActivityModelRepository; 278 279 private ActivityModel mActivityModel; 280 private ChooserRequest mRequest; 281 private ProfileHelper mProfiles; 282 private ProfileAvailability mProfileAvailability; 283 @Nullable private ShareResultSender mShareResultSender; 284 285 private ChooserRefinementManager mRefinementManager; 286 287 private ChooserContentPreviewUi mChooserContentPreviewUi; 288 289 private boolean mShouldDisplayLandscape; 290 private long mChooserShownTime; 291 protected boolean mIsSuccessfullySelected; 292 293 private int mCurrAvailableWidth = 0; 294 private Insets mLastAppliedInsets = null; 295 private int mLastNumberOfChildren = -1; 296 private int mMaxTargetsPerRow = 1; 297 298 private static final int MAX_LOG_RANK_POSITION = 12; 299 300 // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters. 301 private static final int MAX_EXTRA_INITIAL_INTENTS = 2; 302 private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; 303 304 private SharedPreferences mPinnedSharedPrefs; 305 private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings"; 306 307 private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5); 308 309 private int mScrollStatus = SCROLL_STATUS_IDLE; 310 311 private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate = 312 new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout); 313 314 private final Map<Integer, ProfileRecord> mProfileRecords = new LinkedHashMap<>(); 315 316 private boolean mExcludeSharedText = false; 317 /** 318 * When we intend to finish the activity with a shared element transition, we can't immediately 319 * finish() when the transition is invoked, as the receiving end may not be able to start the 320 * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop 321 * in order to wait for the transition to begin. 322 */ 323 private boolean mFinishWhenStopped = false; 324 325 private final AtomicLong mIntentReceivedTime = new AtomicLong(-1); 326 createActivityModel()327 protected ActivityModel createActivityModel() { 328 return ActivityModel.createFrom(this); 329 } 330 331 private ChooserViewModel mViewModel; 332 333 @NonNull 334 @Override getDefaultViewModelCreationExtras()335 public CreationExtras getDefaultViewModelCreationExtras() { 336 // DEFAULT_ARGS_KEY extra is saved for each ViewModel we create. ComponentActivity puts the 337 // initial intent's extra into DEFAULT_ARGS_KEY thus we store these values 2 times (3 if we 338 // count the initial intent). We don't need those values to be saved as they don't capture 339 // the state. 340 return replaceDefaultArgs(super.getDefaultViewModelCreationExtras()); 341 } 342 343 @Override onCreate(Bundle savedInstanceState)344 protected void onCreate(Bundle savedInstanceState) { 345 super.onCreate(savedInstanceState); 346 Log.i(TAG, "onCreate"); 347 mActivityModelRepository.initialize(this::createActivityModel); 348 349 setTheme(R.style.Theme_DeviceDefault_Chooser); 350 351 // Initializer is invoked when this function returns, via Lifecycle. 352 mChooserHelper.setInitializer(this::initialize); 353 mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged); 354 mChooserHelper.setOnPendingSelection(this::onPendingSelection); 355 if (unselectFinalItem()) { 356 mChooserHelper.setOnHasSelections(this::onHasSelections); 357 } 358 } 359 private int mInitialProfile = -1; 360 361 @Override onStart()362 protected final void onStart() { 363 super.onStart(); 364 this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 365 } 366 367 @Override onResume()368 protected final void onResume() { 369 super.onResume(); 370 Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); 371 mFinishWhenStopped = false; 372 mRefinementManager.onActivityResume(); 373 } 374 375 @Override onStop()376 protected final void onStop() { 377 super.onStop(); 378 379 final Window window = this.getWindow(); 380 final WindowManager.LayoutParams attrs = window.getAttributes(); 381 attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 382 window.setAttributes(attrs); 383 384 if (mRegistered) { 385 mPersonalPackageMonitor.unregister(); 386 if (mWorkPackageMonitor != null) { 387 mWorkPackageMonitor.unregister(); 388 } 389 mRegistered = false; 390 } 391 final Intent intent = getIntent(); 392 if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction() 393 && !mRetainInOnStop) { 394 // This resolver is in the unusual situation where it has been 395 // launched at the top of a new task. We don't let it be added 396 // to the recent tasks shown to the user, and we need to make sure 397 // that each time we are launched we get the correct launching 398 // uid (not re-using the same resolver from an old launching uid), 399 // so we will now finish ourself since being no longer visible, 400 // the user probably can't get back to us. 401 if (!isChangingConfigurations()) { 402 Log.d(TAG, "finishing in onStop"); 403 finish(); 404 } 405 } 406 407 if (mRefinementManager != null) { 408 mRefinementManager.onActivityStop(isChangingConfigurations()); 409 } 410 411 if (mFinishWhenStopped) { 412 mFinishWhenStopped = false; 413 finish(); 414 } 415 } 416 417 @Override onSaveInstanceState(Bundle outState)418 protected final void onSaveInstanceState(Bundle outState) { 419 super.onSaveInstanceState(outState); 420 if (mViewPager != null) { 421 outState.putInt( 422 LAST_SHOWN_PROFILE, mChooserMultiProfilePagerAdapter.getActiveProfile()); 423 } 424 } 425 426 @Override onRestart()427 protected final void onRestart() { 428 super.onRestart(); 429 if (mChooserMultiProfilePagerAdapter.hasPageForProfile(Profile.Type.PRIVATE.ordinal()) 430 && !mProfileAvailability.isAvailable(mProfiles.getPrivateProfile())) { 431 Log.d(TAG, "Exiting due to unavailable profile"); 432 finish(); 433 return; 434 } 435 436 if (!mRegistered) { 437 mPersonalPackageMonitor.register( 438 this, 439 getMainLooper(), 440 mProfiles.getPersonalHandle(), 441 false); 442 if (mProfiles.getWorkProfilePresent()) { 443 if (mWorkPackageMonitor == null) { 444 mWorkPackageMonitor = createPackageMonitor( 445 mChooserMultiProfilePagerAdapter.getWorkListAdapter()); 446 } 447 mWorkPackageMonitor.register( 448 this, 449 getMainLooper(), 450 mProfiles.getWorkHandle(), 451 false); 452 } 453 mRegistered = true; 454 } 455 mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); 456 } 457 458 @Override onDestroy()459 protected void onDestroy() { 460 super.onDestroy(); 461 if (!isChangingConfigurations() && mPickOptionRequest != null) { 462 mPickOptionRequest.cancel(); 463 } 464 if (mChooserMultiProfilePagerAdapter != null) { 465 mChooserMultiProfilePagerAdapter.destroy(); 466 } 467 468 if (isFinishing()) { 469 mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); 470 if (interactiveSession() && mViewModel != null) { 471 mViewModel.getInteractiveSessionInteractor().endSession(); 472 } 473 } 474 475 mBackgroundThreadPoolExecutor.shutdownNow(); 476 477 destroyProfileRecords(); 478 } 479 480 /** DO NOT CALL. Only for use from ChooserHelper as a callback. */ initialize()481 private void initialize() { 482 483 mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class); 484 mRequest = mViewModel.getRequest().getValue(); 485 mActivityModel = mViewModel.getActivityModel(); 486 487 mProfiles = new ProfileHelper( 488 mUserInteractor, 489 mBackgroundDispatcher); 490 491 mProfileAvailability = new ProfileAvailability( 492 mUserInteractor, 493 getCoroutineScope(getLifecycle()), 494 mBackgroundDispatcher); 495 496 mProfileAvailability.setOnProfileStatusChange(this::onWorkProfileStatusUpdated); 497 498 mIntentReceivedTime.set(System.currentTimeMillis()); 499 mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); 500 501 mPinnedSharedPrefs = getPinnedSharedPrefs(this); 502 updateShareResultSender(); 503 504 mMaxTargetsPerRow = 505 getResources().getInteger(R.integer.config_chooser_max_targets_per_row); 506 mShouldDisplayLandscape = 507 shouldDisplayLandscape(getResources().getConfiguration().orientation); 508 509 setRetainInOnStop(mRequest.shouldRetainInOnStop()); 510 createProfileRecords( 511 new AppPredictorFactory( 512 this, 513 Objects.toString(mRequest.getSharedText(), null), 514 mRequest.getShareTargetFilter(), 515 mAppPredictionAvailable 516 ), 517 mRequest.getShareTargetFilter() 518 ); 519 520 521 mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( 522 /* context = */ this, 523 mProfilePagerResources, 524 mRequest, 525 mProfiles, 526 mProfileRecords.values(), 527 mProfileAvailability, 528 mRequest.getInitialIntents(), 529 mMaxTargetsPerRow); 530 531 maybeDisableRecentsScreenshot(mProfiles, mProfileAvailability); 532 533 if (!configureContentView(mTargetDataLoader)) { 534 mPersonalPackageMonitor = createPackageMonitor( 535 mChooserMultiProfilePagerAdapter.getPersonalListAdapter()); 536 mPersonalPackageMonitor.register( 537 this, 538 getMainLooper(), 539 mProfiles.getPersonalHandle(), 540 false 541 ); 542 if (mProfiles.getWorkProfilePresent()) { 543 mWorkPackageMonitor = createPackageMonitor( 544 mChooserMultiProfilePagerAdapter.getWorkListAdapter()); 545 mWorkPackageMonitor.register( 546 this, 547 getMainLooper(), 548 mProfiles.getWorkHandle(), 549 false 550 ); 551 } 552 mRegistered = true; 553 final ResolverDrawerLayout rdl = findViewById( 554 com.android.internal.R.id.contentPanel); 555 if (rdl != null) { 556 rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() { 557 @Override 558 public void onDismissed() { 559 finish(); 560 } 561 }); 562 563 boolean hasTouchScreen = mPackageManager 564 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); 565 566 if (isVoiceInteraction() || !hasTouchScreen) { 567 rdl.setCollapsed(false); 568 } 569 570 rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 571 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 572 rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets); 573 574 mResolverDrawerLayout = rdl; 575 } 576 577 Intent intent = mRequest.getTargetIntent(); 578 final Set<String> categories = intent.getCategories(); 579 MetricsLogger.action(this, 580 mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() 581 ? MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED 582 : MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED, 583 intent.getAction() + ":" + intent.getType() + ":" 584 + (categories != null ? Arrays.toString(categories.toArray()) 585 : "")); 586 } 587 588 getEventLog().logSharesheetTriggered(); 589 mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); 590 mRefinementManager.getRefinementCompletion().observe(this, completion -> { 591 if (completion.consume()) { 592 if (completion.getRefinedIntent() == null) { 593 finish(); 594 return; 595 } 596 597 // Prepare to regenerate our "system actions" based on the refined intent. 598 // TODO: optimize if needed. `TARGET_INFO` cases don't require a new action 599 // factory at all. And if we break up `ChooserActionFactory`, we could avoid 600 // resolving a new editor intent unless we're handling an `EDIT_ACTION`. 601 ChooserActionFactory refinedActionFactory = 602 createChooserActionFactory(completion.getRefinedIntent()); 603 switch (completion.getType()) { 604 case TARGET_INFO: { 605 TargetInfo refinedTarget = completion 606 .getOriginalTargetInfo() 607 .tryToCloneWithAppliedRefinement( 608 completion.getRefinedIntent()); 609 if (refinedTarget == null) { 610 Log.e(TAG, "Failed to apply refinement to any matching source intent"); 611 } else { 612 maybeRemoveSharedText(refinedTarget); 613 614 // We already block suspended targets from going to refinement, and we 615 // probably can't recover a Chooser session if that's the reason the 616 // refined target fails to launch now. Fire-and-forget the refined 617 // launch, and make sure Sharesheet gets cleaned up regardless of the 618 // outcome of that launch.launch; ignore 619 620 safelyStartActivity(refinedTarget); 621 } 622 } 623 break; 624 625 case COPY_ACTION: { 626 if (refinedActionFactory.getCopyButtonRunnable() != null) { 627 refinedActionFactory.getCopyButtonRunnable().run(); 628 } 629 } 630 break; 631 632 case EDIT_ACTION: { 633 if (refinedActionFactory.getEditButtonRunnable() != null) { 634 refinedActionFactory.getEditButtonRunnable().run(); 635 } 636 } 637 break; 638 } 639 640 finish(); 641 } 642 }); 643 ChooserContentPreviewUi.ActionFactory actionFactory = 644 decorateActionFactoryWithRefinement( 645 createChooserActionFactory(mRequest.getTargetIntent())); 646 mChooserContentPreviewUi = new ChooserContentPreviewUi( 647 getCoroutineScope(getLifecycle()), 648 mViewModel.getPreviewDataProvider(), 649 mRequest, 650 mViewModel.getImageLoader(), 651 actionFactory, 652 createModifyShareActionFactory(), 653 mEnterTransitionAnimationDelegate, 654 new HeadlineGeneratorImpl(this), 655 mRequest.getContentTypeHint(), 656 mRequest.getMetadataText()); 657 updateStickyContentPreview(); 658 if (shouldShowStickyContentPreview()) { 659 getEventLog().logActionShareWithPreview( 660 mChooserContentPreviewUi.getPreferredContentPreview()); 661 } 662 mChooserShownTime = System.currentTimeMillis(); 663 final long systemCost = mChooserShownTime - mIntentReceivedTime.get(); 664 getEventLog().logChooserActivityShown( 665 isWorkProfile(), mRequest.getTargetType(), systemCost); 666 if (mResolverDrawerLayout != null) { 667 mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); 668 669 mResolverDrawerLayout.setOnCollapsedChangedListener( 670 isCollapsed -> { 671 mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed); 672 getEventLog().logSharesheetExpansionChanged(isCollapsed); 673 }); 674 } 675 if (DEBUG) { 676 Log.d(TAG, "System Time Cost is " + systemCost); 677 } 678 getEventLog().logShareStarted( 679 mRequest.getReferrerPackage(), 680 mRequest.getTargetType(), 681 mRequest.getCallerChooserTargets().size(), 682 mRequest.getInitialIntents().size(), 683 isWorkProfile(), 684 mChooserContentPreviewUi.getPreferredContentPreview(), 685 mRequest.getTargetAction(), 686 mRequest.getChooserActions().size(), 687 mRequest.getModifyShareAction() != null 688 ); 689 mEnterTransitionAnimationDelegate.postponeTransition(); 690 mInitialProfile = findSelectedProfile(); 691 Tracer.INSTANCE.markLaunched(); 692 693 if (isInteractiveSession()) { 694 configureInteractiveSessionWindow(); 695 updateInteractiveArea(); 696 } 697 } 698 maybeDisableRecentsScreenshot( ProfileHelper profileHelper, ProfileAvailability profileAvailability)699 private void maybeDisableRecentsScreenshot( 700 ProfileHelper profileHelper, ProfileAvailability profileAvailability) { 701 for (Profile profile : profileHelper.getProfiles()) { 702 if (profile.getType() == Profile.Type.PRIVATE) { 703 if (profileAvailability.isAvailable(profile)) { 704 // Show blank screen in Recent preview if private profile is available 705 // to not leak its presence. 706 setRecentsScreenshotEnabled(false); 707 } 708 return; 709 } 710 } 711 } 712 onChooserRequestChanged(ChooserRequest chooserRequest)713 private void onChooserRequestChanged(ChooserRequest chooserRequest) { 714 if (mRequest == chooserRequest) { 715 return; 716 } 717 boolean recreateAdapters = shouldUpdateAdapters(mRequest, chooserRequest); 718 mRequest = chooserRequest; 719 updateShareResultSender(); 720 mChooserContentPreviewUi.updateModifyShareAction(); 721 if (recreateAdapters) { 722 recreatePagerAdapter(); 723 } else { 724 setTabsViewEnabled(true); 725 } 726 } 727 onPendingSelection()728 private void onPendingSelection() { 729 setTabsViewEnabled(false); 730 } 731 onHasSelections(boolean hasSelections)732 private void onHasSelections(boolean hasSelections) { 733 mChooserMultiProfilePagerAdapter.setTargetsEnabled(hasSelections); 734 } 735 configureInteractiveSessionWindow()736 private void configureInteractiveSessionWindow() { 737 if (!isInteractiveSession()) { 738 Log.wtf(TAG, "Unexpected user of the method; should be an interactive session"); 739 return; 740 } 741 final Window window = getWindow(); 742 if (window == null) { 743 return; 744 } 745 window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 746 window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY); 747 } 748 updateInteractiveArea()749 private void updateInteractiveArea() { 750 if (!isInteractiveSession()) { 751 Log.wtf(TAG, "Unexpected user of the method; should be an interactive session"); 752 return; 753 } 754 final View contentView = findViewById(android.R.id.content); 755 final ResolverDrawerLayout rdl = mResolverDrawerLayout; 756 if (contentView == null || rdl == null) { 757 return; 758 } 759 final Rect rect = new Rect(); 760 contentView.getViewTreeObserver().addOnComputeInternalInsetsListener((info) -> { 761 int oldTop = rect.top; 762 rdl.getBoundsInWindow(rect, true); 763 int left = rect.left; 764 int top = rect.top; 765 ResolverDrawerLayoutExt.getVisibleDrawerRect(rdl, rect); 766 rect.offset(left, top); 767 if (oldTop != rect.top) { 768 mViewModel.getInteractiveSessionInteractor().sendTopDrawerTopOffsetChange(rect.top); 769 } 770 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 771 info.touchableRegion.set(new Rect(rect)); 772 }); 773 } 774 onAppTargetsLoaded(ResolverListAdapter listAdapter)775 private void onAppTargetsLoaded(ResolverListAdapter listAdapter) { 776 Log.d(TAG, "onAppTargetsLoaded(" 777 + "listAdapter.userHandle=" + listAdapter.getUserHandle() + ")"); 778 779 if (mChooserMultiProfilePagerAdapter == null) { 780 return; 781 } 782 if (!isProfilePagerAdapterAttached() 783 && listAdapter == mChooserMultiProfilePagerAdapter.getActiveListAdapter()) { 784 mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager); 785 setTabsViewEnabled(true); 786 } 787 } 788 updateShareResultSender()789 private void updateShareResultSender() { 790 IntentSender chosenComponentSender = mRequest.getChosenComponentSender(); 791 if (chosenComponentSender != null) { 792 mShareResultSender = mShareResultSenderFactory.create( 793 mViewModel.getActivityModel().getLaunchedFromUid(), chosenComponentSender); 794 } else { 795 mShareResultSender = null; 796 } 797 } 798 shouldUpdateAdapters( ChooserRequest oldChooserRequest, ChooserRequest newChooserRequest)799 private boolean shouldUpdateAdapters( 800 ChooserRequest oldChooserRequest, ChooserRequest newChooserRequest) { 801 Intent oldTargetIntent = oldChooserRequest.getTargetIntent(); 802 Intent newTargetIntent = newChooserRequest.getTargetIntent(); 803 List<Intent> oldAltIntents = oldChooserRequest.getAdditionalTargets(); 804 List<Intent> newAltIntents = newChooserRequest.getAdditionalTargets(); 805 List<ComponentName> oldExcluded = oldChooserRequest.getFilteredComponentNames(); 806 List<ComponentName> newExcluded = newChooserRequest.getFilteredComponentNames(); 807 808 // TODO: a workaround for the unnecessary target reloading caused by multiple flow updates - 809 // an artifact of the current implementation; revisit. 810 return !oldTargetIntent.equals(newTargetIntent) 811 || !oldAltIntents.equals(newAltIntents) 812 || (shareouselUpdateExcludeComponentsExtra() 813 && !oldExcluded.equals(newExcluded)); 814 } 815 recreatePagerAdapter()816 private void recreatePagerAdapter() { 817 destroyProfileRecords(); 818 createProfileRecords( 819 new AppPredictorFactory( 820 this, 821 Objects.toString(mRequest.getSharedText(), null), 822 mRequest.getShareTargetFilter(), 823 mAppPredictionAvailable 824 ), 825 mRequest.getShareTargetFilter() 826 ); 827 828 int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage(); 829 if (mChooserMultiProfilePagerAdapter != null) { 830 mChooserMultiProfilePagerAdapter.destroy(); 831 } 832 // Update the pager adapter but do not attach it to the view till the targets are reloaded, 833 // see onChooserAppTargetsLoaded method. 834 ChooserMultiProfilePagerAdapter oldPagerAdapter = 835 mChooserMultiProfilePagerAdapter; 836 mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( 837 /* context = */ this, 838 mProfilePagerResources, 839 mRequest, 840 mProfiles, 841 mProfileRecords.values(), 842 mProfileAvailability, 843 mRequest.getInitialIntents(), 844 mMaxTargetsPerRow); 845 mChooserMultiProfilePagerAdapter.setCurrentPage(currentPage); 846 for (int i = 0, count = mChooserMultiProfilePagerAdapter.getItemCount(); i < count; i++) { 847 mChooserMultiProfilePagerAdapter.getPageAdapterForIndex(i) 848 .getListAdapter().setAnimateItems(false); 849 } 850 if (mPersonalPackageMonitor != null) { 851 mPersonalPackageMonitor.unregister(); 852 } 853 mPersonalPackageMonitor = createPackageMonitor( 854 mChooserMultiProfilePagerAdapter.getPersonalListAdapter()); 855 mPersonalPackageMonitor.register( 856 this, 857 getMainLooper(), 858 mProfiles.getPersonalHandle(), 859 false); 860 if (mProfiles.getWorkProfilePresent()) { 861 if (mWorkPackageMonitor != null) { 862 mWorkPackageMonitor.unregister(); 863 } 864 mWorkPackageMonitor = createPackageMonitor( 865 mChooserMultiProfilePagerAdapter.getWorkListAdapter()); 866 mWorkPackageMonitor.register( 867 this, 868 getMainLooper(), 869 mProfiles.getWorkHandle(), 870 false); 871 } 872 postRebuildList( 873 mChooserMultiProfilePagerAdapter.rebuildTabs( 874 mProfiles.getWorkProfilePresent() || mProfiles.getPrivateProfilePresent())); 875 if (fixShortcutsFlashingFixed() && oldPagerAdapter != null) { 876 for (int i = 0, count = mChooserMultiProfilePagerAdapter.getCount(); i < count; i++) { 877 ChooserListAdapter listAdapter = 878 mChooserMultiProfilePagerAdapter.getPageAdapterForIndex(i) 879 .getListAdapter(); 880 ChooserListAdapter oldListAdapter = 881 oldPagerAdapter.getListAdapterForUserHandle(listAdapter.getUserHandle()); 882 if (oldListAdapter != null) { 883 listAdapter.copyDirectTargetsFrom(oldListAdapter); 884 listAdapter.setDirectTargetsEnabled(false); 885 } 886 } 887 } 888 setTabsViewEnabled(false); 889 if (mSystemWindowInsets != null) { 890 applyFooterView(mSystemWindowInsets.bottom); 891 } 892 } 893 setTabsViewEnabled(boolean isEnabled)894 private void setTabsViewEnabled(boolean isEnabled) { 895 TabWidget tabs = mTabHost.getTabWidget(); 896 if (tabs != null) { 897 tabs.setEnabled(isEnabled); 898 } 899 View tabContent = mTabHost.findViewById(com.android.internal.R.id.profile_pager); 900 if (tabContent != null) { 901 tabContent.setEnabled(isEnabled); 902 } 903 } 904 905 @Override onRestoreInstanceState(@onNull Bundle savedInstanceState)906 protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { 907 if (mViewPager != null) { 908 int profile = savedInstanceState.getInt(LAST_SHOWN_PROFILE); 909 int profileNumber = mChooserMultiProfilePagerAdapter.getPageNumberForProfile(profile); 910 if (profileNumber != -1) { 911 mViewPager.setCurrentItem(profileNumber); 912 mInitialProfile = profile; 913 } 914 } 915 mChooserMultiProfilePagerAdapter.clearInactiveProfileCache(); 916 } 917 918 ////////////////////////////////////////////////////////////////////////////////////////////// 919 // Inherited methods 920 ////////////////////////////////////////////////////////////////////////////////////////////// 921 isAutolaunching()922 private boolean isAutolaunching() { 923 return !mRegistered && isFinishing(); 924 } 925 maybeAutolaunchIfSingleTarget()926 private boolean maybeAutolaunchIfSingleTarget() { 927 int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); 928 if (count != 1) { 929 return false; 930 } 931 932 if (mChooserMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) { 933 return false; 934 } 935 936 // Only one target, so we're a candidate to auto-launch! 937 final TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter() 938 .targetInfoForPosition(0, false); 939 if (shouldAutoLaunchSingleChoice(target)) { 940 Log.d(TAG, "auto launching " + target + " and finishing."); 941 safelyStartActivity(target); 942 finish(); 943 return true; 944 } 945 return false; 946 } 947 isTwoPagePersonalAndWorkConfiguration()948 private boolean isTwoPagePersonalAndWorkConfiguration() { 949 return (mChooserMultiProfilePagerAdapter.getCount() == 2) 950 && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL) 951 && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK); 952 } 953 954 /** 955 * When we have a personal and a work profile, we auto launch in the following scenario: 956 * - There is 1 resolved target on each profile 957 * - That target is the same app on both profiles 958 * - The target app has permission to communicate cross profiles 959 * - The target app has declared it supports cross-profile communication via manifest metadata 960 */ maybeAutolaunchIfCrossProfileSupported()961 private boolean maybeAutolaunchIfCrossProfileSupported() { 962 if (!isTwoPagePersonalAndWorkConfiguration()) { 963 return false; 964 } 965 966 ResolverListAdapter activeListAdapter = 967 (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) 968 ? mChooserMultiProfilePagerAdapter.getPersonalListAdapter() 969 : mChooserMultiProfilePagerAdapter.getWorkListAdapter(); 970 971 ResolverListAdapter inactiveListAdapter = 972 (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) 973 ? mChooserMultiProfilePagerAdapter.getWorkListAdapter() 974 : mChooserMultiProfilePagerAdapter.getPersonalListAdapter(); 975 976 if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) { 977 return false; 978 } 979 980 if ((activeListAdapter.getUnfilteredCount() != 1) 981 || (inactiveListAdapter.getUnfilteredCount() != 1)) { 982 return false; 983 } 984 985 TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false); 986 TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false); 987 if (!Objects.equals( 988 activeProfileTarget.getResolvedComponentName(), 989 inactiveProfileTarget.getResolvedComponentName())) { 990 return false; 991 } 992 993 if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) { 994 return false; 995 } 996 997 String packageName = activeProfileTarget.getResolvedComponentName().getPackageName(); 998 if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) { 999 return false; 1000 } 1001 1002 DevicePolicyEventLogger 1003 .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET) 1004 .setBoolean(activeListAdapter.getUserHandle() 1005 .equals(mProfiles.getPersonalHandle())) 1006 .setStrings(getMetricsCategory()) 1007 .write(); 1008 safelyStartActivity(activeProfileTarget); 1009 Log.d(TAG, "auto launching! " + activeProfileTarget); 1010 finish(); 1011 return true; 1012 } 1013 1014 /** 1015 * @return {@code true} if a resolved target is autolaunched, otherwise {@code false} 1016 */ maybeAutolaunchActivity()1017 private boolean maybeAutolaunchActivity() { 1018 if (isInteractiveSession()) { 1019 return false; 1020 } 1021 int numberOfProfiles = mChooserMultiProfilePagerAdapter.getItemCount(); 1022 // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the 1023 // correct intent-picker UIs (e.g., mini-resolver) if it was launched without 1024 // ACTION_SEND. 1025 if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) { 1026 return true; 1027 } else if (maybeAutolaunchIfCrossProfileSupported()) { 1028 return true; 1029 } 1030 return false; 1031 } 1032 1033 @Override // ResolverListCommunicator onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing, boolean rebuildCompleted)1034 public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing, 1035 boolean rebuildCompleted) { 1036 if (isAutolaunching()) { 1037 return; 1038 } 1039 if (mChooserMultiProfilePagerAdapter 1040 .shouldShowEmptyStateScreen((ChooserListAdapter) listAdapter)) { 1041 mChooserMultiProfilePagerAdapter 1042 .showEmptyResolverListEmptyState((ChooserListAdapter) listAdapter); 1043 } else { 1044 mChooserMultiProfilePagerAdapter.showListView((ChooserListAdapter) listAdapter); 1045 } 1046 // showEmptyResolverListEmptyState can mark the tab as loaded, 1047 // which is a precondition for auto launching 1048 if (rebuildCompleted && maybeAutolaunchActivity()) { 1049 return; 1050 } 1051 if (doPostProcessing) { 1052 maybeCreateHeader(listAdapter); 1053 onListRebuilt(listAdapter, rebuildCompleted); 1054 } 1055 } 1056 getOrLoadDisplayLabel(TargetInfo info)1057 private CharSequence getOrLoadDisplayLabel(TargetInfo info) { 1058 if (info.isDisplayResolveInfo()) { 1059 mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info); 1060 } 1061 CharSequence displayLabel = info.getDisplayLabel(); 1062 return displayLabel == null ? "" : displayLabel; 1063 } 1064 getTitleForAction(Intent intent, int defaultTitleRes)1065 protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) { 1066 final ActionTitle title = ActionTitle.forAction(intent.getAction()); 1067 1068 // While there may already be a filtered item, we can only use it in the title if the list 1069 // is already sorted and all information relevant to it is already in the list. 1070 final boolean named = 1071 mChooserMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0; 1072 if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) { 1073 return getString(defaultTitleRes); 1074 } else { 1075 return named 1076 ? getString( 1077 title.namedTitleRes, 1078 getOrLoadDisplayLabel( 1079 mChooserMultiProfilePagerAdapter 1080 .getActiveListAdapter().getFilteredItem())) 1081 : getString(title.titleRes); 1082 } 1083 } 1084 1085 /** 1086 * Configure the area above the app selection list (title, content preview, etc). 1087 */ maybeCreateHeader(ResolverListAdapter listAdapter)1088 private void maybeCreateHeader(ResolverListAdapter listAdapter) { 1089 if (mHeaderCreatorUser != null 1090 && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) { 1091 return; 1092 } 1093 if (!mProfiles.getWorkProfilePresent() 1094 && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) { 1095 final TextView titleView = findViewById(com.android.internal.R.id.title); 1096 if (titleView != null) { 1097 titleView.setVisibility(View.GONE); 1098 } 1099 } 1100 1101 CharSequence title = mRequest.getTitle() != null 1102 ? mRequest.getTitle() 1103 : getTitleForAction(mRequest.getTargetIntent(), 1104 mRequest.getDefaultTitleResource()); 1105 1106 if (!TextUtils.isEmpty(title)) { 1107 final TextView titleView = findViewById(com.android.internal.R.id.title); 1108 if (titleView != null) { 1109 titleView.setText(title); 1110 } 1111 setTitle(title); 1112 } 1113 1114 final ImageView iconView = findViewById(com.android.internal.R.id.icon); 1115 if (iconView != null) { 1116 listAdapter.loadFilteredItemIconTaskAsync(iconView); 1117 } 1118 mHeaderCreatorUser = listAdapter.getUserHandle(); 1119 } 1120 1121 /** Start the activity specified by the {@link TargetInfo}.*/ safelyStartActivity(TargetInfo cti)1122 public final void safelyStartActivity(TargetInfo cti) { 1123 // In case cloned apps are present, we would want to start those apps in cloned user 1124 // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle 1125 // identifies the correct user space in such cases. 1126 UserHandle activityUserHandle = cti.getResolveInfo().userHandle; 1127 safelyStartActivityAsUser(cti, activityUserHandle, null); 1128 } 1129 safelyStartActivityAsUser( TargetInfo cti, UserHandle user, @Nullable Bundle options)1130 protected final void safelyStartActivityAsUser( 1131 TargetInfo cti, UserHandle user, @Nullable Bundle options) { 1132 // We're dispatching intents that might be coming from legacy apps, so 1133 // don't kill ourselves. 1134 StrictMode.disableDeathOnFileUriExposure(); 1135 try { 1136 safelyStartActivityInternal(cti, user, options); 1137 } finally { 1138 StrictMode.enableDeathOnFileUriExposure(); 1139 } 1140 } 1141 1142 @VisibleForTesting safelyStartActivityInternal( TargetInfo cti, UserHandle user, @Nullable Bundle options)1143 protected void safelyStartActivityInternal( 1144 TargetInfo cti, UserHandle user, @Nullable Bundle options) { 1145 // If the target is suspended, the activity will not be successfully launched. 1146 // Do not unregister from package manager updates in this case 1147 if (!cti.isSuspended() && mRegistered) { 1148 if (mPersonalPackageMonitor != null) { 1149 mPersonalPackageMonitor.unregister(); 1150 } 1151 if (mWorkPackageMonitor != null) { 1152 mWorkPackageMonitor.unregister(); 1153 } 1154 mRegistered = false; 1155 } 1156 // If needed, show that intent is forwarded 1157 // from managed profile to owner or other way around. 1158 String profileSwitchMessage = mIntentForwarding.forwardMessageFor( 1159 mRequest.getTargetIntent()); 1160 if (profileSwitchMessage != null) { 1161 Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show(); 1162 } 1163 try { 1164 if (cti.startAsCaller(this, options, user.getIdentifier())) { 1165 // Prevent sending a second chooser result when starting the edit action intent. 1166 if (!cti.getTargetIntent().hasExtra(EDIT_SOURCE)) { 1167 maybeSendShareResult(cti, user); 1168 } 1169 maybeLogCrossProfileTargetLaunch(cti, user); 1170 } 1171 } catch (RuntimeException e) { 1172 Slog.wtf(TAG, 1173 "Unable to launch as uid " + mActivityModel.getLaunchedFromUid() 1174 + " package " + mActivityModel.getLaunchedFromPackage() 1175 + ", while running in " + ActivityThread.currentProcessName(), e); 1176 } 1177 } 1178 maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle)1179 private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) { 1180 if (!mProfiles.getWorkProfilePresent() || currentUserHandle.equals(getUser())) { 1181 return; 1182 } 1183 DevicePolicyEventLogger 1184 .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED) 1185 .setBoolean(currentUserHandle.equals(mProfiles.getPersonalHandle())) 1186 .setStrings(getMetricsCategory(), 1187 cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target") 1188 .write(); 1189 } 1190 getLatencyTracker()1191 private LatencyTracker getLatencyTracker() { 1192 return LatencyTracker.getInstance(this); 1193 } 1194 1195 /** 1196 * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets 1197 * called and we are launched in a new task. 1198 */ setRetainInOnStop(boolean retainInOnStop)1199 protected final void setRetainInOnStop(boolean retainInOnStop) { 1200 mRetainInOnStop = retainInOnStop; 1201 } 1202 1203 // @NonFinalForTesting 1204 @VisibleForTesting createCrossProfileIntentsChecker()1205 protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { 1206 return new CrossProfileIntentsChecker(getContentResolver()); 1207 } 1208 createEmptyStateProvider( ProfileHelper profileHelper, ProfileAvailability profileAvailability)1209 protected final EmptyStateProvider createEmptyStateProvider( 1210 ProfileHelper profileHelper, 1211 ProfileAvailability profileAvailability) { 1212 EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider(); 1213 1214 EmptyStateProvider workProfileOffEmptyStateProvider = 1215 new WorkProfilePausedEmptyStateProvider( 1216 this, 1217 profileHelper, 1218 profileAvailability, 1219 /* onSwitchOnWorkSelectedListener = */ 1220 () -> { 1221 if (mOnSwitchOnWorkSelectedListener != null) { 1222 mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); 1223 } 1224 }, 1225 getMetricsCategory()); 1226 1227 EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider( 1228 mProfiles, 1229 mProfileAvailability, 1230 getMetricsCategory(), 1231 mProfilePagerResources 1232 ); 1233 1234 // Return composite provider, the order matters (the higher, the more priority) 1235 return new CompositeEmptyStateProvider( 1236 blockerEmptyStateProvider, 1237 workProfileOffEmptyStateProvider, 1238 noAppsEmptyStateProvider 1239 ); 1240 } 1241 1242 /** 1243 * Returns the {@link List} of {@link UserHandle} to pass on to the 1244 * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}. 1245 */ getResolverRankerServiceUserHandleList(UserHandle userHandle)1246 private List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) { 1247 return getResolverRankerServiceUserHandleListInternal(userHandle); 1248 } 1249 getResolverRankerServiceUserHandleListInternal(UserHandle userHandle)1250 private List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle userHandle) { 1251 List<UserHandle> userList = new ArrayList<>(); 1252 userList.add(userHandle); 1253 // Add clonedProfileUserHandle to the list only if we are: 1254 // a. Building the Personal Tab. 1255 // b. CloneProfile exists on the device. 1256 if (userHandle.equals(mProfiles.getPersonalHandle()) 1257 && mProfiles.getCloneUserPresent()) { 1258 userList.add(mProfiles.getCloneHandle()); 1259 } 1260 return userList; 1261 } 1262 1263 /** 1264 * Start activity as a fixed user handle. 1265 * @param cti TargetInfo to be launched. 1266 * @param user User to launch this activity as. 1267 */ 1268 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) safelyStartActivityAsUser(TargetInfo cti, UserHandle user)1269 public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) { 1270 safelyStartActivityAsUser(cti, user, null); 1271 } 1272 1273 @Override // ResolverListCommunicator onHandlePackagesChanged(ResolverListAdapter listAdapter)1274 public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) { 1275 mChooserMultiProfilePagerAdapter.onHandlePackagesChanged( 1276 (ChooserListAdapter) listAdapter, 1277 mProfileAvailability.getWaitingToEnableProfile()); 1278 } 1279 optionForChooserTarget(TargetInfo target, int index)1280 final Option optionForChooserTarget(TargetInfo target, int index) { 1281 return new Option(getOrLoadDisplayLabel(target), index); 1282 } 1283 1284 @Override // ResolverListCommunicator sendVoiceChoicesIfNeeded()1285 public final void sendVoiceChoicesIfNeeded() { 1286 if (!isVoiceInteraction()) { 1287 // Clearly not needed. 1288 return; 1289 } 1290 1291 int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getCount(); 1292 final Option[] options = new Option[count]; 1293 for (int i = 0; i < options.length; i++) { 1294 TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getItem(i); 1295 if (target == null) { 1296 // If this occurs, a new set of targets is being loaded. Let that complete, 1297 // and have the next call to send voice choices proceed instead. 1298 return; 1299 } 1300 options[i] = optionForChooserTarget(target, i); 1301 } 1302 1303 mPickOptionRequest = new ResolverActivity.PickTargetOptionRequest( 1304 new VoiceInteractor.Prompt(getTitle()), options, null); 1305 getVoiceInteractor().submitRequest(mPickOptionRequest); 1306 } 1307 1308 /** 1309 * Sets up the content view. 1310 * @return <code>true</code> if the activity is finishing and creation should halt. 1311 */ configureContentView(TargetDataLoader targetDataLoader)1312 private boolean configureContentView(TargetDataLoader targetDataLoader) { 1313 if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null) { 1314 throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() " 1315 + "cannot be null."); 1316 } 1317 Trace.beginSection("configureContentView"); 1318 // We partially rebuild the inactive adapter to determine if we should auto launch 1319 // isTabLoaded will be true here if the empty state screen is shown instead of the list. 1320 boolean rebuildCompleted = mChooserMultiProfilePagerAdapter.rebuildTabs( 1321 mProfiles.getWorkProfilePresent()); 1322 1323 mLayoutId = R.layout.chooser_grid_scrollable_preview; 1324 1325 setContentView(mLayoutId); 1326 mTabHost = findViewById(com.android.internal.R.id.profile_tabhost); 1327 mViewPager = requireViewById(com.android.internal.R.id.profile_pager); 1328 mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager); 1329 ChooserNestedScrollView scrollableContainer = 1330 requireViewById(R.id.chooser_scrollable_container); 1331 if (keyboardNavigationFix()) { 1332 scrollableContainer.setRequestChildFocusPredicate((child, focused) -> 1333 // TabHost view will request focus on the newly activated tab. The RecyclerView 1334 // from the tab gets focused and notifies its parents (including 1335 // NestedScrollView) about it through #requestChildFocus method call. 1336 // NestedScrollView's view implementation of the method will scroll to the 1337 // focused view. As we don't want to change drawer's position upon tab change, 1338 // ignore focus requests from tab RecyclerViews. 1339 focused == null || focused.getId() != com.android.internal.R.id.resolver_list); 1340 } 1341 boolean result = postRebuildList(rebuildCompleted); 1342 Trace.endSection(); 1343 return result; 1344 } 1345 1346 /** 1347 * Finishing procedures to be performed after the list has been rebuilt. 1348 * </p>Subclasses must call postRebuildListInternal at the end of postRebuildList. 1349 * @param rebuildCompleted 1350 * @return <code>true</code> if the activity is finishing and creation should halt. 1351 */ postRebuildList(boolean rebuildCompleted)1352 protected boolean postRebuildList(boolean rebuildCompleted) { 1353 return postRebuildListInternal(rebuildCompleted); 1354 } 1355 1356 /** 1357 * Add a label to signify that the user can pick a different app. 1358 * @param adapter The adapter used to provide data to item views. 1359 */ addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter)1360 public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) { 1361 final boolean useHeader = adapter.hasFilteredItem(); 1362 if (useHeader) { 1363 FrameLayout stub = findViewById(com.android.internal.R.id.stub); 1364 stub.setVisibility(View.VISIBLE); 1365 TextView textView = (TextView) LayoutInflater.from(this).inflate( 1366 R.layout.resolver_different_item_header, null, false); 1367 if (mProfiles.getWorkProfilePresent()) { 1368 textView.setGravity(Gravity.CENTER); 1369 } 1370 stub.addView(textView); 1371 } 1372 } setupViewVisibilities()1373 private void setupViewVisibilities() { 1374 ChooserListAdapter activeListAdapter = 1375 mChooserMultiProfilePagerAdapter.getActiveListAdapter(); 1376 if (!mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) { 1377 addUseDifferentAppLabelIfNecessary(activeListAdapter); 1378 } 1379 } 1380 /** 1381 * Finishing procedures to be performed after the list has been rebuilt. 1382 * @param rebuildCompleted 1383 * @return <code>true</code> if the activity is finishing and creation should halt. 1384 */ postRebuildListInternal(boolean rebuildCompleted)1385 final boolean postRebuildListInternal(boolean rebuildCompleted) { 1386 int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); 1387 1388 // We only rebuild asynchronously when we have multiple elements to sort. In the case where 1389 // we're already done, we can check if we should auto-launch immediately. 1390 if (rebuildCompleted && maybeAutolaunchActivity()) { 1391 return true; 1392 } 1393 1394 setupViewVisibilities(); 1395 1396 if (mProfiles.getWorkProfilePresent() 1397 || (mProfiles.getPrivateProfilePresent() 1398 && mProfileAvailability.isAvailable( 1399 requireNonNull(mProfiles.getPrivateProfile())))) { 1400 setupProfileTabs(); 1401 } 1402 1403 return false; 1404 } 1405 setupProfileTabs()1406 private void setupProfileTabs() { 1407 mChooserMultiProfilePagerAdapter.setupProfileTabs( 1408 getLayoutInflater(), 1409 mTabHost, 1410 mViewPager, 1411 R.layout.resolver_profile_tab_button, 1412 com.android.internal.R.id.profile_pager, 1413 () -> onProfileTabSelected(mViewPager.getCurrentItem()), 1414 new OnProfileSelectedListener() { 1415 @Override 1416 public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {} 1417 1418 @Override 1419 public void onProfilePageStateChanged(int state) { 1420 onHorizontalSwipeStateChanged(state); 1421 } 1422 }); 1423 mOnSwitchOnWorkSelectedListener = () -> { 1424 View workTab = mTabHost.getTabWidget().getChildAt( 1425 mChooserMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK)); 1426 workTab.setFocusable(true); 1427 workTab.setFocusableInTouchMode(true); 1428 workTab.requestFocus(); 1429 }; 1430 } 1431 1432 ////////////////////////////////////////////////////////////////////////////////////////////// 1433 ////////////////////////////////////////////////////////////////////////////////////////////// 1434 createProfileRecords( AppPredictorFactory factory, IntentFilter targetIntentFilter)1435 private void createProfileRecords( 1436 AppPredictorFactory factory, IntentFilter targetIntentFilter) { 1437 1438 Profile launchedAsProfile = mProfiles.getLaunchedAsProfile(); 1439 for (Profile profile : mProfiles.getProfiles()) { 1440 if (profile.getType() == Profile.Type.PRIVATE 1441 && !mProfileAvailability.isAvailable(profile)) { 1442 continue; 1443 } 1444 ProfileRecord record = createProfileRecord( 1445 profile, 1446 targetIntentFilter, 1447 launchedAsProfile.equals(profile) 1448 ? mRequest.getCallerChooserTargets() 1449 : Collections.emptyList(), 1450 factory); 1451 if (profile.equals(launchedAsProfile) && record.shortcutLoader == null) { 1452 Tracer.INSTANCE.endLaunchToShortcutTrace(); 1453 } 1454 } 1455 } 1456 createProfileRecord( Profile profile, IntentFilter targetIntentFilter, List<ChooserTarget> callerTargets, AppPredictorFactory factory)1457 private ProfileRecord createProfileRecord( 1458 Profile profile, 1459 IntentFilter targetIntentFilter, 1460 List<ChooserTarget> callerTargets, 1461 AppPredictorFactory factory) { 1462 UserHandle userHandle = profile.getPrimary().getHandle(); 1463 AppPredictor appPredictor = factory.create(userHandle); 1464 ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() 1465 ? null 1466 : createShortcutLoader( 1467 this, 1468 appPredictor, 1469 userHandle, 1470 targetIntentFilter, 1471 shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult)); 1472 ProfileRecord record = new ProfileRecord( 1473 profile, appPredictor, shortcutLoader, callerTargets); 1474 mProfileRecords.put(userHandle.getIdentifier(), record); 1475 return record; 1476 } 1477 1478 @Nullable getProfileRecord(UserHandle userHandle)1479 private ProfileRecord getProfileRecord(UserHandle userHandle) { 1480 return mProfileRecords.get(userHandle.getIdentifier()); 1481 } 1482 1483 @VisibleForTesting createShortcutLoader( Context context, AppPredictor appPredictor, UserHandle userHandle, IntentFilter targetIntentFilter, Consumer<ShortcutLoader.Result> callback)1484 protected ShortcutLoader createShortcutLoader( 1485 Context context, 1486 AppPredictor appPredictor, 1487 UserHandle userHandle, 1488 IntentFilter targetIntentFilter, 1489 Consumer<ShortcutLoader.Result> callback) { 1490 return new ShortcutLoader( 1491 context, 1492 getCoroutineScope(getLifecycle()), 1493 appPredictor, 1494 userHandle, 1495 targetIntentFilter, 1496 callback); 1497 } 1498 getPinnedSharedPrefs(Context context)1499 static SharedPreferences getPinnedSharedPrefs(Context context) { 1500 return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE); 1501 } 1502 createMultiProfilePagerAdapter( Context context, ProfilePagerResources profilePagerResources, ChooserRequest request, ProfileHelper profileHelper, Collection<ProfileRecord> profileRecords, ProfileAvailability profileAvailability, List<Intent> initialIntents, int maxTargetsPerRow)1503 private ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( 1504 Context context, 1505 ProfilePagerResources profilePagerResources, 1506 ChooserRequest request, 1507 ProfileHelper profileHelper, 1508 Collection<ProfileRecord> profileRecords, 1509 ProfileAvailability profileAvailability, 1510 List<Intent> initialIntents, 1511 int maxTargetsPerRow) { 1512 Log.d(TAG, "createMultiProfilePagerAdapter"); 1513 1514 Profile launchedAs = profileHelper.getLaunchedAsProfile(); 1515 1516 Intent[] initialIntentArray = initialIntents.toArray(new Intent[0]); 1517 List<Intent> payloadIntents = request.getPayloadIntents(); 1518 1519 List<TabConfig<ChooserGridAdapter>> tabs = new ArrayList<>(); 1520 for (ProfileRecord record : profileRecords) { 1521 Profile profile = record.profile; 1522 ChooserGridAdapter adapter = createChooserGridAdapter( 1523 context, 1524 payloadIntents, 1525 profile.equals(launchedAs) ? initialIntentArray : null, 1526 profile.getPrimary().getHandle() 1527 ); 1528 tabs.add(new TabConfig<>( 1529 /* profile = */ profile.getType().ordinal(), 1530 profilePagerResources.profileTabLabel(profile.getType()), 1531 profilePagerResources.profileTabAccessibilityLabel(profile.getType()), 1532 /* tabTag = */ profile.getType().name(), 1533 adapter)); 1534 } 1535 1536 EmptyStateProvider emptyStateProvider = 1537 createEmptyStateProvider(profileHelper, profileAvailability); 1538 1539 Supplier<Boolean> workProfileQuietModeChecker = 1540 () -> !(profileHelper.getWorkProfilePresent() 1541 && profileAvailability.isAvailable( 1542 requireNonNull(profileHelper.getWorkProfile()))); 1543 1544 return new ChooserMultiProfilePagerAdapter( 1545 /* context */ this, 1546 ImmutableList.copyOf(tabs), 1547 emptyStateProvider, 1548 workProfileQuietModeChecker, 1549 launchedAs.getType().ordinal(), 1550 profileHelper.getWorkHandle(), 1551 profileHelper.getCloneHandle(), 1552 maxTargetsPerRow); 1553 } 1554 createBlockerEmptyStateProvider()1555 protected EmptyStateProvider createBlockerEmptyStateProvider() { 1556 return new NoCrossProfileEmptyStateProvider( 1557 mProfiles, 1558 mDevicePolicyResources, 1559 createCrossProfileIntentsChecker(), 1560 mRequest.isSendActionTarget()); 1561 } 1562 findSelectedProfile()1563 private int findSelectedProfile() { 1564 return mProfiles.getLaunchedAsProfileType().ordinal(); 1565 } 1566 1567 /** 1568 * Check if the profile currently used is a work profile. 1569 * @return true if it is work profile, false if it is parent profile (or no work profile is 1570 * set up) 1571 */ isWorkProfile()1572 private boolean isWorkProfile() { 1573 return mProfiles.getLaunchedAsProfileType() == Profile.Type.WORK; 1574 } 1575 1576 //@Override createPackageMonitor(ResolverListAdapter listAdapter)1577 protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { 1578 return new PackageMonitor() { 1579 @Override 1580 public void onSomePackagesChanged() { 1581 handlePackagesChanged(listAdapter); 1582 } 1583 }; 1584 } 1585 1586 /** 1587 * Update UI to reflect changes in data. 1588 */ 1589 @Override 1590 public void handlePackagesChanged() { 1591 handlePackagesChanged(/* listAdapter */ null); 1592 } 1593 1594 /** 1595 * Update UI to reflect changes in data. 1596 * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if 1597 * available. 1598 */ 1599 private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) { 1600 // Refresh pinned items 1601 mPinnedSharedPrefs = getPinnedSharedPrefs(this); 1602 if (rebuildAdaptersOnTargetPinning()) { 1603 recreatePagerAdapter(); 1604 } else { 1605 if (listAdapter == null) { 1606 mChooserMultiProfilePagerAdapter.refreshPackagesInAllTabs(); 1607 } else { 1608 listAdapter.handlePackagesChanged(); 1609 } 1610 } 1611 } 1612 1613 @Override 1614 public void onConfigurationChanged(Configuration newConfig) { 1615 super.onConfigurationChanged(newConfig); 1616 mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); 1617 1618 if (mSystemWindowInsets != null) { 1619 int topSpacing = isInteractiveSession() ? getInteractiveSessionTopSpacing() : 0; 1620 mResolverDrawerLayout.setPadding( 1621 mSystemWindowInsets.left, 1622 mSystemWindowInsets.top + topSpacing, 1623 mSystemWindowInsets.right, 1624 0); 1625 } 1626 if (mViewPager.isLayoutRtl()) { 1627 mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager); 1628 } 1629 1630 mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation); 1631 mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); 1632 mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow); 1633 adjustMaxPreviewWidth(); 1634 adjustPreviewWidth(newConfig.orientation, null); 1635 updateStickyContentPreview(); 1636 updateTabPadding(); 1637 } 1638 1639 private boolean shouldDisplayLandscape(int orientation) { 1640 // Sharesheet fixes the # of items per row and therefore can not correctly lay out 1641 // when in the restricted size of multi-window mode. In the future, would be nice 1642 // to use minimum dp size requirements instead 1643 return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode(); 1644 } 1645 1646 private void adjustMaxPreviewWidth() { 1647 if (mResolverDrawerLayout == null) { 1648 return; 1649 } 1650 mResolverDrawerLayout.setMaxWidth( 1651 getResources().getDimensionPixelSize(R.dimen.chooser_width)); 1652 } 1653 1654 private void adjustPreviewWidth(int orientation, View parent) { 1655 int width = -1; 1656 if (mShouldDisplayLandscape) { 1657 width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width); 1658 } 1659 1660 parent = parent == null ? getWindow().getDecorView() : parent; 1661 1662 updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent); 1663 } 1664 1665 private void updateTabPadding() { 1666 if (mProfiles.getWorkProfilePresent()) { 1667 View tabs = findViewById(com.android.internal.R.id.tabs); 1668 float iconSize = getResources().getDimension(R.dimen.chooser_icon_size); 1669 // The entire width consists of icons or padding. Divide the item padding in half to get 1670 // paddingHorizontal. 1671 float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize) 1672 / mMaxTargetsPerRow / 2; 1673 // Subtract the margin the buttons already have. 1674 padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin); 1675 tabs.setPadding((int) padding, 0, (int) padding, 0); 1676 } 1677 } 1678 1679 private void updateLayoutWidth(int layoutResourceId, int width, View parent) { 1680 View view = parent.findViewById(layoutResourceId); 1681 if (view != null && view.getLayoutParams() != null) { 1682 LayoutParams params = view.getLayoutParams(); 1683 params.width = width; 1684 view.setLayoutParams(params); 1685 } 1686 } 1687 1688 /** 1689 * Create a view that will be shown in the content preview area 1690 * @param parent reference to the parent container where the view should be attached to 1691 * @return content preview view 1692 */ 1693 protected ViewGroup createContentPreviewView(ViewGroup parent) { 1694 ViewGroup layout = mChooserContentPreviewUi.displayContentPreview( 1695 getResources(), 1696 getLayoutInflater(), 1697 parent, 1698 requireViewById(R.id.chooser_headline_row_container)); 1699 1700 if (layout != null) { 1701 adjustPreviewWidth(getResources().getConfiguration().orientation, layout); 1702 } 1703 1704 return layout; 1705 } 1706 1707 @Nullable 1708 private View getFirstVisibleImgPreviewView() { 1709 View imagePreview = findViewById(R.id.scrollable_image_preview); 1710 return imagePreview instanceof ImagePreviewView 1711 ? ((ImagePreviewView) imagePreview).getTransitionView() 1712 : null; 1713 } 1714 1715 /** 1716 * Wrapping the ContentResolver call to expose for easier mocking, 1717 * and to avoid mocking Android core classes. 1718 */ 1719 @VisibleForTesting 1720 public Cursor queryResolver(ContentResolver resolver, Uri uri) { 1721 return resolver.query(uri, null, null, null, null); 1722 } 1723 1724 private void destroyProfileRecords() { 1725 mProfileRecords.values().forEach(ProfileRecord::destroy); 1726 mProfileRecords.clear(); 1727 } 1728 1729 @Override // ResolverListCommunicator 1730 public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { 1731 Intent result = defIntent; 1732 if (mRequest.getReplacementExtras() != null) { 1733 final Bundle replExtras = 1734 mRequest.getReplacementExtras().getBundle(aInfo.packageName); 1735 if (replExtras != null) { 1736 result = new Intent(defIntent); 1737 result.putExtras(replExtras); 1738 } 1739 } 1740 if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT) 1741 || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) { 1742 result = Intent.createChooser(result, 1743 getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE)); 1744 1745 // Don't auto-launch single intents if the intent is being forwarded. This is done 1746 // because automatically launching a resolving application as a response to the user 1747 // action of switching accounts is pretty unexpected. 1748 result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); 1749 } 1750 return result; 1751 } 1752 1753 private void maybeSendShareResult(TargetInfo cti, UserHandle launchedAsUser) { 1754 if (mShareResultSender != null) { 1755 final ComponentName target = cti.getResolvedComponentName(); 1756 if (target != null) { 1757 boolean crossProfile = !UserHandle.of(UserHandle.myUserId()).equals(launchedAsUser); 1758 mShareResultSender.onComponentSelected( 1759 target, cti.isChooserTargetInfo(), crossProfile); 1760 } 1761 } 1762 } 1763 1764 private void addCallerChooserTargets(ChooserListAdapter adapter) { 1765 ProfileRecord record = getProfileRecord(adapter.getUserHandle()); 1766 List<ChooserTarget> callerTargets = record == null 1767 ? Collections.emptyList() 1768 : record.callerTargets; 1769 if (!callerTargets.isEmpty()) { 1770 adapter.addServiceResults( 1771 /* origTarget */ null, 1772 new ArrayList<>(mRequest.getCallerChooserTargets()), 1773 TARGET_TYPE_DEFAULT, 1774 /* directShareShortcutInfoCache */ Collections.emptyMap(), 1775 /* directShareAppTargetCache */ Collections.emptyMap()); 1776 } 1777 } 1778 1779 @Override // ResolverListCommunicator 1780 public boolean shouldGetActivityMetadata() { 1781 return true; 1782 } 1783 1784 public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { 1785 if (target.isSuspended()) { 1786 return false; 1787 } 1788 1789 // TODO: migrate to ChooserRequest 1790 return mViewModel.getActivityModel().getIntent() 1791 .getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); 1792 } 1793 1794 private void showTargetDetails(TargetInfo targetInfo) { 1795 if (targetInfo == null) return; 1796 1797 List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets(); 1798 if (targetList.isEmpty()) { 1799 Log.e(TAG, "No displayable data to show target details"); 1800 return; 1801 } 1802 1803 // TODO: implement these type-conditioned behaviors polymorphically, and consider moving 1804 // the logic into `ChooserTargetActionsDialogFragment.show()`. 1805 boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned(); 1806 IntentFilter intentFilter; 1807 intentFilter = targetInfo.isSelectableTargetInfo() 1808 ? mRequest.getShareTargetFilter() : null; 1809 String shortcutTitle = targetInfo.isSelectableTargetInfo() 1810 ? targetInfo.getDisplayLabel().toString() : null; 1811 String shortcutIdKey = targetInfo.getDirectShareShortcutId(); 1812 1813 ChooserTargetActionsDialogFragment.show( 1814 getSupportFragmentManager(), 1815 targetList, 1816 // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be 1817 // resolved correctly within the same tab. 1818 targetInfo.getResolveInfo().userHandle, 1819 shortcutIdKey, 1820 shortcutTitle, 1821 isShortcutPinned, 1822 intentFilter); 1823 } 1824 1825 protected boolean onTargetSelected(TargetInfo target) { 1826 if (mRefinementManager.maybeHandleSelection( 1827 target, 1828 mRequest.getRefinementIntentSender(), 1829 getApplication(), 1830 getMainThreadHandler())) { 1831 return false; 1832 } 1833 updateModelAndChooserCounts(target); 1834 maybeRemoveSharedText(target); 1835 safelyStartActivity(target); 1836 1837 // Rely on the ActivityManager to pop up a dialog regarding app suspension 1838 // and return false 1839 return !target.isSuspended(); 1840 } 1841 1842 @Override 1843 public void startSelected(int which, /* unused */ boolean always, boolean filtered) { 1844 ChooserListAdapter currentListAdapter = 1845 mChooserMultiProfilePagerAdapter.getActiveListAdapter(); 1846 TargetInfo targetInfo = currentListAdapter 1847 .targetInfoForPosition(which, filtered); 1848 if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) { 1849 return; 1850 } 1851 1852 final long selectionCost = System.currentTimeMillis() - mChooserShownTime; 1853 1854 if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) { 1855 MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; 1856 if (!mti.hasSelected()) { 1857 // Add userHandle based badge to the stackedAppDialogBox. 1858 ChooserStackedAppDialogFragment.show( 1859 getSupportFragmentManager(), 1860 mti, 1861 which, 1862 targetInfo.getResolveInfo().userHandle); 1863 return; 1864 } 1865 } 1866 if (isFinishing()) { 1867 return; 1868 } 1869 1870 TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter() 1871 .targetInfoForPosition(which, filtered); 1872 if (target != null) { 1873 if (onTargetSelected(target)) { 1874 MetricsLogger.action( 1875 this, MetricsEvent.ACTION_APP_DISAMBIG_TAP); 1876 MetricsLogger.action(this, 1877 mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() 1878 ? MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED 1879 : MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED); 1880 Log.d(TAG, "onTargetSelected() returned true, finishing! " + target); 1881 finish(); 1882 } 1883 } 1884 1885 // TODO: both of the conditions around this switch logic *should* be redundant, and 1886 // can be removed if certain invariants can be guaranteed. In particular, it seems 1887 // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably* 1888 // expected to be null only at out-of-bounds indexes where `getPositionTargetType()` 1889 // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't 1890 // need to null-check targetInfo. We only need the null check if it's possible that 1891 // the ChooserListAdapter contains null elements "in the middle" of its list data, 1892 // such that they're classified as belonging to one of the real target types. That 1893 // should probably never happen. But why would this method ever be invoked with a 1894 // null target at all? Even an out-of-bounds index should never be "selected"... 1895 if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) { 1896 switch (currentListAdapter.getPositionTargetType(which)) { 1897 case ChooserListAdapter.TARGET_SERVICE: 1898 getEventLog().logShareTargetSelected( 1899 EventLog.SELECTION_TYPE_SERVICE, 1900 targetInfo.getResolveInfo().activityInfo.processName, 1901 which, 1902 /* directTargetAlsoRanked= */ getRankedPosition(targetInfo), 1903 mRequest.getCallerChooserTargets().size(), 1904 targetInfo.getHashedTargetIdForMetrics(this), 1905 targetInfo.isPinned(), 1906 mIsSuccessfullySelected, 1907 selectionCost 1908 ); 1909 return; 1910 case ChooserListAdapter.TARGET_CALLER: 1911 case ChooserListAdapter.TARGET_STANDARD: 1912 getEventLog().logShareTargetSelected( 1913 EventLog.SELECTION_TYPE_APP, 1914 targetInfo.getResolveInfo().activityInfo.processName, 1915 (which - currentListAdapter.getSurfacedTargetInfo().size()), 1916 /* directTargetAlsoRanked= */ -1, 1917 currentListAdapter.getCallerTargetCount(), 1918 /* directTargetHashed= */ null, 1919 targetInfo.isPinned(), 1920 mIsSuccessfullySelected, 1921 selectionCost 1922 ); 1923 return; 1924 case ChooserListAdapter.TARGET_STANDARD_AZ: 1925 // A-Z targets are unranked standard targets; we use a value of -1 to mark that 1926 // they are from the alphabetical pool. 1927 // TODO: why do we log a different selection type if the -1 value already 1928 // designates the same condition? 1929 getEventLog().logShareTargetSelected( 1930 EventLog.SELECTION_TYPE_STANDARD, 1931 targetInfo.getResolveInfo().activityInfo.processName, 1932 /* value= */ -1, 1933 /* directTargetAlsoRanked= */ -1, 1934 /* numCallerProvided= */ 0, 1935 /* directTargetHashed= */ null, 1936 /* isPinned= */ false, 1937 mIsSuccessfullySelected, 1938 selectionCost 1939 ); 1940 } 1941 } 1942 } 1943 1944 private int getRankedPosition(TargetInfo targetInfo) { 1945 String targetPackageName = 1946 targetInfo.getChooserTargetComponentName().getPackageName(); 1947 ChooserListAdapter currentListAdapter = 1948 mChooserMultiProfilePagerAdapter.getActiveListAdapter(); 1949 int maxRankedResults = Math.min( 1950 currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION); 1951 1952 for (int i = 0; i < maxRankedResults; i++) { 1953 if (currentListAdapter.getDisplayResolveInfo(i) 1954 .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) { 1955 return i; 1956 } 1957 } 1958 return -1; 1959 } 1960 1961 protected void applyFooterView(int height) { 1962 mChooserMultiProfilePagerAdapter.setFooterHeightInEveryAdapter(height); 1963 } 1964 1965 private void logDirectShareTargetReceived(UserHandle forUser) { 1966 ProfileRecord profileRecord = getProfileRecord(forUser); 1967 if (profileRecord == null) { 1968 return; 1969 } 1970 getEventLog().logDirectShareTargetReceived( 1971 MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER, 1972 (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime)); 1973 } 1974 1975 void updateModelAndChooserCounts(TargetInfo info) { 1976 if (info != null && info.isMultiDisplayResolveInfo()) { 1977 info = ((MultiDisplayResolveInfo) info).getSelectedTarget(); 1978 } 1979 if (info != null) { 1980 sendClickToAppPredictor(info); 1981 final ResolveInfo ri = info.getResolveInfo(); 1982 Intent targetIntent = mRequest.getTargetIntent(); 1983 if (ri != null && ri.activityInfo != null && targetIntent != null) { 1984 ChooserListAdapter currentListAdapter = 1985 mChooserMultiProfilePagerAdapter.getActiveListAdapter(); 1986 if (currentListAdapter != null) { 1987 sendImpressionToAppPredictor(info, currentListAdapter); 1988 currentListAdapter.updateModel(info); 1989 currentListAdapter.updateChooserCounts( 1990 ri.activityInfo.packageName, 1991 targetIntent.getAction(), 1992 ri.userHandle); 1993 } 1994 if (DEBUG) { 1995 Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName); 1996 Log.d(TAG, "Action to be updated is " + targetIntent.getAction()); 1997 } 1998 } else if (DEBUG) { 1999 Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo"); 2000 } 2001 } 2002 mIsSuccessfullySelected = true; 2003 } 2004 2005 private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) { 2006 Intent targetIntent = targetInfo.getTargetIntent(); 2007 if (targetIntent == null) { 2008 return; 2009 } 2010 Intent originalTargetIntent = new Intent(mRequest.getTargetIntent()); 2011 // Our TargetInfo implementations add associated component to the intent, let's do the same 2012 // for the sake of the comparison below. 2013 if (targetIntent.getComponent() != null) { 2014 originalTargetIntent.setComponent(targetIntent.getComponent()); 2015 } 2016 // Use filterEquals as a way to check that the primary intent is in use (and not an 2017 // alternative one). For example, an app is sharing an image and a link with mime type 2018 // "image/png" and provides an alternative intent to share only the link with mime type 2019 // "text/uri". Should there be a target that accepts only the latter, the alternative intent 2020 // will be used and we don't want to exclude the link from it. 2021 if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) { 2022 targetIntent.removeExtra(Intent.EXTRA_TEXT); 2023 } 2024 } 2025 2026 private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) { 2027 // Send DS target impression info to AppPredictor, only when user chooses app share. 2028 if (targetInfo.isChooserTargetInfo()) { 2029 return; 2030 } 2031 2032 AppPredictor directShareAppPredictor = getAppPredictor( 2033 mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); 2034 if (directShareAppPredictor == null) { 2035 return; 2036 } 2037 List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo(); 2038 List<AppTargetId> targetIds = new ArrayList<>(); 2039 for (TargetInfo chooserTargetInfo : surfacedTargetInfo) { 2040 ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo(); 2041 if (shortcutInfo != null) { 2042 ComponentName componentName = 2043 chooserTargetInfo.getChooserTargetComponentName(); 2044 targetIds.add(new AppTargetId( 2045 String.format( 2046 "%s/%s/%s", 2047 shortcutInfo.getId(), 2048 componentName.flattenToString(), 2049 SHORTCUT_TARGET))); 2050 } 2051 } 2052 directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds); 2053 } 2054 2055 private void sendClickToAppPredictor(TargetInfo targetInfo) { 2056 if (!targetInfo.isChooserTargetInfo()) { 2057 return; 2058 } 2059 2060 AppPredictor directShareAppPredictor = getAppPredictor( 2061 mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); 2062 if (directShareAppPredictor == null) { 2063 return; 2064 } 2065 AppTarget appTarget = targetInfo.getDirectShareAppTarget(); 2066 if (appTarget != null) { 2067 // This is a direct share click that was provided by the APS 2068 directShareAppPredictor.notifyAppTargetEvent( 2069 new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) 2070 .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE) 2071 .build()); 2072 } 2073 } 2074 2075 @Nullable 2076 private AppPredictor getAppPredictor(UserHandle userHandle) { 2077 ProfileRecord record = getProfileRecord(userHandle); 2078 // We cannot use APS service when clone profile is present as APS service cannot sort 2079 // cross profile targets as of now. 2080 return ((record == null) || (mProfiles.getCloneUserPresent())) 2081 ? null : record.appPredictor; 2082 } 2083 2084 protected EventLog getEventLog() { 2085 return mEventLog; 2086 } 2087 2088 private ChooserGridAdapter createChooserGridAdapter( 2089 Context context, 2090 List<Intent> payloadIntents, 2091 Intent[] initialIntents, 2092 UserHandle userHandle) { 2093 ChooserListAdapter chooserListAdapter = createChooserListAdapter( 2094 context, 2095 payloadIntents, 2096 initialIntents, 2097 /* TODO: not used, remove. rList= */ null, 2098 /* TODO: not used, remove. filterLastUsed= */ false, 2099 createListController(userHandle), 2100 userHandle, 2101 mRequest.getTargetIntent(), 2102 mRequest.getReferrerFillInIntent(), 2103 mMaxTargetsPerRow 2104 ); 2105 2106 return new ChooserGridAdapter( 2107 context, 2108 new ChooserGridAdapter.ChooserActivityDelegate() { 2109 @Override 2110 public void onTargetSelected(int itemIndex) { 2111 startSelected(itemIndex, false, true); 2112 } 2113 2114 @Override 2115 public void onTargetLongPressed(int selectedPosition) { 2116 final TargetInfo longPressedTargetInfo = 2117 mChooserMultiProfilePagerAdapter 2118 .getActiveListAdapter() 2119 .targetInfoForPosition( 2120 selectedPosition, /* filtered= */ true); 2121 // Only a direct share target or an app target is expected 2122 if (longPressedTargetInfo.isDisplayResolveInfo() 2123 || longPressedTargetInfo.isSelectableTargetInfo()) { 2124 showTargetDetails(longPressedTargetInfo); 2125 } 2126 } 2127 }, 2128 chooserListAdapter, 2129 shouldShowContentPreview(), 2130 mMaxTargetsPerRow); 2131 } 2132 2133 @VisibleForTesting 2134 public ChooserListAdapter createChooserListAdapter( 2135 Context context, 2136 List<Intent> payloadIntents, 2137 Intent[] initialIntents, 2138 List<ResolveInfo> rList, 2139 boolean filterLastUsed, 2140 ResolverListController resolverListController, 2141 UserHandle userHandle, 2142 Intent targetIntent, 2143 Intent referrerFillInIntent, 2144 int maxTargetsPerRow) { 2145 UserHandle initialIntentsUserSpace = mProfiles.getQueryIntentsHandle(userHandle); 2146 return new ChooserListAdapter( 2147 context, 2148 payloadIntents, 2149 initialIntents, 2150 rList, 2151 filterLastUsed, 2152 resolverListController, 2153 userHandle, 2154 targetIntent, 2155 referrerFillInIntent, 2156 this, 2157 mPackageManager, 2158 getEventLog(), 2159 maxTargetsPerRow, 2160 initialIntentsUserSpace, 2161 mTargetDataLoader, 2162 () -> { 2163 ProfileRecord record = getProfileRecord(userHandle); 2164 if (record != null && record.shortcutLoader != null) { 2165 record.shortcutLoader.reset(); 2166 } 2167 }); 2168 } 2169 2170 private void onWorkProfileStatusUpdated() { 2171 UserHandle workUser = mProfiles.getWorkHandle(); 2172 ProfileRecord record = workUser == null ? null : getProfileRecord(workUser); 2173 if (record != null && record.shortcutLoader != null) { 2174 record.shortcutLoader.reset(); 2175 } 2176 if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle().equals( 2177 mProfiles.getWorkHandle())) { 2178 mChooserMultiProfilePagerAdapter.rebuildActiveTab(true); 2179 } else { 2180 mChooserMultiProfilePagerAdapter.clearInactiveProfileCache(); 2181 } 2182 } 2183 2184 @VisibleForTesting 2185 protected ChooserListController createListController(UserHandle userHandle) { 2186 AppPredictor appPredictor = getAppPredictor(userHandle); 2187 AbstractResolverComparator resolverComparator; 2188 if (appPredictor != null) { 2189 resolverComparator = new AppPredictionServiceResolverComparator( 2190 this, 2191 mRequest.getTargetIntent(), 2192 mRequest.getLaunchedFromPackage(), 2193 appPredictor, 2194 userHandle, 2195 getEventLog(), 2196 mNearbyShare.orElse(null) 2197 ); 2198 } else { 2199 resolverComparator = 2200 new ResolverRankerServiceResolverComparator( 2201 this, 2202 mRequest.getTargetIntent(), 2203 mRequest.getReferrerPackage(), 2204 null, 2205 getEventLog(), 2206 getResolverRankerServiceUserHandleList(userHandle), 2207 mNearbyShare.orElse(null)); 2208 } 2209 2210 return new ChooserListController( 2211 this, 2212 mPackageManager, 2213 mRequest.getTargetIntent(), 2214 mRequest.getReferrerPackage(), 2215 mViewModel.getActivityModel().getLaunchedFromUid(), 2216 resolverComparator, 2217 mProfiles.getQueryIntentsHandle(userHandle), 2218 mRequest.getFilteredComponentNames(), 2219 mPinnedSharedPrefs); 2220 } 2221 2222 private ChooserContentPreviewUi.ActionFactory decorateActionFactoryWithRefinement( 2223 ChooserContentPreviewUi.ActionFactory originalFactory) { 2224 if (!refineSystemActions()) { 2225 return originalFactory; 2226 } 2227 2228 return new ChooserContentPreviewUi.ActionFactory() { 2229 @Override 2230 @Nullable 2231 public Runnable getEditButtonRunnable() { 2232 if (originalFactory.getEditButtonRunnable() == null) return null; 2233 return () -> { 2234 if (!mRefinementManager.maybeHandleSelection( 2235 RefinementType.EDIT_ACTION, 2236 List.of(mRequest.getTargetIntent()), 2237 null, 2238 mRequest.getRefinementIntentSender(), 2239 getApplication(), 2240 getMainThreadHandler())) { 2241 originalFactory.getEditButtonRunnable().run(); 2242 } 2243 }; 2244 } 2245 2246 @Override 2247 @Nullable 2248 public Runnable getCopyButtonRunnable() { 2249 if (originalFactory.getCopyButtonRunnable() == null) return null; 2250 return () -> { 2251 if (!mRefinementManager.maybeHandleSelection( 2252 RefinementType.COPY_ACTION, 2253 List.of(mRequest.getTargetIntent()), 2254 null, 2255 mRequest.getRefinementIntentSender(), 2256 getApplication(), 2257 getMainThreadHandler())) { 2258 originalFactory.getCopyButtonRunnable().run(); 2259 } 2260 }; 2261 } 2262 2263 @Override 2264 public List<ActionRow.Action> createCustomActions() { 2265 return originalFactory.createCustomActions(); 2266 } 2267 2268 @Override 2269 @Nullable 2270 public ActionRow.Action getModifyShareAction() { 2271 return originalFactory.getModifyShareAction(); 2272 } 2273 2274 @Override 2275 public Consumer<Boolean> getExcludeSharedTextAction() { 2276 return originalFactory.getExcludeSharedTextAction(); 2277 } 2278 }; 2279 } 2280 2281 private ChooserActionFactory createChooserActionFactory(Intent targetIntent) { 2282 return new ChooserActionFactory( 2283 this, 2284 targetIntent, 2285 mRequest.getLaunchedFromPackage(), 2286 mRequest.getChooserActions(), 2287 mImageEditor, 2288 getEventLog(), 2289 (isExcluded) -> mExcludeSharedText = isExcluded, 2290 this::getFirstVisibleImgPreviewView, 2291 new ChooserActionFactory.ActionActivityStarter() { 2292 @Override 2293 public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { 2294 safelyStartActivityAsUser( 2295 targetInfo, 2296 mProfiles.getPersonalHandle() 2297 ); 2298 Log.d(TAG, "safelyStartActivityAsPersonalProfileUser(" 2299 + targetInfo + "): finishing!"); 2300 finish(); 2301 } 2302 2303 @Override 2304 public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( 2305 TargetInfo targetInfo, View sharedElement, String sharedElementName) { 2306 ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( 2307 ChooserActivity.this, sharedElement, sharedElementName); 2308 safelyStartActivityAsUser( 2309 targetInfo, 2310 mProfiles.getPersonalHandle(), 2311 options.toBundle()); 2312 // Can't finish right away because the shared element transition may not 2313 // be ready to start. 2314 mFinishWhenStopped = true; 2315 } 2316 }, 2317 mShareResultSender, 2318 this::finishWithStatus, 2319 mClipboardManager); 2320 } 2321 2322 private Supplier<ActionRow.Action> createModifyShareActionFactory() { 2323 return () -> ChooserActionFactory.createCustomAction( 2324 ChooserActivity.this, 2325 mRequest.getModifyShareAction(), 2326 () -> getEventLog().logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE), 2327 mShareResultSender, 2328 this::finishWithStatus); 2329 } 2330 2331 private void finishWithStatus(@Nullable Integer status) { 2332 if (status != null) { 2333 setResult(status); 2334 } 2335 Log.d(TAG, "finishWithStatus: result=" + status); 2336 finish(); 2337 } 2338 2339 /* 2340 * Need to dynamically adjust how many icons can fit per row before we add them, 2341 * which also means setting the correct offset to initially show the content 2342 * preview area + 2 rows of targets 2343 */ 2344 private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 2345 int oldTop, int oldRight, int oldBottom) { 2346 if (mChooserMultiProfilePagerAdapter == null || !isProfilePagerAdapterAttached()) { 2347 return; 2348 } 2349 RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); 2350 ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter(); 2351 // Skip height calculation if recycler view was scrolled to prevent it inaccurately 2352 // calculating the height, as the logic below does not account for the scrolled offset. 2353 if (gridAdapter == null || recyclerView == null 2354 || recyclerView.computeVerticalScrollOffset() != 0) { 2355 return; 2356 } 2357 if (delayDrawerOffsetCalculation() && !gridAdapter.getListAdapter().areAppTargetsReady()) { 2358 return; 2359 } 2360 2361 final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight(); 2362 final int maxChooserWidth = getResources().getDimensionPixelSize(R.dimen.chooser_width); 2363 boolean isLayoutUpdated = 2364 gridAdapter.calculateChooserTargetWidth( 2365 maxChooserWidth >= 0 2366 ? Math.min(maxChooserWidth, availableWidth) 2367 : availableWidth) 2368 || recyclerView.getAdapter() == null 2369 || availableWidth != mCurrAvailableWidth; 2370 2371 mCurrAvailableWidth = availableWidth; 2372 if (isLayoutUpdated) { 2373 // It is very important we call setAdapter from here. Otherwise in some cases 2374 // the resolver list doesn't get populated, such as b/150922090, b/150918223 2375 // and b/150936654 2376 recyclerView.setAdapter(gridAdapter); 2377 ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount( 2378 mMaxTargetsPerRow); 2379 2380 updateTabPadding(); 2381 } 2382 2383 if (mChooserMultiProfilePagerAdapter.getActiveProfile() != mInitialProfile) { 2384 return; 2385 } 2386 2387 getMainThreadHandler().post(() -> { 2388 if (mResolverDrawerLayout == null) { 2389 return; 2390 } 2391 int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter); 2392 mResolverDrawerLayout.setCollapsibleHeightReserved(offset); 2393 mEnterTransitionAnimationDelegate.markOffsetCalculated(); 2394 mLastAppliedInsets = mSystemWindowInsets; 2395 }); 2396 } 2397 2398 private int calculateDrawerOffset( 2399 int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) { 2400 2401 int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; 2402 int rowsToShow = gridAdapter.getServiceTargetRowCount() 2403 + gridAdapter.getCallerAndRankedTargetRowCount(); 2404 2405 // then this is most likely not a SEND_* action, so check 2406 // the app target count 2407 if (rowsToShow == 0) { 2408 rowsToShow = gridAdapter.getRowCount(); 2409 } 2410 2411 // still zero? then use a default height and leave, which 2412 // can happen when there are no targets to show 2413 if (rowsToShow == 0 && !shouldShowStickyContentPreview()) { 2414 offset += getResources().getDimensionPixelSize( 2415 R.dimen.chooser_max_collapsed_height); 2416 return offset; 2417 } 2418 2419 View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container); 2420 if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) { 2421 offset += stickyContentPreview.getHeight(); 2422 } 2423 2424 if (mProfiles.getWorkProfilePresent()) { 2425 offset += findViewById(com.android.internal.R.id.tabs).getHeight(); 2426 } 2427 2428 if (recyclerView.getVisibility() == View.VISIBLE) { 2429 rowsToShow = Math.min(4, rowsToShow); 2430 boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow); 2431 mLastNumberOfChildren = recyclerView.getChildCount(); 2432 for (int i = 0, childCount = recyclerView.getChildCount(); 2433 i < childCount && rowsToShow > 0; i++) { 2434 View child = recyclerView.getChildAt(i); 2435 if (((GridLayoutManager.LayoutParams) 2436 child.getLayoutParams()).getSpanIndex() != 0) { 2437 continue; 2438 } 2439 int height = child.getHeight(); 2440 offset += height; 2441 if (shouldShowExtraRow) { 2442 offset += height; 2443 } 2444 rowsToShow--; 2445 } 2446 } else { 2447 ViewGroup currentEmptyStateView = 2448 mChooserMultiProfilePagerAdapter.getActiveEmptyStateView(); 2449 if (currentEmptyStateView.getVisibility() == View.VISIBLE) { 2450 offset += currentEmptyStateView.getHeight(); 2451 } 2452 } 2453 2454 return Math.min(offset, bottom - top); 2455 } 2456 2457 private boolean isProfilePagerAdapterAttached() { 2458 return mChooserMultiProfilePagerAdapter == mViewPager.getAdapter(); 2459 } 2460 2461 /** 2462 * If we have a tabbed view and are showing 1 row in the current profile and an empty 2463 * state screen in another profile, to prevent cropping of the empty state screen we show 2464 * a second row in the current profile. 2465 */ 2466 private boolean shouldShowExtraRow(int rowsToShow) { 2467 return rowsToShow == 1 2468 && mChooserMultiProfilePagerAdapter 2469 .shouldShowEmptyStateScreenInAnyInactiveAdapter(); 2470 } 2471 2472 protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { 2473 Log.d(TAG, "onListRebuilt(listAdapter.userHandle=" + listAdapter.getUserHandle() + ", " 2474 + "rebuildComplete=" + rebuildComplete + ")"); 2475 setupScrollListener(); 2476 maybeSetupGlobalLayoutListener(); 2477 2478 ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; 2479 UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle(); 2480 if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) { 2481 mChooserMultiProfilePagerAdapter.getActiveAdapterView() 2482 .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()); 2483 mChooserMultiProfilePagerAdapter 2484 .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage()); 2485 } 2486 2487 //TODO: move this block inside ChooserListAdapter (should be called when 2488 // ResolverListAdapter#mPostListReadyRunnable is executed. 2489 chooserListAdapter.updateAlphabeticalList( 2490 rebuildComplete, 2491 () -> onAppTargetsLoaded(listAdapter)); 2492 2493 if (rebuildComplete) { 2494 long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle); 2495 if (duration >= 0) { 2496 Log.d(TAG, "app target loading time " + duration + " ms"); 2497 } 2498 if (!fixShortcutsFlashingFixed()) { 2499 addCallerChooserTargets(chooserListAdapter); 2500 } 2501 getEventLog().logSharesheetAppLoadComplete(); 2502 maybeQueryAdditionalPostProcessingTargets( 2503 listProfileUserHandle, 2504 chooserListAdapter.getDisplayResolveInfos()); 2505 mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET); 2506 } 2507 } 2508 2509 private void maybeQueryAdditionalPostProcessingTargets( 2510 UserHandle userHandle, 2511 DisplayResolveInfo[] displayResolveInfos) { 2512 ProfileRecord record = getProfileRecord(userHandle); 2513 if (record == null || record.shortcutLoader == null) { 2514 return; 2515 } 2516 record.loadingStartTime = SystemClock.elapsedRealtime(); 2517 record.shortcutLoader.updateAppTargets(displayResolveInfos); 2518 } 2519 2520 @MainThread 2521 private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) { 2522 if (DEBUG) { 2523 Log.d(TAG, "onShortcutsLoaded for user: " + userHandle); 2524 } 2525 mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache()); 2526 mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache()); 2527 ChooserListAdapter adapter = 2528 mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle); 2529 if (adapter != null) { 2530 if (fixShortcutsFlashingFixed()) { 2531 adapter.setDirectTargetsEnabled(true); 2532 adapter.resetDirectTargets(); 2533 addCallerChooserTargets(adapter); 2534 } 2535 for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) { 2536 adapter.addServiceResults( 2537 resultInfo.getAppTarget(), 2538 resultInfo.getShortcuts(), 2539 result.isFromAppPredictor() 2540 ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE 2541 : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, 2542 mDirectShareShortcutInfoCache, 2543 mDirectShareAppTargetCache); 2544 } 2545 adapter.completeServiceTargetLoading(); 2546 } 2547 2548 if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == adapter) { 2549 long duration = Tracer.INSTANCE.endLaunchToShortcutTrace(); 2550 if (duration >= 0) { 2551 Log.d(TAG, "stat to first shortcut time: " + duration + " ms"); 2552 } 2553 } 2554 logDirectShareTargetReceived(userHandle); 2555 sendVoiceChoicesIfNeeded(); 2556 getEventLog().logSharesheetDirectLoadComplete(); 2557 } 2558 2559 private void setupScrollListener() { 2560 if (mResolverDrawerLayout == null) { 2561 return; 2562 } 2563 int elevatedViewResId = mProfiles.getWorkProfilePresent() 2564 ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header; 2565 final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId); 2566 final float defaultElevation = elevatedView.getElevation(); 2567 final float chooserHeaderScrollElevation = 2568 getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation); 2569 mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener( 2570 new RecyclerView.OnScrollListener() { 2571 @Override 2572 public void onScrollStateChanged(RecyclerView view, int scrollState) { 2573 if (scrollState == RecyclerView.SCROLL_STATE_IDLE) { 2574 if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) { 2575 mScrollStatus = SCROLL_STATUS_IDLE; 2576 setHorizontalScrollingEnabled(true); 2577 } 2578 } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) { 2579 if (mScrollStatus == SCROLL_STATUS_IDLE) { 2580 mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL; 2581 setHorizontalScrollingEnabled(false); 2582 } 2583 } 2584 } 2585 2586 @Override 2587 public void onScrolled(RecyclerView view, int dx, int dy) { 2588 if (view.getChildCount() > 0) { 2589 View child = view.getLayoutManager().findViewByPosition(0); 2590 if (child == null || child.getTop() < 0) { 2591 elevatedView.setElevation(chooserHeaderScrollElevation); 2592 return; 2593 } 2594 } 2595 2596 elevatedView.setElevation(defaultElevation); 2597 } 2598 }); 2599 } 2600 2601 private void maybeSetupGlobalLayoutListener() { 2602 if (mProfiles.getWorkProfilePresent()) { 2603 return; 2604 } 2605 final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); 2606 recyclerView.getViewTreeObserver() 2607 .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 2608 @Override 2609 public void onGlobalLayout() { 2610 // Fixes an issue were the accessibility border disappears on list creation. 2611 recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 2612 final TextView titleView = findViewById(com.android.internal.R.id.title); 2613 if (titleView != null) { 2614 titleView.setFocusable(true); 2615 titleView.setFocusableInTouchMode(true); 2616 titleView.requestFocus(); 2617 titleView.requestAccessibilityFocus(); 2618 } 2619 } 2620 }); 2621 } 2622 2623 /** 2624 * The sticky content preview is shown only when we have a tabbed view. It's shown above 2625 * the tabs so it is not part of the scrollable list. If we are not in tabbed view, 2626 * we instead show the content preview as a regular list item. 2627 */ 2628 private boolean shouldShowStickyContentPreview() { 2629 return shouldShowStickyContentPreviewNoOrientationCheck(); 2630 } 2631 2632 private boolean shouldShowStickyContentPreviewNoOrientationCheck() { 2633 if (isInteractiveSession() || !shouldShowContentPreview()) { 2634 return false; 2635 } 2636 ResolverListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle( 2637 UserHandle.of(UserHandle.myUserId())); 2638 boolean isEmpty = adapter == null || adapter.getCount() == 0; 2639 return !isEmpty || shouldShowContentPreviewWhenEmpty(); 2640 } 2641 2642 /** 2643 * This method could be used to override the default behavior when we hide the preview area 2644 * when the current tab doesn't have any items. 2645 * 2646 * @return true if we want to show the content preview area even if the tab for the current 2647 * user is empty 2648 */ 2649 protected boolean shouldShowContentPreviewWhenEmpty() { 2650 return false; 2651 } 2652 2653 /** 2654 * @return true if we want to show the content preview area 2655 */ 2656 protected boolean shouldShowContentPreview() { 2657 return mRequest.isSendActionTarget(); 2658 } 2659 2660 private void updateStickyContentPreview() { 2661 if (shouldShowStickyContentPreviewNoOrientationCheck()) { 2662 // The sticky content preview is only shown when we show the work and personal tabs. 2663 // We don't show it in landscape as otherwise there is no room for scrolling. 2664 // If the sticky content preview will be shown at some point with orientation change, 2665 // then always preload it to avoid subsequent resizing of the share sheet. 2666 ViewGroup contentPreviewContainer = 2667 findViewById(com.android.internal.R.id.content_preview_container); 2668 if (contentPreviewContainer.getChildCount() == 0) { 2669 ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer); 2670 contentPreviewContainer.addView(contentPreviewView); 2671 } 2672 } 2673 if (shouldShowStickyContentPreview()) { 2674 showStickyContentPreview(); 2675 } else { 2676 hideStickyContentPreview(); 2677 } 2678 } 2679 2680 private void showStickyContentPreview() { 2681 if (isStickyContentPreviewShowing()) { 2682 return; 2683 } 2684 ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); 2685 contentPreviewContainer.setVisibility(View.VISIBLE); 2686 } 2687 2688 private boolean isStickyContentPreviewShowing() { 2689 ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); 2690 return contentPreviewContainer.getVisibility() == View.VISIBLE; 2691 } 2692 2693 private void hideStickyContentPreview() { 2694 if (!isStickyContentPreviewShowing()) { 2695 return; 2696 } 2697 ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); 2698 contentPreviewContainer.setVisibility(View.GONE); 2699 } 2700 2701 protected String getMetricsCategory() { 2702 return METRICS_CATEGORY_CHOOSER; 2703 } 2704 2705 protected void onProfileTabSelected(int currentPage) { 2706 setupViewVisibilities(); 2707 maybeLogProfileChange(); 2708 if (mProfiles.getWorkProfilePresent()) { 2709 // The device policy logger is only concerned with sessions that include a work profile. 2710 DevicePolicyEventLogger 2711 .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS) 2712 .setInt(currentPage) 2713 .setStrings(getMetricsCategory()) 2714 .write(); 2715 } 2716 2717 // This fixes an edge case where after performing a variety of gestures, vertical scrolling 2718 // ends up disabled. That's because at some point the old tab's vertical scrolling is 2719 // disabled and the new tab's is enabled. For context, see b/159997845 2720 setVerticalScrollEnabled(true); 2721 if (mResolverDrawerLayout != null) { 2722 mResolverDrawerLayout.scrollNestedScrollableChildBackToTop(); 2723 } 2724 } 2725 2726 private int getInteractiveSessionTopSpacing() { 2727 return getResources().getDimensionPixelSize(R.dimen.chooser_preview_image_height_tall); 2728 } 2729 2730 private boolean isInteractiveSession() { 2731 return interactiveSession() && mRequest.getInteractiveSessionCallback() != null 2732 && !isTaskRoot(); 2733 } 2734 2735 protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { 2736 mSystemWindowInsets = insets.getInsets(WindowInsets.Type.systemBars()); 2737 mChooserMultiProfilePagerAdapter 2738 .setEmptyStateBottomOffset(mSystemWindowInsets.bottom); 2739 2740 final int topSpacing = isInteractiveSession() ? getInteractiveSessionTopSpacing() : 0; 2741 mResolverDrawerLayout.setPadding( 2742 mSystemWindowInsets.left, 2743 mSystemWindowInsets.top + topSpacing, 2744 mSystemWindowInsets.right, 2745 0); 2746 2747 // Need extra padding so the list can fully scroll up 2748 // To accommodate for window insets 2749 applyFooterView(mSystemWindowInsets.bottom); 2750 2751 if (mResolverDrawerLayout != null) { 2752 mResolverDrawerLayout.requestLayout(); 2753 } 2754 return WindowInsets.CONSUMED; 2755 } 2756 2757 private void setHorizontalScrollingEnabled(boolean enabled) { 2758 mViewPager.setSwipingEnabled(enabled); 2759 } 2760 2761 private void setVerticalScrollEnabled(boolean enabled) { 2762 ChooserGridLayoutManager layoutManager = 2763 (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView() 2764 .getLayoutManager(); 2765 layoutManager.setVerticalScrollEnabled(enabled); 2766 } 2767 2768 void onHorizontalSwipeStateChanged(int state) { 2769 if (state == ViewPager.SCROLL_STATE_DRAGGING) { 2770 if (mScrollStatus == SCROLL_STATUS_IDLE) { 2771 mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL; 2772 setVerticalScrollEnabled(false); 2773 } 2774 } else if (state == ViewPager.SCROLL_STATE_IDLE) { 2775 if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) { 2776 mScrollStatus = SCROLL_STATUS_IDLE; 2777 setVerticalScrollEnabled(true); 2778 } 2779 } 2780 } 2781 2782 protected void maybeLogProfileChange() { 2783 getEventLog().logSharesheetProfileChanged(); 2784 } 2785 2786 private static class ProfileRecord { 2787 public final Profile profile; 2788 2789 /** The {@link AppPredictor} for this profile, if any. */ 2790 @Nullable 2791 public final AppPredictor appPredictor; 2792 /** 2793 * null if we should not load shortcuts. 2794 */ 2795 @Nullable 2796 public final ShortcutLoader shortcutLoader; 2797 public final List<ChooserTarget> callerTargets; 2798 public long loadingStartTime; 2799 2800 private ProfileRecord( 2801 Profile profile, 2802 @Nullable AppPredictor appPredictor, 2803 @Nullable ShortcutLoader shortcutLoader, 2804 List<ChooserTarget> callerTargets) { 2805 this.profile = profile; 2806 this.appPredictor = appPredictor; 2807 this.shortcutLoader = shortcutLoader; 2808 this.callerTargets = callerTargets; 2809 } 2810 2811 public void destroy() { 2812 if (appPredictor != null) { 2813 appPredictor.destroy(); 2814 } 2815 if (shortcutLoader != null) { 2816 shortcutLoader.destroy(); 2817 } 2818 } 2819 } 2820 } 2821