/* * Copyright (C) 2024 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.VoiceInteractor.PickOptionRequest.Option; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import static androidx.lifecycle.LifecycleKt.getCoroutineScope; import static com.android.intentresolver.ChooserActionFactory.EDIT_SOURCE; import static com.android.intentresolver.ext.CreationExtrasExtKt.addDefaultArgs; import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL; import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK; import static com.android.intentresolver.ui.model.ActivityModel.ACTIVITY_MODEL_KEY; import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; import static java.util.Objects.requireNonNull; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.VoiceInteractor; import android.app.admin.DevicePolicyEventLogger; import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; import android.content.ClipboardManager; 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.StrictMode; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.service.chooser.ChooserTarget; import android.stats.devicepolicy.DevicePolicyEnums; import android.text.TextUtils; import android.util.Log; import android.util.Slog; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TabHost; import android.widget.TabWidget; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.viewmodel.CreationExtras; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; import com.android.intentresolver.ChooserRefinementManager.RefinementType; 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.data.model.ChooserRequest; import com.android.intentresolver.data.repository.DevicePolicyResources; import com.android.intentresolver.domain.interactor.UserInteractor; import com.android.intentresolver.emptystate.CompositeEmptyStateProvider; import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; import com.android.intentresolver.emptystate.EmptyStateProvider; import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider; import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.icons.Caching; import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.inject.Background; 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.platform.AppPredictionAvailable; import com.android.intentresolver.platform.ImageEditor; import com.android.intentresolver.platform.NearbyShare; import com.android.intentresolver.profiles.ChooserMultiProfilePagerAdapter; import com.android.intentresolver.profiles.MultiProfilePagerAdapter.ProfileType; import com.android.intentresolver.profiles.OnProfileSelectedListener; import com.android.intentresolver.profiles.OnSwitchOnWorkSelectedListener; import com.android.intentresolver.profiles.TabConfig; import com.android.intentresolver.shared.model.Profile; import com.android.intentresolver.shortcuts.AppPredictorFactory; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.intentresolver.ui.ActionTitle; import com.android.intentresolver.ui.ProfilePagerResources; import com.android.intentresolver.ui.ShareResultSender; import com.android.intentresolver.ui.ShareResultSenderFactory; import com.android.intentresolver.ui.model.ActivityModel; import com.android.intentresolver.ui.viewmodel.ChooserViewModel; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.LatencyTracker; import com.google.common.collect.ImmutableList; import dagger.hilt.android.AndroidEntryPoint; import kotlin.Pair; import kotlinx.coroutines.CoroutineDispatcher; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Supplier; import javax.inject.Inject; import javax.inject.Provider; /** * The Chooser Activity handles intent resolution specifically for sharing intents - * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}. * */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @AndroidEntryPoint(FragmentActivity.class) public class ChooserActivity extends Hilt_ChooserActivity implements ResolverListAdapter.ResolverListCommunicator, PackagesChangedListener, StartsSelectedItem { 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. */ 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"; ////////////////////////////////////////////////////////////////////////////////////////////// // Inherited properties. ////////////////////////////////////////////////////////////////////////////////////////////// private static final String TAB_TAG_PERSONAL = "personal"; private static final String TAB_TAG_WORK = "work"; private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key"; public static final String METRICS_CATEGORY_CHOOSER = "intent_chooser"; private int mLayoutId; private UserHandle mHeaderCreatorUser; private boolean mRegistered; private PackageMonitor mPersonalPackageMonitor; private PackageMonitor mWorkPackageMonitor; protected ResolverDrawerLayout mResolverDrawerLayout; private TabHost mTabHost; private ResolverViewPager mViewPager; protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter; protected final LatencyTracker mLatencyTracker = getLatencyTracker(); /** See {@link #setRetainInOnStop}. */ private boolean mRetainInOnStop; protected Insets mSystemWindowInsets = null; private ResolverActivity.PickTargetOptionRequest mPickOptionRequest; @Nullable private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// // 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<>(); static final int TARGET_TYPE_DEFAULT = 0; static final int TARGET_TYPE_CHOOSER_TARGET = 1; static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2; 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; @Inject public UserInteractor mUserInteractor; @Inject @Background public CoroutineDispatcher mBackgroundDispatcher; @Inject public ChooserHelper mChooserHelper; @Inject public FeatureFlags mFeatureFlags; @Inject public android.service.chooser.FeatureFlags mChooserServiceFeatureFlags; @Inject public EventLog mEventLog; @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable; @Inject @ImageEditor public Optional mImageEditor; @Inject @NearbyShare public Optional mNearbyShare; protected TargetDataLoader mTargetDataLoader; @Inject public Provider mTargetDataLoaderProvider; @Inject @Caching public Provider mCachingTargetDataLoaderProvider; @Inject public DevicePolicyResources mDevicePolicyResources; @Inject public ProfilePagerResources mProfilePagerResources; @Inject public PackageManager mPackageManager; @Inject public ClipboardManager mClipboardManager; @Inject public IntentForwarding mIntentForwarding; @Inject public ShareResultSenderFactory mShareResultSenderFactory; private ActivityModel mActivityModel; private ChooserRequest mRequest; private ProfileHelper mProfiles; private ProfileAvailability mProfileAvailability; @Nullable private ShareResultSender mShareResultSender; private ChooserRefinementManager mRefinementManager; private ChooserContentPreviewUi mChooserContentPreviewUi; private boolean mShouldDisplayLandscape; 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; private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate = new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout); private final Map mProfileRecords = new HashMap<>(); 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; private final AtomicLong mIntentReceivedTime = new AtomicLong(-1); protected ActivityModel createActivityModel() { return ActivityModel.createFrom(this); } private ChooserViewModel mViewModel; @NonNull @Override public CreationExtras getDefaultViewModelCreationExtras() { return addDefaultArgs( super.getDefaultViewModelCreationExtras(), new Pair<>(ACTIVITY_MODEL_KEY, createActivityModel())); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.i(TAG, "onCreate"); mTargetDataLoader = mChooserServiceFeatureFlags.chooserPayloadToggling() ? mCachingTargetDataLoaderProvider.get() : mTargetDataLoaderProvider.get(); setTheme(R.style.Theme_DeviceDefault_Chooser); // Initializer is invoked when this function returns, via Lifecycle. mChooserHelper.setInitializer(this::initialize); if (mChooserServiceFeatureFlags.chooserPayloadToggling()) { mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged); mChooserHelper.setOnPendingSelection(this::onPendingSelection); } } @Override protected final void onStart() { super.onStart(); this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); } @Override protected final void onResume() { super.onResume(); Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); mFinishWhenStopped = false; mRefinementManager.onActivityResume(); } @Override protected final void onStop() { super.onStop(); final Window window = this.getWindow(); final WindowManager.LayoutParams attrs = window.getAttributes(); attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; window.setAttributes(attrs); if (mRegistered) { mPersonalPackageMonitor.unregister(); if (mWorkPackageMonitor != null) { mWorkPackageMonitor.unregister(); } mRegistered = false; } final Intent intent = getIntent(); if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction() && !mRetainInOnStop) { // This resolver is in the unusual situation where it has been // launched at the top of a new task. We don't let it be added // to the recent tasks shown to the user, and we need to make sure // that each time we are launched we get the correct launching // uid (not re-using the same resolver from an old launching uid), // so we will now finish ourself since being no longer visible, // the user probably can't get back to us. if (!isChangingConfigurations()) { Log.d(TAG, "finishing in onStop"); finish(); } } if (mRefinementManager != null) { mRefinementManager.onActivityStop(isChangingConfigurations()); } if (mFinishWhenStopped) { mFinishWhenStopped = false; finish(); } } @Override protected final void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mViewPager != null) { outState.putInt(LAST_SHOWN_TAB_KEY, mViewPager.getCurrentItem()); } } @Override protected final void onRestart() { super.onRestart(); if (mFeatureFlags.fixPrivateSpaceLockedOnRestart()) { if (mChooserMultiProfilePagerAdapter.hasPageForProfile(Profile.Type.PRIVATE.ordinal()) && !mProfileAvailability.isAvailable(mProfiles.getPrivateProfile())) { Log.d(TAG, "Exiting due to unavailable profile"); finish(); return; } } if (!mRegistered) { mPersonalPackageMonitor.register( this, getMainLooper(), mProfiles.getPersonalHandle(), false); if (mProfiles.getWorkProfilePresent()) { if (mWorkPackageMonitor == null) { mWorkPackageMonitor = createPackageMonitor( mChooserMultiProfilePagerAdapter.getWorkListAdapter()); } mWorkPackageMonitor.register( this, getMainLooper(), mProfiles.getWorkHandle(), false); } mRegistered = true; } mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); } @Override protected void onDestroy() { super.onDestroy(); if (!isChangingConfigurations() && mPickOptionRequest != null) { mPickOptionRequest.cancel(); } if (mChooserMultiProfilePagerAdapter != null) { mChooserMultiProfilePagerAdapter.destroy(); } if (isFinishing()) { mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); } mBackgroundThreadPoolExecutor.shutdownNow(); destroyProfileRecords(); } /** DO NOT CALL. Only for use from ChooserHelper as a callback. */ private void initialize() { mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class); mRequest = mViewModel.getRequest().getValue(); mActivityModel = mViewModel.getActivityModel(); mProfiles = new ProfileHelper( mUserInteractor, getCoroutineScope(getLifecycle()), mBackgroundDispatcher, mFeatureFlags); mProfileAvailability = new ProfileAvailability( mUserInteractor, getCoroutineScope(getLifecycle()), mBackgroundDispatcher); mProfileAvailability.setOnProfileStatusChange(this::onWorkProfileStatusUpdated); mIntentReceivedTime.set(System.currentTimeMillis()); mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); mPinnedSharedPrefs = getPinnedSharedPrefs(this); updateShareResultSender(); mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); mShouldDisplayLandscape = shouldDisplayLandscape(getResources().getConfiguration().orientation); setRetainInOnStop(mRequest.shouldRetainInOnStop()); createProfileRecords( new AppPredictorFactory( this, Objects.toString(mRequest.getSharedText(), null), mRequest.getShareTargetFilter(), mAppPredictionAvailable ), mRequest.getShareTargetFilter() ); mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( /* context = */ this, mProfilePagerResources, mRequest, mProfiles, mProfileAvailability, mRequest.getInitialIntents(), mMaxTargetsPerRow); maybeDisableRecentsScreenshot(mProfiles, mProfileAvailability); if (!configureContentView(mTargetDataLoader)) { mPersonalPackageMonitor = createPackageMonitor( mChooserMultiProfilePagerAdapter.getPersonalListAdapter()); mPersonalPackageMonitor.register( this, getMainLooper(), mProfiles.getPersonalHandle(), false ); if (mProfiles.getWorkProfilePresent()) { mWorkPackageMonitor = createPackageMonitor( mChooserMultiProfilePagerAdapter.getWorkListAdapter()); mWorkPackageMonitor.register( this, getMainLooper(), mProfiles.getWorkHandle(), false ); } mRegistered = true; final ResolverDrawerLayout rdl = findViewById( com.android.internal.R.id.contentPanel); if (rdl != null) { rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() { @Override public void onDismissed() { finish(); } }); boolean hasTouchScreen = mPackageManager .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); if (isVoiceInteraction() || !hasTouchScreen) { rdl.setCollapsed(false); } rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets); mResolverDrawerLayout = rdl; } Intent intent = mRequest.getTargetIntent(); final Set categories = intent.getCategories(); MetricsLogger.action(this, mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() ? MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED : MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED, intent.getAction() + ":" + intent.getType() + ":" + (categories != null ? Arrays.toString(categories.toArray()) : "")); } getEventLog().logSharesheetTriggered(); mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); mRefinementManager.getRefinementCompletion().observe(this, completion -> { if (completion.consume()) { if (completion.getRefinedIntent() == null) { finish(); return; } // Prepare to regenerate our "system actions" based on the refined intent. // TODO: optimize if needed. `TARGET_INFO` cases don't require a new action // factory at all. And if we break up `ChooserActionFactory`, we could avoid // resolving a new editor intent unless we're handling an `EDIT_ACTION`. ChooserActionFactory refinedActionFactory = createChooserActionFactory(completion.getRefinedIntent()); switch (completion.getType()) { case TARGET_INFO: { TargetInfo refinedTarget = completion .getOriginalTargetInfo() .tryToCloneWithAppliedRefinement( completion.getRefinedIntent()); if (refinedTarget == null) { Log.e(TAG, "Failed to apply refinement to any matching source intent"); } else { maybeRemoveSharedText(refinedTarget); // 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, and make sure Sharesheet gets cleaned up regardless of the // outcome of that launch.launch; ignore safelyStartActivity(refinedTarget); } } break; case COPY_ACTION: { if (refinedActionFactory.getCopyButtonRunnable() != null) { refinedActionFactory.getCopyButtonRunnable().run(); } } break; case EDIT_ACTION: { if (refinedActionFactory.getEditButtonRunnable() != null) { refinedActionFactory.getEditButtonRunnable().run(); } } break; } finish(); } }); BasePreviewViewModel previewViewModel = new ViewModelProvider(this, createPreviewViewModelFactory()) .get(BasePreviewViewModel.class); previewViewModel.init( mRequest.getTargetIntent(), mRequest.getAdditionalContentUri(), mChooserServiceFeatureFlags.chooserPayloadToggling()); ChooserContentPreviewUi.ActionFactory actionFactory = decorateActionFactoryWithRefinement( createChooserActionFactory(mRequest.getTargetIntent())); mChooserContentPreviewUi = new ChooserContentPreviewUi( getCoroutineScope(getLifecycle()), previewViewModel.getPreviewDataProvider(), mRequest.getTargetIntent(), previewViewModel.getImageLoader(), actionFactory, createModifyShareActionFactory(), mEnterTransitionAnimationDelegate, new HeadlineGeneratorImpl(this), mRequest.getContentTypeHint(), mRequest.getMetadataText(), mChooserServiceFeatureFlags.chooserPayloadToggling()); updateStickyContentPreview(); if (shouldShowStickyContentPreview()) { getEventLog().logActionShareWithPreview( mChooserContentPreviewUi.getPreferredContentPreview()); } mChooserShownTime = System.currentTimeMillis(); final long systemCost = mChooserShownTime - mIntentReceivedTime.get(); getEventLog().logChooserActivityShown( isWorkProfile(), mRequest.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( mRequest.getReferrerPackage(), mRequest.getTargetType(), mRequest.getCallerChooserTargets().size(), mRequest.getInitialIntents().size(), isWorkProfile(), mChooserContentPreviewUi.getPreferredContentPreview(), mRequest.getTargetAction(), mRequest.getChooserActions().size(), mRequest.getModifyShareAction() != null ); mEnterTransitionAnimationDelegate.postponeTransition(); Tracer.INSTANCE.markLaunched(); } private void maybeDisableRecentsScreenshot( ProfileHelper profileHelper, ProfileAvailability profileAvailability) { for (Profile profile : profileHelper.getProfiles()) { if (profile.getType() == Profile.Type.PRIVATE) { if (profileAvailability.isAvailable(profile)) { // Show blank screen in Recent preview if private profile is available // to not leak its presence. setRecentsScreenshotEnabled(false); } return; } } } private void onChooserRequestChanged(ChooserRequest chooserRequest) { // intentional reference comparison if (mRequest == chooserRequest) { return; } boolean recreateAdapters = shouldUpdateAdapters(mRequest, chooserRequest); mRequest = chooserRequest; updateShareResultSender(); mChooserContentPreviewUi.updateModifyShareAction(); if (recreateAdapters) { recreatePagerAdapter(); } else { setTabsViewEnabled(true); } } private void onPendingSelection() { setTabsViewEnabled(false); } private void onAppTargetsLoaded(ResolverListAdapter listAdapter) { Log.d(TAG, "onAppTargetsLoaded(" + "listAdapter.userHandle=" + listAdapter.getUserHandle() + ")"); if (mChooserMultiProfilePagerAdapter == null) { return; } if (!isProfilePagerAdapterAttached() && listAdapter == mChooserMultiProfilePagerAdapter.getActiveListAdapter()) { mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager); setTabsViewEnabled(true); } } private void updateShareResultSender() { IntentSender chosenComponentSender = mRequest.getChosenComponentSender(); if (chosenComponentSender != null) { mShareResultSender = mShareResultSenderFactory.create( mViewModel.getActivityModel().getLaunchedFromUid(), chosenComponentSender); } else { mShareResultSender = null; } } private boolean shouldUpdateAdapters( ChooserRequest oldChooserRequest, ChooserRequest newChooserRequest) { Intent oldTargetIntent = oldChooserRequest.getTargetIntent(); Intent newTargetIntent = newChooserRequest.getTargetIntent(); List oldAltIntents = oldChooserRequest.getAdditionalTargets(); List newAltIntents = newChooserRequest.getAdditionalTargets(); // TODO: a workaround for the unnecessary target reloading caused by multiple flow updates - // an artifact of the current implementation; revisit. return !oldTargetIntent.equals(newTargetIntent) || !oldAltIntents.equals(newAltIntents); } private void recreatePagerAdapter() { if (!mChooserServiceFeatureFlags.chooserPayloadToggling()) { return; } destroyProfileRecords(); createProfileRecords( new AppPredictorFactory( this, Objects.toString(mRequest.getSharedText(), null), mRequest.getShareTargetFilter(), mAppPredictionAvailable ), mRequest.getShareTargetFilter() ); int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage(); if (mChooserMultiProfilePagerAdapter != null) { mChooserMultiProfilePagerAdapter.destroy(); } // Update the pager adapter but do not attach it to the view till the targets are reloaded, // see onChooserAppTargetsLoaded method. mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( /* context = */ this, mProfilePagerResources, mRequest, mProfiles, mProfileAvailability, mRequest.getInitialIntents(), mMaxTargetsPerRow); mChooserMultiProfilePagerAdapter.setCurrentPage(currentPage); for (int i = 0, count = mChooserMultiProfilePagerAdapter.getItemCount(); i < count; i++) { mChooserMultiProfilePagerAdapter.getPageAdapterForIndex(i) .getListAdapter().setAnimateItems(false); } if (mPersonalPackageMonitor != null) { mPersonalPackageMonitor.unregister(); } mPersonalPackageMonitor = createPackageMonitor( mChooserMultiProfilePagerAdapter.getPersonalListAdapter()); mPersonalPackageMonitor.register( this, getMainLooper(), mProfiles.getPersonalHandle(), false); if (mProfiles.getWorkProfilePresent()) { if (mWorkPackageMonitor != null) { mWorkPackageMonitor.unregister(); } mWorkPackageMonitor = createPackageMonitor( mChooserMultiProfilePagerAdapter.getWorkListAdapter()); mWorkPackageMonitor.register( this, getMainLooper(), mProfiles.getWorkHandle(), false); } postRebuildList( mChooserMultiProfilePagerAdapter.rebuildTabs( mProfiles.getWorkProfilePresent() || mProfiles.getPrivateProfilePresent())); setTabsViewEnabled(false); } private void setTabsViewEnabled(boolean isEnabled) { TabWidget tabs = mTabHost.getTabWidget(); if (tabs != null) { tabs.setEnabled(isEnabled); } View tabContent = mTabHost.findViewById(com.android.internal.R.id.profile_pager); if (tabContent != null) { tabContent.setEnabled(isEnabled); } } @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { if (mViewPager != null) { mViewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY)); } mChooserMultiProfilePagerAdapter.clearInactiveProfileCache(); } ////////////////////////////////////////////////////////////////////////////////////////////// // Inherited methods ////////////////////////////////////////////////////////////////////////////////////////////// private boolean isAutolaunching() { return !mRegistered && isFinishing(); } private boolean maybeAutolaunchIfSingleTarget() { int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); if (count != 1) { return false; } if (mChooserMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) { return false; } // Only one target, so we're a candidate to auto-launch! final TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter() .targetInfoForPosition(0, false); if (shouldAutoLaunchSingleChoice(target)) { Log.d(TAG, "auto launching " + target + " and finishing."); safelyStartActivity(target); finish(); return true; } return false; } private boolean isTwoPagePersonalAndWorkConfiguration() { return (mChooserMultiProfilePagerAdapter.getCount() == 2) && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL) && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK); } /** * When we have a personal and a work profile, we auto launch in the following scenario: * - There is 1 resolved target on each profile * - That target is the same app on both profiles * - The target app has permission to communicate cross profiles * - The target app has declared it supports cross-profile communication via manifest metadata */ private boolean maybeAutolaunchIfCrossProfileSupported() { if (!isTwoPagePersonalAndWorkConfiguration()) { return false; } ResolverListAdapter activeListAdapter = (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) ? mChooserMultiProfilePagerAdapter.getPersonalListAdapter() : mChooserMultiProfilePagerAdapter.getWorkListAdapter(); ResolverListAdapter inactiveListAdapter = (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) ? mChooserMultiProfilePagerAdapter.getWorkListAdapter() : mChooserMultiProfilePagerAdapter.getPersonalListAdapter(); if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) { return false; } if ((activeListAdapter.getUnfilteredCount() != 1) || (inactiveListAdapter.getUnfilteredCount() != 1)) { return false; } TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false); TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false); if (!Objects.equals( activeProfileTarget.getResolvedComponentName(), inactiveProfileTarget.getResolvedComponentName())) { return false; } if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) { return false; } String packageName = activeProfileTarget.getResolvedComponentName().getPackageName(); if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) { return false; } DevicePolicyEventLogger .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET) .setBoolean(activeListAdapter.getUserHandle() .equals(mProfiles.getPersonalHandle())) .setStrings(getMetricsCategory()) .write(); safelyStartActivity(activeProfileTarget); Log.d(TAG, "auto launching! " + activeProfileTarget); finish(); return true; } /** * @return {@code true} if a resolved target is autolaunched, otherwise {@code false} */ private boolean maybeAutolaunchActivity() { int numberOfProfiles = mChooserMultiProfilePagerAdapter.getItemCount(); // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the // correct intent-picker UIs (e.g., mini-resolver) if it was launched without // ACTION_SEND. if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) { return true; } else if (maybeAutolaunchIfCrossProfileSupported()) { return true; } return false; } @Override // ResolverListCommunicator public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing, boolean rebuildCompleted) { if (isAutolaunching()) { return; } if (mChooserMultiProfilePagerAdapter .shouldShowEmptyStateScreen((ChooserListAdapter) listAdapter)) { mChooserMultiProfilePagerAdapter .showEmptyResolverListEmptyState((ChooserListAdapter) listAdapter); } else { mChooserMultiProfilePagerAdapter.showListView((ChooserListAdapter) listAdapter); } // showEmptyResolverListEmptyState can mark the tab as loaded, // which is a precondition for auto launching if (rebuildCompleted && maybeAutolaunchActivity()) { return; } if (doPostProcessing) { maybeCreateHeader(listAdapter); onListRebuilt(listAdapter, rebuildCompleted); } } private CharSequence getOrLoadDisplayLabel(TargetInfo info) { if (info.isDisplayResolveInfo()) { mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info); } CharSequence displayLabel = info.getDisplayLabel(); return displayLabel == null ? "" : displayLabel; } protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) { final ActionTitle title = ActionTitle.forAction(intent.getAction()); // While there may already be a filtered item, we can only use it in the title if the list // is already sorted and all information relevant to it is already in the list. final boolean named = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0; if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) { return getString(defaultTitleRes); } else { return named ? getString( title.namedTitleRes, getOrLoadDisplayLabel( mChooserMultiProfilePagerAdapter .getActiveListAdapter().getFilteredItem())) : getString(title.titleRes); } } /** * Configure the area above the app selection list (title, content preview, etc). */ private void maybeCreateHeader(ResolverListAdapter listAdapter) { if (mHeaderCreatorUser != null && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) { return; } if (!mProfiles.getWorkProfilePresent() && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) { final TextView titleView = findViewById(com.android.internal.R.id.title); if (titleView != null) { titleView.setVisibility(View.GONE); } } CharSequence title = mRequest.getTitle() != null ? mRequest.getTitle() : getTitleForAction(mRequest.getTargetIntent(), mRequest.getDefaultTitleResource()); if (!TextUtils.isEmpty(title)) { final TextView titleView = findViewById(com.android.internal.R.id.title); if (titleView != null) { titleView.setText(title); } setTitle(title); } final ImageView iconView = findViewById(com.android.internal.R.id.icon); if (iconView != null) { listAdapter.loadFilteredItemIconTaskAsync(iconView); } mHeaderCreatorUser = listAdapter.getUserHandle(); } /** Start the activity specified by the {@link TargetInfo}.*/ public final void safelyStartActivity(TargetInfo cti) { // In case cloned apps are present, we would want to start those apps in cloned user // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle // identifies the correct user space in such cases. UserHandle activityUserHandle = cti.getResolveInfo().userHandle; safelyStartActivityAsUser(cti, activityUserHandle, null); } protected final void safelyStartActivityAsUser( TargetInfo cti, UserHandle user, @Nullable Bundle options) { // We're dispatching intents that might be coming from legacy apps, so // don't kill ourselves. StrictMode.disableDeathOnFileUriExposure(); try { safelyStartActivityInternal(cti, user, options); } finally { StrictMode.enableDeathOnFileUriExposure(); } } @VisibleForTesting protected void safelyStartActivityInternal( TargetInfo cti, UserHandle user, @Nullable Bundle options) { // If the target is suspended, the activity will not be successfully launched. // Do not unregister from package manager updates in this case if (!cti.isSuspended() && mRegistered) { if (mPersonalPackageMonitor != null) { mPersonalPackageMonitor.unregister(); } if (mWorkPackageMonitor != null) { mWorkPackageMonitor.unregister(); } mRegistered = false; } // If needed, show that intent is forwarded // from managed profile to owner or other way around. String profileSwitchMessage = mIntentForwarding.forwardMessageFor( mRequest.getTargetIntent()); if (profileSwitchMessage != null) { Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show(); } try { if (cti.startAsCaller(this, options, user.getIdentifier())) { // Prevent sending a second chooser result when starting the edit action intent. if (!cti.getTargetIntent().hasExtra(EDIT_SOURCE)) { maybeSendShareResult(cti); } maybeLogCrossProfileTargetLaunch(cti, user); } } catch (RuntimeException e) { Slog.wtf(TAG, "Unable to launch as uid " + mActivityModel.getLaunchedFromUid() + " package " + mActivityModel.getLaunchedFromPackage() + ", while running in " + ActivityThread.currentProcessName(), e); } } private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) { if (!mProfiles.getWorkProfilePresent() || currentUserHandle.equals(getUser())) { return; } DevicePolicyEventLogger .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED) .setBoolean(currentUserHandle.equals(mProfiles.getPersonalHandle())) .setStrings(getMetricsCategory(), cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target") .write(); } private LatencyTracker getLatencyTracker() { return LatencyTracker.getInstance(this); } /** * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets * called and we are launched in a new task. */ protected final void setRetainInOnStop(boolean retainInOnStop) { mRetainInOnStop = retainInOnStop; } // @NonFinalForTesting @VisibleForTesting protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { return new CrossProfileIntentsChecker(getContentResolver()); } protected final EmptyStateProvider createEmptyStateProvider( ProfileHelper profileHelper, ProfileAvailability profileAvailability) { EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider(); EmptyStateProvider workProfileOffEmptyStateProvider = new WorkProfilePausedEmptyStateProvider( this, profileHelper, profileAvailability, /* onSwitchOnWorkSelectedListener = */ () -> { if (mOnSwitchOnWorkSelectedListener != null) { mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); } }, getMetricsCategory()); EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider( mProfiles, mProfileAvailability, getMetricsCategory(), mProfilePagerResources ); // Return composite provider, the order matters (the higher, the more priority) return new CompositeEmptyStateProvider( blockerEmptyStateProvider, workProfileOffEmptyStateProvider, noAppsEmptyStateProvider ); } /** * Returns the {@link List} of {@link UserHandle} to pass on to the * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}. */ private List getResolverRankerServiceUserHandleList(UserHandle userHandle) { return getResolverRankerServiceUserHandleListInternal(userHandle); } private List getResolverRankerServiceUserHandleListInternal(UserHandle userHandle) { List userList = new ArrayList<>(); userList.add(userHandle); // Add clonedProfileUserHandle to the list only if we are: // a. Building the Personal Tab. // b. CloneProfile exists on the device. if (userHandle.equals(mProfiles.getPersonalHandle()) && mProfiles.getCloneUserPresent()) { userList.add(mProfiles.getCloneHandle()); } return userList; } /** * Start activity as a fixed user handle. * @param cti TargetInfo to be launched. * @param user User to launch this activity as. */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) { safelyStartActivityAsUser(cti, user, null); } @Override // ResolverListCommunicator public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) { mChooserMultiProfilePagerAdapter.onHandlePackagesChanged( (ChooserListAdapter) listAdapter, mProfileAvailability.getWaitingToEnableProfile()); } final Option optionForChooserTarget(TargetInfo target, int index) { return new Option(getOrLoadDisplayLabel(target), index); } @Override // ResolverListCommunicator public final void sendVoiceChoicesIfNeeded() { if (!isVoiceInteraction()) { // Clearly not needed. return; } int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getCount(); final Option[] options = new Option[count]; for (int i = 0; i < options.length; i++) { TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getItem(i); if (target == null) { // If this occurs, a new set of targets is being loaded. Let that complete, // and have the next call to send voice choices proceed instead. return; } options[i] = optionForChooserTarget(target, i); } mPickOptionRequest = new ResolverActivity.PickTargetOptionRequest( new VoiceInteractor.Prompt(getTitle()), options, null); getVoiceInteractor().submitRequest(mPickOptionRequest); } /** * Sets up the content view. * @return true if the activity is finishing and creation should halt. */ private boolean configureContentView(TargetDataLoader targetDataLoader) { if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null) { throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() " + "cannot be null."); } Trace.beginSection("configureContentView"); // We partially rebuild the inactive adapter to determine if we should auto launch // isTabLoaded will be true here if the empty state screen is shown instead of the list. boolean rebuildCompleted = mChooserMultiProfilePagerAdapter.rebuildTabs( mProfiles.getWorkProfilePresent()); mLayoutId = R.layout.chooser_grid_scrollable_preview; setContentView(mLayoutId); mTabHost = findViewById(com.android.internal.R.id.profile_tabhost); mViewPager = requireViewById(com.android.internal.R.id.profile_pager); mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager); boolean result = postRebuildList(rebuildCompleted); Trace.endSection(); return result; } /** * Finishing procedures to be performed after the list has been rebuilt. *

Subclasses must call postRebuildListInternal at the end of postRebuildList. * @param rebuildCompleted * @return true if the activity is finishing and creation should halt. */ protected boolean postRebuildList(boolean rebuildCompleted) { return postRebuildListInternal(rebuildCompleted); } /** * Add a label to signify that the user can pick a different app. * @param adapter The adapter used to provide data to item views. */ public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) { final boolean useHeader = adapter.hasFilteredItem(); if (useHeader) { FrameLayout stub = findViewById(com.android.internal.R.id.stub); stub.setVisibility(View.VISIBLE); TextView textView = (TextView) LayoutInflater.from(this).inflate( R.layout.resolver_different_item_header, null, false); if (mProfiles.getWorkProfilePresent()) { textView.setGravity(Gravity.CENTER); } stub.addView(textView); } } private void setupViewVisibilities() { ChooserListAdapter activeListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); if (!mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) { addUseDifferentAppLabelIfNecessary(activeListAdapter); } } /** * Finishing procedures to be performed after the list has been rebuilt. * @param rebuildCompleted * @return true if the activity is finishing and creation should halt. */ final boolean postRebuildListInternal(boolean rebuildCompleted) { int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); // We only rebuild asynchronously when we have multiple elements to sort. In the case where // we're already done, we can check if we should auto-launch immediately. if (rebuildCompleted && maybeAutolaunchActivity()) { return true; } setupViewVisibilities(); if (mProfiles.getWorkProfilePresent() || (mProfiles.getPrivateProfilePresent() && mProfileAvailability.isAvailable( requireNonNull(mProfiles.getPrivateProfile())))) { setupProfileTabs(); } return false; } private void setupProfileTabs() { mChooserMultiProfilePagerAdapter.setupProfileTabs( getLayoutInflater(), mTabHost, mViewPager, R.layout.resolver_profile_tab_button, com.android.internal.R.id.profile_pager, () -> onProfileTabSelected(mViewPager.getCurrentItem()), new OnProfileSelectedListener() { @Override public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {} @Override public void onProfilePageStateChanged(int state) { onHorizontalSwipeStateChanged(state); } }); mOnSwitchOnWorkSelectedListener = () -> { View workTab = mTabHost.getTabWidget().getChildAt( mChooserMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK)); workTab.setFocusable(true); workTab.setFocusableInTouchMode(true); workTab.requestFocus(); }; } ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// private void createProfileRecords( AppPredictorFactory factory, IntentFilter targetIntentFilter) { UserHandle mainUserHandle = mProfiles.getPersonalHandle(); ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory); if (record.shortcutLoader == null) { Tracer.INSTANCE.endLaunchToShortcutTrace(); } UserHandle workUserHandle = mProfiles.getWorkHandle(); if (workUserHandle != null) { createProfileRecord(workUserHandle, targetIntentFilter, factory); } UserHandle privateUserHandle = mProfiles.getPrivateHandle(); if (privateUserHandle != null && mProfileAvailability.isAvailable( requireNonNull(mProfiles.getPrivateProfile()))) { createProfileRecord(privateUserHandle, targetIntentFilter, factory); } } private ProfileRecord createProfileRecord( UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) { AppPredictor appPredictor = factory.create(userHandle); ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() ? null : createShortcutLoader( this, 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()); } @VisibleForTesting protected ShortcutLoader createShortcutLoader( Context context, AppPredictor appPredictor, UserHandle userHandle, IntentFilter targetIntentFilter, Consumer callback) { return new ShortcutLoader( context, getCoroutineScope(getLifecycle()), appPredictor, userHandle, targetIntentFilter, callback); } static SharedPreferences getPinnedSharedPrefs(Context context) { return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE); } private ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( Context context, ProfilePagerResources profilePagerResources, ChooserRequest request, ProfileHelper profileHelper, ProfileAvailability profileAvailability, List initialIntents, int maxTargetsPerRow) { Log.d(TAG, "createMultiProfilePagerAdapter"); Profile launchedAs = profileHelper.getLaunchedAsProfile(); Intent[] initialIntentArray = initialIntents.toArray(new Intent[0]); List payloadIntents = request.getPayloadIntents(); List> tabs = new ArrayList<>(); for (Profile profile : profileHelper.getProfiles()) { if (profile.getType() == Profile.Type.PRIVATE && !profileAvailability.isAvailable(profile)) { continue; } ChooserGridAdapter adapter = createChooserGridAdapter( context, payloadIntents, profile.equals(launchedAs) ? initialIntentArray : null, profile.getPrimary().getHandle() ); tabs.add(new TabConfig<>( /* profile = */ profile.getType().ordinal(), profilePagerResources.profileTabLabel(profile.getType()), profilePagerResources.profileTabAccessibilityLabel(profile.getType()), /* tabTag = */ profile.getType().name(), adapter)); } EmptyStateProvider emptyStateProvider = createEmptyStateProvider(profileHelper, profileAvailability); Supplier workProfileQuietModeChecker = () -> !(profileHelper.getWorkProfilePresent() && profileAvailability.isAvailable( requireNonNull(profileHelper.getWorkProfile()))); return new ChooserMultiProfilePagerAdapter( /* context */ this, ImmutableList.copyOf(tabs), emptyStateProvider, workProfileQuietModeChecker, launchedAs.getType().ordinal(), profileHelper.getWorkHandle(), profileHelper.getCloneHandle(), maxTargetsPerRow); } protected EmptyStateProvider createBlockerEmptyStateProvider() { return new NoCrossProfileEmptyStateProvider( mProfiles, mDevicePolicyResources, createCrossProfileIntentsChecker(), mRequest.isSendActionTarget()); } private int findSelectedProfile() { return mProfiles.getLaunchedAsProfileType().ordinal(); } /** * 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) */ private boolean isWorkProfile() { return mProfiles.getLaunchedAsProfileType() == Profile.Type.WORK; } //@Override protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { return new PackageMonitor() { @Override public void onSomePackagesChanged() { handlePackagesChanged(listAdapter); } }; } /** * Update UI to reflect changes in data. */ @Override 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.refreshPackagesInAllTabs(); } else { listAdapter.handlePackagesChanged(); } } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); if (mSystemWindowInsets != null) { mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, mSystemWindowInsets.right, 0); } if (mViewPager.isLayoutRtl()) { mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager); } 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 (mProfiles.getWorkProfilePresent()) { 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, requireViewById(R.id.chooser_headline_row_container)); 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); } private void destroyProfileRecords() { mProfileRecords.values().forEach(ProfileRecord::destroy); mProfileRecords.clear(); } @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { Intent result = defIntent; if (mRequest.getReplacementExtras() != null) { final Bundle replExtras = mRequest.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; } private void maybeSendShareResult(TargetInfo cti) { if (mShareResultSender != null) { final ComponentName target = cti.getResolvedComponentName(); if (target != null) { mShareResultSender.onComponentSelected(target, cti.isChooserTargetInfo()); } } } private void addCallerChooserTargets() { if (!mRequest.getCallerChooserTargets().isEmpty()) { // Send the caller's chooser targets only to the default profile. if (mChooserMultiProfilePagerAdapter.getActiveProfile() == findSelectedProfile()) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( /* origTarget */ null, new ArrayList<>(mRequest.getCallerChooserTargets()), TARGET_TYPE_DEFAULT, /* directShareShortcutInfoCache */ Collections.emptyMap(), /* directShareAppTargetCache */ Collections.emptyMap()); } } } @Override // ResolverListCommunicator public boolean shouldGetActivityMetadata() { return true; } public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { if (target.isSuspended()) { return false; } // TODO: migrate to ChooserRequest return mViewModel.getActivityModel().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; intentFilter = targetInfo.isSelectableTargetInfo() ? mRequest.getShareTargetFilter() : 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); } protected boolean onTargetSelected(TargetInfo target) { if (mRefinementManager.maybeHandleSelection( target, mRequest.getRefinementIntentSender(), getApplication(), getMainThreadHandler())) { return false; } updateModelAndChooserCounts(target); maybeRemoveSharedText(target); safelyStartActivity(target); // Rely on the ActivityManager to pop up a dialog regarding app suspension // and return false return !target.isSuspended(); } @Override public void startSelected(int which, /* unused */ 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; } } if (isFinishing()) { return; } TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter() .targetInfoForPosition(which, filtered); if (target != null) { if (onTargetSelected(target)) { MetricsLogger.action( this, MetricsEvent.ACTION_APP_DISAMBIG_TAP); MetricsLogger.action(this, mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() ? MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED : MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED); Log.d(TAG, "onTargetSelected() returned true, finishing! " + target); finish(); } } // 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), mRequest.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 ); } } } 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; } protected void applyFooterView(int height) { mChooserMultiProfilePagerAdapter.setFooterHeightInEveryAdapter(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 = mRequest.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(@NonNull TargetInfo targetInfo) { Intent targetIntent = targetInfo.getTargetIntent(); if (targetIntent == null) { return; } Intent originalTargetIntent = new Intent(mRequest.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) || (mProfiles.getCloneUserPresent())) ? null : record.appPredictor; } protected EventLog getEventLog() { return mEventLog; } private ChooserGridAdapter createChooserGridAdapter( Context context, List payloadIntents, Intent[] initialIntents, UserHandle userHandle) { ChooserListAdapter chooserListAdapter = createChooserListAdapter( context, payloadIntents, initialIntents, /* TODO: not used, remove. rList= */ null, /* TODO: not used, remove. filterLastUsed= */ false, createListController(userHandle), userHandle, mRequest.getTargetIntent(), mRequest.getReferrerFillInIntent(), mMaxTargetsPerRow ); return new ChooserGridAdapter( context, new ChooserGridAdapter.ChooserActivityDelegate() { @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); } } }, chooserListAdapter, shouldShowContentPreview(), mMaxTargetsPerRow, mFeatureFlags); } @VisibleForTesting public ChooserListAdapter createChooserListAdapter( Context context, List payloadIntents, Intent[] initialIntents, List rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, Intent referrerFillInIntent, int maxTargetsPerRow) { UserHandle initialIntentsUserSpace = mProfiles.getQueryIntentsHandle(userHandle); return new ChooserListAdapter( context, payloadIntents, initialIntents, rList, filterLastUsed, createListController(userHandle), userHandle, targetIntent, referrerFillInIntent, this, mPackageManager, getEventLog(), maxTargetsPerRow, initialIntentsUserSpace, mTargetDataLoader, () -> { ProfileRecord record = getProfileRecord(userHandle); if (record != null && record.shortcutLoader != null) { record.shortcutLoader.reset(); } }, mFeatureFlags); } private void onWorkProfileStatusUpdated() { UserHandle workUser = mProfiles.getWorkHandle(); ProfileRecord record = workUser == null ? null : getProfileRecord(workUser); if (record != null && record.shortcutLoader != null) { record.shortcutLoader.reset(); } if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle().equals( mProfiles.getWorkHandle())) { mChooserMultiProfilePagerAdapter.rebuildActiveTab(true); } else { mChooserMultiProfilePagerAdapter.clearInactiveProfileCache(); } } @VisibleForTesting protected ChooserListController createListController(UserHandle userHandle) { AppPredictor appPredictor = getAppPredictor(userHandle); AbstractResolverComparator resolverComparator; if (appPredictor != null) { resolverComparator = new AppPredictionServiceResolverComparator( this, mRequest.getTargetIntent(), mRequest.getLaunchedFromPackage(), appPredictor, userHandle, getEventLog(), mNearbyShare.orElse(null) ); } else { resolverComparator = new ResolverRankerServiceResolverComparator( this, mRequest.getTargetIntent(), mRequest.getReferrerPackage(), null, getEventLog(), getResolverRankerServiceUserHandleList(userHandle), mNearbyShare.orElse(null)); } return new ChooserListController( this, mPackageManager, mRequest.getTargetIntent(), mRequest.getReferrerPackage(), mViewModel.getActivityModel().getLaunchedFromUid(), resolverComparator, mProfiles.getQueryIntentsHandle(userHandle), mRequest.getFilteredComponentNames(), mPinnedSharedPrefs); } @VisibleForTesting protected ViewModelProvider.Factory createPreviewViewModelFactory() { return PreviewViewModel.Companion.getFactory(); } private ChooserContentPreviewUi.ActionFactory decorateActionFactoryWithRefinement( ChooserContentPreviewUi.ActionFactory originalFactory) { if (!mFeatureFlags.refineSystemActions()) { return originalFactory; } return new ChooserContentPreviewUi.ActionFactory() { @Override @Nullable public Runnable getEditButtonRunnable() { return () -> { if (!mRefinementManager.maybeHandleSelection( RefinementType.EDIT_ACTION, List.of(mRequest.getTargetIntent()), null, mRequest.getRefinementIntentSender(), getApplication(), getMainThreadHandler())) { originalFactory.getEditButtonRunnable().run(); } }; } @Override @Nullable public Runnable getCopyButtonRunnable() { return () -> { if (!mRefinementManager.maybeHandleSelection( RefinementType.COPY_ACTION, List.of(mRequest.getTargetIntent()), null, mRequest.getRefinementIntentSender(), getApplication(), getMainThreadHandler())) { originalFactory.getCopyButtonRunnable().run(); } }; } @Override public List createCustomActions() { return originalFactory.createCustomActions(); } @Override @Nullable public ActionRow.Action getModifyShareAction() { return originalFactory.getModifyShareAction(); } @Override public Consumer getExcludeSharedTextAction() { return originalFactory.getExcludeSharedTextAction(); } }; } private ChooserActionFactory createChooserActionFactory(Intent targetIntent) { return new ChooserActionFactory( this, targetIntent, mRequest.getLaunchedFromPackage(), mRequest.getChooserActions(), mImageEditor, getEventLog(), (isExcluded) -> mExcludeSharedText = isExcluded, this::getFirstVisibleImgPreviewView, new ChooserActionFactory.ActionActivityStarter() { @Override public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { safelyStartActivityAsUser( targetInfo, mProfiles.getPersonalHandle() ); Log.d(TAG, "safelyStartActivityAsPersonalProfileUser(" + targetInfo + "): finishing!"); finish(); } @Override public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( TargetInfo targetInfo, View sharedElement, String sharedElementName) { ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( ChooserActivity.this, sharedElement, sharedElementName); safelyStartActivityAsUser( targetInfo, mProfiles.getPersonalHandle(), options.toBundle()); // Can't finish right away because the shared element transition may not // be ready to start. mFinishWhenStopped = true; } }, mShareResultSender, this::finishWithStatus, mClipboardManager, mFeatureFlags); } private Supplier createModifyShareActionFactory() { return () -> ChooserActionFactory.createCustomAction( ChooserActivity.this, mRequest.getModifyShareAction(), () -> getEventLog().logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE), mShareResultSender, this::finishWithStatus); } private void finishWithStatus(@Nullable Integer status) { if (status != null) { setResult(status); } Log.d(TAG, "finishWithStatus: result=" + 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 || !isProfilePagerAdapterAttached()) { 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(); } int currentProfile = mChooserMultiProfilePagerAdapter.getActiveProfile(); 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.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 (mProfiles.getWorkProfilePresent()) { 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 = mChooserMultiProfilePagerAdapter.getActiveEmptyStateView(); if (currentEmptyStateView.getVisibility() == View.VISIBLE) { offset += currentEmptyStateView.getHeight(); } } return Math.min(offset, bottom - top); } private boolean isProfilePagerAdapterAttached() { return mChooserMultiProfilePagerAdapter == mViewPager.getAdapter(); } /** * If we have a tabbed view and are showing 1 row in the current profile and an empty * state screen in another profile, to prevent cropping of the empty state screen we show * a second row in the current profile. */ private boolean shouldShowExtraRow(int rowsToShow) { return rowsToShow == 1 && mChooserMultiProfilePagerAdapter .shouldShowEmptyStateScreenInAnyInactiveAdapter(); } protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { Log.d(TAG, "onListRebuilt(listAdapter.userHandle=" + listAdapter.getUserHandle() + ", " + "rebuildComplete=" + rebuildComplete + ")"); setupScrollListener(); maybeSetupGlobalLayoutListener(); ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle(); if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) { mChooserMultiProfilePagerAdapter.getActiveAdapterView() .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()); mChooserMultiProfilePagerAdapter .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage()); } //TODO: move this block inside ChooserListAdapter (should be called when // ResolverListAdapter#mPostListReadyRunnable is executed. if (chooserListAdapter.getDisplayResolveInfoCount() == 0) { Log.d(TAG, "getDisplayResolveInfoCount() == 0"); if (rebuildComplete && mChooserServiceFeatureFlags.chooserPayloadToggling()) { onAppTargetsLoaded(listAdapter); } chooserListAdapter.notifyDataSetChanged(); } else { if (mChooserServiceFeatureFlags.chooserPayloadToggling()) { chooserListAdapter.updateAlphabeticalList( () -> onAppTargetsLoaded(listAdapter)); } else { chooserListAdapter.updateAlphabeticalList(); } } if (rebuildComplete) { long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle); if (duration >= 0) { Log.d(TAG, "app target loading time " + duration + " ms"); } addCallerChooserTargets(); getEventLog().logSharesheetAppLoadComplete(); maybeQueryAdditionalPostProcessingTargets( listProfileUserHandle, chooserListAdapter.getDisplayResolveInfos()); mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET); } } private void maybeQueryAdditionalPostProcessingTargets( UserHandle userHandle, DisplayResolveInfo[] displayResolveInfos) { ProfileRecord record = getProfileRecord(userHandle); if (record == null || record.shortcutLoader == null) { return; } record.loadingStartTime = SystemClock.elapsedRealtime(); record.shortcutLoader.updateAppTargets(displayResolveInfos); } @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 (mChooserMultiProfilePagerAdapter.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 = mProfiles.getWorkProfilePresent() ? 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() { @Override 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); } } } @Override 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 (mProfiles.getWorkProfilePresent()) { 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() { if (!shouldShowContentPreview()) { return false; } ResolverListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle( UserHandle.of(UserHandle.myUserId())); boolean isEmpty = adapter == null || adapter.getCount() == 0; return !isEmpty || shouldShowContentPreviewWhenEmpty(); } /** * 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 mRequest.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); } protected String getMetricsCategory() { return METRICS_CATEGORY_CHOOSER; } protected void onProfileTabSelected(int currentPage) { setupViewVisibilities(); maybeLogProfileChange(); if (mProfiles.getWorkProfilePresent()) { // The device policy logger is only concerned with sessions that include a work profile. DevicePolicyEventLogger .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS) .setInt(currentPage) .setStrings(getMetricsCategory()) .write(); } // 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(); } } protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { mSystemWindowInsets = insets.getInsets(WindowInsets.Type.systemBars()); if (mFeatureFlags.fixEmptyStatePaddingBug() || mProfiles.getWorkProfilePresent()) { mChooserMultiProfilePagerAdapter .setEmptyStateBottomOffset(mSystemWindowInsets.bottom); } mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, mSystemWindowInsets.right, 0); // Need extra padding so the list can fully scroll up // To accommodate for window insets applyFooterView(mSystemWindowInsets.bottom); if (mResolverDrawerLayout != null) { mResolverDrawerLayout.requestLayout(); } return WindowInsets.CONSUMED; } private void setHorizontalScrollingEnabled(boolean enabled) { mViewPager.setSwipingEnabled(enabled); } private void setVerticalScrollEnabled(boolean enabled) { ChooserGridLayoutManager layoutManager = (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView() .getLayoutManager(); layoutManager.setVerticalScrollEnabled(enabled); } 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); } } } 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(); } } } }