/* * 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.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.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.widget.TextView; import androidx.annotation.MainThread; import androidx.lifecycle.ViewModelProvider; 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.BasePreviewViewModel; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; import com.android.intentresolver.contentpreview.PreviewViewModel; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.icons.DefaultTargetDataLoader; import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.logging.EventLog; import com.android.intentresolver.measurements.Tracer; 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.ImagePreviewView; import com.android.internal.annotations.VisibleForTesting; 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 boolean DEBUG = true; public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; private static final String SHORTCUT_TARGET = "shortcut_target"; // 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 {} 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 EventLog mEventLog; 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; /** * When we intend to finish the activity with a shared element transition, we can't immediately * finish() when the transition is invoked, as the receiving end may not be able to start the * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop * in order to wait for the transition to begin. */ private boolean mFinishWhenStopped = false; public ChooserActivity() {} @Override protected void onCreate(Bundle savedInstanceState) { Tracer.INSTANCE.markLaunched(); final long intentReceivedTime = System.currentTimeMillis(); mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); getEventLog().logSharesheetTriggered(); mFeatureFlagRepository = createFeatureFlagRepository(); mIntegratedDeviceComponents = getIntegratedDeviceComponents(); try { mChooserRequest = new ChooserRequestParameters( getIntent(), getReferrerPackageName(), getReferrer(), mFeatureFlagRepository); } catch (IllegalArgumentException e) { Log.e(TAG, "Caller provided invalid Chooser request parameters", e); finish(); super_onCreate(null); return; } mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); mRefinementManager.getRefinementCompletion().observe(this, completion -> { if (completion.consume()) { TargetInfo targetInfo = completion.getTargetInfo(); // targetInfo is non-null if the refinement process was successful. if (targetInfo != null) { maybeRemoveSharedText(targetInfo); // We already block suspended targets from going to refinement, and we probably // can't recover a Chooser session if that's the reason the refined target fails // to launch now. Fire-and-forget the refined launch; ignore the return value // and just make sure the Sharesheet session gets cleaned up regardless. ChooserActivity.super.onTargetSelected(targetInfo, false); } finish(); } }); BasePreviewViewModel previewViewModel = new ViewModelProvider(this, createPreviewViewModelFactory()) .get(BasePreviewViewModel.class); mChooserContentPreviewUi = new ChooserContentPreviewUi( getLifecycle(), previewViewModel.createOrReuseProvider(mChooserRequest), mChooserRequest.getTargetIntent(), previewViewModel.createOrReuseImageLoader(), createChooserActionFactory(), mEnterTransitionAnimationDelegate, new HeadlineGeneratorImpl(this)); 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.getAdditionalTargets(), mChooserRequest.getTitle(), mChooserRequest.getDefaultTitleResource(), mChooserRequest.getInitialIntents(), /* resolutionList= */ null, /* supportsAlwaysUseOption= */ false, new DefaultTargetDataLoader(this, getLifecycle(), false), /* safeForwardingMode= */ true); mChooserShownTime = System.currentTimeMillis(); final long systemCost = mChooserShownTime - intentReceivedTime; getEventLog().logChooserActivityShown( isWorkProfile(), mChooserRequest.getTargetType(), systemCost); if (mResolverDrawerLayout != null) { mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); mResolverDrawerLayout.setOnCollapsedChangedListener( isCollapsed -> { mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed); getEventLog().logSharesheetExpansionChanged(isCollapsed); }); } if (DEBUG) { Log.d(TAG, "System Time Cost is " + systemCost); } getEventLog().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(); ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory); if (record.shortcutLoader == null) { Tracer.INSTANCE.endLaunchToShortcutTrace(); } UserHandle workUserHandle = getWorkProfileUserHandle(); if (workUserHandle != null) { createProfileRecord(workUserHandle, targetIntentFilter, factory); } } private ProfileRecord 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)); ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader); mProfileRecords.put(userHandle.getIdentifier(), record); return record; } @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, getLifecycle(), 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, TargetDataLoader targetDataLoader) { if (shouldShowTabs()) { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles( initialIntents, rList, filterLastUsed, targetDataLoader); } else { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile( initialIntents, rList, filterLastUsed, targetDataLoader); } 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(), getTabOwnerUserHandleForLaunch()); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List rList, boolean filterLastUsed, TargetDataLoader targetDataLoader) { ChooserGridAdapter adapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, initialIntents, rList, filterLastUsed, /* userHandle */ getPersonalProfileUserHandle(), targetDataLoader); return new ChooserMultiProfilePagerAdapter( /* context */ this, adapter, createEmptyStateProvider(/* workProfileUserHandle= */ null), /* workProfileQuietModeChecker= */ () -> false, /* workProfileUserHandle= */ null, getCloneProfileUserHandle(), mMaxTargetsPerRow); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List rList, boolean filterLastUsed, TargetDataLoader targetDataLoader) { int selectedProfile = findSelectedProfile(); ChooserGridAdapter personalAdapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, selectedProfile == PROFILE_PERSONAL ? initialIntents : null, rList, filterLastUsed, /* userHandle */ getPersonalProfileUserHandle(), targetDataLoader); ChooserGridAdapter workAdapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, selectedProfile == PROFILE_WORK ? initialIntents : null, rList, filterLastUsed, /* userHandle */ getWorkProfileUserHandle(), targetDataLoader); return new ChooserMultiProfilePagerAdapter( /* context */ this, personalAdapter, workAdapter, createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()), () -> mWorkProfileAvailability.isQuietModeEnabled(), selectedProfile, getWorkProfileUserHandle(), getCloneProfileUserHandle(), mMaxTargetsPerRow); } private int findSelectedProfile() { int selectedProfile = getSelectedProfileExtra(); if (selectedProfile == -1) { selectedProfile = getProfileForUser(getTabOwnerUserHandleForLaunch()); } return selectedProfile; } @Override protected boolean postRebuildList(boolean rebuildCompleted) { updateStickyContentPreview(); if (shouldShowStickyContentPreview() || mChooserMultiProfilePagerAdapter .getCurrentRootAdapter().getSystemRowCount() != 0) { getEventLog().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) { handlePackageChangePerProfile(mChooserMultiProfilePagerAdapter.getActiveListAdapter()); if (mChooserMultiProfilePagerAdapter.getCount() > 1) { handlePackageChangePerProfile( mChooserMultiProfilePagerAdapter.getInactiveListAdapter()); } } else { handlePackageChangePerProfile(listAdapter); } updateProfileViewButton(); } private void handlePackageChangePerProfile(ResolverListAdapter adapter) { ProfileRecord record = getProfileRecord(adapter.getUserHandle()); if (record != null && record.shortcutLoader != null) { record.shortcutLoader.reset(); } adapter.handlePackagesChanged(); } @Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); mFinishWhenStopped = false; mRefinementManager.onActivityResume(); } @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_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 imagePreview = findViewById(R.id.scrollable_image_preview); return imagePreview instanceof ImagePreviewView ? ((ImagePreviewView) imagePreview).getTransitionView() : 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); } @Override protected void onStop() { super.onStop(); mRefinementManager.onActivityStop(isChangingConfigurations()); if (mFinishWhenStopped) { mFinishWhenStopped = false; finish(); } } @Override protected void onDestroy() { super.onDestroy(); if (isFinishing()) { mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); } 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); } } } } private void addCallerChooserTargets() { if (!mChooserRequest.getCallerChooserTargets().isEmpty()) { // Send the caller's chooser targets only to the default profile. UserHandle defaultUser = (findSelectedProfile() == PROFILE_WORK) ? getAnnotatedUserHandles().workProfileUserHandle : getAnnotatedUserHandles().personalProfileUserHandle; if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle() == defaultUser) { 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, // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be // resolved correctly within the same tab. targetInfo.getResolveInfo().userHandle, shortcutIdKey, shortcutTitle, isShortcutPinned, intentFilter); } @Override protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { if (mRefinementManager.maybeHandleSelection( target, mChooserRequest.getRefinementIntentSender(), getApplication(), getMainThreadHandler())) { 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 != null) && targetInfo.isMultiDisplayResolveInfo()) { MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; if (!mti.hasSelected()) { // Add userHandle based badge to the stackedAppDialogBox. ChooserStackedAppDialogFragment.show( getSupportFragmentManager(), mti, which, targetInfo.getResolveInfo().userHandle); return; } } super.startSelected(which, always, filtered); // TODO: both of the conditions around this switch logic *should* be redundant, and // can be removed if certain invariants can be guaranteed. In particular, it seems // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably* // expected to be null only at out-of-bounds indexes where `getPositionTargetType()` // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't // need to null-check targetInfo. We only need the null check if it's possible that // the ChooserListAdapter contains null elements "in the middle" of its list data, // such that they're classified as belonging to one of the real target types. That // should probably never happen. But why would this method ever be invoked with a // null target at all? Even an out-of-bounds index should never be "selected"... if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) { switch (currentListAdapter.getPositionTargetType(which)) { case ChooserListAdapter.TARGET_SERVICE: getEventLog().logShareTargetSelected( EventLog.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: getEventLog().logShareTargetSelected( EventLog.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? getEventLog().logShareTargetSelected( EventLog.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; } getEventLog().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); currentListAdapter.updateChooserCounts( ri.activityInfo.packageName, targetIntent.getAction(), ri.userHandle); } 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); // We cannot use APS service when clone profile is present as APS service cannot sort // cross profile targets as of now. return (record == null || getCloneProfileUserHandle() != null) ? null : record.appPredictor; } /** * Sort intents alphabetically based on display label. */ static class AzInfoComparator implements Comparator { Comparator mComparator; AzInfoComparator(Context context) { Collator collator = Collator .getInstance(context.getResources().getConfiguration().locale); // Adding two stage comparator, first stage compares using displayLabel, next stage // compares using resolveInfo.userHandle mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator) .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier()); } @Override public int compare( DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) { return mComparator.compare(lhsp, rhsp); } } protected EventLog getEventLog() { if (mEventLog == null) { mEventLog = new EventLog(); } return mEventLog; } public class ChooserListController extends ResolverListController { public ChooserListController( Context context, PackageManager pm, Intent targetIntent, String referrerPackageName, int launchedFromUid, AbstractResolverComparator resolverComparator, UserHandle queryIntentsAsUser) { super( context, pm, targetIntent, referrerPackageName, launchedFromUid, resolverComparator, queryIntentsAsUser); } @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, TargetDataLoader targetDataLoader) { ChooserListAdapter chooserListAdapter = createChooserListAdapter( context, payloadIntents, initialIntents, rList, filterLastUsed, createListController(userHandle), userHandle, getTargetIntent(), mChooserRequest, mMaxTargetsPerRow, targetDataLoader); 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(); } }, chooserListAdapter, shouldShowContentPreview(), mMaxTargetsPerRow); } @VisibleForTesting public ChooserListAdapter createChooserListAdapter( Context context, List payloadIntents, Intent[] initialIntents, List rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, ChooserRequestParameters chooserRequest, int maxTargetsPerRow, TargetDataLoader targetDataLoader) { UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() && userHandle.equals(getPersonalProfileUserHandle()) ? getCloneProfileUserHandle() : userHandle; return new ChooserListAdapter( context, payloadIntents, initialIntents, rList, filterLastUsed, createListController(userHandle), userHandle, targetIntent, this, context.getPackageManager(), getEventLog(), chooserRequest, maxTargetsPerRow, initialIntentsUserSpace, targetDataLoader); } @Override protected void onWorkProfileStatusUpdated() { UserHandle workUser = getWorkProfileUserHandle(); ProfileRecord record = workUser == null ? null : getProfileRecord(workUser); if (record != null && record.shortcutLoader != null) { record.shortcutLoader.reset(); } super.onWorkProfileStatusUpdated(); } @Override @VisibleForTesting protected ChooserListController createListController(UserHandle userHandle) { AppPredictor appPredictor = getAppPredictor(userHandle); AbstractResolverComparator resolverComparator; if (appPredictor != null) { resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(), getReferrerPackageName(), appPredictor, userHandle, getEventLog(), getIntegratedDeviceComponents().getNearbySharingComponent()); } else { resolverComparator = new ResolverRankerServiceResolverComparator( this, getTargetIntent(), getReferrerPackageName(), null, getEventLog(), getResolverRankerServiceUserHandleList(userHandle), getIntegratedDeviceComponents().getNearbySharingComponent()); } return new ChooserListController( this, mPm, getTargetIntent(), getReferrerPackageName(), getAnnotatedUserHandles().userIdOfCallingApp, resolverComparator, getQueryIntentsUser(userHandle)); } @VisibleForTesting protected ViewModelProvider.Factory createPreviewViewModelFactory() { return PreviewViewModel.Companion.getFactory(); } private ChooserActionFactory createChooserActionFactory() { return new ChooserActionFactory( this, mChooserRequest, mIntegratedDeviceComponents, getEventLog(), (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()); // Can't finish right away because the shared element transition may not // be ready to start. mFinishWhenStopped = true; } }, (status) -> { if (status != null) { setResult(status); } finish(); }); } /* * 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) { int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; 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) { 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; } rowsToShow--; } } 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_WORK}, if the given user handle matches work user handle. * Returns {@link #PROFILE_PERSONAL}, otherwise. **/ private int getProfileForUser(UserHandle currentUserHandle) { if (currentUserHandle.equals(getWorkProfileUserHandle())) { return PROFILE_WORK; } // We return personal profile, as it is the default when there is no work profile, personal // profile represents rootUser, clonedUser & secondaryUser, covering all use cases. return PROFILE_PERSONAL; } 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) { long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listAdapter.getUserHandle()); if (duration >= 0) { Log.d(TAG, "app target loading time " + duration + " ms"); } addCallerChooserTargets(); getEventLog().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 || record.shortcutLoader == null) { return; } record.loadingStartTime = SystemClock.elapsedRealtime(); record.shortcutLoader.updateAppTargets(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(); } if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) { long duration = Tracer.INSTANCE.endLaunchToShortcutTrace(); if (duration >= 0) { Log.d(TAG, "stat to first shortcut time: " + duration + " ms"); } } logDirectShareTargetReceived(userHandle); sendVoiceChoicesIfNeeded(); getEventLog().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(); } 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 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() { // 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); } } } @Override protected void maybeLogProfileChange() { getEventLog().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 (appPredictor != null) { appPredictor.destroy(); } } } }