1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver; 18 19 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL; 20 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK; 21 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL; 22 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK; 23 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; 24 import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; 25 import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; 26 27 import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; 28 29 import android.annotation.IntDef; 30 import android.annotation.Nullable; 31 import android.app.Activity; 32 import android.app.ActivityManager; 33 import android.app.ActivityOptions; 34 import android.app.prediction.AppPredictor; 35 import android.app.prediction.AppTarget; 36 import android.app.prediction.AppTargetEvent; 37 import android.app.prediction.AppTargetId; 38 import android.content.ComponentName; 39 import android.content.ContentResolver; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.IntentFilter; 43 import android.content.IntentSender; 44 import android.content.SharedPreferences; 45 import android.content.pm.ActivityInfo; 46 import android.content.pm.PackageManager; 47 import android.content.pm.ResolveInfo; 48 import android.content.pm.ShortcutInfo; 49 import android.content.res.Configuration; 50 import android.database.Cursor; 51 import android.graphics.Insets; 52 import android.net.Uri; 53 import android.os.Bundle; 54 import android.os.Environment; 55 import android.os.SystemClock; 56 import android.os.UserHandle; 57 import android.os.UserManager; 58 import android.os.storage.StorageManager; 59 import android.service.chooser.ChooserTarget; 60 import android.util.Log; 61 import android.util.Slog; 62 import android.util.SparseArray; 63 import android.view.View; 64 import android.view.ViewGroup; 65 import android.view.ViewGroup.LayoutParams; 66 import android.view.ViewTreeObserver; 67 import android.view.WindowInsets; 68 import android.widget.TextView; 69 70 import androidx.annotation.MainThread; 71 import androidx.lifecycle.ViewModelProvider; 72 import androidx.recyclerview.widget.GridLayoutManager; 73 import androidx.recyclerview.widget.RecyclerView; 74 import androidx.viewpager.widget.ViewPager; 75 76 import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; 77 import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; 78 import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; 79 import com.android.intentresolver.chooser.DisplayResolveInfo; 80 import com.android.intentresolver.chooser.MultiDisplayResolveInfo; 81 import com.android.intentresolver.chooser.TargetInfo; 82 import com.android.intentresolver.contentpreview.BasePreviewViewModel; 83 import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; 84 import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; 85 import com.android.intentresolver.contentpreview.PreviewViewModel; 86 import com.android.intentresolver.flags.FeatureFlagRepository; 87 import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; 88 import com.android.intentresolver.grid.ChooserGridAdapter; 89 import com.android.intentresolver.icons.DefaultTargetDataLoader; 90 import com.android.intentresolver.icons.TargetDataLoader; 91 import com.android.intentresolver.logging.EventLog; 92 import com.android.intentresolver.measurements.Tracer; 93 import com.android.intentresolver.model.AbstractResolverComparator; 94 import com.android.intentresolver.model.AppPredictionServiceResolverComparator; 95 import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; 96 import com.android.intentresolver.shortcuts.AppPredictorFactory; 97 import com.android.intentresolver.shortcuts.ShortcutLoader; 98 import com.android.intentresolver.widget.ImagePreviewView; 99 import com.android.internal.annotations.VisibleForTesting; 100 import com.android.internal.content.PackageMonitor; 101 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 102 103 import java.io.File; 104 import java.lang.annotation.Retention; 105 import java.lang.annotation.RetentionPolicy; 106 import java.text.Collator; 107 import java.util.ArrayList; 108 import java.util.Collections; 109 import java.util.Comparator; 110 import java.util.HashMap; 111 import java.util.List; 112 import java.util.Map; 113 import java.util.Objects; 114 import java.util.concurrent.ExecutorService; 115 import java.util.concurrent.Executors; 116 import java.util.function.Consumer; 117 118 /** 119 * The Chooser Activity handles intent resolution specifically for sharing intents - 120 * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}. 121 * 122 */ 123 public class ChooserActivity extends ResolverActivity implements 124 ResolverListAdapter.ResolverListCommunicator { 125 private static final String TAG = "ChooserActivity"; 126 127 /** 128 * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself 129 * in onStop when launched in a new task. If this extra is set to true, we do not finish 130 * ourselves when onStop gets called. 131 */ 132 public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP 133 = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP"; 134 135 /** 136 * Transition name for the first image preview. 137 * To be used for shared element transition into this activity. 138 * @hide 139 */ 140 public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image"; 141 142 private static final boolean DEBUG = true; 143 144 public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; 145 private static final String SHORTCUT_TARGET = "shortcut_target"; 146 147 // TODO: these data structures are for one-time use in shuttling data from where they're 148 // populated in `ShortcutToChooserTargetConverter` to where they're consumed in 149 // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`. 150 // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their 151 // intermediate data, and then these members can be removed. 152 private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>(); 153 private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>(); 154 155 public static final int TARGET_TYPE_DEFAULT = 0; 156 public static final int TARGET_TYPE_CHOOSER_TARGET = 1; 157 public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2; 158 public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3; 159 160 private static final int SCROLL_STATUS_IDLE = 0; 161 private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1; 162 private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; 163 164 @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = { 165 TARGET_TYPE_DEFAULT, 166 TARGET_TYPE_CHOOSER_TARGET, 167 TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, 168 TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE 169 }) 170 @Retention(RetentionPolicy.SOURCE) 171 public @interface ShareTargetType {} 172 173 private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents; 174 175 /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the 176 * only assignment there, and expect it to be ready by the time we ever use it -- 177 * someday if we move all the usage to a component with a narrower lifecycle (something that 178 * matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we 179 * should be able to make this assignment as "final." 180 */ 181 @Nullable 182 private ChooserRequestParameters mChooserRequest; 183 184 private ChooserRefinementManager mRefinementManager; 185 186 private FeatureFlagRepository mFeatureFlagRepository; 187 private ChooserContentPreviewUi mChooserContentPreviewUi; 188 189 private boolean mShouldDisplayLandscape; 190 // statsd logger wrapper 191 protected EventLog mEventLog; 192 193 private long mChooserShownTime; 194 protected boolean mIsSuccessfullySelected; 195 196 private int mCurrAvailableWidth = 0; 197 private Insets mLastAppliedInsets = null; 198 private int mLastNumberOfChildren = -1; 199 private int mMaxTargetsPerRow = 1; 200 201 private static final int MAX_LOG_RANK_POSITION = 12; 202 203 // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters. 204 private static final int MAX_EXTRA_INITIAL_INTENTS = 2; 205 private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; 206 207 private SharedPreferences mPinnedSharedPrefs; 208 private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings"; 209 210 private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5); 211 212 private int mScrollStatus = SCROLL_STATUS_IDLE; 213 214 @VisibleForTesting 215 protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter; 216 private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate = 217 new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout); 218 219 private View mContentView = null; 220 221 private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>(); 222 223 private boolean mExcludeSharedText = false; 224 /** 225 * When we intend to finish the activity with a shared element transition, we can't immediately 226 * finish() when the transition is invoked, as the receiving end may not be able to start the 227 * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop 228 * in order to wait for the transition to begin. 229 */ 230 private boolean mFinishWhenStopped = false; 231 ChooserActivity()232 public ChooserActivity() {} 233 234 @Override onCreate(Bundle savedInstanceState)235 protected void onCreate(Bundle savedInstanceState) { 236 Tracer.INSTANCE.markLaunched(); 237 final long intentReceivedTime = System.currentTimeMillis(); 238 mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); 239 240 getEventLog().logSharesheetTriggered(); 241 242 mFeatureFlagRepository = createFeatureFlagRepository(); 243 mIntegratedDeviceComponents = getIntegratedDeviceComponents(); 244 245 try { 246 mChooserRequest = new ChooserRequestParameters( 247 getIntent(), 248 getReferrerPackageName(), 249 getReferrer(), 250 mFeatureFlagRepository); 251 } catch (IllegalArgumentException e) { 252 Log.e(TAG, "Caller provided invalid Chooser request parameters", e); 253 finish(); 254 super_onCreate(null); 255 return; 256 } 257 258 mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); 259 260 mRefinementManager.getRefinementCompletion().observe(this, completion -> { 261 if (completion.consume()) { 262 TargetInfo targetInfo = completion.getTargetInfo(); 263 // targetInfo is non-null if the refinement process was successful. 264 if (targetInfo != null) { 265 maybeRemoveSharedText(targetInfo); 266 267 // We already block suspended targets from going to refinement, and we probably 268 // can't recover a Chooser session if that's the reason the refined target fails 269 // to launch now. Fire-and-forget the refined launch; ignore the return value 270 // and just make sure the Sharesheet session gets cleaned up regardless. 271 ChooserActivity.super.onTargetSelected(targetInfo, false); 272 } 273 274 finish(); 275 } 276 }); 277 278 BasePreviewViewModel previewViewModel = 279 new ViewModelProvider(this, createPreviewViewModelFactory()) 280 .get(BasePreviewViewModel.class); 281 mChooserContentPreviewUi = new ChooserContentPreviewUi( 282 getLifecycle(), 283 previewViewModel.createOrReuseProvider(mChooserRequest), 284 mChooserRequest.getTargetIntent(), 285 previewViewModel.createOrReuseImageLoader(), 286 createChooserActionFactory(), 287 mEnterTransitionAnimationDelegate, 288 new HeadlineGeneratorImpl(this)); 289 290 mPinnedSharedPrefs = getPinnedSharedPrefs(this); 291 292 mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); 293 mShouldDisplayLandscape = 294 shouldDisplayLandscape(getResources().getConfiguration().orientation); 295 setRetainInOnStop(mChooserRequest.shouldRetainInOnStop()); 296 297 createProfileRecords( 298 new AppPredictorFactory( 299 getApplicationContext(), 300 mChooserRequest.getSharedText(), 301 mChooserRequest.getTargetIntentFilter()), 302 mChooserRequest.getTargetIntentFilter()); 303 304 super.onCreate( 305 savedInstanceState, 306 mChooserRequest.getTargetIntent(), 307 mChooserRequest.getAdditionalTargets(), 308 mChooserRequest.getTitle(), 309 mChooserRequest.getDefaultTitleResource(), 310 mChooserRequest.getInitialIntents(), 311 /* resolutionList= */ null, 312 /* supportsAlwaysUseOption= */ false, 313 new DefaultTargetDataLoader(this, getLifecycle(), false), 314 /* safeForwardingMode= */ true); 315 316 mChooserShownTime = System.currentTimeMillis(); 317 final long systemCost = mChooserShownTime - intentReceivedTime; 318 getEventLog().logChooserActivityShown( 319 isWorkProfile(), mChooserRequest.getTargetType(), systemCost); 320 321 if (mResolverDrawerLayout != null) { 322 mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); 323 324 mResolverDrawerLayout.setOnCollapsedChangedListener( 325 isCollapsed -> { 326 mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed); 327 getEventLog().logSharesheetExpansionChanged(isCollapsed); 328 }); 329 } 330 331 if (DEBUG) { 332 Log.d(TAG, "System Time Cost is " + systemCost); 333 } 334 335 getEventLog().logShareStarted( 336 getReferrerPackageName(), 337 mChooserRequest.getTargetType(), 338 mChooserRequest.getCallerChooserTargets().size(), 339 (mChooserRequest.getInitialIntents() == null) 340 ? 0 : mChooserRequest.getInitialIntents().length, 341 isWorkProfile(), 342 mChooserContentPreviewUi.getPreferredContentPreview(), 343 mChooserRequest.getTargetAction(), 344 mChooserRequest.getChooserActions().size(), 345 mChooserRequest.getModifyShareAction() != null 346 ); 347 348 mEnterTransitionAnimationDelegate.postponeTransition(); 349 } 350 351 @VisibleForTesting getIntegratedDeviceComponents()352 protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { 353 return ChooserIntegratedDeviceComponents.get(this, new SecureSettings()); 354 } 355 356 @Override appliedThemeResId()357 protected int appliedThemeResId() { 358 return R.style.Theme_DeviceDefault_Chooser; 359 } 360 createFeatureFlagRepository()361 protected FeatureFlagRepository createFeatureFlagRepository() { 362 return new FeatureFlagRepositoryFactory().create(getApplicationContext()); 363 } 364 createProfileRecords( AppPredictorFactory factory, IntentFilter targetIntentFilter)365 private void createProfileRecords( 366 AppPredictorFactory factory, IntentFilter targetIntentFilter) { 367 UserHandle mainUserHandle = getPersonalProfileUserHandle(); 368 ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory); 369 if (record.shortcutLoader == null) { 370 Tracer.INSTANCE.endLaunchToShortcutTrace(); 371 } 372 373 UserHandle workUserHandle = getWorkProfileUserHandle(); 374 if (workUserHandle != null) { 375 createProfileRecord(workUserHandle, targetIntentFilter, factory); 376 } 377 } 378 createProfileRecord( UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory)379 private ProfileRecord createProfileRecord( 380 UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) { 381 AppPredictor appPredictor = factory.create(userHandle); 382 ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() 383 ? null 384 : createShortcutLoader( 385 getApplicationContext(), 386 appPredictor, 387 userHandle, 388 targetIntentFilter, 389 shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult)); 390 ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader); 391 mProfileRecords.put(userHandle.getIdentifier(), record); 392 return record; 393 } 394 395 @Nullable getProfileRecord(UserHandle userHandle)396 private ProfileRecord getProfileRecord(UserHandle userHandle) { 397 return mProfileRecords.get(userHandle.getIdentifier(), null); 398 } 399 400 @VisibleForTesting createShortcutLoader( Context context, AppPredictor appPredictor, UserHandle userHandle, IntentFilter targetIntentFilter, Consumer<ShortcutLoader.Result> callback)401 protected ShortcutLoader createShortcutLoader( 402 Context context, 403 AppPredictor appPredictor, 404 UserHandle userHandle, 405 IntentFilter targetIntentFilter, 406 Consumer<ShortcutLoader.Result> callback) { 407 return new ShortcutLoader( 408 context, 409 getLifecycle(), 410 appPredictor, 411 userHandle, 412 targetIntentFilter, 413 callback); 414 } 415 getPinnedSharedPrefs(Context context)416 static SharedPreferences getPinnedSharedPrefs(Context context) { 417 // The code below is because in the android:ui process, no one can hear you scream. 418 // The package info in the context isn't initialized in the way it is for normal apps, 419 // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we 420 // build the path manually below using the same policy that appears in ContextImpl. 421 // This fails silently under the hood if there's a problem, so if we find ourselves in 422 // the case where we don't have access to credential encrypted storage we just won't 423 // have our pinned target info. 424 final File prefsFile = new File(new File( 425 Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL, 426 context.getUserId(), context.getPackageName()), 427 "shared_prefs"), 428 PINNED_SHARED_PREFS_NAME + ".xml"); 429 return context.getSharedPreferences(prefsFile, MODE_PRIVATE); 430 } 431 432 @Override createMultiProfilePagerAdapter( Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, TargetDataLoader targetDataLoader)433 protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( 434 Intent[] initialIntents, 435 List<ResolveInfo> rList, 436 boolean filterLastUsed, 437 TargetDataLoader targetDataLoader) { 438 if (shouldShowTabs()) { 439 mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles( 440 initialIntents, rList, filterLastUsed, targetDataLoader); 441 } else { 442 mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile( 443 initialIntents, rList, filterLastUsed, targetDataLoader); 444 } 445 return mChooserMultiProfilePagerAdapter; 446 } 447 448 @Override createBlockerEmptyStateProvider()449 protected EmptyStateProvider createBlockerEmptyStateProvider() { 450 final boolean isSendAction = mChooserRequest.isSendActionTarget(); 451 452 final EmptyState noWorkToPersonalEmptyState = 453 new DevicePolicyBlockerEmptyState( 454 /* context= */ this, 455 /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, 456 /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, 457 /* devicePolicyStringSubtitleId= */ 458 isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL, 459 /* defaultSubtitleResource= */ 460 isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation 461 : R.string.resolver_cant_access_personal_apps_explanation, 462 /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, 463 /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); 464 465 final EmptyState noPersonalToWorkEmptyState = 466 new DevicePolicyBlockerEmptyState( 467 /* context= */ this, 468 /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, 469 /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, 470 /* devicePolicyStringSubtitleId= */ 471 isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK, 472 /* defaultSubtitleResource= */ 473 isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation 474 : R.string.resolver_cant_access_work_apps_explanation, 475 /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, 476 /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); 477 478 return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), 479 noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, 480 createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch()); 481 } 482 createChooserMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, TargetDataLoader targetDataLoader)483 private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( 484 Intent[] initialIntents, 485 List<ResolveInfo> rList, 486 boolean filterLastUsed, 487 TargetDataLoader targetDataLoader) { 488 ChooserGridAdapter adapter = createChooserGridAdapter( 489 /* context */ this, 490 /* payloadIntents */ mIntents, 491 initialIntents, 492 rList, 493 filterLastUsed, 494 /* userHandle */ getPersonalProfileUserHandle(), 495 targetDataLoader); 496 return new ChooserMultiProfilePagerAdapter( 497 /* context */ this, 498 adapter, 499 createEmptyStateProvider(/* workProfileUserHandle= */ null), 500 /* workProfileQuietModeChecker= */ () -> false, 501 /* workProfileUserHandle= */ null, 502 getCloneProfileUserHandle(), 503 mMaxTargetsPerRow); 504 } 505 createChooserMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, TargetDataLoader targetDataLoader)506 private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( 507 Intent[] initialIntents, 508 List<ResolveInfo> rList, 509 boolean filterLastUsed, 510 TargetDataLoader targetDataLoader) { 511 int selectedProfile = findSelectedProfile(); 512 ChooserGridAdapter personalAdapter = createChooserGridAdapter( 513 /* context */ this, 514 /* payloadIntents */ mIntents, 515 selectedProfile == PROFILE_PERSONAL ? initialIntents : null, 516 rList, 517 filterLastUsed, 518 /* userHandle */ getPersonalProfileUserHandle(), 519 targetDataLoader); 520 ChooserGridAdapter workAdapter = createChooserGridAdapter( 521 /* context */ this, 522 /* payloadIntents */ mIntents, 523 selectedProfile == PROFILE_WORK ? initialIntents : null, 524 rList, 525 filterLastUsed, 526 /* userHandle */ getWorkProfileUserHandle(), 527 targetDataLoader); 528 return new ChooserMultiProfilePagerAdapter( 529 /* context */ this, 530 personalAdapter, 531 workAdapter, 532 createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()), 533 () -> mWorkProfileAvailability.isQuietModeEnabled(), 534 selectedProfile, 535 getWorkProfileUserHandle(), 536 getCloneProfileUserHandle(), 537 mMaxTargetsPerRow); 538 } 539 findSelectedProfile()540 private int findSelectedProfile() { 541 int selectedProfile = getSelectedProfileExtra(); 542 if (selectedProfile == -1) { 543 selectedProfile = getProfileForUser(getTabOwnerUserHandleForLaunch()); 544 } 545 return selectedProfile; 546 } 547 548 @Override postRebuildList(boolean rebuildCompleted)549 protected boolean postRebuildList(boolean rebuildCompleted) { 550 updateStickyContentPreview(); 551 if (shouldShowStickyContentPreview() 552 || mChooserMultiProfilePagerAdapter 553 .getCurrentRootAdapter().getSystemRowCount() != 0) { 554 getEventLog().logActionShareWithPreview( 555 mChooserContentPreviewUi.getPreferredContentPreview()); 556 } 557 return postRebuildListInternal(rebuildCompleted); 558 } 559 560 /** 561 * Check if the profile currently used is a work profile. 562 * @return true if it is work profile, false if it is parent profile (or no work profile is 563 * set up) 564 */ isWorkProfile()565 protected boolean isWorkProfile() { 566 return getSystemService(UserManager.class) 567 .getUserInfo(UserHandle.myUserId()).isManagedProfile(); 568 } 569 570 @Override createPackageMonitor(ResolverListAdapter listAdapter)571 protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { 572 return new PackageMonitor() { 573 @Override 574 public void onSomePackagesChanged() { 575 handlePackagesChanged(listAdapter); 576 } 577 }; 578 } 579 580 /** 581 * Update UI to reflect changes in data. 582 */ 583 public void handlePackagesChanged() { 584 handlePackagesChanged(/* listAdapter */ null); 585 } 586 587 /** 588 * Update UI to reflect changes in data. 589 * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if 590 * available. 591 */ 592 private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) { 593 // Refresh pinned items 594 mPinnedSharedPrefs = getPinnedSharedPrefs(this); 595 if (listAdapter == null) { 596 handlePackageChangePerProfile(mChooserMultiProfilePagerAdapter.getActiveListAdapter()); 597 if (mChooserMultiProfilePagerAdapter.getCount() > 1) { 598 handlePackageChangePerProfile( 599 mChooserMultiProfilePagerAdapter.getInactiveListAdapter()); 600 } 601 } else { 602 handlePackageChangePerProfile(listAdapter); 603 } 604 updateProfileViewButton(); 605 } 606 607 private void handlePackageChangePerProfile(ResolverListAdapter adapter) { 608 ProfileRecord record = getProfileRecord(adapter.getUserHandle()); 609 if (record != null && record.shortcutLoader != null) { 610 record.shortcutLoader.reset(); 611 } 612 adapter.handlePackagesChanged(); 613 } 614 615 @Override 616 protected void onResume() { 617 super.onResume(); 618 Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); 619 mFinishWhenStopped = false; 620 mRefinementManager.onActivityResume(); 621 } 622 623 @Override 624 public void onConfigurationChanged(Configuration newConfig) { 625 super.onConfigurationChanged(newConfig); 626 ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); 627 if (viewPager.isLayoutRtl()) { 628 mMultiProfilePagerAdapter.setupViewPager(viewPager); 629 } 630 631 mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation); 632 mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); 633 mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow); 634 adjustPreviewWidth(newConfig.orientation, null); 635 updateStickyContentPreview(); 636 updateTabPadding(); 637 } 638 639 private boolean shouldDisplayLandscape(int orientation) { 640 // Sharesheet fixes the # of items per row and therefore can not correctly lay out 641 // when in the restricted size of multi-window mode. In the future, would be nice 642 // to use minimum dp size requirements instead 643 return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode(); 644 } 645 646 private void adjustPreviewWidth(int orientation, View parent) { 647 int width = -1; 648 if (mShouldDisplayLandscape) { 649 width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width); 650 } 651 652 parent = parent == null ? getWindow().getDecorView() : parent; 653 654 updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent); 655 } 656 657 private void updateTabPadding() { 658 if (shouldShowTabs()) { 659 View tabs = findViewById(com.android.internal.R.id.tabs); 660 float iconSize = getResources().getDimension(R.dimen.chooser_icon_size); 661 // The entire width consists of icons or padding. Divide the item padding in half to get 662 // paddingHorizontal. 663 float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize) 664 / mMaxTargetsPerRow / 2; 665 // Subtract the margin the buttons already have. 666 padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin); 667 tabs.setPadding((int) padding, 0, (int) padding, 0); 668 } 669 } 670 671 private void updateLayoutWidth(int layoutResourceId, int width, View parent) { 672 View view = parent.findViewById(layoutResourceId); 673 if (view != null && view.getLayoutParams() != null) { 674 LayoutParams params = view.getLayoutParams(); 675 params.width = width; 676 view.setLayoutParams(params); 677 } 678 } 679 680 /** 681 * Create a view that will be shown in the content preview area 682 * @param parent reference to the parent container where the view should be attached to 683 * @return content preview view 684 */ 685 protected ViewGroup createContentPreviewView(ViewGroup parent) { 686 ViewGroup layout = mChooserContentPreviewUi.displayContentPreview( 687 getResources(), 688 getLayoutInflater(), 689 parent); 690 691 if (layout != null) { 692 adjustPreviewWidth(getResources().getConfiguration().orientation, layout); 693 } 694 695 return layout; 696 } 697 698 @Nullable 699 private View getFirstVisibleImgPreviewView() { 700 View imagePreview = findViewById(R.id.scrollable_image_preview); 701 return imagePreview instanceof ImagePreviewView 702 ? ((ImagePreviewView) imagePreview).getTransitionView() 703 : null; 704 } 705 706 /** 707 * Wrapping the ContentResolver call to expose for easier mocking, 708 * and to avoid mocking Android core classes. 709 */ 710 @VisibleForTesting 711 public Cursor queryResolver(ContentResolver resolver, Uri uri) { 712 return resolver.query(uri, null, null, null, null); 713 } 714 715 @Override 716 protected void onStop() { 717 super.onStop(); 718 mRefinementManager.onActivityStop(isChangingConfigurations()); 719 720 if (mFinishWhenStopped) { 721 mFinishWhenStopped = false; 722 finish(); 723 } 724 } 725 726 @Override 727 protected void onDestroy() { 728 super.onDestroy(); 729 730 if (isFinishing()) { 731 mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); 732 } 733 734 mBackgroundThreadPoolExecutor.shutdownNow(); 735 736 destroyProfileRecords(); 737 } 738 739 private void destroyProfileRecords() { 740 for (int i = 0; i < mProfileRecords.size(); ++i) { 741 mProfileRecords.valueAt(i).destroy(); 742 } 743 mProfileRecords.clear(); 744 } 745 746 @Override // ResolverListCommunicator 747 public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { 748 if (mChooserRequest == null) { 749 return defIntent; 750 } 751 752 Intent result = defIntent; 753 if (mChooserRequest.getReplacementExtras() != null) { 754 final Bundle replExtras = 755 mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName); 756 if (replExtras != null) { 757 result = new Intent(defIntent); 758 result.putExtras(replExtras); 759 } 760 } 761 if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT) 762 || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) { 763 result = Intent.createChooser(result, 764 getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE)); 765 766 // Don't auto-launch single intents if the intent is being forwarded. This is done 767 // because automatically launching a resolving application as a response to the user 768 // action of switching accounts is pretty unexpected. 769 result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); 770 } 771 return result; 772 } 773 774 @Override 775 public void onActivityStarted(TargetInfo cti) { 776 if (mChooserRequest.getChosenComponentSender() != null) { 777 final ComponentName target = cti.getResolvedComponentName(); 778 if (target != null) { 779 final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target); 780 try { 781 mChooserRequest.getChosenComponentSender().sendIntent( 782 this, Activity.RESULT_OK, fillIn, null, null); 783 } catch (IntentSender.SendIntentException e) { 784 Slog.e(TAG, "Unable to launch supplied IntentSender to report " 785 + "the chosen component: " + e); 786 } 787 } 788 } 789 } 790 791 private void addCallerChooserTargets() { 792 if (!mChooserRequest.getCallerChooserTargets().isEmpty()) { 793 // Send the caller's chooser targets only to the default profile. 794 UserHandle defaultUser = (findSelectedProfile() == PROFILE_WORK) 795 ? getAnnotatedUserHandles().workProfileUserHandle 796 : getAnnotatedUserHandles().personalProfileUserHandle; 797 if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle() == defaultUser) { 798 mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( 799 /* origTarget */ null, 800 new ArrayList<>(mChooserRequest.getCallerChooserTargets()), 801 TARGET_TYPE_DEFAULT, 802 /* directShareShortcutInfoCache */ Collections.emptyMap(), 803 /* directShareAppTargetCache */ Collections.emptyMap()); 804 } 805 } 806 } 807 808 @Override 809 public int getLayoutResource() { 810 return R.layout.chooser_grid; 811 } 812 813 @Override // ResolverListCommunicator 814 public boolean shouldGetActivityMetadata() { 815 return true; 816 } 817 818 @Override 819 public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { 820 // Note that this is only safe because the Intent handled by the ChooserActivity is 821 // guaranteed to contain no extras unknown to the local ClassLoader. That is why this 822 // method can not be replaced in the ResolverActivity whole hog. 823 if (!super.shouldAutoLaunchSingleChoice(target)) { 824 return false; 825 } 826 827 return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); 828 } 829 830 private void showTargetDetails(TargetInfo targetInfo) { 831 if (targetInfo == null) return; 832 833 List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets(); 834 if (targetList.isEmpty()) { 835 Log.e(TAG, "No displayable data to show target details"); 836 return; 837 } 838 839 // TODO: implement these type-conditioned behaviors polymorphically, and consider moving 840 // the logic into `ChooserTargetActionsDialogFragment.show()`. 841 boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned(); 842 IntentFilter intentFilter = targetInfo.isSelectableTargetInfo() 843 ? mChooserRequest.getTargetIntentFilter() : null; 844 String shortcutTitle = targetInfo.isSelectableTargetInfo() 845 ? targetInfo.getDisplayLabel().toString() : null; 846 String shortcutIdKey = targetInfo.getDirectShareShortcutId(); 847 848 ChooserTargetActionsDialogFragment.show( 849 getSupportFragmentManager(), 850 targetList, 851 // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be 852 // resolved correctly within the same tab. 853 targetInfo.getResolveInfo().userHandle, 854 shortcutIdKey, 855 shortcutTitle, 856 isShortcutPinned, 857 intentFilter); 858 } 859 860 @Override 861 protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { 862 if (mRefinementManager.maybeHandleSelection( 863 target, 864 mChooserRequest.getRefinementIntentSender(), 865 getApplication(), 866 getMainThreadHandler())) { 867 return false; 868 } 869 updateModelAndChooserCounts(target); 870 maybeRemoveSharedText(target); 871 return super.onTargetSelected(target, alwaysCheck); 872 } 873 874 @Override 875 public void startSelected(int which, boolean always, boolean filtered) { 876 ChooserListAdapter currentListAdapter = 877 mChooserMultiProfilePagerAdapter.getActiveListAdapter(); 878 TargetInfo targetInfo = currentListAdapter 879 .targetInfoForPosition(which, filtered); 880 if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) { 881 return; 882 } 883 884 final long selectionCost = System.currentTimeMillis() - mChooserShownTime; 885 886 if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) { 887 MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; 888 if (!mti.hasSelected()) { 889 // Add userHandle based badge to the stackedAppDialogBox. 890 ChooserStackedAppDialogFragment.show( 891 getSupportFragmentManager(), 892 mti, 893 which, 894 targetInfo.getResolveInfo().userHandle); 895 return; 896 } 897 } 898 899 super.startSelected(which, always, filtered); 900 901 // TODO: both of the conditions around this switch logic *should* be redundant, and 902 // can be removed if certain invariants can be guaranteed. In particular, it seems 903 // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably* 904 // expected to be null only at out-of-bounds indexes where `getPositionTargetType()` 905 // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't 906 // need to null-check targetInfo. We only need the null check if it's possible that 907 // the ChooserListAdapter contains null elements "in the middle" of its list data, 908 // such that they're classified as belonging to one of the real target types. That 909 // should probably never happen. But why would this method ever be invoked with a 910 // null target at all? Even an out-of-bounds index should never be "selected"... 911 if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) { 912 switch (currentListAdapter.getPositionTargetType(which)) { 913 case ChooserListAdapter.TARGET_SERVICE: 914 getEventLog().logShareTargetSelected( 915 EventLog.SELECTION_TYPE_SERVICE, 916 targetInfo.getResolveInfo().activityInfo.processName, 917 which, 918 /* directTargetAlsoRanked= */ getRankedPosition(targetInfo), 919 mChooserRequest.getCallerChooserTargets().size(), 920 targetInfo.getHashedTargetIdForMetrics(this), 921 targetInfo.isPinned(), 922 mIsSuccessfullySelected, 923 selectionCost 924 ); 925 return; 926 case ChooserListAdapter.TARGET_CALLER: 927 case ChooserListAdapter.TARGET_STANDARD: 928 getEventLog().logShareTargetSelected( 929 EventLog.SELECTION_TYPE_APP, 930 targetInfo.getResolveInfo().activityInfo.processName, 931 (which - currentListAdapter.getSurfacedTargetInfo().size()), 932 /* directTargetAlsoRanked= */ -1, 933 currentListAdapter.getCallerTargetCount(), 934 /* directTargetHashed= */ null, 935 targetInfo.isPinned(), 936 mIsSuccessfullySelected, 937 selectionCost 938 ); 939 return; 940 case ChooserListAdapter.TARGET_STANDARD_AZ: 941 // A-Z targets are unranked standard targets; we use a value of -1 to mark that 942 // they are from the alphabetical pool. 943 // TODO: why do we log a different selection type if the -1 value already 944 // designates the same condition? 945 getEventLog().logShareTargetSelected( 946 EventLog.SELECTION_TYPE_STANDARD, 947 targetInfo.getResolveInfo().activityInfo.processName, 948 /* value= */ -1, 949 /* directTargetAlsoRanked= */ -1, 950 /* numCallerProvided= */ 0, 951 /* directTargetHashed= */ null, 952 /* isPinned= */ false, 953 mIsSuccessfullySelected, 954 selectionCost 955 ); 956 return; 957 } 958 } 959 } 960 961 private int getRankedPosition(TargetInfo targetInfo) { 962 String targetPackageName = 963 targetInfo.getChooserTargetComponentName().getPackageName(); 964 ChooserListAdapter currentListAdapter = 965 mChooserMultiProfilePagerAdapter.getActiveListAdapter(); 966 int maxRankedResults = Math.min( 967 currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION); 968 969 for (int i = 0; i < maxRankedResults; i++) { 970 if (currentListAdapter.getDisplayResolveInfo(i) 971 .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) { 972 return i; 973 } 974 } 975 return -1; 976 } 977 978 @Override 979 protected boolean shouldAddFooterView() { 980 // To accommodate for window insets 981 return true; 982 } 983 984 @Override 985 protected void applyFooterView(int height) { 986 int count = mChooserMultiProfilePagerAdapter.getItemCount(); 987 988 for (int i = 0; i < count; i++) { 989 mChooserMultiProfilePagerAdapter.getAdapterForIndex(i).setFooterHeight(height); 990 } 991 } 992 993 private void logDirectShareTargetReceived(UserHandle forUser) { 994 ProfileRecord profileRecord = getProfileRecord(forUser); 995 if (profileRecord == null) { 996 return; 997 } 998 getEventLog().logDirectShareTargetReceived( 999 MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER, 1000 (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime)); 1001 } 1002 1003 void updateModelAndChooserCounts(TargetInfo info) { 1004 if (info != null && info.isMultiDisplayResolveInfo()) { 1005 info = ((MultiDisplayResolveInfo) info).getSelectedTarget(); 1006 } 1007 if (info != null) { 1008 sendClickToAppPredictor(info); 1009 final ResolveInfo ri = info.getResolveInfo(); 1010 Intent targetIntent = getTargetIntent(); 1011 if (ri != null && ri.activityInfo != null && targetIntent != null) { 1012 ChooserListAdapter currentListAdapter = 1013 mChooserMultiProfilePagerAdapter.getActiveListAdapter(); 1014 if (currentListAdapter != null) { 1015 sendImpressionToAppPredictor(info, currentListAdapter); 1016 currentListAdapter.updateModel(info); 1017 currentListAdapter.updateChooserCounts( 1018 ri.activityInfo.packageName, 1019 targetIntent.getAction(), 1020 ri.userHandle); 1021 } 1022 if (DEBUG) { 1023 Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName); 1024 Log.d(TAG, "Action to be updated is " + targetIntent.getAction()); 1025 } 1026 } else if (DEBUG) { 1027 Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo"); 1028 } 1029 } 1030 mIsSuccessfullySelected = true; 1031 } 1032 1033 private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) { 1034 Intent targetIntent = targetInfo.getTargetIntent(); 1035 if (targetIntent == null) { 1036 return; 1037 } 1038 Intent originalTargetIntent = new Intent(mChooserRequest.getTargetIntent()); 1039 // Our TargetInfo implementations add associated component to the intent, let's do the same 1040 // for the sake of the comparison below. 1041 if (targetIntent.getComponent() != null) { 1042 originalTargetIntent.setComponent(targetIntent.getComponent()); 1043 } 1044 // Use filterEquals as a way to check that the primary intent is in use (and not an 1045 // alternative one). For example, an app is sharing an image and a link with mime type 1046 // "image/png" and provides an alternative intent to share only the link with mime type 1047 // "text/uri". Should there be a target that accepts only the latter, the alternative intent 1048 // will be used and we don't want to exclude the link from it. 1049 if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) { 1050 targetIntent.removeExtra(Intent.EXTRA_TEXT); 1051 } 1052 } 1053 1054 private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) { 1055 // Send DS target impression info to AppPredictor, only when user chooses app share. 1056 if (targetInfo.isChooserTargetInfo()) { 1057 return; 1058 } 1059 1060 AppPredictor directShareAppPredictor = getAppPredictor( 1061 mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); 1062 if (directShareAppPredictor == null) { 1063 return; 1064 } 1065 List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo(); 1066 List<AppTargetId> targetIds = new ArrayList<>(); 1067 for (TargetInfo chooserTargetInfo : surfacedTargetInfo) { 1068 ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo(); 1069 if (shortcutInfo != null) { 1070 ComponentName componentName = 1071 chooserTargetInfo.getChooserTargetComponentName(); 1072 targetIds.add(new AppTargetId( 1073 String.format( 1074 "%s/%s/%s", 1075 shortcutInfo.getId(), 1076 componentName.flattenToString(), 1077 SHORTCUT_TARGET))); 1078 } 1079 } 1080 directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds); 1081 } 1082 1083 private void sendClickToAppPredictor(TargetInfo targetInfo) { 1084 if (!targetInfo.isChooserTargetInfo()) { 1085 return; 1086 } 1087 1088 AppPredictor directShareAppPredictor = getAppPredictor( 1089 mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); 1090 if (directShareAppPredictor == null) { 1091 return; 1092 } 1093 AppTarget appTarget = targetInfo.getDirectShareAppTarget(); 1094 if (appTarget != null) { 1095 // This is a direct share click that was provided by the APS 1096 directShareAppPredictor.notifyAppTargetEvent( 1097 new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) 1098 .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE) 1099 .build()); 1100 } 1101 } 1102 1103 @Nullable 1104 private AppPredictor getAppPredictor(UserHandle userHandle) { 1105 ProfileRecord record = getProfileRecord(userHandle); 1106 // We cannot use APS service when clone profile is present as APS service cannot sort 1107 // cross profile targets as of now. 1108 return (record == null || getCloneProfileUserHandle() != null) ? null : record.appPredictor; 1109 } 1110 1111 /** 1112 * Sort intents alphabetically based on display label. 1113 */ 1114 static class AzInfoComparator implements Comparator<DisplayResolveInfo> { 1115 Comparator<DisplayResolveInfo> mComparator; 1116 AzInfoComparator(Context context) { 1117 Collator collator = Collator 1118 .getInstance(context.getResources().getConfiguration().locale); 1119 // Adding two stage comparator, first stage compares using displayLabel, next stage 1120 // compares using resolveInfo.userHandle 1121 mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator) 1122 .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier()); 1123 } 1124 1125 @Override 1126 public int compare( 1127 DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) { 1128 return mComparator.compare(lhsp, rhsp); 1129 } 1130 } 1131 1132 protected EventLog getEventLog() { 1133 if (mEventLog == null) { 1134 mEventLog = new EventLog(); 1135 } 1136 return mEventLog; 1137 } 1138 1139 public class ChooserListController extends ResolverListController { 1140 public ChooserListController( 1141 Context context, 1142 PackageManager pm, 1143 Intent targetIntent, 1144 String referrerPackageName, 1145 int launchedFromUid, 1146 AbstractResolverComparator resolverComparator, 1147 UserHandle queryIntentsAsUser) { 1148 super( 1149 context, 1150 pm, 1151 targetIntent, 1152 referrerPackageName, 1153 launchedFromUid, 1154 resolverComparator, 1155 queryIntentsAsUser); 1156 } 1157 1158 @Override 1159 boolean isComponentFiltered(ComponentName name) { 1160 return mChooserRequest.getFilteredComponentNames().contains(name); 1161 } 1162 1163 @Override 1164 public boolean isComponentPinned(ComponentName name) { 1165 return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); 1166 } 1167 } 1168 1169 @VisibleForTesting 1170 public ChooserGridAdapter createChooserGridAdapter( 1171 Context context, 1172 List<Intent> payloadIntents, 1173 Intent[] initialIntents, 1174 List<ResolveInfo> rList, 1175 boolean filterLastUsed, 1176 UserHandle userHandle, 1177 TargetDataLoader targetDataLoader) { 1178 ChooserListAdapter chooserListAdapter = createChooserListAdapter( 1179 context, 1180 payloadIntents, 1181 initialIntents, 1182 rList, 1183 filterLastUsed, 1184 createListController(userHandle), 1185 userHandle, 1186 getTargetIntent(), 1187 mChooserRequest, 1188 mMaxTargetsPerRow, 1189 targetDataLoader); 1190 1191 return new ChooserGridAdapter( 1192 context, 1193 new ChooserGridAdapter.ChooserActivityDelegate() { 1194 @Override 1195 public boolean shouldShowTabs() { 1196 return ChooserActivity.this.shouldShowTabs(); 1197 } 1198 1199 @Override 1200 public View buildContentPreview(ViewGroup parent) { 1201 return createContentPreviewView(parent); 1202 } 1203 1204 @Override 1205 public void onTargetSelected(int itemIndex) { 1206 startSelected(itemIndex, false, true); 1207 } 1208 1209 @Override 1210 public void onTargetLongPressed(int selectedPosition) { 1211 final TargetInfo longPressedTargetInfo = 1212 mChooserMultiProfilePagerAdapter 1213 .getActiveListAdapter() 1214 .targetInfoForPosition( 1215 selectedPosition, /* filtered= */ true); 1216 // Only a direct share target or an app target is expected 1217 if (longPressedTargetInfo.isDisplayResolveInfo() 1218 || longPressedTargetInfo.isSelectableTargetInfo()) { 1219 showTargetDetails(longPressedTargetInfo); 1220 } 1221 } 1222 1223 @Override 1224 public void updateProfileViewButton(View newButtonFromProfileRow) { 1225 mProfileView = newButtonFromProfileRow; 1226 mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); 1227 ChooserActivity.this.updateProfileViewButton(); 1228 } 1229 }, 1230 chooserListAdapter, 1231 shouldShowContentPreview(), 1232 mMaxTargetsPerRow); 1233 } 1234 1235 @VisibleForTesting 1236 public ChooserListAdapter createChooserListAdapter( 1237 Context context, 1238 List<Intent> payloadIntents, 1239 Intent[] initialIntents, 1240 List<ResolveInfo> rList, 1241 boolean filterLastUsed, 1242 ResolverListController resolverListController, 1243 UserHandle userHandle, 1244 Intent targetIntent, 1245 ChooserRequestParameters chooserRequest, 1246 int maxTargetsPerRow, 1247 TargetDataLoader targetDataLoader) { 1248 UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() 1249 && userHandle.equals(getPersonalProfileUserHandle()) 1250 ? getCloneProfileUserHandle() : userHandle; 1251 return new ChooserListAdapter( 1252 context, 1253 payloadIntents, 1254 initialIntents, 1255 rList, 1256 filterLastUsed, 1257 createListController(userHandle), 1258 userHandle, 1259 targetIntent, 1260 this, 1261 context.getPackageManager(), 1262 getEventLog(), 1263 chooserRequest, 1264 maxTargetsPerRow, 1265 initialIntentsUserSpace, 1266 targetDataLoader); 1267 } 1268 1269 @Override 1270 protected void onWorkProfileStatusUpdated() { 1271 UserHandle workUser = getWorkProfileUserHandle(); 1272 ProfileRecord record = workUser == null ? null : getProfileRecord(workUser); 1273 if (record != null && record.shortcutLoader != null) { 1274 record.shortcutLoader.reset(); 1275 } 1276 super.onWorkProfileStatusUpdated(); 1277 } 1278 1279 @Override 1280 @VisibleForTesting 1281 protected ChooserListController createListController(UserHandle userHandle) { 1282 AppPredictor appPredictor = getAppPredictor(userHandle); 1283 AbstractResolverComparator resolverComparator; 1284 if (appPredictor != null) { 1285 resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(), 1286 getReferrerPackageName(), appPredictor, userHandle, getEventLog(), 1287 getIntegratedDeviceComponents().getNearbySharingComponent()); 1288 } else { 1289 resolverComparator = 1290 new ResolverRankerServiceResolverComparator( 1291 this, 1292 getTargetIntent(), 1293 getReferrerPackageName(), 1294 null, 1295 getEventLog(), 1296 getResolverRankerServiceUserHandleList(userHandle), 1297 getIntegratedDeviceComponents().getNearbySharingComponent()); 1298 } 1299 1300 return new ChooserListController( 1301 this, 1302 mPm, 1303 getTargetIntent(), 1304 getReferrerPackageName(), 1305 getAnnotatedUserHandles().userIdOfCallingApp, 1306 resolverComparator, 1307 getQueryIntentsUser(userHandle)); 1308 } 1309 1310 @VisibleForTesting 1311 protected ViewModelProvider.Factory createPreviewViewModelFactory() { 1312 return PreviewViewModel.Companion.getFactory(); 1313 } 1314 1315 private ChooserActionFactory createChooserActionFactory() { 1316 return new ChooserActionFactory( 1317 this, 1318 mChooserRequest, 1319 mIntegratedDeviceComponents, 1320 getEventLog(), 1321 (isExcluded) -> mExcludeSharedText = isExcluded, 1322 this::getFirstVisibleImgPreviewView, 1323 new ChooserActionFactory.ActionActivityStarter() { 1324 @Override 1325 public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { 1326 safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle()); 1327 finish(); 1328 } 1329 1330 @Override 1331 public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( 1332 TargetInfo targetInfo, View sharedElement, String sharedElementName) { 1333 ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( 1334 ChooserActivity.this, sharedElement, sharedElementName); 1335 safelyStartActivityAsUser( 1336 targetInfo, getPersonalProfileUserHandle(), options.toBundle()); 1337 // Can't finish right away because the shared element transition may not 1338 // be ready to start. 1339 mFinishWhenStopped = true; 1340 1341 } 1342 }, 1343 (status) -> { 1344 if (status != null) { 1345 setResult(status); 1346 } 1347 finish(); 1348 }); 1349 } 1350 1351 /* 1352 * Need to dynamically adjust how many icons can fit per row before we add them, 1353 * which also means setting the correct offset to initially show the content 1354 * preview area + 2 rows of targets 1355 */ 1356 private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 1357 int oldTop, int oldRight, int oldBottom) { 1358 if (mChooserMultiProfilePagerAdapter == null) { 1359 return; 1360 } 1361 RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); 1362 ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter(); 1363 // Skip height calculation if recycler view was scrolled to prevent it inaccurately 1364 // calculating the height, as the logic below does not account for the scrolled offset. 1365 if (gridAdapter == null || recyclerView == null 1366 || recyclerView.computeVerticalScrollOffset() != 0) { 1367 return; 1368 } 1369 1370 final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight(); 1371 boolean isLayoutUpdated = 1372 gridAdapter.calculateChooserTargetWidth(availableWidth) 1373 || recyclerView.getAdapter() == null 1374 || availableWidth != mCurrAvailableWidth; 1375 1376 boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets); 1377 1378 if (isLayoutUpdated 1379 || insetsChanged 1380 || mLastNumberOfChildren != recyclerView.getChildCount()) { 1381 mCurrAvailableWidth = availableWidth; 1382 if (isLayoutUpdated) { 1383 // It is very important we call setAdapter from here. Otherwise in some cases 1384 // the resolver list doesn't get populated, such as b/150922090, b/150918223 1385 // and b/150936654 1386 recyclerView.setAdapter(gridAdapter); 1387 ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount( 1388 mMaxTargetsPerRow); 1389 1390 updateTabPadding(); 1391 } 1392 1393 UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle(); 1394 int currentProfile = getProfileForUser(currentUserHandle); 1395 int initialProfile = findSelectedProfile(); 1396 if (currentProfile != initialProfile) { 1397 return; 1398 } 1399 1400 if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) { 1401 return; 1402 } 1403 1404 getMainThreadHandler().post(() -> { 1405 if (mResolverDrawerLayout == null || gridAdapter == null) { 1406 return; 1407 } 1408 int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter); 1409 mResolverDrawerLayout.setCollapsibleHeightReserved(offset); 1410 mEnterTransitionAnimationDelegate.markOffsetCalculated(); 1411 mLastAppliedInsets = mSystemWindowInsets; 1412 }); 1413 } 1414 } 1415 1416 private int calculateDrawerOffset( 1417 int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) { 1418 1419 int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; 1420 int rowsToShow = gridAdapter.getSystemRowCount() 1421 + gridAdapter.getProfileRowCount() 1422 + gridAdapter.getServiceTargetRowCount() 1423 + gridAdapter.getCallerAndRankedTargetRowCount(); 1424 1425 // then this is most likely not a SEND_* action, so check 1426 // the app target count 1427 if (rowsToShow == 0) { 1428 rowsToShow = gridAdapter.getRowCount(); 1429 } 1430 1431 // still zero? then use a default height and leave, which 1432 // can happen when there are no targets to show 1433 if (rowsToShow == 0 && !shouldShowStickyContentPreview()) { 1434 offset += getResources().getDimensionPixelSize( 1435 R.dimen.chooser_max_collapsed_height); 1436 return offset; 1437 } 1438 1439 View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container); 1440 if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) { 1441 offset += stickyContentPreview.getHeight(); 1442 } 1443 1444 if (shouldShowTabs()) { 1445 offset += findViewById(com.android.internal.R.id.tabs).getHeight(); 1446 } 1447 1448 if (recyclerView.getVisibility() == View.VISIBLE) { 1449 rowsToShow = Math.min(4, rowsToShow); 1450 boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow); 1451 mLastNumberOfChildren = recyclerView.getChildCount(); 1452 for (int i = 0, childCount = recyclerView.getChildCount(); 1453 i < childCount && rowsToShow > 0; i++) { 1454 View child = recyclerView.getChildAt(i); 1455 if (((GridLayoutManager.LayoutParams) 1456 child.getLayoutParams()).getSpanIndex() != 0) { 1457 continue; 1458 } 1459 int height = child.getHeight(); 1460 offset += height; 1461 if (shouldShowExtraRow) { 1462 offset += height; 1463 } 1464 rowsToShow--; 1465 } 1466 } else { 1467 ViewGroup currentEmptyStateView = getActiveEmptyStateView(); 1468 if (currentEmptyStateView.getVisibility() == View.VISIBLE) { 1469 offset += currentEmptyStateView.getHeight(); 1470 } 1471 } 1472 1473 return Math.min(offset, bottom - top); 1474 } 1475 1476 /** 1477 * If we have a tabbed view and are showing 1 row in the current profile and an empty 1478 * state screen in the other profile, to prevent cropping of the empty state screen we show 1479 * a second row in the current profile. 1480 */ 1481 private boolean shouldShowExtraRow(int rowsToShow) { 1482 return shouldShowTabs() 1483 && rowsToShow == 1 1484 && mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen( 1485 mChooserMultiProfilePagerAdapter.getInactiveListAdapter()); 1486 } 1487 1488 /** 1489 * Returns {@link #PROFILE_WORK}, if the given user handle matches work user handle. 1490 * Returns {@link #PROFILE_PERSONAL}, otherwise. 1491 **/ 1492 private int getProfileForUser(UserHandle currentUserHandle) { 1493 if (currentUserHandle.equals(getWorkProfileUserHandle())) { 1494 return PROFILE_WORK; 1495 } 1496 // We return personal profile, as it is the default when there is no work profile, personal 1497 // profile represents rootUser, clonedUser & secondaryUser, covering all use cases. 1498 return PROFILE_PERSONAL; 1499 } 1500 1501 private ViewGroup getActiveEmptyStateView() { 1502 int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage(); 1503 return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage); 1504 } 1505 1506 @Override // ResolverListCommunicator 1507 public void onHandlePackagesChanged(ResolverListAdapter listAdapter) { 1508 mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged(); 1509 super.onHandlePackagesChanged(listAdapter); 1510 } 1511 1512 @Override 1513 public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { 1514 setupScrollListener(); 1515 maybeSetupGlobalLayoutListener(); 1516 1517 ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; 1518 if (chooserListAdapter.getUserHandle() 1519 .equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) { 1520 mChooserMultiProfilePagerAdapter.getActiveAdapterView() 1521 .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()); 1522 mChooserMultiProfilePagerAdapter 1523 .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage()); 1524 } 1525 1526 if (chooserListAdapter.getDisplayResolveInfoCount() == 0) { 1527 chooserListAdapter.notifyDataSetChanged(); 1528 } else { 1529 chooserListAdapter.updateAlphabeticalList(); 1530 } 1531 1532 if (rebuildComplete) { 1533 long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listAdapter.getUserHandle()); 1534 if (duration >= 0) { 1535 Log.d(TAG, "app target loading time " + duration + " ms"); 1536 } 1537 addCallerChooserTargets(); 1538 getEventLog().logSharesheetAppLoadComplete(); 1539 maybeQueryAdditionalPostProcessingTargets(chooserListAdapter); 1540 mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET); 1541 } 1542 } 1543 1544 private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) { 1545 UserHandle userHandle = chooserListAdapter.getUserHandle(); 1546 ProfileRecord record = getProfileRecord(userHandle); 1547 if (record == null || record.shortcutLoader == null) { 1548 return; 1549 } 1550 record.loadingStartTime = SystemClock.elapsedRealtime(); 1551 record.shortcutLoader.updateAppTargets(chooserListAdapter.getDisplayResolveInfos()); 1552 } 1553 1554 @MainThread 1555 private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) { 1556 if (DEBUG) { 1557 Log.d(TAG, "onShortcutsLoaded for user: " + userHandle); 1558 } 1559 mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache()); 1560 mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache()); 1561 ChooserListAdapter adapter = 1562 mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle); 1563 if (adapter != null) { 1564 for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) { 1565 adapter.addServiceResults( 1566 resultInfo.getAppTarget(), 1567 resultInfo.getShortcuts(), 1568 result.isFromAppPredictor() 1569 ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE 1570 : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, 1571 mDirectShareShortcutInfoCache, 1572 mDirectShareAppTargetCache); 1573 } 1574 adapter.completeServiceTargetLoading(); 1575 } 1576 1577 if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) { 1578 long duration = Tracer.INSTANCE.endLaunchToShortcutTrace(); 1579 if (duration >= 0) { 1580 Log.d(TAG, "stat to first shortcut time: " + duration + " ms"); 1581 } 1582 } 1583 logDirectShareTargetReceived(userHandle); 1584 sendVoiceChoicesIfNeeded(); 1585 getEventLog().logSharesheetDirectLoadComplete(); 1586 } 1587 1588 private void setupScrollListener() { 1589 if (mResolverDrawerLayout == null) { 1590 return; 1591 } 1592 int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header; 1593 final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId); 1594 final float defaultElevation = elevatedView.getElevation(); 1595 final float chooserHeaderScrollElevation = 1596 getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation); 1597 mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener( 1598 new RecyclerView.OnScrollListener() { 1599 public void onScrollStateChanged(RecyclerView view, int scrollState) { 1600 if (scrollState == RecyclerView.SCROLL_STATE_IDLE) { 1601 if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) { 1602 mScrollStatus = SCROLL_STATUS_IDLE; 1603 setHorizontalScrollingEnabled(true); 1604 } 1605 } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) { 1606 if (mScrollStatus == SCROLL_STATUS_IDLE) { 1607 mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL; 1608 setHorizontalScrollingEnabled(false); 1609 } 1610 } 1611 } 1612 1613 public void onScrolled(RecyclerView view, int dx, int dy) { 1614 if (view.getChildCount() > 0) { 1615 View child = view.getLayoutManager().findViewByPosition(0); 1616 if (child == null || child.getTop() < 0) { 1617 elevatedView.setElevation(chooserHeaderScrollElevation); 1618 return; 1619 } 1620 } 1621 1622 elevatedView.setElevation(defaultElevation); 1623 } 1624 }); 1625 } 1626 1627 private void maybeSetupGlobalLayoutListener() { 1628 if (shouldShowTabs()) { 1629 return; 1630 } 1631 final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); 1632 recyclerView.getViewTreeObserver() 1633 .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 1634 @Override 1635 public void onGlobalLayout() { 1636 // Fixes an issue were the accessibility border disappears on list creation. 1637 recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 1638 final TextView titleView = findViewById(com.android.internal.R.id.title); 1639 if (titleView != null) { 1640 titleView.setFocusable(true); 1641 titleView.setFocusableInTouchMode(true); 1642 titleView.requestFocus(); 1643 titleView.requestAccessibilityFocus(); 1644 } 1645 } 1646 }); 1647 } 1648 1649 /** 1650 * The sticky content preview is shown only when we have a tabbed view. It's shown above 1651 * the tabs so it is not part of the scrollable list. If we are not in tabbed view, 1652 * we instead show the content preview as a regular list item. 1653 */ 1654 private boolean shouldShowStickyContentPreview() { 1655 return shouldShowStickyContentPreviewNoOrientationCheck(); 1656 } 1657 1658 private boolean shouldShowStickyContentPreviewNoOrientationCheck() { 1659 return shouldShowTabs() 1660 && (mMultiProfilePagerAdapter.getListAdapterForUserHandle( 1661 UserHandle.of(UserHandle.myUserId())).getCount() > 0 1662 || shouldShowContentPreviewWhenEmpty()) 1663 && shouldShowContentPreview(); 1664 } 1665 1666 /** 1667 * This method could be used to override the default behavior when we hide the preview area 1668 * when the current tab doesn't have any items. 1669 * 1670 * @return true if we want to show the content preview area even if the tab for the current 1671 * user is empty 1672 */ 1673 protected boolean shouldShowContentPreviewWhenEmpty() { 1674 return false; 1675 } 1676 1677 /** 1678 * @return true if we want to show the content preview area 1679 */ 1680 protected boolean shouldShowContentPreview() { 1681 return (mChooserRequest != null) && mChooserRequest.isSendActionTarget(); 1682 } 1683 1684 private void updateStickyContentPreview() { 1685 if (shouldShowStickyContentPreviewNoOrientationCheck()) { 1686 // The sticky content preview is only shown when we show the work and personal tabs. 1687 // We don't show it in landscape as otherwise there is no room for scrolling. 1688 // If the sticky content preview will be shown at some point with orientation change, 1689 // then always preload it to avoid subsequent resizing of the share sheet. 1690 ViewGroup contentPreviewContainer = 1691 findViewById(com.android.internal.R.id.content_preview_container); 1692 if (contentPreviewContainer.getChildCount() == 0) { 1693 ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer); 1694 contentPreviewContainer.addView(contentPreviewView); 1695 } 1696 } 1697 if (shouldShowStickyContentPreview()) { 1698 showStickyContentPreview(); 1699 } else { 1700 hideStickyContentPreview(); 1701 } 1702 } 1703 1704 private void showStickyContentPreview() { 1705 if (isStickyContentPreviewShowing()) { 1706 return; 1707 } 1708 ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); 1709 contentPreviewContainer.setVisibility(View.VISIBLE); 1710 } 1711 1712 private boolean isStickyContentPreviewShowing() { 1713 ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); 1714 return contentPreviewContainer.getVisibility() == View.VISIBLE; 1715 } 1716 1717 private void hideStickyContentPreview() { 1718 if (!isStickyContentPreviewShowing()) { 1719 return; 1720 } 1721 ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); 1722 contentPreviewContainer.setVisibility(View.GONE); 1723 } 1724 1725 private View findRootView() { 1726 if (mContentView == null) { 1727 mContentView = findViewById(android.R.id.content); 1728 } 1729 return mContentView; 1730 } 1731 1732 /** 1733 * Intentionally override the {@link ResolverActivity} implementation as we only need that 1734 * implementation for the intent resolver case. 1735 */ 1736 @Override 1737 public void onButtonClick(View v) {} 1738 1739 /** 1740 * Intentionally override the {@link ResolverActivity} implementation as we only need that 1741 * implementation for the intent resolver case. 1742 */ 1743 @Override 1744 protected void resetButtonBar() {} 1745 1746 @Override 1747 protected String getMetricsCategory() { 1748 return METRICS_CATEGORY_CHOOSER; 1749 } 1750 1751 @Override 1752 protected void onProfileTabSelected() { 1753 // This fixes an edge case where after performing a variety of gestures, vertical scrolling 1754 // ends up disabled. That's because at some point the old tab's vertical scrolling is 1755 // disabled and the new tab's is enabled. For context, see b/159997845 1756 setVerticalScrollEnabled(true); 1757 if (mResolverDrawerLayout != null) { 1758 mResolverDrawerLayout.scrollNestedScrollableChildBackToTop(); 1759 } 1760 } 1761 1762 @Override 1763 protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { 1764 if (shouldShowTabs()) { 1765 mChooserMultiProfilePagerAdapter 1766 .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom()); 1767 mChooserMultiProfilePagerAdapter.setupContainerPadding( 1768 getActiveEmptyStateView().findViewById(com.android.internal.R.id.resolver_empty_state_container)); 1769 } 1770 1771 WindowInsets result = super.onApplyWindowInsets(v, insets); 1772 if (mResolverDrawerLayout != null) { 1773 mResolverDrawerLayout.requestLayout(); 1774 } 1775 return result; 1776 } 1777 1778 private void setHorizontalScrollingEnabled(boolean enabled) { 1779 ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); 1780 viewPager.setSwipingEnabled(enabled); 1781 } 1782 1783 private void setVerticalScrollEnabled(boolean enabled) { 1784 ChooserGridLayoutManager layoutManager = 1785 (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView() 1786 .getLayoutManager(); 1787 layoutManager.setVerticalScrollEnabled(enabled); 1788 } 1789 1790 @Override 1791 void onHorizontalSwipeStateChanged(int state) { 1792 if (state == ViewPager.SCROLL_STATE_DRAGGING) { 1793 if (mScrollStatus == SCROLL_STATUS_IDLE) { 1794 mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL; 1795 setVerticalScrollEnabled(false); 1796 } 1797 } else if (state == ViewPager.SCROLL_STATE_IDLE) { 1798 if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) { 1799 mScrollStatus = SCROLL_STATUS_IDLE; 1800 setVerticalScrollEnabled(true); 1801 } 1802 } 1803 } 1804 1805 @Override 1806 protected void maybeLogProfileChange() { 1807 getEventLog().logSharesheetProfileChanged(); 1808 } 1809 1810 private static class ProfileRecord { 1811 /** The {@link AppPredictor} for this profile, if any. */ 1812 @Nullable 1813 public final AppPredictor appPredictor; 1814 /** 1815 * null if we should not load shortcuts. 1816 */ 1817 @Nullable 1818 public final ShortcutLoader shortcutLoader; 1819 public long loadingStartTime; 1820 1821 private ProfileRecord( 1822 @Nullable AppPredictor appPredictor, 1823 @Nullable ShortcutLoader shortcutLoader) { 1824 this.appPredictor = appPredictor; 1825 this.shortcutLoader = shortcutLoader; 1826 } 1827 1828 public void destroy() { 1829 if (appPredictor != null) { 1830 appPredictor.destroy(); 1831 } 1832 } 1833 } 1834 } 1835