blob: ae5be26d13dc12da8c9308c8e0a4f0bb77d76445 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.intentresolver;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Insets;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import androidx.annotation.MainThread;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
import com.android.intentresolver.flags.Flags;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.grid.DirectShareViewHolder;
import com.android.intentresolver.model.AbstractResolverComparator;
import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
/**
* The Chooser Activity handles intent resolution specifically for sharing intents -
* for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
*
*/
public class ChooserActivity extends ResolverActivity implements
ResolverListAdapter.ResolverListCommunicator {
private static final String TAG = "ChooserActivity";
/**
* Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
* in onStop when launched in a new task. If this extra is set to true, we do not finish
* ourselves when onStop gets called.
*/
public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
= "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
/**
* Transition name for the first image preview.
* To be used for shared element transition into this activity.
* @hide
*/
public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image";
private static final String PREF_NUM_SHEET_EXPANSIONS = "pref_num_sheet_expansions";
private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon";
private static final boolean DEBUG = true;
public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
private static final String SHORTCUT_TARGET = "shortcut_target";
private static final String PLURALS_COUNT = "count";
private static final String PLURALS_FILE_NAME = "file_name";
private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
// TODO: these data structures are for one-time use in shuttling data from where they're
// populated in `ShortcutToChooserTargetConverter` to where they're consumed in
// `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`.
// That flow should be refactored so that `ChooserActivity` isn't responsible for holding their
// intermediate data, and then these members can be removed.
private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
public static final int TARGET_TYPE_DEFAULT = 0;
public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
private static final int SCROLL_STATUS_IDLE = 0;
private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
@IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
TARGET_TYPE_DEFAULT,
TARGET_TYPE_CHOOSER_TARGET,
TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
})
@Retention(RetentionPolicy.SOURCE)
public @interface ShareTargetType {}
public static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f;
private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
private final int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
DEFAULT_SALT_EXPIRATION_DAYS);
private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
/* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
* only assignment there, and expect it to be ready by the time we ever use it --
* someday if we move all the usage to a component with a narrower lifecycle (something that
* matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we
* should be able to make this assignment as "final."
*/
@Nullable
private ChooserRequestParameters mChooserRequest;
private ChooserRefinementManager mRefinementManager;
private FeatureFlagRepository mFeatureFlagRepository;
private ChooserContentPreviewUi mChooserContentPreviewUi;
private boolean mShouldDisplayLandscape;
// statsd logger wrapper
protected ChooserActivityLogger mChooserActivityLogger;
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
private int mCurrAvailableWidth = 0;
private Insets mLastAppliedInsets = null;
private int mLastNumberOfChildren = -1;
private int mMaxTargetsPerRow = 1;
private static final int MAX_LOG_RANK_POSITION = 12;
// TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters.
private static final int MAX_EXTRA_INITIAL_INTENTS = 2;
private static final int MAX_EXTRA_CHOOSER_TARGETS = 2;
private SharedPreferences mPinnedSharedPrefs;
private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
private int mScrollStatus = SCROLL_STATUS_IDLE;
@VisibleForTesting
protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =
new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout);
private View mContentView = null;
private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
private boolean mExcludeSharedText = false;
public ChooserActivity() {}
@Override
protected void onCreate(Bundle savedInstanceState) {
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
getChooserActivityLogger().logSharesheetTriggered();
mFeatureFlagRepository = createFeatureFlagRepository();
mIntegratedDeviceComponents = getIntegratedDeviceComponents();
try {
mChooserRequest = new ChooserRequestParameters(
getIntent(),
getReferrerPackageName(),
getReferrer(),
mIntegratedDeviceComponents,
mFeatureFlagRepository);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
finish();
super_onCreate(null);
return;
}
mRefinementManager = new ChooserRefinementManager(
this,
mChooserRequest.getRefinementIntentSender(),
(validatedRefinedTarget) -> {
maybeRemoveSharedText(validatedRefinedTarget);
if (super.onTargetSelected(validatedRefinedTarget, false)) {
finish();
}
},
() -> {
mRefinementManager.destroy();
finish();
});
mChooserContentPreviewUi = new ChooserContentPreviewUi(
mChooserRequest.getTargetIntent(),
getContentResolver(),
this::isImageType,
createPreviewImageLoader(),
createChooserActionFactory(),
mEnterTransitionAnimationDelegate,
mFeatureFlagRepository);
setAdditionalTargets(mChooserRequest.getAdditionalTargets());
setSafeForwardingMode(true);
mPinnedSharedPrefs = getPinnedSharedPrefs(this);
mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
mShouldDisplayLandscape =
shouldDisplayLandscape(getResources().getConfiguration().orientation);
setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
createProfileRecords(
new AppPredictorFactory(
getApplicationContext(),
mChooserRequest.getSharedText(),
mChooserRequest.getTargetIntentFilter()),
mChooserRequest.getTargetIntentFilter());
super.onCreate(
savedInstanceState,
mChooserRequest.getTargetIntent(),
mChooserRequest.getTitle(),
mChooserRequest.getDefaultTitleResource(),
mChooserRequest.getInitialIntents(),
/* rList: List<ResolveInfo> = */ null,
/* supportsAlwaysUseOption = */ false);
mChooserShownTime = System.currentTimeMillis();
final long systemCost = mChooserShownTime - intentReceivedTime;
getChooserActivityLogger().logChooserActivityShown(
isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
if (mResolverDrawerLayout != null) {
mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
// expand/shrink direct share 4 -> 8 viewgroup
if (mChooserRequest.isSendActionTarget()) {
mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll);
}
mResolverDrawerLayout.setOnCollapsedChangedListener(
new ResolverDrawerLayout.OnCollapsedChangedListener() {
// Only consider one expansion per activity creation
private boolean mWrittenOnce = false;
@Override
public void onCollapsedChanged(boolean isCollapsed) {
if (!isCollapsed && !mWrittenOnce) {
incrementNumSheetExpansions();
mWrittenOnce = true;
}
getChooserActivityLogger()
.logSharesheetExpansionChanged(isCollapsed);
}
});
}
if (DEBUG) {
Log.d(TAG, "System Time Cost is " + systemCost);
}
getChooserActivityLogger().logShareStarted(
getReferrerPackageName(),
mChooserRequest.getTargetType(),
mChooserRequest.getCallerChooserTargets().size(),
(mChooserRequest.getInitialIntents() == null)
? 0 : mChooserRequest.getInitialIntents().length,
isWorkProfile(),
mChooserContentPreviewUi.getPreferredContentPreview(),
mChooserRequest.getTargetAction(),
mChooserRequest.getChooserActions().size(),
mChooserRequest.getModifyShareAction() != null
);
mEnterTransitionAnimationDelegate.postponeTransition();
}
@VisibleForTesting
protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
return ChooserIntegratedDeviceComponents.get(this, new SecureSettings());
}
@Override
protected int appliedThemeResId() {
return R.style.Theme_DeviceDefault_Chooser;
}
protected FeatureFlagRepository createFeatureFlagRepository() {
return new FeatureFlagRepositoryFactory().create(getApplicationContext());
}
private void createProfileRecords(
AppPredictorFactory factory, IntentFilter targetIntentFilter) {
UserHandle mainUserHandle = getPersonalProfileUserHandle();
createProfileRecord(mainUserHandle, targetIntentFilter, factory);
UserHandle workUserHandle = getWorkProfileUserHandle();
if (workUserHandle != null) {
createProfileRecord(workUserHandle, targetIntentFilter, factory);
}
}
private void createProfileRecord(
UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
AppPredictor appPredictor = factory.create(userHandle);
ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
? null
: createShortcutLoader(
getApplicationContext(),
appPredictor,
userHandle,
targetIntentFilter,
shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
mProfileRecords.put(
userHandle.getIdentifier(),
new ProfileRecord(appPredictor, shortcutLoader));
}
@Nullable
private ProfileRecord getProfileRecord(UserHandle userHandle) {
return mProfileRecords.get(userHandle.getIdentifier(), null);
}
@VisibleForTesting
protected ShortcutLoader createShortcutLoader(
Context context,
AppPredictor appPredictor,
UserHandle userHandle,
IntentFilter targetIntentFilter,
Consumer<ShortcutLoader.Result> callback) {
return new ShortcutLoader(
context,
appPredictor,
userHandle,
targetIntentFilter,
callback);
}
static SharedPreferences getPinnedSharedPrefs(Context context) {
// The code below is because in the android:ui process, no one can hear you scream.
// The package info in the context isn't initialized in the way it is for normal apps,
// so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
// build the path manually below using the same policy that appears in ContextImpl.
// This fails silently under the hood if there's a problem, so if we find ourselves in
// the case where we don't have access to credential encrypted storage we just won't
// have our pinned target info.
final File prefsFile = new File(new File(
Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL,
context.getUserId(), context.getPackageName()),
"shared_prefs"),
PINNED_SHARED_PREFS_NAME + ".xml");
return context.getSharedPreferences(prefsFile, MODE_PRIVATE);
}
@Override
protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed) {
if (shouldShowTabs()) {
mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles(
initialIntents, rList, filterLastUsed);
} else {
mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile(
initialIntents, rList, filterLastUsed);
}
return mChooserMultiProfilePagerAdapter;
}
@Override
protected EmptyStateProvider createBlockerEmptyStateProvider() {
final boolean isSendAction = mChooserRequest.isSendActionTarget();
final EmptyState noWorkToPersonalEmptyState =
new DevicePolicyBlockerEmptyState(
/* context= */ this,
/* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
/* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
/* devicePolicyStringSubtitleId= */
isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL,
/* defaultSubtitleResource= */
isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation
: R.string.resolver_cant_access_personal_apps_explanation,
/* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
/* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
final EmptyState noPersonalToWorkEmptyState =
new DevicePolicyBlockerEmptyState(
/* context= */ this,
/* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
/* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
/* devicePolicyStringSubtitleId= */
isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK,
/* defaultSubtitleResource= */
isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation
: R.string.resolver_cant_access_work_apps_explanation,
/* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
/* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
createCrossProfileIntentsChecker(), createMyUserIdProvider());
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed) {
ChooserGridAdapter adapter = createChooserGridAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
initialIntents,
rList,
filterLastUsed,
/* userHandle */ UserHandle.of(UserHandle.myUserId()));
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
adapter,
createEmptyStateProvider(/* workProfileUserHandle= */ null),
/* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null,
mMaxTargetsPerRow);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed) {
int selectedProfile = findSelectedProfile();
ChooserGridAdapter personalAdapter = createChooserGridAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
rList,
filterLastUsed,
/* userHandle */ getPersonalProfileUserHandle());
ChooserGridAdapter workAdapter = createChooserGridAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
selectedProfile == PROFILE_WORK ? initialIntents : null,
rList,
filterLastUsed,
/* userHandle */ getWorkProfileUserHandle());
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
personalAdapter,
workAdapter,
createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()),
() -> mWorkProfileAvailability.isQuietModeEnabled(),
selectedProfile,
getWorkProfileUserHandle(),
mMaxTargetsPerRow);
}
private int findSelectedProfile() {
int selectedProfile = getSelectedProfileExtra();
if (selectedProfile == -1) {
selectedProfile = getProfileForUser(getUser());
}
return selectedProfile;
}
@Override
protected boolean postRebuildList(boolean rebuildCompleted) {
updateStickyContentPreview();
if (shouldShowStickyContentPreview()
|| mChooserMultiProfilePagerAdapter
.getCurrentRootAdapter().getSystemRowCount() != 0) {
getChooserActivityLogger().logActionShareWithPreview(
mChooserContentPreviewUi.getPreferredContentPreview());
}
return postRebuildListInternal(rebuildCompleted);
}
/**
* Check if the profile currently used is a work profile.
* @return true if it is work profile, false if it is parent profile (or no work profile is
* set up)
*/
protected boolean isWorkProfile() {
return getSystemService(UserManager.class)
.getUserInfo(UserHandle.myUserId()).isManagedProfile();
}
@Override
protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
return new PackageMonitor() {
@Override
public void onSomePackagesChanged() {
handlePackagesChanged(listAdapter);
}
};
}
/**
* Update UI to reflect changes in data.
*/
public void handlePackagesChanged() {
handlePackagesChanged(/* listAdapter */ null);
}
/**
* Update UI to reflect changes in data.
* <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 (listAdapter == null) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
if (mChooserMultiProfilePagerAdapter.getCount() > 1) {
mChooserMultiProfilePagerAdapter.getInactiveListAdapter().handlePackagesChanged();
}
} else {
listAdapter.handlePackagesChanged();
}
updateProfileViewButton();
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
maybeCancelFinishAnimation();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
if (viewPager.isLayoutRtl()) {
mMultiProfilePagerAdapter.setupViewPager(viewPager);
}
mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow);
adjustPreviewWidth(newConfig.orientation, null);
updateStickyContentPreview();
updateTabPadding();
}
private boolean shouldDisplayLandscape(int orientation) {
// Sharesheet fixes the # of items per row and therefore can not correctly lay out
// when in the restricted size of multi-window mode. In the future, would be nice
// to use minimum dp size requirements instead
return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode();
}
private void adjustPreviewWidth(int orientation, View parent) {
int width = -1;
if (mShouldDisplayLandscape) {
width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width);
}
parent = parent == null ? getWindow().getDecorView() : parent;
updateLayoutWidth(com.android.internal.R.id.content_preview_text_layout, width, parent);
updateLayoutWidth(com.android.internal.R.id.content_preview_title_layout, width, parent);
updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent);
}
private void updateTabPadding() {
if (shouldShowTabs()) {
View tabs = findViewById(com.android.internal.R.id.tabs);
float iconSize = getResources().getDimension(R.dimen.chooser_icon_size);
// The entire width consists of icons or padding. Divide the item padding in half to get
// paddingHorizontal.
float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize)
/ mMaxTargetsPerRow / 2;
// Subtract the margin the buttons already have.
padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin);
tabs.setPadding((int) padding, 0, (int) padding, 0);
}
}
private void updateLayoutWidth(int layoutResourceId, int width, View parent) {
View view = parent.findViewById(layoutResourceId);
if (view != null && view.getLayoutParams() != null) {
LayoutParams params = view.getLayoutParams();
params.width = width;
view.setLayoutParams(params);
}
}
/**
* Create a view that will be shown in the content preview area
* @param parent reference to the parent container where the view should be attached to
* @return content preview view
*/
protected ViewGroup createContentPreviewView(ViewGroup parent) {
ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
getResources(),
getLayoutInflater(),
parent);
if (layout != null) {
adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
}
return layout;
}
@Nullable
private View getFirstVisibleImgPreviewView() {
View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large);
return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
}
/**
* Wrapping the ContentResolver call to expose for easier mocking,
* and to avoid mocking Android core classes.
*/
@VisibleForTesting
public Cursor queryResolver(ContentResolver resolver, Uri uri) {
return resolver.query(uri, null, null, null, null);
}
@VisibleForTesting
protected boolean isImageType(String mimeType) {
return mimeType != null && mimeType.startsWith("image/");
}
private int getNumSheetExpansions() {
return getPreferences(Context.MODE_PRIVATE).getInt(PREF_NUM_SHEET_EXPANSIONS, 0);
}
private void incrementNumSheetExpansions() {
getPreferences(Context.MODE_PRIVATE).edit().putInt(PREF_NUM_SHEET_EXPANSIONS,
getNumSheetExpansions() + 1).apply();
}
@Override
protected void onStop() {
super.onStop();
if (maybeCancelFinishAnimation()) {
finish();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (isFinishing()) {
mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
}
if (mRefinementManager != null) { // TODO: null-checked in case of early-destroy, or skip?
mRefinementManager.destroy();
mRefinementManager = null;
}
mBackgroundThreadPoolExecutor.shutdownNow();
destroyProfileRecords();
}
private void destroyProfileRecords() {
for (int i = 0; i < mProfileRecords.size(); ++i) {
mProfileRecords.valueAt(i).destroy();
}
mProfileRecords.clear();
}
@Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
if (mChooserRequest == null) {
return defIntent;
}
Intent result = defIntent;
if (mChooserRequest.getReplacementExtras() != null) {
final Bundle replExtras =
mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName);
if (replExtras != null) {
result = new Intent(defIntent);
result.putExtras(replExtras);
}
}
if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)
|| aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
result = Intent.createChooser(result,
getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
// Don't auto-launch single intents if the intent is being forwarded. This is done
// because automatically launching a resolving application as a response to the user
// action of switching accounts is pretty unexpected.
result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
}
return result;
}
@Override
public void onActivityStarted(TargetInfo cti) {
if (mChooserRequest.getChosenComponentSender() != null) {
final ComponentName target = cti.getResolvedComponentName();
if (target != null) {
final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
try {
mChooserRequest.getChosenComponentSender().sendIntent(
this, Activity.RESULT_OK, fillIn, null, null);
} catch (IntentSender.SendIntentException e) {
Slog.e(TAG, "Unable to launch supplied IntentSender to report "
+ "the chosen component: " + e);
}
}
}
}
@Override
public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
if (mChooserRequest.getCallerChooserTargets().size() > 0) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
/* origTarget */ null,
new ArrayList<>(mChooserRequest.getCallerChooserTargets()),
TARGET_TYPE_DEFAULT,
/* directShareShortcutInfoCache */ Collections.emptyMap(),
/* directShareAppTargetCache */ Collections.emptyMap());
}
}
@Override
public int getLayoutResource() {
return R.layout.chooser_grid;
}
@Override // ResolverListCommunicator
public boolean shouldGetActivityMetadata() {
return true;
}
@Override
public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
// Note that this is only safe because the Intent handled by the ChooserActivity is
// guaranteed to contain no extras unknown to the local ClassLoader. That is why this
// method can not be replaced in the ResolverActivity whole hog.
if (!super.shouldAutoLaunchSingleChoice(target)) {
return false;
}
return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
}
private void showTargetDetails(TargetInfo targetInfo) {
if (targetInfo == null) return;
List<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 = targetInfo.isSelectableTargetInfo()
? mChooserRequest.getTargetIntentFilter() : null;
String shortcutTitle = targetInfo.isSelectableTargetInfo()
? targetInfo.getDisplayLabel().toString() : null;
String shortcutIdKey = targetInfo.getDirectShareShortcutId();
ChooserTargetActionsDialogFragment.show(
getSupportFragmentManager(),
targetList,
mChooserMultiProfilePagerAdapter.getCurrentUserHandle(),
shortcutIdKey,
shortcutTitle,
isShortcutPinned,
intentFilter);
}
@Override
protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
if (mRefinementManager.maybeHandleSelection(target)) {
return false;
}
updateModelAndChooserCounts(target);
maybeRemoveSharedText(target);
return super.onTargetSelected(target, alwaysCheck);
}
@Override
public void startSelected(int which, boolean always, boolean filtered) {
ChooserListAdapter currentListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
TargetInfo targetInfo = currentListAdapter
.targetInfoForPosition(which, filtered);
if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) {
return;
}
final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
if (targetInfo.isMultiDisplayResolveInfo()) {
MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
if (!mti.hasSelected()) {
ChooserStackedAppDialogFragment.show(
getSupportFragmentManager(),
mti,
which,
mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
return;
}
}
super.startSelected(which, always, filtered);
if (currentListAdapter.getCount() > 0) {
switch (currentListAdapter.getPositionTargetType(which)) {
case ChooserListAdapter.TARGET_SERVICE:
getChooserActivityLogger().logShareTargetSelected(
ChooserActivityLogger.SELECTION_TYPE_SERVICE,
targetInfo.getResolveInfo().activityInfo.processName,
which,
/* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
mChooserRequest.getCallerChooserTargets().size(),
targetInfo.getHashedTargetIdForMetrics(this),
targetInfo.isPinned(),
mIsSuccessfullySelected,
selectionCost
);
return;
case ChooserListAdapter.TARGET_CALLER:
case ChooserListAdapter.TARGET_STANDARD:
getChooserActivityLogger().logShareTargetSelected(
ChooserActivityLogger.SELECTION_TYPE_APP,
targetInfo.getResolveInfo().activityInfo.processName,
(which - currentListAdapter.getSurfacedTargetInfo().size()),
/* directTargetAlsoRanked= */ -1,
currentListAdapter.getCallerTargetCount(),
/* directTargetHashed= */ null,
targetInfo.isPinned(),
mIsSuccessfullySelected,
selectionCost
);
return;
case ChooserListAdapter.TARGET_STANDARD_AZ:
// A-Z targets are unranked standard targets; we use a value of -1 to mark that
// they are from the alphabetical pool.
// TODO: why do we log a different selection type if the -1 value already
// designates the same condition?
getChooserActivityLogger().logShareTargetSelected(
ChooserActivityLogger.SELECTION_TYPE_STANDARD,
targetInfo.getResolveInfo().activityInfo.processName,
/* value= */ -1,
/* directTargetAlsoRanked= */ -1,
/* numCallerProvided= */ 0,
/* directTargetHashed= */ null,
/* isPinned= */ false,
mIsSuccessfullySelected,
selectionCost
);
return;
}
}
}
private int getRankedPosition(TargetInfo targetInfo) {
String targetPackageName =
targetInfo.getChooserTargetComponentName().getPackageName();
ChooserListAdapter currentListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
int maxRankedResults = Math.min(
currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION);
for (int i = 0; i < maxRankedResults; i++) {
if (currentListAdapter.getDisplayResolveInfo(i)
.getResolveInfo().activityInfo.packageName.equals(targetPackageName)) {
return i;
}
}
return -1;
}
@Override
protected boolean shouldAddFooterView() {
// To accommodate for window insets
return true;
}
@Override
protected void applyFooterView(int height) {
int count = mChooserMultiProfilePagerAdapter.getItemCount();
for (int i = 0; i < count; i++) {
mChooserMultiProfilePagerAdapter.getAdapterForIndex(i).setFooterHeight(height);
}
}
private void logDirectShareTargetReceived(UserHandle forUser) {
ProfileRecord profileRecord = getProfileRecord(forUser);
if (profileRecord == null) {
return;
}
getChooserActivityLogger().logDirectShareTargetReceived(
MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
(int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
}
void updateModelAndChooserCounts(TargetInfo info) {
if (info != null && info.isMultiDisplayResolveInfo()) {
info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
}
if (info != null) {
sendClickToAppPredictor(info);
final ResolveInfo ri = info.getResolveInfo();
Intent targetIntent = getTargetIntent();
if (ri != null && ri.activityInfo != null && targetIntent != null) {
ChooserListAdapter currentListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
if (currentListAdapter != null) {
sendImpressionToAppPredictor(info, currentListAdapter);
currentListAdapter.updateModel(info.getResolvedComponentName());
currentListAdapter.updateChooserCounts(ri.activityInfo.packageName,
targetIntent.getAction());
}
if (DEBUG) {
Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName);
Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
}
} else if (DEBUG) {
Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
}
}
mIsSuccessfullySelected = true;
}
private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) {
Intent targetIntent = targetInfo.getTargetIntent();
if (targetIntent == null) {
return;
}
Intent originalTargetIntent = new Intent(mChooserRequest.getTargetIntent());
// Our TargetInfo implementations add associated component to the intent, let's do the same
// for the sake of the comparison below.
if (targetIntent.getComponent() != null) {
originalTargetIntent.setComponent(targetIntent.getComponent());
}
// Use filterEquals as a way to check that the primary intent is in use (and not an
// alternative one). For example, an app is sharing an image and a link with mime type
// "image/png" and provides an alternative intent to share only the link with mime type
// "text/uri". Should there be a target that accepts only the latter, the alternative intent
// will be used and we don't want to exclude the link from it.
if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) {
targetIntent.removeExtra(Intent.EXTRA_TEXT);
}
}
private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
// Send DS target impression info to AppPredictor, only when user chooses app share.
if (targetInfo.isChooserTargetInfo()) {
return;
}
AppPredictor directShareAppPredictor = getAppPredictor(
mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
if (directShareAppPredictor == null) {
return;
}
List<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);
return (record == null) ? null : record.appPredictor;
}
/**
* Sort intents alphabetically based on display label.
*/
static class AzInfoComparator implements Comparator<DisplayResolveInfo> {
Collator mCollator;
AzInfoComparator(Context context) {
mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
}
@Override
public int compare(
DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) {
return mCollator.compare(lhsp.getDisplayLabel(), rhsp.getDisplayLabel());
}
}
protected ChooserActivityLogger getChooserActivityLogger() {
if (mChooserActivityLogger == null) {
mChooserActivityLogger = new ChooserActivityLogger();
}
return mChooserActivityLogger;
}
public class ChooserListController extends ResolverListController {
public ChooserListController(
Context context,
PackageManager pm,
Intent targetIntent,
String referrerPackageName,
int launchedFromUid,
AbstractResolverComparator resolverComparator) {
super(
context,
pm,
targetIntent,
referrerPackageName,
launchedFromUid,
resolverComparator);
}
@Override
boolean isComponentFiltered(ComponentName name) {
return mChooserRequest.getFilteredComponentNames().contains(name);
}
@Override
public boolean isComponentPinned(ComponentName name) {
return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
}
}
@VisibleForTesting
public ChooserGridAdapter createChooserGridAdapter(
Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed,
UserHandle userHandle) {
ChooserListAdapter chooserListAdapter = createChooserListAdapter(
context,
payloadIntents,
initialIntents,
rList,
filterLastUsed,
createListController(userHandle),
userHandle,
getTargetIntent(),
mChooserRequest,
mMaxTargetsPerRow);
return new ChooserGridAdapter(
context,
new ChooserGridAdapter.ChooserActivityDelegate() {
@Override
public boolean shouldShowTabs() {
return ChooserActivity.this.shouldShowTabs();
}
@Override
public View buildContentPreview(ViewGroup parent) {
return createContentPreviewView(parent);
}
@Override
public void onTargetSelected(int itemIndex) {
startSelected(itemIndex, false, true);
}
@Override
public void onTargetLongPressed(int selectedPosition) {
final TargetInfo longPressedTargetInfo =
mChooserMultiProfilePagerAdapter
.getActiveListAdapter()
.targetInfoForPosition(
selectedPosition, /* filtered= */ true);
// Only a direct share target or an app target is expected
if (longPressedTargetInfo.isDisplayResolveInfo()
|| longPressedTargetInfo.isSelectableTargetInfo()) {
showTargetDetails(longPressedTargetInfo);
}
}
@Override
public void updateProfileViewButton(View newButtonFromProfileRow) {
mProfileView = newButtonFromProfileRow;
mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
ChooserActivity.this.updateProfileViewButton();
}
@Override
public int getValidTargetCount() {
return mChooserMultiProfilePagerAdapter
.getActiveListAdapter()
.getSelectableServiceTargetCount();
}
@Override
public void updateDirectShareExpansion(DirectShareViewHolder directShareGroup) {
RecyclerView activeAdapterView =
mChooserMultiProfilePagerAdapter.getActiveAdapterView();
if (mResolverDrawerLayout.isCollapsed()) {
directShareGroup.collapse(activeAdapterView);
} else {
directShareGroup.expand(activeAdapterView);
}
}
@Override
public void handleScrollToExpandDirectShare(
DirectShareViewHolder directShareGroup, int y, int oldy) {
directShareGroup.handleScroll(
mChooserMultiProfilePagerAdapter.getActiveAdapterView(),
y,
oldy,
mMaxTargetsPerRow);
}
},
chooserListAdapter,
shouldShowContentPreview(),
mMaxTargetsPerRow,
getNumSheetExpansions());
}
@VisibleForTesting
public ChooserListAdapter createChooserListAdapter(
Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed,
ResolverListController resolverListController,
UserHandle userHandle,
Intent targetIntent,
ChooserRequestParameters chooserRequest,
int maxTargetsPerRow) {
return new ChooserListAdapter(
context,
payloadIntents,
initialIntents,
rList,
filterLastUsed,
resolverListController,
userHandle,
targetIntent,
this,
context.getPackageManager(),
getChooserActivityLogger(),
chooserRequest,
maxTargetsPerRow);
}
@Override
@VisibleForTesting
protected ChooserListController createListController(UserHandle userHandle) {
AppPredictor appPredictor = getAppPredictor(userHandle);
AbstractResolverComparator resolverComparator;
if (appPredictor != null) {
resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger());
} else {
resolverComparator =
new ResolverRankerServiceResolverComparator(this, getTargetIntent(),
getReferrerPackageName(), null, getChooserActivityLogger());
}
return new ChooserListController(
this,
mPm,
getTargetIntent(),
getReferrerPackageName(),
getAnnotatedUserHandles().userIdOfCallingApp,
resolverComparator);
}
@VisibleForTesting
protected ImageLoader createPreviewImageLoader() {
final int cacheSize;
if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) {
float chooserWidth = getResources().getDimension(R.dimen.chooser_width);
float imageWidth = getResources().getDimension(R.dimen.chooser_preview_image_width);
cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2);
} else {
cacheSize = 3;
}
return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize);
}
private ChooserActionFactory createChooserActionFactory() {
return new ChooserActionFactory(
this,
mChooserRequest,
mFeatureFlagRepository,
mIntegratedDeviceComponents,
getChooserActivityLogger(),
(isExcluded) -> mExcludeSharedText = isExcluded,
this::getFirstVisibleImgPreviewView,
new ChooserActionFactory.ActionActivityStarter() {
@Override
public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle());
finish();
}
@Override
public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
TargetInfo targetInfo, View sharedElement, String sharedElementName) {
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
ChooserActivity.this, sharedElement, sharedElementName);
safelyStartActivityAsUser(
targetInfo, getPersonalProfileUserHandle(), options.toBundle());
startFinishAnimation();
}
},
(status) -> {
if (status != null) {
setResult(status);
}
finish();
});
}
private void handleScroll(View view, int x, int y, int oldx, int oldy) {
if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().handleScroll(view, y, oldy);
}
}
/*
* Need to dynamically adjust how many icons can fit per row before we add them,
* which also means setting the correct offset to initially show the content
* preview area + 2 rows of targets
*/
private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
if (mChooserMultiProfilePagerAdapter == null) {
return;
}
RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
// Skip height calculation if recycler view was scrolled to prevent it inaccurately
// calculating the height, as the logic below does not account for the scrolled offset.
if (gridAdapter == null || recyclerView == null
|| recyclerView.computeVerticalScrollOffset() != 0) {
return;
}
final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
boolean isLayoutUpdated =
gridAdapter.calculateChooserTargetWidth(availableWidth)
|| recyclerView.getAdapter() == null
|| availableWidth != mCurrAvailableWidth;
boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
if (isLayoutUpdated
|| insetsChanged
|| mLastNumberOfChildren != recyclerView.getChildCount()) {
mCurrAvailableWidth = availableWidth;
if (isLayoutUpdated) {
// It is very important we call setAdapter from here. Otherwise in some cases
// the resolver list doesn't get populated, such as b/150922090, b/150918223
// and b/150936654
recyclerView.setAdapter(gridAdapter);
((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount(
mMaxTargetsPerRow);
updateTabPadding();
}
UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle();
int currentProfile = getProfileForUser(currentUserHandle);
int initialProfile = findSelectedProfile();
if (currentProfile != initialProfile) {
return;
}
if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
return;
}
getMainThreadHandler().post(() -> {
if (mResolverDrawerLayout == null || gridAdapter == null) {
return;
}
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
mEnterTransitionAnimationDelegate.markOffsetCalculated();
mLastAppliedInsets = mSystemWindowInsets;
});
}
}
private int calculateDrawerOffset(
int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) {
final int bottomInset = mSystemWindowInsets != null
? mSystemWindowInsets.bottom : 0;
int offset = bottomInset;
int rowsToShow = gridAdapter.getSystemRowCount()
+ gridAdapter.getProfileRowCount()
+ gridAdapter.getServiceTargetRowCount()
+ gridAdapter.getCallerAndRankedTargetRowCount();
// then this is most likely not a SEND_* action, so check
// the app target count
if (rowsToShow == 0) {
rowsToShow = gridAdapter.getRowCount();
}
// still zero? then use a default height and leave, which
// can happen when there are no targets to show
if (rowsToShow == 0 && !shouldShowStickyContentPreview()) {
offset += getResources().getDimensionPixelSize(
R.dimen.chooser_max_collapsed_height);
return offset;
}
View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container);
if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) {
offset += stickyContentPreview.getHeight();
}
if (shouldShowTabs()) {
offset += findViewById(com.android.internal.R.id.tabs).getHeight();
}
if (recyclerView.getVisibility() == View.VISIBLE) {
int directShareHeight = 0;
rowsToShow = Math.min(4, rowsToShow);
boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow);
mLastNumberOfChildren = recyclerView.getChildCount();
for (int i = 0, childCount = recyclerView.getChildCount();
i < childCount && rowsToShow > 0; i++) {
View child = recyclerView.getChildAt(i);
if (((GridLayoutManager.LayoutParams)
child.getLayoutParams()).getSpanIndex() != 0) {
continue;
}
int height = child.getHeight();
offset += height;
if (shouldShowExtraRow) {
offset += height;
}
if (gridAdapter.getTargetType(
recyclerView.getChildAdapterPosition(child))
== ChooserListAdapter.TARGET_SERVICE) {
directShareHeight = height;
}
rowsToShow--;
}
boolean isExpandable = getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode();
if (directShareHeight != 0 && shouldShowContentPreview()
&& isExpandable) {
// make sure to leave room for direct share 4->8 expansion
int requiredExpansionHeight =
(int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE);
int topInset = mSystemWindowInsets != null ? mSystemWindowInsets.top : 0;
int minHeight = bottom - top - mResolverDrawerLayout.getAlwaysShowHeight()
- requiredExpansionHeight - topInset - bottomInset;
offset = Math.min(offset, minHeight);
}
} else {
ViewGroup currentEmptyStateView = getActiveEmptyStateView();
if (currentEmptyStateView.getVisibility() == View.VISIBLE) {
offset += currentEmptyStateView.getHeight();
}
}
return Math.min(offset, bottom - top);
}
/**
* If we have a tabbed view and are showing 1 row in the current profile and an empty
* state screen in the other profile, to prevent cropping of the empty state screen we show
* a second row in the current profile.
*/
private boolean shouldShowExtraRow(int rowsToShow) {
return shouldShowTabs()
&& rowsToShow == 1
&& mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(
mChooserMultiProfilePagerAdapter.getInactiveListAdapter());
}
/**
* Returns {@link #PROFILE_PERSONAL}, {@link #PROFILE_WORK}, or -1 if the given user handle
* does not match either the personal or work user handle.
**/
private int getProfileForUser(UserHandle currentUserHandle) {
if (currentUserHandle.equals(getPersonalProfileUserHandle())) {
return PROFILE_PERSONAL;
} else if (currentUserHandle.equals(getWorkProfileUserHandle())) {
return PROFILE_WORK;
}
Log.e(TAG, "User " + currentUserHandle + " does not belong to a personal or work profile.");
return -1;
}
private ViewGroup getActiveEmptyStateView() {
int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage();
return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage);
}
@Override // ResolverListCommunicator
public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged();
super.onHandlePackagesChanged(listAdapter);
}
@Override
public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
setupScrollListener();
maybeSetupGlobalLayoutListener();
ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
if (chooserListAdapter.getUserHandle()
.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
mChooserMultiProfilePagerAdapter.getActiveAdapterView()
.setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());
mChooserMultiProfilePagerAdapter
.setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
}
if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
chooserListAdapter.notifyDataSetChanged();
} else {
chooserListAdapter.updateAlphabeticalList();
}
if (rebuildComplete) {
getChooserActivityLogger().logSharesheetAppLoadComplete();
maybeQueryAdditionalPostProcessingTargets(chooserListAdapter);
mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
}
}
private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
UserHandle userHandle = chooserListAdapter.getUserHandle();
ProfileRecord record = getProfileRecord(userHandle);
if (record == null) {
return;
}
if (record.shortcutLoader == null) {
return;
}
record.loadingStartTime = SystemClock.elapsedRealtime();
record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos());
}
@MainThread
private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) {
if (DEBUG) {
Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
}
mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache());
mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache());
ChooserListAdapter adapter =
mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
if (adapter != null) {
for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) {
adapter.addServiceResults(
resultInfo.getAppTarget(),
resultInfo.getShortcuts(),
result.isFromAppPredictor()
? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
: TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
mDirectShareShortcutInfoCache,
mDirectShareAppTargetCache);
}
adapter.completeServiceTargetLoading();
}
logDirectShareTargetReceived(userHandle);
sendVoiceChoicesIfNeeded();
getChooserActivityLogger().logSharesheetDirectLoadComplete();
}
private void setupScrollListener() {
if (mResolverDrawerLayout == null) {
return;
}
int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header;
final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId);
final float defaultElevation = elevatedView.getElevation();
final float chooserHeaderScrollElevation =
getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);
mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
new RecyclerView.OnScrollListener() {
public void onScrollStateChanged(RecyclerView view, int scrollState) {
if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
mScrollStatus = SCROLL_STATUS_IDLE;
setHorizontalScrollingEnabled(true);
}
} else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
if (mScrollStatus == SCROLL_STATUS_IDLE) {
mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL;
setHorizontalScrollingEnabled(false);
}
}
}
public void onScrolled(RecyclerView view, int dx, int dy) {
if (view.getChildCount() > 0) {
View child = view.getLayoutManager().findViewByPosition(0);
if (child == null || child.getTop() < 0) {
elevatedView.setElevation(chooserHeaderScrollElevation);
return;
}
}
elevatedView.setElevation(defaultElevation);
}
});
}
private void maybeSetupGlobalLayoutListener() {
if (shouldShowTabs()) {
return;
}
final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
recyclerView.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// Fixes an issue were the accessibility border disappears on list creation.
recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
final TextView titleView = findViewById(com.android.internal.R.id.title);
if (titleView != null) {
titleView.setFocusable(true);
titleView.setFocusableInTouchMode(true);
titleView.requestFocus();
titleView.requestAccessibilityFocus();
}
}
});
}
/**
* The sticky content preview is shown only when we have a tabbed view. It's shown above
* the tabs so it is not part of the scrollable list. If we are not in tabbed view,
* we instead show the content preview as a regular list item.
*/
private boolean shouldShowStickyContentPreview() {
return shouldShowStickyContentPreviewNoOrientationCheck()
&& !getResources().getBoolean(R.bool.resolver_landscape_phone);
}
private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
return shouldShowTabs()
&& (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
UserHandle.of(UserHandle.myUserId())).getCount() > 0
|| shouldShowContentPreviewWhenEmpty())
&& shouldShowContentPreview();
}
/**
* This method could be used to override the default behavior when we hide the preview area
* when the current tab doesn't have any items.
*
* @return true if we want to show the content preview area even if the tab for the current
* user is empty
*/
protected boolean shouldShowContentPreviewWhenEmpty() {
return false;
}
/**
* @return true if we want to show the content preview area
*/
protected boolean shouldShowContentPreview() {
return (mChooserRequest != null) && mChooserRequest.isSendActionTarget();
}
private void updateStickyContentPreview() {
if (shouldShowStickyContentPreviewNoOrientationCheck()) {
// The sticky content preview is only shown when we show the work and personal tabs.
// We don't show it in landscape as otherwise there is no room for scrolling.
// If the sticky content preview will be shown at some point with orientation change,
// then always preload it to avoid subsequent resizing of the share sheet.
ViewGroup contentPreviewContainer =
findViewById(com.android.internal.R.id.content_preview_container);
if (contentPreviewContainer.getChildCount() == 0) {
ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
contentPreviewContainer.addView(contentPreviewView);
}
}
if (shouldShowStickyContentPreview()) {
showStickyContentPreview();
} else {
hideStickyContentPreview();
}
}
private void showStickyContentPreview() {
if (isStickyContentPreviewShowing()) {
return;
}
ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
contentPreviewContainer.setVisibility(View.VISIBLE);
}
private boolean isStickyContentPreviewShowing() {
ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
return contentPreviewContainer.getVisibility() == View.VISIBLE;
}
private void hideStickyContentPreview() {
if (!isStickyContentPreviewShowing()) {
return;
}
ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
contentPreviewContainer.setVisibility(View.GONE);
}
private void startFinishAnimation() {
View rootView = findRootView();
if (rootView != null) {
rootView.startAnimation(new FinishAnimation(this, rootView));
}
}
private boolean maybeCancelFinishAnimation() {
View rootView = findRootView();
Animation animation = (rootView == null) ? null : rootView.getAnimation();
if (animation instanceof FinishAnimation) {
boolean hasEnded = animation.hasEnded();
animation.cancel();
rootView.clearAnimation();
return !hasEnded;
}
return false;
}
private View findRootView() {
if (mContentView == null) {
mContentView = findViewById(android.R.id.content);
}
return mContentView;
}
/**
* Intentionally override the {@link ResolverActivity} implementation as we only need that
* implementation for the intent resolver case.
*/
@Override
public void onButtonClick(View v) {}
/**
* Intentionally override the {@link ResolverActivity} implementation as we only need that
* implementation for the intent resolver case.
*/
@Override
protected void resetButtonBar() {}
@Override
protected String getMetricsCategory() {
return METRICS_CATEGORY_CHOOSER;
}
@Override
protected void onProfileTabSelected() {
ChooserGridAdapter currentRootAdapter =
mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
currentRootAdapter.updateDirectShareExpansion();
// This fixes an edge case where after performing a variety of gestures, vertical scrolling
// ends up disabled. That's because at some point the old tab's vertical scrolling is
// disabled and the new tab's is enabled. For context, see b/159997845
setVerticalScrollEnabled(true);
if (mResolverDrawerLayout != null) {
mResolverDrawerLayout.scrollNestedScrollableChildBackToTop();
}
}
@Override
protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
if (shouldShowTabs()) {
mChooserMultiProfilePagerAdapter
.setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom());
mChooserMultiProfilePagerAdapter.setupContainerPadding(
getActiveEmptyStateView().findViewById(com.android.internal.R.id.resolver_empty_state_container));
}
WindowInsets result = super.onApplyWindowInsets(v, insets);
if (mResolverDrawerLayout != null) {
mResolverDrawerLayout.requestLayout();
}
return result;
}
private void setHorizontalScrollingEnabled(boolean enabled) {
ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
viewPager.setSwipingEnabled(enabled);
}
private void setVerticalScrollEnabled(boolean enabled) {
ChooserGridLayoutManager layoutManager =
(ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView()
.getLayoutManager();
layoutManager.setVerticalScrollEnabled(enabled);
}
@Override
void onHorizontalSwipeStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_DRAGGING) {
if (mScrollStatus == SCROLL_STATUS_IDLE) {
mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL;
setVerticalScrollEnabled(false);
}
} else if (state == ViewPager.SCROLL_STATE_IDLE) {
if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) {
mScrollStatus = SCROLL_STATUS_IDLE;
setVerticalScrollEnabled(true);
}
}
}
/**
* Used in combination with the scene transition when launching the image editor
*/
private static class FinishAnimation extends AlphaAnimation implements
Animation.AnimationListener {
@Nullable
private Activity mActivity;
@Nullable
private View mRootView;
private final float mFromAlpha;
FinishAnimation(@NonNull Activity activity, @NonNull View rootView) {
super(rootView.getAlpha(), 0.0f);
mActivity = activity;
mRootView = rootView;
mFromAlpha = rootView.getAlpha();
setInterpolator(new LinearInterpolator());
long duration = activity.getWindow().getTransitionBackgroundFadeDuration();
setDuration(duration);
// The scene transition animation looks better when it's not overlapped with this
// fade-out animation thus the delay.
// It is most likely that the image editor will cause this activity to stop and this
// animation will be cancelled in the background without running (i.e. we'll animate
// only when this activity remains partially visible after the image editor launch).
setStartOffset(duration);
super.setAnimationListener(this);
}
@Override
public void setAnimationListener(AnimationListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void cancel() {
if (mRootView != null) {
mRootView.setAlpha(mFromAlpha);
}
cleanup();
super.cancel();
}
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
Activity activity = mActivity;
cleanup();
if (activity != null) {
activity.finish();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
private void cleanup() {
mActivity = null;
mRootView = null;
}
}
@Override
protected void maybeLogProfileChange() {
getChooserActivityLogger().logSharesheetProfileChanged();
}
private static class ProfileRecord {
/** The {@link AppPredictor} for this profile, if any. */
@Nullable
public final AppPredictor appPredictor;
/**
* null if we should not load shortcuts.
*/
@Nullable
public final ShortcutLoader shortcutLoader;
public long loadingStartTime;
private ProfileRecord(
@Nullable AppPredictor appPredictor,
@Nullable ShortcutLoader shortcutLoader) {
this.appPredictor = appPredictor;
this.shortcutLoader = shortcutLoader;
}
public void destroy() {
if (shortcutLoader != null) {
shortcutLoader.destroy();
}
if (appPredictor != null) {
appPredictor.destroy();
}
}
}
}