/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.intentresolver; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Insets; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; import android.provider.DeviceConfig; import android.service.chooser.ChooserTarget; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.LinearInterpolator; import android.widget.TextView; import androidx.annotation.MainThread; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.flags.Flags; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.grid.DirectShareViewHolder; import com.android.intentresolver.model.AbstractResolverComparator; import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.shortcuts.AppPredictorFactory; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; /** * The Chooser Activity handles intent resolution specifically for sharing intents - * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}. * */ public class ChooserActivity extends ResolverActivity implements ResolverListAdapter.ResolverListCommunicator { private static final String TAG = "ChooserActivity"; /** * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself * in onStop when launched in a new task. If this extra is set to true, we do not finish * ourselves when onStop gets called. */ public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP"; /** * Transition name for the first image preview. * To be used for shared element transition into this activity. * @hide */ public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image"; private static final String PREF_NUM_SHEET_EXPANSIONS = "pref_num_sheet_expansions"; private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label"; private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon"; private static final boolean DEBUG = true; public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; private static final String SHORTCUT_TARGET = "shortcut_target"; private static final String PLURALS_COUNT = "count"; private static final String PLURALS_FILE_NAME = "file_name"; private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image"; // TODO: these data structures are for one-time use in shuttling data from where they're // populated in `ShortcutToChooserTargetConverter` to where they're consumed in // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`. // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their // intermediate data, and then these members can be removed. private final Map mDirectShareAppTargetCache = new HashMap<>(); private final Map mDirectShareShortcutInfoCache = new HashMap<>(); public static final int TARGET_TYPE_DEFAULT = 0; public static final int TARGET_TYPE_CHOOSER_TARGET = 1; public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2; public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3; private static final int SCROLL_STATUS_IDLE = 0; private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1; private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = { TARGET_TYPE_DEFAULT, TARGET_TYPE_CHOOSER_TARGET, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE }) @Retention(RetentionPolicy.SOURCE) public @interface ShareTargetType {} public static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f; private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7; private final int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS, DEFAULT_SALT_EXPIRATION_DAYS); private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents; /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the * only assignment there, and expect it to be ready by the time we ever use it -- * someday if we move all the usage to a component with a narrower lifecycle (something that * matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we * should be able to make this assignment as "final." */ @Nullable private ChooserRequestParameters mChooserRequest; private ChooserRefinementManager mRefinementManager; private FeatureFlagRepository mFeatureFlagRepository; private ChooserContentPreviewUi mChooserContentPreviewUi; private boolean mShouldDisplayLandscape; // statsd logger wrapper protected ChooserActivityLogger mChooserActivityLogger; private long mChooserShownTime; protected boolean mIsSuccessfullySelected; private int mCurrAvailableWidth = 0; private Insets mLastAppliedInsets = null; private int mLastNumberOfChildren = -1; private int mMaxTargetsPerRow = 1; private static final int MAX_LOG_RANK_POSITION = 12; // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters. private static final int MAX_EXTRA_INITIAL_INTENTS = 2; private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; private SharedPreferences mPinnedSharedPrefs; private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings"; private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5); private int mScrollStatus = SCROLL_STATUS_IDLE; @VisibleForTesting protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter; private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate = new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout); private View mContentView = null; private final SparseArray mProfileRecords = new SparseArray<>(); private boolean mExcludeSharedText = false; public ChooserActivity() {} @Override protected void onCreate(Bundle savedInstanceState) { final long intentReceivedTime = System.currentTimeMillis(); mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); getChooserActivityLogger().logSharesheetTriggered(); mFeatureFlagRepository = createFeatureFlagRepository(); mIntegratedDeviceComponents = getIntegratedDeviceComponents(); try { mChooserRequest = new ChooserRequestParameters( getIntent(), getReferrerPackageName(), getReferrer(), mIntegratedDeviceComponents, mFeatureFlagRepository); } catch (IllegalArgumentException e) { Log.e(TAG, "Caller provided invalid Chooser request parameters", e); finish(); super_onCreate(null); return; } mRefinementManager = new ChooserRefinementManager( this, mChooserRequest.getRefinementIntentSender(), (validatedRefinedTarget) -> { maybeRemoveSharedText(validatedRefinedTarget); if (super.onTargetSelected(validatedRefinedTarget, false)) { finish(); } }, () -> { mRefinementManager.destroy(); finish(); }); mChooserContentPreviewUi = new ChooserContentPreviewUi( mChooserRequest.getTargetIntent(), getContentResolver(), this::isImageType, createPreviewImageLoader(), createChooserActionFactory(), mEnterTransitionAnimationDelegate, mFeatureFlagRepository); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); setSafeForwardingMode(true); mPinnedSharedPrefs = getPinnedSharedPrefs(this); mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); mShouldDisplayLandscape = shouldDisplayLandscape(getResources().getConfiguration().orientation); setRetainInOnStop(mChooserRequest.shouldRetainInOnStop()); createProfileRecords( new AppPredictorFactory( getApplicationContext(), mChooserRequest.getSharedText(), mChooserRequest.getTargetIntentFilter()), mChooserRequest.getTargetIntentFilter()); super.onCreate( savedInstanceState, mChooserRequest.getTargetIntent(), mChooserRequest.getTitle(), mChooserRequest.getDefaultTitleResource(), mChooserRequest.getInitialIntents(), /* rList: List = */ null, /* supportsAlwaysUseOption = */ false); mChooserShownTime = System.currentTimeMillis(); final long systemCost = mChooserShownTime - intentReceivedTime; getChooserActivityLogger().logChooserActivityShown( isWorkProfile(), mChooserRequest.getTargetType(), systemCost); if (mResolverDrawerLayout != null) { mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); // expand/shrink direct share 4 -> 8 viewgroup if (mChooserRequest.isSendActionTarget()) { mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll); } mResolverDrawerLayout.setOnCollapsedChangedListener( new ResolverDrawerLayout.OnCollapsedChangedListener() { // Only consider one expansion per activity creation private boolean mWrittenOnce = false; @Override public void onCollapsedChanged(boolean isCollapsed) { if (!isCollapsed && !mWrittenOnce) { incrementNumSheetExpansions(); mWrittenOnce = true; } getChooserActivityLogger() .logSharesheetExpansionChanged(isCollapsed); } }); } if (DEBUG) { Log.d(TAG, "System Time Cost is " + systemCost); } getChooserActivityLogger().logShareStarted( getReferrerPackageName(), mChooserRequest.getTargetType(), mChooserRequest.getCallerChooserTargets().size(), (mChooserRequest.getInitialIntents() == null) ? 0 : mChooserRequest.getInitialIntents().length, isWorkProfile(), mChooserContentPreviewUi.getPreferredContentPreview(), mChooserRequest.getTargetAction(), mChooserRequest.getChooserActions().size(), mChooserRequest.getModifyShareAction() != null ); mEnterTransitionAnimationDelegate.postponeTransition(); } @VisibleForTesting protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { return ChooserIntegratedDeviceComponents.get(this, new SecureSettings()); } @Override protected int appliedThemeResId() { return R.style.Theme_DeviceDefault_Chooser; } protected FeatureFlagRepository createFeatureFlagRepository() { return new FeatureFlagRepositoryFactory().create(getApplicationContext()); } private void createProfileRecords( AppPredictorFactory factory, IntentFilter targetIntentFilter) { UserHandle mainUserHandle = getPersonalProfileUserHandle(); createProfileRecord(mainUserHandle, targetIntentFilter, factory); UserHandle workUserHandle = getWorkProfileUserHandle(); if (workUserHandle != null) { createProfileRecord(workUserHandle, targetIntentFilter, factory); } } private void createProfileRecord( UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) { AppPredictor appPredictor = factory.create(userHandle); ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() ? null : createShortcutLoader( getApplicationContext(), appPredictor, userHandle, targetIntentFilter, shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult)); mProfileRecords.put( userHandle.getIdentifier(), new ProfileRecord(appPredictor, shortcutLoader)); } @Nullable private ProfileRecord getProfileRecord(UserHandle userHandle) { return mProfileRecords.get(userHandle.getIdentifier(), null); } @VisibleForTesting protected ShortcutLoader createShortcutLoader( Context context, AppPredictor appPredictor, UserHandle userHandle, IntentFilter targetIntentFilter, Consumer callback) { return new ShortcutLoader( context, appPredictor, userHandle, targetIntentFilter, callback); } static SharedPreferences getPinnedSharedPrefs(Context context) { // The code below is because in the android:ui process, no one can hear you scream. // The package info in the context isn't initialized in the way it is for normal apps, // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we // build the path manually below using the same policy that appears in ContextImpl. // This fails silently under the hood if there's a problem, so if we find ourselves in // the case where we don't have access to credential encrypted storage we just won't // have our pinned target info. final File prefsFile = new File(new File( Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL, context.getUserId(), context.getPackageName()), "shared_prefs"), PINNED_SHARED_PREFS_NAME + ".xml"); return context.getSharedPreferences(prefsFile, MODE_PRIVATE); } @Override protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( Intent[] initialIntents, List rList, boolean filterLastUsed) { if (shouldShowTabs()) { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles( initialIntents, rList, filterLastUsed); } else { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile( initialIntents, rList, filterLastUsed); } return mChooserMultiProfilePagerAdapter; } @Override protected EmptyStateProvider createBlockerEmptyStateProvider() { final boolean isSendAction = mChooserRequest.isSendActionTarget(); final EmptyState noWorkToPersonalEmptyState = new DevicePolicyBlockerEmptyState( /* context= */ this, /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, /* devicePolicyStringSubtitleId= */ isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL, /* defaultSubtitleResource= */ isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation : R.string.resolver_cant_access_personal_apps_explanation, /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); final EmptyState noPersonalToWorkEmptyState = new DevicePolicyBlockerEmptyState( /* context= */ this, /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, /* devicePolicyStringSubtitleId= */ isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK, /* defaultSubtitleResource= */ isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation : R.string.resolver_cant_access_work_apps_explanation, /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, createCrossProfileIntentsChecker(), createMyUserIdProvider()); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List rList, boolean filterLastUsed) { ChooserGridAdapter adapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, initialIntents, rList, filterLastUsed, /* userHandle */ UserHandle.of(UserHandle.myUserId())); return new ChooserMultiProfilePagerAdapter( /* context */ this, adapter, createEmptyStateProvider(/* workProfileUserHandle= */ null), /* workProfileQuietModeChecker= */ () -> false, /* workProfileUserHandle= */ null, mMaxTargetsPerRow); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List rList, boolean filterLastUsed) { int selectedProfile = findSelectedProfile(); ChooserGridAdapter personalAdapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, selectedProfile == PROFILE_PERSONAL ? initialIntents : null, rList, filterLastUsed, /* userHandle */ getPersonalProfileUserHandle()); ChooserGridAdapter workAdapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, selectedProfile == PROFILE_WORK ? initialIntents : null, rList, filterLastUsed, /* userHandle */ getWorkProfileUserHandle()); return new ChooserMultiProfilePagerAdapter( /* context */ this, personalAdapter, workAdapter, createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()), () -> mWorkProfileAvailability.isQuietModeEnabled(), selectedProfile, getWorkProfileUserHandle(), mMaxTargetsPerRow); } private int findSelectedProfile() { int selectedProfile = getSelectedProfileExtra(); if (selectedProfile == -1) { selectedProfile = getProfileForUser(getUser()); } return selectedProfile; } @Override protected boolean postRebuildList(boolean rebuildCompleted) { updateStickyContentPreview(); if (shouldShowStickyContentPreview() || mChooserMultiProfilePagerAdapter .getCurrentRootAdapter().getSystemRowCount() != 0) { getChooserActivityLogger().logActionShareWithPreview( mChooserContentPreviewUi.getPreferredContentPreview()); } return postRebuildListInternal(rebuildCompleted); } /** * Check if the profile currently used is a work profile. * @return true if it is work profile, false if it is parent profile (or no work profile is * set up) */ protected boolean isWorkProfile() { return getSystemService(UserManager.class) .getUserInfo(UserHandle.myUserId()).isManagedProfile(); } @Override protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { return new PackageMonitor() { @Override public void onSomePackagesChanged() { handlePackagesChanged(listAdapter); } }; } /** * Update UI to reflect changes in data. */ public void handlePackagesChanged() { handlePackagesChanged(/* listAdapter */ null); } /** * Update UI to reflect changes in data. *

If {@code listAdapter} is {@code null}, both profile list adapters are updated if * available. */ private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) { // Refresh pinned items mPinnedSharedPrefs = getPinnedSharedPrefs(this); if (listAdapter == null) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); if (mChooserMultiProfilePagerAdapter.getCount() > 1) { mChooserMultiProfilePagerAdapter.getInactiveListAdapter().handlePackagesChanged(); } } else { listAdapter.handlePackagesChanged(); } updateProfileViewButton(); } @Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); maybeCancelFinishAnimation(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); if (viewPager.isLayoutRtl()) { mMultiProfilePagerAdapter.setupViewPager(viewPager); } mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation); mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow); adjustPreviewWidth(newConfig.orientation, null); updateStickyContentPreview(); updateTabPadding(); } private boolean shouldDisplayLandscape(int orientation) { // Sharesheet fixes the # of items per row and therefore can not correctly lay out // when in the restricted size of multi-window mode. In the future, would be nice // to use minimum dp size requirements instead return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode(); } private void adjustPreviewWidth(int orientation, View parent) { int width = -1; if (mShouldDisplayLandscape) { width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width); } parent = parent == null ? getWindow().getDecorView() : parent; updateLayoutWidth(com.android.internal.R.id.content_preview_text_layout, width, parent); updateLayoutWidth(com.android.internal.R.id.content_preview_title_layout, width, parent); updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent); } private void updateTabPadding() { if (shouldShowTabs()) { View tabs = findViewById(com.android.internal.R.id.tabs); float iconSize = getResources().getDimension(R.dimen.chooser_icon_size); // The entire width consists of icons or padding. Divide the item padding in half to get // paddingHorizontal. float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize) / mMaxTargetsPerRow / 2; // Subtract the margin the buttons already have. padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin); tabs.setPadding((int) padding, 0, (int) padding, 0); } } private void updateLayoutWidth(int layoutResourceId, int width, View parent) { View view = parent.findViewById(layoutResourceId); if (view != null && view.getLayoutParams() != null) { LayoutParams params = view.getLayoutParams(); params.width = width; view.setLayoutParams(params); } } /** * Create a view that will be shown in the content preview area * @param parent reference to the parent container where the view should be attached to * @return content preview view */ protected ViewGroup createContentPreviewView(ViewGroup parent) { ViewGroup layout = mChooserContentPreviewUi.displayContentPreview( getResources(), getLayoutInflater(), parent); if (layout != null) { adjustPreviewWidth(getResources().getConfiguration().orientation, layout); } return layout; } @Nullable private View getFirstVisibleImgPreviewView() { View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large); return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null; } /** * Wrapping the ContentResolver call to expose for easier mocking, * and to avoid mocking Android core classes. */ @VisibleForTesting public Cursor queryResolver(ContentResolver resolver, Uri uri) { return resolver.query(uri, null, null, null, null); } @VisibleForTesting protected boolean isImageType(String mimeType) { return mimeType != null && mimeType.startsWith("image/"); } private int getNumSheetExpansions() { return getPreferences(Context.MODE_PRIVATE).getInt(PREF_NUM_SHEET_EXPANSIONS, 0); } private void incrementNumSheetExpansions() { getPreferences(Context.MODE_PRIVATE).edit().putInt(PREF_NUM_SHEET_EXPANSIONS, getNumSheetExpansions() + 1).apply(); } @Override protected void onStop() { super.onStop(); if (maybeCancelFinishAnimation()) { finish(); } } @Override protected void onDestroy() { super.onDestroy(); if (isFinishing()) { mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); } if (mRefinementManager != null) { // TODO: null-checked in case of early-destroy, or skip? mRefinementManager.destroy(); mRefinementManager = null; } mBackgroundThreadPoolExecutor.shutdownNow(); destroyProfileRecords(); } private void destroyProfileRecords() { for (int i = 0; i < mProfileRecords.size(); ++i) { mProfileRecords.valueAt(i).destroy(); } mProfileRecords.clear(); } @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { if (mChooserRequest == null) { return defIntent; } Intent result = defIntent; if (mChooserRequest.getReplacementExtras() != null) { final Bundle replExtras = mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName); if (replExtras != null) { result = new Intent(defIntent); result.putExtras(replExtras); } } if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT) || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) { result = Intent.createChooser(result, getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE)); // Don't auto-launch single intents if the intent is being forwarded. This is done // because automatically launching a resolving application as a response to the user // action of switching accounts is pretty unexpected. result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); } return result; } @Override public void onActivityStarted(TargetInfo cti) { if (mChooserRequest.getChosenComponentSender() != null) { final ComponentName target = cti.getResolvedComponentName(); if (target != null) { final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target); try { mChooserRequest.getChosenComponentSender().sendIntent( this, Activity.RESULT_OK, fillIn, null, null); } catch (IntentSender.SendIntentException e) { Slog.e(TAG, "Unable to launch supplied IntentSender to report " + "the chosen component: " + e); } } } } @Override public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) { if (mChooserRequest.getCallerChooserTargets().size() > 0) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( /* origTarget */ null, new ArrayList<>(mChooserRequest.getCallerChooserTargets()), TARGET_TYPE_DEFAULT, /* directShareShortcutInfoCache */ Collections.emptyMap(), /* directShareAppTargetCache */ Collections.emptyMap()); } } @Override public int getLayoutResource() { return R.layout.chooser_grid; } @Override // ResolverListCommunicator public boolean shouldGetActivityMetadata() { return true; } @Override public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { // Note that this is only safe because the Intent handled by the ChooserActivity is // guaranteed to contain no extras unknown to the local ClassLoader. That is why this // method can not be replaced in the ResolverActivity whole hog. if (!super.shouldAutoLaunchSingleChoice(target)) { return false; } return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); } private void showTargetDetails(TargetInfo targetInfo) { if (targetInfo == null) return; List targetList = targetInfo.getAllDisplayTargets(); if (targetList.isEmpty()) { Log.e(TAG, "No displayable data to show target details"); return; } // TODO: implement these type-conditioned behaviors polymorphically, and consider moving // the logic into `ChooserTargetActionsDialogFragment.show()`. boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned(); IntentFilter intentFilter = targetInfo.isSelectableTargetInfo() ? mChooserRequest.getTargetIntentFilter() : null; String shortcutTitle = targetInfo.isSelectableTargetInfo() ? targetInfo.getDisplayLabel().toString() : null; String shortcutIdKey = targetInfo.getDirectShareShortcutId(); ChooserTargetActionsDialogFragment.show( getSupportFragmentManager(), targetList, mChooserMultiProfilePagerAdapter.getCurrentUserHandle(), shortcutIdKey, shortcutTitle, isShortcutPinned, intentFilter); } @Override protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { if (mRefinementManager.maybeHandleSelection(target)) { return false; } updateModelAndChooserCounts(target); maybeRemoveSharedText(target); return super.onTargetSelected(target, alwaysCheck); } @Override public void startSelected(int which, boolean always, boolean filtered) { ChooserListAdapter currentListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); TargetInfo targetInfo = currentListAdapter .targetInfoForPosition(which, filtered); if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) { return; } final long selectionCost = System.currentTimeMillis() - mChooserShownTime; if (targetInfo.isMultiDisplayResolveInfo()) { MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; if (!mti.hasSelected()) { ChooserStackedAppDialogFragment.show( getSupportFragmentManager(), mti, which, mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); return; } } super.startSelected(which, always, filtered); if (currentListAdapter.getCount() > 0) { switch (currentListAdapter.getPositionTargetType(which)) { case ChooserListAdapter.TARGET_SERVICE: getChooserActivityLogger().logShareTargetSelected( ChooserActivityLogger.SELECTION_TYPE_SERVICE, targetInfo.getResolveInfo().activityInfo.processName, which, /* directTargetAlsoRanked= */ getRankedPosition(targetInfo), mChooserRequest.getCallerChooserTargets().size(), targetInfo.getHashedTargetIdForMetrics(this), targetInfo.isPinned(), mIsSuccessfullySelected, selectionCost ); return; case ChooserListAdapter.TARGET_CALLER: case ChooserListAdapter.TARGET_STANDARD: getChooserActivityLogger().logShareTargetSelected( ChooserActivityLogger.SELECTION_TYPE_APP, targetInfo.getResolveInfo().activityInfo.processName, (which - currentListAdapter.getSurfacedTargetInfo().size()), /* directTargetAlsoRanked= */ -1, currentListAdapter.getCallerTargetCount(), /* directTargetHashed= */ null, targetInfo.isPinned(), mIsSuccessfullySelected, selectionCost ); return; case ChooserListAdapter.TARGET_STANDARD_AZ: // A-Z targets are unranked standard targets; we use a value of -1 to mark that // they are from the alphabetical pool. // TODO: why do we log a different selection type if the -1 value already // designates the same condition? getChooserActivityLogger().logShareTargetSelected( ChooserActivityLogger.SELECTION_TYPE_STANDARD, targetInfo.getResolveInfo().activityInfo.processName, /* value= */ -1, /* directTargetAlsoRanked= */ -1, /* numCallerProvided= */ 0, /* directTargetHashed= */ null, /* isPinned= */ false, mIsSuccessfullySelected, selectionCost ); return; } } } private int getRankedPosition(TargetInfo targetInfo) { String targetPackageName = targetInfo.getChooserTargetComponentName().getPackageName(); ChooserListAdapter currentListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); int maxRankedResults = Math.min( currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION); for (int i = 0; i < maxRankedResults; i++) { if (currentListAdapter.getDisplayResolveInfo(i) .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) { return i; } } return -1; } @Override protected boolean shouldAddFooterView() { // To accommodate for window insets return true; } @Override protected void applyFooterView(int height) { int count = mChooserMultiProfilePagerAdapter.getItemCount(); for (int i = 0; i < count; i++) { mChooserMultiProfilePagerAdapter.getAdapterForIndex(i).setFooterHeight(height); } } private void logDirectShareTargetReceived(UserHandle forUser) { ProfileRecord profileRecord = getProfileRecord(forUser); if (profileRecord == null) { return; } getChooserActivityLogger().logDirectShareTargetReceived( MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER, (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime)); } void updateModelAndChooserCounts(TargetInfo info) { if (info != null && info.isMultiDisplayResolveInfo()) { info = ((MultiDisplayResolveInfo) info).getSelectedTarget(); } if (info != null) { sendClickToAppPredictor(info); final ResolveInfo ri = info.getResolveInfo(); Intent targetIntent = getTargetIntent(); if (ri != null && ri.activityInfo != null && targetIntent != null) { ChooserListAdapter currentListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); if (currentListAdapter != null) { sendImpressionToAppPredictor(info, currentListAdapter); currentListAdapter.updateModel(info.getResolvedComponentName()); currentListAdapter.updateChooserCounts(ri.activityInfo.packageName, targetIntent.getAction()); } if (DEBUG) { Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName); Log.d(TAG, "Action to be updated is " + targetIntent.getAction()); } } else if (DEBUG) { Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo"); } } mIsSuccessfullySelected = true; } private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) { Intent targetIntent = targetInfo.getTargetIntent(); if (targetIntent == null) { return; } Intent originalTargetIntent = new Intent(mChooserRequest.getTargetIntent()); // Our TargetInfo implementations add associated component to the intent, let's do the same // for the sake of the comparison below. if (targetIntent.getComponent() != null) { originalTargetIntent.setComponent(targetIntent.getComponent()); } // Use filterEquals as a way to check that the primary intent is in use (and not an // alternative one). For example, an app is sharing an image and a link with mime type // "image/png" and provides an alternative intent to share only the link with mime type // "text/uri". Should there be a target that accepts only the latter, the alternative intent // will be used and we don't want to exclude the link from it. if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) { targetIntent.removeExtra(Intent.EXTRA_TEXT); } } private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) { // Send DS target impression info to AppPredictor, only when user chooses app share. if (targetInfo.isChooserTargetInfo()) { return; } AppPredictor directShareAppPredictor = getAppPredictor( mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); if (directShareAppPredictor == null) { return; } List surfacedTargetInfo = adapter.getSurfacedTargetInfo(); List targetIds = new ArrayList<>(); for (TargetInfo chooserTargetInfo : surfacedTargetInfo) { ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo(); if (shortcutInfo != null) { ComponentName componentName = chooserTargetInfo.getChooserTargetComponentName(); targetIds.add(new AppTargetId( String.format( "%s/%s/%s", shortcutInfo.getId(), componentName.flattenToString(), SHORTCUT_TARGET))); } } directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds); } private void sendClickToAppPredictor(TargetInfo targetInfo) { if (!targetInfo.isChooserTargetInfo()) { return; } AppPredictor directShareAppPredictor = getAppPredictor( mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); if (directShareAppPredictor == null) { return; } AppTarget appTarget = targetInfo.getDirectShareAppTarget(); if (appTarget != null) { // This is a direct share click that was provided by the APS directShareAppPredictor.notifyAppTargetEvent( new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE) .build()); } } @Nullable private AppPredictor getAppPredictor(UserHandle userHandle) { ProfileRecord record = getProfileRecord(userHandle); return (record == null) ? null : record.appPredictor; } /** * Sort intents alphabetically based on display label. */ static class AzInfoComparator implements Comparator { Collator mCollator; AzInfoComparator(Context context) { mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); } @Override public int compare( DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) { return mCollator.compare(lhsp.getDisplayLabel(), rhsp.getDisplayLabel()); } } protected ChooserActivityLogger getChooserActivityLogger() { if (mChooserActivityLogger == null) { mChooserActivityLogger = new ChooserActivityLogger(); } return mChooserActivityLogger; } public class ChooserListController extends ResolverListController { public ChooserListController( Context context, PackageManager pm, Intent targetIntent, String referrerPackageName, int launchedFromUid, AbstractResolverComparator resolverComparator) { super( context, pm, targetIntent, referrerPackageName, launchedFromUid, resolverComparator); } @Override boolean isComponentFiltered(ComponentName name) { return mChooserRequest.getFilteredComponentNames().contains(name); } @Override public boolean isComponentPinned(ComponentName name) { return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); } } @VisibleForTesting public ChooserGridAdapter createChooserGridAdapter( Context context, List payloadIntents, Intent[] initialIntents, List rList, boolean filterLastUsed, UserHandle userHandle) { ChooserListAdapter chooserListAdapter = createChooserListAdapter( context, payloadIntents, initialIntents, rList, filterLastUsed, createListController(userHandle), userHandle, getTargetIntent(), mChooserRequest, mMaxTargetsPerRow); return new ChooserGridAdapter( context, new ChooserGridAdapter.ChooserActivityDelegate() { @Override public boolean shouldShowTabs() { return ChooserActivity.this.shouldShowTabs(); } @Override public View buildContentPreview(ViewGroup parent) { return createContentPreviewView(parent); } @Override public void onTargetSelected(int itemIndex) { startSelected(itemIndex, false, true); } @Override public void onTargetLongPressed(int selectedPosition) { final TargetInfo longPressedTargetInfo = mChooserMultiProfilePagerAdapter .getActiveListAdapter() .targetInfoForPosition( selectedPosition, /* filtered= */ true); // Only a direct share target or an app target is expected if (longPressedTargetInfo.isDisplayResolveInfo() || longPressedTargetInfo.isSelectableTargetInfo()) { showTargetDetails(longPressedTargetInfo); } } @Override public void updateProfileViewButton(View newButtonFromProfileRow) { mProfileView = newButtonFromProfileRow; mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); ChooserActivity.this.updateProfileViewButton(); } @Override public int getValidTargetCount() { return mChooserMultiProfilePagerAdapter .getActiveListAdapter() .getSelectableServiceTargetCount(); } @Override public void updateDirectShareExpansion(DirectShareViewHolder directShareGroup) { RecyclerView activeAdapterView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); if (mResolverDrawerLayout.isCollapsed()) { directShareGroup.collapse(activeAdapterView); } else { directShareGroup.expand(activeAdapterView); } } @Override public void handleScrollToExpandDirectShare( DirectShareViewHolder directShareGroup, int y, int oldy) { directShareGroup.handleScroll( mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy, mMaxTargetsPerRow); } }, chooserListAdapter, shouldShowContentPreview(), mMaxTargetsPerRow, getNumSheetExpansions()); } @VisibleForTesting public ChooserListAdapter createChooserListAdapter( Context context, List payloadIntents, Intent[] initialIntents, List rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, ChooserRequestParameters chooserRequest, int maxTargetsPerRow) { return new ChooserListAdapter( context, payloadIntents, initialIntents, rList, filterLastUsed, resolverListController, userHandle, targetIntent, this, context.getPackageManager(), getChooserActivityLogger(), chooserRequest, maxTargetsPerRow); } @Override @VisibleForTesting protected ChooserListController createListController(UserHandle userHandle) { AppPredictor appPredictor = getAppPredictor(userHandle); AbstractResolverComparator resolverComparator; if (appPredictor != null) { resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(), getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger()); } else { resolverComparator = new ResolverRankerServiceResolverComparator(this, getTargetIntent(), getReferrerPackageName(), null, getChooserActivityLogger()); } return new ChooserListController( this, mPm, getTargetIntent(), getReferrerPackageName(), getAnnotatedUserHandles().userIdOfCallingApp, resolverComparator); } @VisibleForTesting protected ImageLoader createPreviewImageLoader() { final int cacheSize; if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { float chooserWidth = getResources().getDimension(R.dimen.chooser_width); float imageWidth = getResources().getDimension(R.dimen.chooser_preview_image_width); cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2); } else { cacheSize = 3; } return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize); } private ChooserActionFactory createChooserActionFactory() { return new ChooserActionFactory( this, mChooserRequest, mFeatureFlagRepository, mIntegratedDeviceComponents, getChooserActivityLogger(), (isExcluded) -> mExcludeSharedText = isExcluded, this::getFirstVisibleImgPreviewView, new ChooserActionFactory.ActionActivityStarter() { @Override public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle()); finish(); } @Override public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( TargetInfo targetInfo, View sharedElement, String sharedElementName) { ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( ChooserActivity.this, sharedElement, sharedElementName); safelyStartActivityAsUser( targetInfo, getPersonalProfileUserHandle(), options.toBundle()); startFinishAnimation(); } }, (status) -> { if (status != null) { setResult(status); } finish(); }); } private void handleScroll(View view, int x, int y, int oldx, int oldy) { if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) { mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().handleScroll(view, y, oldy); } } /* * Need to dynamically adjust how many icons can fit per row before we add them, * which also means setting the correct offset to initially show the content * preview area + 2 rows of targets */ private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (mChooserMultiProfilePagerAdapter == null) { return; } RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter(); // Skip height calculation if recycler view was scrolled to prevent it inaccurately // calculating the height, as the logic below does not account for the scrolled offset. if (gridAdapter == null || recyclerView == null || recyclerView.computeVerticalScrollOffset() != 0) { return; } final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight(); boolean isLayoutUpdated = gridAdapter.calculateChooserTargetWidth(availableWidth) || recyclerView.getAdapter() == null || availableWidth != mCurrAvailableWidth; boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets); if (isLayoutUpdated || insetsChanged || mLastNumberOfChildren != recyclerView.getChildCount()) { mCurrAvailableWidth = availableWidth; if (isLayoutUpdated) { // It is very important we call setAdapter from here. Otherwise in some cases // the resolver list doesn't get populated, such as b/150922090, b/150918223 // and b/150936654 recyclerView.setAdapter(gridAdapter); ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount( mMaxTargetsPerRow); updateTabPadding(); } UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle(); int currentProfile = getProfileForUser(currentUserHandle); int initialProfile = findSelectedProfile(); if (currentProfile != initialProfile) { return; } if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) { return; } getMainThreadHandler().post(() -> { if (mResolverDrawerLayout == null || gridAdapter == null) { return; } int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter); mResolverDrawerLayout.setCollapsibleHeightReserved(offset); mEnterTransitionAnimationDelegate.markOffsetCalculated(); mLastAppliedInsets = mSystemWindowInsets; }); } } private int calculateDrawerOffset( int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) { final int bottomInset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; int offset = bottomInset; int rowsToShow = gridAdapter.getSystemRowCount() + gridAdapter.getProfileRowCount() + gridAdapter.getServiceTargetRowCount() + gridAdapter.getCallerAndRankedTargetRowCount(); // then this is most likely not a SEND_* action, so check // the app target count if (rowsToShow == 0) { rowsToShow = gridAdapter.getRowCount(); } // still zero? then use a default height and leave, which // can happen when there are no targets to show if (rowsToShow == 0 && !shouldShowStickyContentPreview()) { offset += getResources().getDimensionPixelSize( R.dimen.chooser_max_collapsed_height); return offset; } View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container); if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) { offset += stickyContentPreview.getHeight(); } if (shouldShowTabs()) { offset += findViewById(com.android.internal.R.id.tabs).getHeight(); } if (recyclerView.getVisibility() == View.VISIBLE) { int directShareHeight = 0; rowsToShow = Math.min(4, rowsToShow); boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow); mLastNumberOfChildren = recyclerView.getChildCount(); for (int i = 0, childCount = recyclerView.getChildCount(); i < childCount && rowsToShow > 0; i++) { View child = recyclerView.getChildAt(i); if (((GridLayoutManager.LayoutParams) child.getLayoutParams()).getSpanIndex() != 0) { continue; } int height = child.getHeight(); offset += height; if (shouldShowExtraRow) { offset += height; } if (gridAdapter.getTargetType( recyclerView.getChildAdapterPosition(child)) == ChooserListAdapter.TARGET_SERVICE) { directShareHeight = height; } rowsToShow--; } boolean isExpandable = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode(); if (directShareHeight != 0 && shouldShowContentPreview() && isExpandable) { // make sure to leave room for direct share 4->8 expansion int requiredExpansionHeight = (int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE); int topInset = mSystemWindowInsets != null ? mSystemWindowInsets.top : 0; int minHeight = bottom - top - mResolverDrawerLayout.getAlwaysShowHeight() - requiredExpansionHeight - topInset - bottomInset; offset = Math.min(offset, minHeight); } } else { ViewGroup currentEmptyStateView = getActiveEmptyStateView(); if (currentEmptyStateView.getVisibility() == View.VISIBLE) { offset += currentEmptyStateView.getHeight(); } } return Math.min(offset, bottom - top); } /** * If we have a tabbed view and are showing 1 row in the current profile and an empty * state screen in the other profile, to prevent cropping of the empty state screen we show * a second row in the current profile. */ private boolean shouldShowExtraRow(int rowsToShow) { return shouldShowTabs() && rowsToShow == 1 && mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen( mChooserMultiProfilePagerAdapter.getInactiveListAdapter()); } /** * Returns {@link #PROFILE_PERSONAL}, {@link #PROFILE_WORK}, or -1 if the given user handle * does not match either the personal or work user handle. **/ private int getProfileForUser(UserHandle currentUserHandle) { if (currentUserHandle.equals(getPersonalProfileUserHandle())) { return PROFILE_PERSONAL; } else if (currentUserHandle.equals(getWorkProfileUserHandle())) { return PROFILE_WORK; } Log.e(TAG, "User " + currentUserHandle + " does not belong to a personal or work profile."); return -1; } private ViewGroup getActiveEmptyStateView() { int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage(); return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage); } @Override // ResolverListCommunicator public void onHandlePackagesChanged(ResolverListAdapter listAdapter) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged(); super.onHandlePackagesChanged(listAdapter); } @Override public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { setupScrollListener(); maybeSetupGlobalLayoutListener(); ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; if (chooserListAdapter.getUserHandle() .equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) { mChooserMultiProfilePagerAdapter.getActiveAdapterView() .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()); mChooserMultiProfilePagerAdapter .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage()); } if (chooserListAdapter.getDisplayResolveInfoCount() == 0) { chooserListAdapter.notifyDataSetChanged(); } else { chooserListAdapter.updateAlphabeticalList(); } if (rebuildComplete) { getChooserActivityLogger().logSharesheetAppLoadComplete(); maybeQueryAdditionalPostProcessingTargets(chooserListAdapter); mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET); } } private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) { UserHandle userHandle = chooserListAdapter.getUserHandle(); ProfileRecord record = getProfileRecord(userHandle); if (record == null) { return; } if (record.shortcutLoader == null) { return; } record.loadingStartTime = SystemClock.elapsedRealtime(); record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos()); } @MainThread private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) { if (DEBUG) { Log.d(TAG, "onShortcutsLoaded for user: " + userHandle); } mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache()); mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache()); ChooserListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle); if (adapter != null) { for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) { adapter.addServiceResults( resultInfo.getAppTarget(), resultInfo.getShortcuts(), result.isFromAppPredictor() ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, mDirectShareShortcutInfoCache, mDirectShareAppTargetCache); } adapter.completeServiceTargetLoading(); } logDirectShareTargetReceived(userHandle); sendVoiceChoicesIfNeeded(); getChooserActivityLogger().logSharesheetDirectLoadComplete(); } private void setupScrollListener() { if (mResolverDrawerLayout == null) { return; } int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header; final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId); final float defaultElevation = elevatedView.getElevation(); final float chooserHeaderScrollElevation = getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation); mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener( new RecyclerView.OnScrollListener() { public void onScrollStateChanged(RecyclerView view, int scrollState) { if (scrollState == RecyclerView.SCROLL_STATE_IDLE) { if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) { mScrollStatus = SCROLL_STATUS_IDLE; setHorizontalScrollingEnabled(true); } } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) { if (mScrollStatus == SCROLL_STATUS_IDLE) { mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL; setHorizontalScrollingEnabled(false); } } } public void onScrolled(RecyclerView view, int dx, int dy) { if (view.getChildCount() > 0) { View child = view.getLayoutManager().findViewByPosition(0); if (child == null || child.getTop() < 0) { elevatedView.setElevation(chooserHeaderScrollElevation); return; } } elevatedView.setElevation(defaultElevation); } }); } private void maybeSetupGlobalLayoutListener() { if (shouldShowTabs()) { return; } final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); recyclerView.getViewTreeObserver() .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // Fixes an issue were the accessibility border disappears on list creation. recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); final TextView titleView = findViewById(com.android.internal.R.id.title); if (titleView != null) { titleView.setFocusable(true); titleView.setFocusableInTouchMode(true); titleView.requestFocus(); titleView.requestAccessibilityFocus(); } } }); } /** * The sticky content preview is shown only when we have a tabbed view. It's shown above * the tabs so it is not part of the scrollable list. If we are not in tabbed view, * we instead show the content preview as a regular list item. */ private boolean shouldShowStickyContentPreview() { return shouldShowStickyContentPreviewNoOrientationCheck() && !getResources().getBoolean(R.bool.resolver_landscape_phone); } private boolean shouldShowStickyContentPreviewNoOrientationCheck() { return shouldShowTabs() && (mMultiProfilePagerAdapter.getListAdapterForUserHandle( UserHandle.of(UserHandle.myUserId())).getCount() > 0 || shouldShowContentPreviewWhenEmpty()) && shouldShowContentPreview(); } /** * This method could be used to override the default behavior when we hide the preview area * when the current tab doesn't have any items. * * @return true if we want to show the content preview area even if the tab for the current * user is empty */ protected boolean shouldShowContentPreviewWhenEmpty() { return false; } /** * @return true if we want to show the content preview area */ protected boolean shouldShowContentPreview() { return (mChooserRequest != null) && mChooserRequest.isSendActionTarget(); } private void updateStickyContentPreview() { if (shouldShowStickyContentPreviewNoOrientationCheck()) { // The sticky content preview is only shown when we show the work and personal tabs. // We don't show it in landscape as otherwise there is no room for scrolling. // If the sticky content preview will be shown at some point with orientation change, // then always preload it to avoid subsequent resizing of the share sheet. ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); if (contentPreviewContainer.getChildCount() == 0) { ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer); contentPreviewContainer.addView(contentPreviewView); } } if (shouldShowStickyContentPreview()) { showStickyContentPreview(); } else { hideStickyContentPreview(); } } private void showStickyContentPreview() { if (isStickyContentPreviewShowing()) { return; } ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); contentPreviewContainer.setVisibility(View.VISIBLE); } private boolean isStickyContentPreviewShowing() { ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); return contentPreviewContainer.getVisibility() == View.VISIBLE; } private void hideStickyContentPreview() { if (!isStickyContentPreviewShowing()) { return; } ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); contentPreviewContainer.setVisibility(View.GONE); } private void startFinishAnimation() { View rootView = findRootView(); if (rootView != null) { rootView.startAnimation(new FinishAnimation(this, rootView)); } } private boolean maybeCancelFinishAnimation() { View rootView = findRootView(); Animation animation = (rootView == null) ? null : rootView.getAnimation(); if (animation instanceof FinishAnimation) { boolean hasEnded = animation.hasEnded(); animation.cancel(); rootView.clearAnimation(); return !hasEnded; } return false; } private View findRootView() { if (mContentView == null) { mContentView = findViewById(android.R.id.content); } return mContentView; } /** * Intentionally override the {@link ResolverActivity} implementation as we only need that * implementation for the intent resolver case. */ @Override public void onButtonClick(View v) {} /** * Intentionally override the {@link ResolverActivity} implementation as we only need that * implementation for the intent resolver case. */ @Override protected void resetButtonBar() {} @Override protected String getMetricsCategory() { return METRICS_CATEGORY_CHOOSER; } @Override protected void onProfileTabSelected() { ChooserGridAdapter currentRootAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter(); currentRootAdapter.updateDirectShareExpansion(); // This fixes an edge case where after performing a variety of gestures, vertical scrolling // ends up disabled. That's because at some point the old tab's vertical scrolling is // disabled and the new tab's is enabled. For context, see b/159997845 setVerticalScrollEnabled(true); if (mResolverDrawerLayout != null) { mResolverDrawerLayout.scrollNestedScrollableChildBackToTop(); } } @Override protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { if (shouldShowTabs()) { mChooserMultiProfilePagerAdapter .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom()); mChooserMultiProfilePagerAdapter.setupContainerPadding( getActiveEmptyStateView().findViewById(com.android.internal.R.id.resolver_empty_state_container)); } WindowInsets result = super.onApplyWindowInsets(v, insets); if (mResolverDrawerLayout != null) { mResolverDrawerLayout.requestLayout(); } return result; } private void setHorizontalScrollingEnabled(boolean enabled) { ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); viewPager.setSwipingEnabled(enabled); } private void setVerticalScrollEnabled(boolean enabled) { ChooserGridLayoutManager layoutManager = (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView() .getLayoutManager(); layoutManager.setVerticalScrollEnabled(enabled); } @Override void onHorizontalSwipeStateChanged(int state) { if (state == ViewPager.SCROLL_STATE_DRAGGING) { if (mScrollStatus == SCROLL_STATUS_IDLE) { mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL; setVerticalScrollEnabled(false); } } else if (state == ViewPager.SCROLL_STATE_IDLE) { if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) { mScrollStatus = SCROLL_STATUS_IDLE; setVerticalScrollEnabled(true); } } } /** * Used in combination with the scene transition when launching the image editor */ private static class FinishAnimation extends AlphaAnimation implements Animation.AnimationListener { @Nullable private Activity mActivity; @Nullable private View mRootView; private final float mFromAlpha; FinishAnimation(@NonNull Activity activity, @NonNull View rootView) { super(rootView.getAlpha(), 0.0f); mActivity = activity; mRootView = rootView; mFromAlpha = rootView.getAlpha(); setInterpolator(new LinearInterpolator()); long duration = activity.getWindow().getTransitionBackgroundFadeDuration(); setDuration(duration); // The scene transition animation looks better when it's not overlapped with this // fade-out animation thus the delay. // It is most likely that the image editor will cause this activity to stop and this // animation will be cancelled in the background without running (i.e. we'll animate // only when this activity remains partially visible after the image editor launch). setStartOffset(duration); super.setAnimationListener(this); } @Override public void setAnimationListener(AnimationListener listener) { throw new UnsupportedOperationException(); } @Override public void cancel() { if (mRootView != null) { mRootView.setAlpha(mFromAlpha); } cleanup(); super.cancel(); } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { Activity activity = mActivity; cleanup(); if (activity != null) { activity.finish(); } } @Override public void onAnimationRepeat(Animation animation) { } private void cleanup() { mActivity = null; mRootView = null; } } @Override protected void maybeLogProfileChange() { getChooserActivityLogger().logSharesheetProfileChanged(); } private static class ProfileRecord { /** The {@link AppPredictor} for this profile, if any. */ @Nullable public final AppPredictor appPredictor; /** * null if we should not load shortcuts. */ @Nullable public final ShortcutLoader shortcutLoader; public long loadingStartTime; private ProfileRecord( @Nullable AppPredictor appPredictor, @Nullable ShortcutLoader shortcutLoader) { this.appPredictor = appPredictor; this.shortcutLoader = shortcutLoader; } public void destroy() { if (shortcutLoader != null) { shortcutLoader.destroy(); } if (appPredictor != null) { appPredictor.destroy(); } } } }