blob: d1bb4dafc42a64560487294d40e117594e36d388 [file] [log] [blame]
/*
* 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.service.chooser.Flags.interactiveChooser;
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.Flags.delayDrawerOffsetCalculation;
import static com.android.intentresolver.Flags.rebuildAdaptersOnTargetPinning;
import static com.android.intentresolver.Flags.refineSystemActions;
import static com.android.intentresolver.Flags.sharesheetEscExit;
import static com.android.intentresolver.Flags.synchronousDrawerOffsetCalculation;
import static com.android.intentresolver.ext.CreationExtrasExtKt.replaceDefaultArgs;
import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL;
import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK;
import static com.android.intentresolver.widget.ViewExtensionsKt.isFullyVisible;
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
import static com.android.systemui.shared.Flags.usePreferredImageEditor;
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.graphics.Rect;
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.KeyEvent;
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.actions.ImageEditorActionFactory;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
import com.android.intentresolver.data.model.ChooserRequest;
import com.android.intentresolver.data.repository.ActivityModelRepository;
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.FallbackImageEditor;
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.ActivityModel;
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.ShareAction;
import com.android.intentresolver.ui.viewmodel.ChooserViewModel;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ChooserNestedScrollView;
import com.android.intentresolver.widget.ImagePreviewView;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.intentresolver.widget.ResolverDrawerLayoutExt;
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 kotlinx.coroutines.CoroutineDispatcher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
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;
/**
* 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_PROFILE = "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<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
private final Map<ChooserTarget, ShortcutInfo> 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 EventLog mEventLog;
@Inject @AppPredictionAvailable public boolean mAppPredictionAvailable;
@Inject @FallbackImageEditor
public Optional<ComponentName> mImageEditor;
@Inject public ImageEditorActionFactory mImageEditorActionFactory;
@Inject @NearbyShare public Optional<ComponentName> mNearbyShare;
@Inject
@Caching
public TargetDataLoader mTargetDataLoader;
@Inject public DevicePolicyResources mDevicePolicyResources;
@Inject public ProfilePagerResources mProfilePagerResources;
@Inject public PackageManager mPackageManager;
@Inject public ClipboardManager mClipboardManager;
@Inject public IntentForwarding mIntentForwarding;
@Inject public ShareResultSenderFactory mShareResultSenderFactory;
@Inject public ActivityModelRepository mActivityModelRepository;
private ActivityModel mActivityModel;
private ChooserRequest mRequest;
private ProfileHelper mProfiles;
private ProfileAvailability mProfileAvailability;
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<Integer, ProfileRecord> mProfileRecords = new LinkedHashMap<>();
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;
private int mInitialProfile = -1;
@NonNull
@Override
public CreationExtras getDefaultViewModelCreationExtras() {
// DEFAULT_ARGS_KEY extra is saved for each ViewModel we create. ComponentActivity puts the
// initial intent's extra into DEFAULT_ARGS_KEY thus we store these values 2 times (3 if we
// count the initial intent). We don't need those values to be saved as they don't capture
// the state.
return replaceDefaultArgs(super.getDefaultViewModelCreationExtras());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "onCreate");
mActivityModelRepository.initialize(this::createActivityModel);
setTheme(R.style.Theme_DeviceDefault_Chooser);
// Initializer is invoked when this function returns, via Lifecycle.
mChooserHelper.setInitializer(this::initialize);
mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged);
mChooserHelper.setOnPendingSelection(this::onPendingSelection);
mChooserHelper.setOnTargetEnabled(this::onTargetEnabledChanged);
}
@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_PROFILE, mChooserMultiProfilePagerAdapter.getActiveProfile());
}
}
@Override
protected final void onRestart() {
super.onRestart();
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);
if (interactiveChooser() && mViewModel != null) {
mViewModel.getInteractiveSessionInteractor().endSession();
}
}
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();
if (isInteractiveSession()) {
maybeUpdateColorScheme();
}
mProfiles = new ProfileHelper(
mUserInteractor,
mBackgroundDispatcher);
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,
mProfileRecords.values(),
mProfileAvailability,
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<String> 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 (usePreferredImageEditor()) {
mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT);
mImageEditorActionFactory.getImageEditorTargetInfoAsync(
getCoroutineScope(getLifecycle()),
completion.getRefinedIntent(),
targetInfo -> launchImageEditor(targetInfo));
return;
} else {
if (refinedActionFactory.getEditButtonRunnable() != null) {
refinedActionFactory.getEditButtonRunnable().run();
}
}
}
break;
}
finish();
}
});
ChooserContentPreviewUi.ActionFactory actionFactory =
decorateActionFactoryWithRefinement(
createChooserActionFactory(mRequest.getTargetIntent()));
mChooserContentPreviewUi = new ChooserContentPreviewUi(
getCoroutineScope(getLifecycle()),
mViewModel.getPreviewDataProvider(),
mRequest,
mViewModel.getImageLoader(),
actionFactory,
createModifyShareActionFactory(),
mEnterTransitionAnimationDelegate,
new HeadlineGeneratorImpl(this),
mRequest.getContentTypeHint(),
mRequest.getMetadataText());
updateStickyContentPreview();
if (usePreferredImageEditor()) {
mImageEditorActionFactory.getImageEditorTargetInfoAsync(
getCoroutineScope(getLifecycle()),
mRequest.getTargetIntent(),
targetInfo -> mChooserContentPreviewUi.setImageEditorCallback(() -> {
if (!mRefinementManager.maybeHandleSelection(
RefinementType.EDIT_ACTION,
List.of(mRequest.getTargetIntent()),
null,
mRequest.getRefinementIntentSender(),
getApplication(),
getMainThreadHandler())) {
// No refinement needed, launch it.
mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT);
launchImageEditor(targetInfo);
}
}));
}
if (shouldShowStickyContentPreview()) {
getEventLog().logActionShareWithPreview(
mChooserContentPreviewUi.getPreferredContentPreview());
}
mChooserShownTime = System.currentTimeMillis();
final long systemCost = mChooserShownTime - mIntentReceivedTime.get();
getEventLog().logChooserActivityShown(
isWorkProfile(), mRequest.getTargetType(), systemCost);
if (mResolverDrawerLayout != null) {
if (synchronousDrawerOffsetCalculation()) {
mResolverDrawerLayout.setCollapsibleHeightReservedDelegate(
this::syncHandleLayoutChange);
} else {
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();
mInitialProfile = findSelectedProfile();
Tracer.INSTANCE.markLaunched();
if (isInteractiveSession()) {
configureInteractiveSessionWindow();
updateInteractiveArea();
}
}
private void maybeUpdateColorScheme() {
if (!isInteractiveSession()) {
Log.wtf(TAG, "This method should be called for an interactive session");
return;
}
final boolean shouldUseNightMode = switch (mRequest.getColorScheme()) {
case SystemDefault ->
// apparently, updating color scheme for an activity invocation can affect
// consequent activity invocations; restore the value from the application
// configuration.
getApplicationContext().getResources().getConfiguration().isNightModeActive();
case Dark -> true;
case Light -> false;
};
Configuration currentConfig = getResources().getConfiguration();
boolean isNightMode = currentConfig.isNightModeActive();
if (isNightMode == shouldUseNightMode) {
return;
}
Configuration newConfig = new Configuration(currentConfig);
int nightModeConfig = shouldUseNightMode
? Configuration.UI_MODE_NIGHT_YES
: Configuration.UI_MODE_NIGHT_NO;
newConfig.uiMode = (~Configuration.UI_MODE_NIGHT_MASK & newConfig.uiMode) | nightModeConfig;
getResources().updateConfiguration(newConfig, getResources().getDisplayMetrics());
}
private void launchImageEditor(TargetInfo editorTargetInfo) {
if (editorTargetInfo == null) return;
mEventLog.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
View imageViewForTransition = getFirstVisibleImgPreviewView();
if (imageViewForTransition != null && isFullyVisible(imageViewForTransition)) {
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
this, imageViewForTransition, ChooserActionFactory.IMAGE_EDITOR_SHARED_ELEMENT);
safelyStartActivityAsUser(
editorTargetInfo,
mProfiles.getPersonalHandle(),
options.toBundle());
} else {
safelyStartActivityAsUser(
editorTargetInfo,
mProfiles.getPersonalHandle()
);
}
finish();
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (sharesheetEscExit() && keyCode == KeyEvent.KEYCODE_ESCAPE) {
finish();
return true;
}
return super.onKeyUp(keyCode, event);
}
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) {
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 onTargetEnabledChanged(boolean isEnabled) {
mChooserMultiProfilePagerAdapter.setTargetsEnabled(isEnabled);
}
private void configureInteractiveSessionWindow() {
if (!isInteractiveSession()) {
Log.wtf(TAG, "Unexpected user of the method; should be an interactive session");
return;
}
final Window window = getWindow();
if (window == null) {
return;
}
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY);
}
private void updateInteractiveArea() {
if (!isInteractiveSession()) {
Log.wtf(TAG, "Unexpected user of the method; should be an interactive session");
return;
}
final View contentView = findViewById(android.R.id.content);
final ResolverDrawerLayout rdl = mResolverDrawerLayout;
if (contentView == null || rdl == null) {
return;
}
final Rect rect = new Rect();
contentView.getViewTreeObserver().addOnComputeInternalInsetsListener((info) -> {
int oldTop = rect.top;
rdl.getBoundsInWindow(rect, true);
int left = rect.left;
int top = rect.top;
ResolverDrawerLayoutExt.getVisibleDrawerRect(rdl, rect);
rect.offset(left, top);
if (oldTop != rect.top) {
Rect r = rect;
Window w = getWindow();
WindowManager.LayoutParams wa = w == null ? null : w.getAttributes();
if (wa != null && (wa.x != 0 || wa.y != 0)) {
r = new Rect(rect);
r.offset(wa.x, wa.y);
}
mViewModel.getInteractiveSessionInteractor().sendChooserWindowSize(r);
}
info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
info.touchableRegion.set(new Rect(rect));
});
}
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();
mShareResultSender = mShareResultSenderFactory.create(
mViewModel.getActivityModel().getLaunchedFromUid(), chosenComponentSender);
}
private boolean shouldUpdateAdapters(
ChooserRequest oldChooserRequest, ChooserRequest newChooserRequest) {
Intent oldTargetIntent = oldChooserRequest.getTargetIntent();
Intent newTargetIntent = newChooserRequest.getTargetIntent();
List<Intent> oldAltIntents = oldChooserRequest.getAdditionalTargets();
List<Intent> newAltIntents = newChooserRequest.getAdditionalTargets();
List<ComponentName> oldExcluded = oldChooserRequest.getFilteredComponentNames();
List<ComponentName> newExcluded = newChooserRequest.getFilteredComponentNames();
// 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)
|| !oldExcluded.equals(newExcluded);
}
private void recreatePagerAdapter() {
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.
ChooserMultiProfilePagerAdapter oldPagerAdapter =
mChooserMultiProfilePagerAdapter;
mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
/* context = */ this,
mProfilePagerResources,
mRequest,
mProfiles,
mProfileRecords.values(),
mProfileAvailability,
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()));
if (oldPagerAdapter != null) {
for (int i = 0, count = mChooserMultiProfilePagerAdapter.getCount(); i < count; i++) {
ChooserListAdapter listAdapter =
mChooserMultiProfilePagerAdapter.getPageAdapterForIndex(i)
.getListAdapter();
ChooserListAdapter oldListAdapter =
oldPagerAdapter.getListAdapterForUserHandle(listAdapter.getUserHandle());
if (oldListAdapter != null) {
listAdapter.copyDirectTargetsFrom(oldListAdapter);
listAdapter.setDirectTargetsEnabled(false);
}
}
}
setTabsViewEnabled(false);
if (mSystemWindowInsets != null) {
applyFooterView(mSystemWindowInsets.bottom);
}
}
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) {
int profile = savedInstanceState.getInt(LAST_SHOWN_PROFILE);
int profileNumber = mChooserMultiProfilePagerAdapter.getPageNumberForProfile(profile);
if (profileNumber != -1) {
mViewPager.setCurrentItem(profileNumber);
mInitialProfile = profile;
}
}
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() {
if (isInteractiveSession()) {
return false;
}
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, user);
}
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<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) {
return getResolverRankerServiceUserHandleListInternal(userHandle);
}
private List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle userHandle) {
List<UserHandle> 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 <code>true</code> 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);
ChooserNestedScrollView scrollableContainer =
requireViewById(R.id.chooser_scrollable_container);
scrollableContainer.setRequestChildFocusPredicate((child, focused) ->
// TabHost view will request focus on the newly activated tab. The RecyclerView
// from the tab gets focused and notifies its parents (including
// NestedScrollView) about it through #requestChildFocus method call.
// NestedScrollView's view implementation of the method will scroll to the
// focused view. As we don't want to change drawer's position upon tab change,
// ignore focus requests from tab RecyclerViews.
focused == null || focused.getId() != com.android.internal.R.id.resolver_list);
boolean result = postRebuildList(rebuildCompleted);
Trace.endSection();
return result;
}
protected void onExitButtonClicked(View v) {
finish();
}
/**
* Finishing procedures to be performed after the list has been rebuilt.
* </p>Subclasses must call postRebuildListInternal at the end of postRebuildList.
* @param rebuildCompleted
* @return <code>true</code> 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 <code>true</code> 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) {
Profile launchedAsProfile = mProfiles.getLaunchedAsProfile();
for (Profile profile : mProfiles.getProfiles()) {
if (profile.getType() == Profile.Type.PRIVATE
&& !mProfileAvailability.isAvailable(profile)) {
continue;
}
ProfileRecord record = createProfileRecord(
profile,
targetIntentFilter,
launchedAsProfile.equals(profile)
? mRequest.getCallerChooserTargets()
: Collections.emptyList(),
factory);
if (profile.equals(launchedAsProfile) && record.shortcutLoader == null) {
Tracer.INSTANCE.endLaunchToShortcutTrace();
}
}
}
private ProfileRecord createProfileRecord(
Profile profile,
IntentFilter targetIntentFilter,
List<ChooserTarget> callerTargets,
AppPredictorFactory factory) {
UserHandle userHandle = profile.getPrimary().getHandle();
AppPredictor appPredictor = factory.create(userHandle);
ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
? null
: createShortcutLoader(
this,
appPredictor,
userHandle,
targetIntentFilter,
shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
ProfileRecord record = new ProfileRecord(
profile, appPredictor, shortcutLoader, callerTargets);
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<ShortcutLoader.Result> 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,
Collection<ProfileRecord> profileRecords,
ProfileAvailability profileAvailability,
int maxTargetsPerRow) {
Log.d(TAG, "createMultiProfilePagerAdapter");
Profile launchedAs = profileHelper.getLaunchedAsProfile();
Intent[] initialIntentArray = request.getInitialIntents().toArray(new Intent[0]);
List<TabConfig<ChooserGridAdapter>> tabs = new ArrayList<>();
for (ProfileRecord record : profileRecords) {
Profile profile = record.profile;
boolean isCrossProfile = !profile.equals(launchedAs);
ChooserGridAdapter adapter = createChooserGridAdapter(
context,
isCrossProfile
? request.getCrossProfilePayloadIntents()
: request.getPayloadIntents(),
isCrossProfile ? null : initialIntentArray,
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<Boolean> 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.
* <p>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 (rebuildAdaptersOnTargetPinning()) {
recreatePagerAdapter();
} else {
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);
adjustMaxPreviewWidth();
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 adjustMaxPreviewWidth() {
if (mResolverDrawerLayout == null) {
return;
}
mResolverDrawerLayout.setMaxWidth(
getResources().getDimensionPixelSize(R.dimen.chooser_width));
}
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, UserHandle launchedAsUser) {
final ComponentName target = cti.getResolvedComponentName();
if (target != null) {
boolean crossProfile = !UserHandle.of(UserHandle.myUserId()).equals(launchedAsUser);
mShareResultSender.onComponentSelected(target, cti.isChooserTargetInfo(), crossProfile);
}
}
private void addCallerChooserTargets(ChooserListAdapter adapter) {
ProfileRecord record = getProfileRecord(adapter.getUserHandle());
List<ChooserTarget> callerTargets = record == null
? Collections.emptyList()
: record.callerTargets;
if (!callerTargets.isEmpty()) {
adapter.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<DisplayResolveInfo> 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<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
List<AppTargetId> 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<Intent> 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);
}
@VisibleForTesting
public ChooserListAdapter createChooserListAdapter(
Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
List<ResolveInfo> 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,
resolverListController,
userHandle,
targetIntent,
referrerFillInIntent,
this,
mPackageManager,
getEventLog(),
maxTargetsPerRow,
initialIntentsUserSpace,
mTargetDataLoader,
() -> {
ProfileRecord record = getProfileRecord(userHandle);
if (record != null && record.shortcutLoader != null) {
record.shortcutLoader.reset();
}
});
}
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);
}
private ChooserContentPreviewUi.ActionFactory decorateActionFactoryWithRefinement(
ChooserContentPreviewUi.ActionFactory originalFactory) {
if (!refineSystemActions()) {
return originalFactory;
}
return new ChooserContentPreviewUi.ActionFactory() {
@Override
@Nullable
public Runnable getEditButtonRunnable() {
if (originalFactory.getEditButtonRunnable() == null) return null;
return () -> {
if (!mRefinementManager.maybeHandleSelection(
RefinementType.EDIT_ACTION,
List.of(mRequest.getTargetIntent()),
null,
mRequest.getRefinementIntentSender(),
getApplication(),
getMainThreadHandler())) {
originalFactory.getEditButtonRunnable().run();
}
};
}
@Override
@Nullable
public Runnable getCopyButtonRunnable() {
if (originalFactory.getCopyButtonRunnable() == null) return null;
return () -> {
if (!mRefinementManager.maybeHandleSelection(
RefinementType.COPY_ACTION,
List.of(mRequest.getTargetIntent()),
null,
mRequest.getRefinementIntentSender(),
getApplication(),
getMainThreadHandler())) {
originalFactory.getCopyButtonRunnable().run();
}
};
}
@Override
public List<ActionRow.Action> createCustomActions() {
return originalFactory.createCustomActions();
}
@Override
@Nullable
public ActionRow.Action getModifyShareAction() {
return originalFactory.getModifyShareAction();
}
@Override
public Consumer<Boolean> 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 safelyStartActivityAsLaunchingUser(TargetInfo targetInfo) {
safelyStartActivityAsUser(
targetInfo,
mUserInteractor.getLaunchedAs()
);
Log.d(TAG, "safelyStartActivityAsPersonalProfileUser("
+ targetInfo + "): finishing!");
finish();
}
@Override
public void safelyStartActivityAsLaunchingUserWithSharedElementTransition(
TargetInfo targetInfo, View sharedElement, String sharedElementName) {
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
ChooserActivity.this, sharedElement, sharedElementName);
safelyStartActivityAsUser(
targetInfo,
mUserInteractor.getLaunchedAs(),
options.toBundle());
// Can't finish right away because the shared element transition may not
// be ready to start.
mFinishWhenStopped = true;
}
},
mShareResultSender,
this::finishWithStatus,
mClipboardManager);
}
private Supplier<ActionRow.Action> 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 (!shouldUpdateDrawerOffset()) {
return;
}
final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
maybeUpdateTabPadding(availableWidth);
mCurrAvailableWidth = availableWidth;
if (mChooserMultiProfilePagerAdapter.getActiveProfile() != mInitialProfile) {
return;
}
RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
getMainThreadHandler().post(() -> {
if (mResolverDrawerLayout == null) {
return;
}
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
mEnterTransitionAnimationDelegate.markOffsetCalculated();
mLastAppliedInsets = mSystemWindowInsets;
});
}
/*
* 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 int syncHandleLayoutChange(
ResolverDrawerLayout drawer, int left, int top, int right, int bottom, int offset) {
if (!shouldUpdateDrawerOffset()) {
return offset;
}
final int availableWidth = right - left
- drawer.getPaddingLeft() - drawer.getPaddingRight();
maybeUpdateTabPadding(availableWidth);
mCurrAvailableWidth = availableWidth;
if (mChooserMultiProfilePagerAdapter.getActiveProfile() != mInitialProfile) {
return offset;
}
mLastAppliedInsets = mSystemWindowInsets;
getMainThreadHandler().post(mEnterTransitionAnimationDelegate::markOffsetCalculated);
RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
return calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
}
private boolean shouldUpdateDrawerOffset() {
if (mChooserMultiProfilePagerAdapter == null || !isProfilePagerAdapterAttached()) {
return false;
}
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 false;
}
return !delayDrawerOffsetCalculation()
|| gridAdapter.getListAdapter().isInitialAppTargetLoad()
|| gridAdapter.getListAdapter().areAppTargetsReady();
}
private void maybeUpdateTabPadding(int availableWidth) {
if (mChooserMultiProfilePagerAdapter == null) {
return;
}
final int maxChooserWidth = getResources().getDimensionPixelSize(R.dimen.chooser_width);
RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
boolean isLayoutUpdated =
gridAdapter.calculateChooserTargetWidth(
maxChooserWidth >= 0
? Math.min(maxChooserWidth, availableWidth)
: availableWidth)
|| recyclerView.getAdapter() == null
|| availableWidth != mCurrAvailableWidth;
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();
}
}
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.
chooserListAdapter.updateAlphabeticalList(
rebuildComplete,
() -> onAppTargetsLoaded(listAdapter));
if (rebuildComplete) {
long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle);
if (duration >= 0) {
Log.d(TAG, "app target loading time " + duration + " ms");
}
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) {
adapter.setDirectTargetsEnabled(true);
adapter.resetDirectTargets();
addCallerChooserTargets(adapter);
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 (isInteractiveSession() || !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();
}
}
private boolean isInteractiveSession() {
return interactiveChooser() && mRequest.getInteractiveSessionCallback() != null
&& !isTaskRoot();
}
protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
mSystemWindowInsets = insets.getInsets(WindowInsets.Type.systemBars());
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 {
public final Profile profile;
/** 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 final List<ChooserTarget> callerTargets;
public long loadingStartTime;
private ProfileRecord(
Profile profile,
@Nullable AppPredictor appPredictor,
@Nullable ShortcutLoader shortcutLoader,
List<ChooserTarget> callerTargets) {
this.profile = profile;
this.appPredictor = appPredictor;
this.shortcutLoader = shortcutLoader;
this.callerTargets = callerTargets;
}
public void destroy() {
if (appPredictor != null) {
appPredictor.destroy();
}
if (shortcutLoader != null) {
shortcutLoader.destroy();
}
}
}
}