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