/* * 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.internal.app; 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.content.ContentProvider.getUserIdFromUri; import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; 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.SharedElementCallback; import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionManager; import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; 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.IntentSender.SendIntentException; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.Path; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.metrics.LogMaker; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.os.Parcelable; import android.os.PatternMatcher; import android.os.ResultReceiver; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; import android.provider.DeviceConfig; import android.provider.DocumentsContract; import android.provider.Downloads; import android.provider.OpenableColumns; import android.provider.Settings; import android.service.chooser.ChooserTarget; import android.text.TextUtils; import android.util.AttributeSet; import android.util.HashedStringCache; import android.util.Log; import android.util.PluralsMessageFormatter; import android.util.Size; import android.util.Slog; import android.view.LayoutInflater; import android.view.View; import android.view.View.MeasureSpec; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.animation.AccelerateInterpolator; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import android.widget.Button; import android.widget.ImageView; import android.widget.Space; import android.widget.TextView; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyState; import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider; import com.android.internal.app.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; import com.android.internal.app.ResolverListAdapter.ViewHolder; import com.android.internal.app.chooser.ChooserTargetInfo; import com.android.internal.app.chooser.DisplayResolveInfo; import com.android.internal.app.chooser.MultiDisplayResolveInfo; import com.android.internal.app.chooser.NotSelectableTargetInfo; import com.android.internal.app.chooser.SelectableTargetInfo; import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator; import com.android.internal.app.chooser.TargetInfo; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 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.FrameworkStatsLog; import com.android.internal.widget.GridLayoutManager; import com.android.internal.widget.RecyclerView; import com.android.internal.widget.ResolverDrawerLayout; import com.android.internal.widget.ViewPager; import com.google.android.collect.Lists; import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URISyntaxException; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; 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.function.Supplier; import java.util.stream.Collectors; /** * This is the legacy ChooserActivity and is not expected to be invoked, it's only here because * MediaAppSelectorActivity is still depending on it. The actual chooser used by the system is * at packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java * * The migration to the new package will be completed in a later release. */ public class ChooserActivity extends ResolverActivity implements ChooserListAdapter.ChooserListCommunicator, SelectableTargetInfoCommunicator { private static final String TAG = "ChooserActivity"; private AppPredictor mPersonalAppPredictor; private AppPredictor mWorkAppPredictor; private boolean mShouldDisplayLandscape; @UnsupportedAppUsage public 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; private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true; // TODO(b/123088566) Share these in a better way. private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share"; public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; public static final String CHOOSER_TARGET = "chooser_target"; private static final String SHORTCUT_TARGET = "shortcut_target"; private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20; public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter"; private static final String SHARED_TEXT_KEY = "shared_text"; 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"; private boolean mIsAppPredictorComponentAvailable; private Map mDirectShareAppTargetCache; private Map mDirectShareShortcutInfoCache; 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; public static final int SELECTION_TYPE_SERVICE = 1; public static final int SELECTION_TYPE_APP = 2; public static final int SELECTION_TYPE_STANDARD = 3; public static final int SELECTION_TYPE_COPY = 4; public static final int SELECTION_TYPE_NEARBY = 5; public static final int SELECTION_TYPE_EDIT = 6; 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; // statsd logger wrapper protected ChooserActivityLogger mChooserActivityLogger; @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 {} /** * The transition time between placeholders for direct share to a message * indicating that non are available. */ private static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200; private static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f; private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7; private int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS, DEFAULT_SALT_EXPIRATION_DAYS); private static final boolean DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP = false; private boolean mIsNearbyShareFirstTargetInRankedApp = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP, DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP); private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 0; 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; @VisibleForTesting int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.SHARESHEET_LIST_VIEW_UPDATE_DELAY, DEFAULT_LIST_VIEW_UPDATE_DELAY_MS); private Bundle mReplacementExtras; private IntentSender mChosenComponentSender; private IntentSender mRefinementIntentSender; private RefinementResultReceiver mRefinementResultReceiver; private ChooserTarget[] mCallerChooserTargets; private ComponentName[] mFilteredComponentNames; private Intent mReferrerFillInIntent; private long mChooserShownTime; protected boolean mIsSuccessfullySelected; private long mQueriedSharingShortcutsTimeMs; private int mCurrAvailableWidth = 0; private Insets mLastAppliedInsets = null; private int mLastNumberOfChildren = -1; private int mMaxTargetsPerRow = 1; private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment"; private static final int MAX_LOG_RANK_POSITION = 12; 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"; @Retention(SOURCE) @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT}) private @interface ContentPreviewType { } // Starting at 1 since 0 is considered "undefined" for some of the database transformations // of tron logs. protected static final int CONTENT_PREVIEW_IMAGE = 1; protected static final int CONTENT_PREVIEW_FILE = 2; protected static final int CONTENT_PREVIEW_TEXT = 3; protected MetricsLogger mMetricsLogger; private ContentPreviewCoordinator mPreviewCoord; private int mScrollStatus = SCROLL_STATUS_IDLE; @VisibleForTesting protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter; private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate = new EnterTransitionAnimationDelegate(); private boolean mRemoveSharedElements = false; private View mContentView = null; private class ContentPreviewCoordinator { private static final int IMAGE_FADE_IN_MILLIS = 150; private static final int IMAGE_LOAD_TIMEOUT = 1; private static final int IMAGE_LOAD_INTO_VIEW = 2; private final int mImageLoadTimeoutMillis = getResources().getInteger(R.integer.config_shortAnimTime); private final View mParentView; private boolean mHideParentOnFail; private boolean mAtLeastOneLoaded = false; class LoadUriTask { public final Uri mUri; public final int mImageResourceId; public final int mExtraCount; public final Bitmap mBmp; LoadUriTask(int imageResourceId, Uri uri, int extraCount, Bitmap bmp) { this.mImageResourceId = imageResourceId; this.mUri = uri; this.mExtraCount = extraCount; this.mBmp = bmp; } } // If at least one image loads within the timeout period, allow other // loads to continue. Otherwise terminate and optionally hide // the parent area private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case IMAGE_LOAD_TIMEOUT: maybeHideContentPreview(); break; case IMAGE_LOAD_INTO_VIEW: if (isFinishing()) break; LoadUriTask task = (LoadUriTask) msg.obj; RoundedRectImageView imageView = mParentView.findViewById( task.mImageResourceId); if (task.mBmp == null) { imageView.setVisibility(View.GONE); maybeHideContentPreview(); return; } mAtLeastOneLoaded = true; imageView.setVisibility(View.VISIBLE); imageView.setAlpha(0.0f); imageView.setImageBitmap(task.mBmp); ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f); fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f)); fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS); fadeAnim.start(); if (task.mExtraCount > 0) { imageView.setExtraImageCount(task.mExtraCount); } setupPreDrawForSharedElementTransition(imageView); } } }; private void setupPreDrawForSharedElementTransition(View v) { v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { v.getViewTreeObserver().removeOnPreDrawListener(this); if (!mRemoveSharedElements && isActivityTransitionRunning()) { // Disable the window animations as it interferes with the // transition animation. getWindow().setWindowAnimations(0); } mEnterTransitionAnimationDelegate.markImagePreviewReady(); return true; } }); } ContentPreviewCoordinator(View parentView, boolean hideParentOnFail) { super(); this.mParentView = parentView; this.mHideParentOnFail = hideParentOnFail; } private void loadUriIntoView(final int imageResourceId, final Uri uri, final int extraImages) { mHandler.sendEmptyMessageDelayed(IMAGE_LOAD_TIMEOUT, mImageLoadTimeoutMillis); AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { int size = getResources().getDimensionPixelSize( R.dimen.chooser_preview_image_max_dimen); final Bitmap bmp = loadThumbnail(uri, new Size(size, size)); final Message msg = Message.obtain(); msg.what = IMAGE_LOAD_INTO_VIEW; msg.obj = new LoadUriTask(imageResourceId, uri, extraImages, bmp); mHandler.sendMessage(msg); }); } private void cancelLoads() { mHandler.removeMessages(IMAGE_LOAD_INTO_VIEW); mHandler.removeMessages(IMAGE_LOAD_TIMEOUT); } private void maybeHideContentPreview() { if (!mAtLeastOneLoaded) { if (mHideParentOnFail) { Log.i(TAG, "Hiding image preview area. Timed out waiting for preview to load" + " within " + mImageLoadTimeoutMillis + "ms."); collapseParentView(); if (shouldShowTabs()) { hideStickyContentPreview(); } else if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) { mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() .hideContentPreview(); } mHideParentOnFail = false; } mRemoveSharedElements = true; mEnterTransitionAnimationDelegate.markImagePreviewReady(); } } private void collapseParentView() { // This will effectively hide the content preview row by forcing the height // to zero. It is faster than forcing a relayout of the listview final View v = mParentView; int widthSpec = MeasureSpec.makeMeasureSpec(v.getWidth(), MeasureSpec.EXACTLY); int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY); v.measure(widthSpec, heightSpec); v.getLayoutParams().height = 0; v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getTop()); v.invalidate(); } } private final ChooserHandler mChooserHandler = new ChooserHandler(); private class ChooserHandler extends Handler { private static final int LIST_VIEW_UPDATE_MESSAGE = 6; private static final int SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS = 7; private void removeAllMessages() { removeMessages(LIST_VIEW_UPDATE_MESSAGE); removeMessages(SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS); } @Override public void handleMessage(Message msg) { if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null || isDestroyed()) { return; } switch (msg.what) { case LIST_VIEW_UPDATE_MESSAGE: if (DEBUG) { Log.d(TAG, "LIST_VIEW_UPDATE_MESSAGE; "); } UserHandle userHandle = (UserHandle) msg.obj; mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle) .refreshListView(); break; case SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS: if (DEBUG) Log.d(TAG, "SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS"); final ServiceResultInfo[] resultInfos = (ServiceResultInfo[]) msg.obj; for (ServiceResultInfo resultInfo : resultInfos) { if (resultInfo.resultTargets != null) { ChooserListAdapter adapterForUserHandle = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle( resultInfo.userHandle); if (adapterForUserHandle != null) { adapterForUserHandle.addServiceResults( resultInfo.originalTarget, resultInfo.resultTargets, msg.arg1, mDirectShareShortcutInfoCache); } } } logDirectShareTargetReceived( MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER); sendVoiceChoicesIfNeeded(); getChooserActivityLogger().logSharesheetDirectLoadComplete(); mChooserMultiProfilePagerAdapter.getActiveListAdapter() .completeServiceTargetLoading(); break; default: super.handleMessage(msg); } } }; @Override protected void onCreate(Bundle savedInstanceState) { final long intentReceivedTime = System.currentTimeMillis(); mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); getChooserActivityLogger().logSharesheetTriggered(); // This is the only place this value is being set. Effectively final. mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable(); mIsSuccessfullySelected = false; Intent intent = getIntent(); Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT); if (targetParcelable instanceof Uri) { try { targetParcelable = Intent.parseUri(targetParcelable.toString(), Intent.URI_INTENT_SCHEME); } catch (URISyntaxException ex) { // doesn't parse as an intent; let the next test fail and error out } } if (!(targetParcelable instanceof Intent)) { Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable); finish(); super.onCreate(null); return; } Intent target = (Intent) targetParcelable; if (target != null) { modifyTargetIntent(target); } Parcelable[] targetsParcelable = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS); if (targetsParcelable != null) { final boolean offset = target == null; Intent[] additionalTargets = new Intent[offset ? targetsParcelable.length - 1 : targetsParcelable.length]; for (int i = 0; i < targetsParcelable.length; i++) { if (!(targetsParcelable[i] instanceof Intent)) { Log.w(TAG, "EXTRA_ALTERNATE_INTENTS array entry #" + i + " is not an Intent: " + targetsParcelable[i]); finish(); super.onCreate(null); return; } final Intent additionalTarget = (Intent) targetsParcelable[i]; if (i == 0 && target == null) { target = additionalTarget; modifyTargetIntent(target); } else { additionalTargets[offset ? i - 1 : i] = additionalTarget; modifyTargetIntent(additionalTarget); } } setAdditionalTargets(additionalTargets); } mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS); // Do not allow the title to be changed when sharing content CharSequence title = null; if (target != null) { if (!isSendAction(target)) { title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE); } else { Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a" + " preview title by using EXTRA_TITLE property of the wrapped" + " EXTRA_INTENT."); } } int defaultTitleRes = 0; if (title == null) { defaultTitleRes = com.android.internal.R.string.chooseActivity; } Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS); Intent[] initialIntents = null; if (pa != null) { int count = Math.min(pa.length, MAX_EXTRA_INITIAL_INTENTS); initialIntents = new Intent[count]; for (int i = 0; i < count; i++) { if (!(pa[i] instanceof Intent)) { Log.w(TAG, "Initial intent #" + i + " not an Intent: " + pa[i]); finish(); super.onCreate(null); return; } final Intent in = (Intent) pa[i]; modifyTargetIntent(in); initialIntents[i] = in; } } mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, getReferrer()); mChosenComponentSender = intent.getParcelableExtra( Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER, android.content.IntentSender.class); mRefinementIntentSender = intent.getParcelableExtra( Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, android.content.IntentSender.class); setSafeForwardingMode(true); mPinnedSharedPrefs = getPinnedSharedPrefs(this); pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS); // Exclude out Nearby from main list if chip is present, to avoid duplication ComponentName nearbySharingComponent = getNearbySharingComponent(); boolean shouldFilterNearby = !shouldNearbyShareBeFirstInRankedRow() && nearbySharingComponent != null; if (pa != null) { ComponentName[] names = new ComponentName[pa.length + (shouldFilterNearby ? 1 : 0)]; for (int i = 0; i < pa.length; i++) { if (!(pa[i] instanceof ComponentName)) { Log.w(TAG, "Filtered component #" + i + " not a ComponentName: " + pa[i]); names = null; break; } names[i] = (ComponentName) pa[i]; } if (shouldFilterNearby) { names[names.length - 1] = nearbySharingComponent; } mFilteredComponentNames = names; } else if (shouldFilterNearby) { mFilteredComponentNames = new ComponentName[1]; mFilteredComponentNames[0] = nearbySharingComponent; } pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS); if (pa != null) { int count = Math.min(pa.length, MAX_EXTRA_CHOOSER_TARGETS); ChooserTarget[] targets = new ChooserTarget[count]; for (int i = 0; i < count; i++) { if (!(pa[i] instanceof ChooserTarget)) { Log.w(TAG, "Chooser target #" + i + " not a ChooserTarget: " + pa[i]); targets = null; break; } targets[i] = (ChooserTarget) pa[i]; } mCallerChooserTargets = targets; } mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); mShouldDisplayLandscape = shouldDisplayLandscape(getResources().getConfiguration().orientation); setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false)); super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents, null, false); mChooserShownTime = System.currentTimeMillis(); final long systemCost = mChooserShownTime - intentReceivedTime; getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN) .setSubtype(isWorkProfile() ? MetricsEvent.MANAGED_PROFILE : MetricsEvent.PARENT_PROFILE) .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, target.getType()) .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost)); if (mResolverDrawerLayout != null) { mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); // expand/shrink direct share 4 -> 8 viewgroup if (isSendAction(target)) { 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( FrameworkStatsLog.SHARESHEET_STARTED, getReferrerPackageName(), target.getType(), mCallerChooserTargets == null ? 0 : mCallerChooserTargets.length, initialIntents == null ? 0 : initialIntents.length, isWorkProfile(), findPreferredContentPreview(getTargetIntent(), getContentResolver()), target.getAction() ); mDirectShareShortcutInfoCache = new HashMap<>(); setEnterSharedElementCallback(new SharedElementCallback() { @Override public void onMapSharedElements(List names, Map sharedElements) { if (mRemoveSharedElements) { names.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME); sharedElements.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME); } super.onMapSharedElements(names, sharedElements); mRemoveSharedElements = false; } }); mEnterTransitionAnimationDelegate.postponeTransition(); } @Override protected int appliedThemeResId() { return R.style.Theme_DeviceDefault_Chooser; } private AppPredictor setupAppPredictorForUser(UserHandle userHandle, AppPredictor.Callback appPredictorCallback) { AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle); if (appPredictor == null) { return null; } mDirectShareAppTargetCache = new HashMap<>(); appPredictor.registerPredictionUpdates(this.getMainExecutor(), appPredictorCallback); return appPredictor; } private ResolverAppPredictorCallback createAppPredictorCallback( ChooserListAdapter chooserListAdapter) { return new ResolverAppPredictorCallback(resultList -> { if (isFinishing() || isDestroyed()) { return; } if (chooserListAdapter.getCount() == 0) { return; } if (resultList.isEmpty() && shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) { // APS may be disabled, so try querying targets ourselves. queryDirectShareTargets(chooserListAdapter, true); return; } final List shareShortcutInfos = new ArrayList<>(); List shortcutResults = new ArrayList<>(); for (AppTarget appTarget : resultList) { if (appTarget.getShortcutInfo() == null) { continue; } shortcutResults.add(appTarget); } resultList = shortcutResults; for (AppTarget appTarget : resultList) { shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo( appTarget.getShortcutInfo(), new ComponentName( appTarget.getPackageName(), appTarget.getClassName()))); } sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList, chooserListAdapter.getUserHandle()); }); } static SharedPreferences getPinnedSharedPrefs(Context context) { // The code below is because in the android:ui process, no one can hear you scream. // The package info in the context isn't initialized in the way it is for normal apps, // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we // build the path manually below using the same policy that appears in ContextImpl. // This fails silently under the hood if there's a problem, so if we find ourselves in // the case where we don't have access to credential encrypted storage we just won't // have our pinned target info. final File prefsFile = new File(new File( Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL, context.getUserId(), context.getPackageName()), "shared_prefs"), PINNED_SHARED_PREFS_NAME + ".xml"); return context.getSharedPreferences(prefsFile, MODE_PRIVATE); } @Override protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( Intent[] initialIntents, List rList, boolean filterLastUsed) { if (shouldShowTabs()) { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles( initialIntents, rList, filterLastUsed); } else { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile( initialIntents, rList, filterLastUsed); } return mChooserMultiProfilePagerAdapter; } @Override protected EmptyStateProvider createBlockerEmptyStateProvider() { final boolean isSendAction = isSendAction(getTargetIntent()); final EmptyState noWorkToPersonalEmptyState = new DevicePolicyBlockerEmptyState( /* context= */ this, /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, /* devicePolicyStringSubtitleId= */ isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL, /* defaultSubtitleResource= */ isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation : R.string.resolver_cant_access_personal_apps_explanation, /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); final EmptyState noPersonalToWorkEmptyState = new DevicePolicyBlockerEmptyState( /* context= */ this, /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, /* devicePolicyStringSubtitleId= */ isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK, /* defaultSubtitleResource= */ isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation : R.string.resolver_cant_access_work_apps_explanation, /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch()); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List rList, boolean filterLastUsed) { ChooserGridAdapter adapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, initialIntents, rList, filterLastUsed, /* userHandle */ getPersonalProfileUserHandle()); return new ChooserMultiProfilePagerAdapter( /* context */ this, adapter, createEmptyStateProvider(/* workProfileUserHandle= */ null), mQuietModeManager, /* workProfileUserHandle= */ null, getCloneProfileUserHandle(), mMaxTargetsPerRow); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List rList, boolean filterLastUsed) { int selectedProfile = findSelectedProfile(); ChooserGridAdapter personalAdapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, selectedProfile == PROFILE_PERSONAL ? initialIntents : null, rList, filterLastUsed, /* userHandle */ getPersonalProfileUserHandle()); ChooserGridAdapter workAdapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, selectedProfile == PROFILE_WORK ? initialIntents : null, rList, filterLastUsed, /* userHandle */ getWorkProfileUserHandle()); return new ChooserMultiProfilePagerAdapter( /* context */ this, personalAdapter, workAdapter, createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()), mQuietModeManager, selectedProfile, getWorkProfileUserHandle(), getCloneProfileUserHandle(), mMaxTargetsPerRow); } private int findSelectedProfile() { int selectedProfile = getSelectedProfileExtra(); if (selectedProfile == -1) { selectedProfile = getProfileForUser(getTabOwnerUserHandleForLaunch()); } return selectedProfile; } @Override protected boolean postRebuildList(boolean rebuildCompleted) { updateStickyContentPreview(); if (shouldShowStickyContentPreview() || mChooserMultiProfilePagerAdapter .getCurrentRootAdapter().getSystemRowCount() != 0) { logActionShareWithPreview(); } return postRebuildListInternal(rebuildCompleted); } /** * Returns true if app prediction service is defined and the component exists on device. */ private boolean isAppPredictionServiceAvailable() { return getPackageManager().getAppPredictionServicePackageName() != null; } /** * Check if the profile currently used is a work profile. * @return true if it is work profile, false if it is parent profile (or no work profile is * set up) */ protected boolean isWorkProfile() { return getSystemService(UserManager.class) .getUserInfo(UserHandle.myUserId()).isManagedProfile(); } @Override protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { return new PackageMonitor() { @Override public void onSomePackagesChanged() { handlePackagesChanged(listAdapter); } }; } /** * Update UI to reflect changes in data. */ public void handlePackagesChanged() { handlePackagesChanged(/* listAdapter */ null); } /** * Update UI to reflect changes in data. *

If {@code listAdapter} is {@code null}, both profile list adapters are updated if * available. */ private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) { // Refresh pinned items mPinnedSharedPrefs = getPinnedSharedPrefs(this); if (listAdapter == null) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); if (mChooserMultiProfilePagerAdapter.getCount() > 1) { mChooserMultiProfilePagerAdapter.getInactiveListAdapter().handlePackagesChanged(); } } else { listAdapter.handlePackagesChanged(); } updateProfileViewButton(); } private void onCopyButtonClicked(View v) { Intent targetIntent = getTargetIntent(); if (targetIntent == null) { finish(); } else { final String action = targetIntent.getAction(); ClipData clipData = null; if (Intent.ACTION_SEND.equals(action)) { String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT); Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM, android.net.Uri.class); if (extraText != null) { clipData = ClipData.newPlainText(null, extraText); } else if (extraStream != null) { clipData = ClipData.newUri(getContentResolver(), null, extraStream); } else { Log.w(TAG, "No data available to copy to clipboard"); return; } } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { final ArrayList streams = targetIntent.getParcelableArrayListExtra( Intent.EXTRA_STREAM, android.net.Uri.class); clipData = ClipData.newUri(getContentResolver(), null, streams.get(0)); for (int i = 1; i < streams.size(); i++) { clipData.addItem(getContentResolver(), new ClipData.Item(streams.get(i))); } } else { // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE // so warn about unexpected action Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard"); return; } ClipboardManager clipboardManager = (ClipboardManager) getSystemService( Context.CLIPBOARD_SERVICE); clipboardManager.setPrimaryClipAsPackage(clipData, getReferrerPackageName()); // Log share completion via copy LogMaker targetLogMaker = new LogMaker( MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET).setSubtype(1); getMetricsLogger().write(targetLogMaker); getChooserActivityLogger().logShareTargetSelected( SELECTION_TYPE_COPY, "", -1, false); setResult(RESULT_OK); finish(); } } @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(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(R.id.content_preview_text_layout, width, parent); updateLayoutWidth(R.id.content_preview_title_layout, width, parent); updateLayoutWidth(R.id.content_preview_file_layout, width, parent); } private void updateTabPadding() { if (shouldShowTabs()) { View tabs = findViewById(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) { Intent targetIntent = getTargetIntent(); int previewType = findPreferredContentPreview(targetIntent, getContentResolver()); return displayContentPreview(previewType, targetIntent, getLayoutInflater(), parent); } @VisibleForTesting protected ComponentName getNearbySharingComponent() { String nearbyComponent = Settings.Secure.getString( getContentResolver(), Settings.Secure.NEARBY_SHARING_COMPONENT); if (TextUtils.isEmpty(nearbyComponent)) { nearbyComponent = getString(R.string.config_defaultNearbySharingComponent); } if (TextUtils.isEmpty(nearbyComponent)) { return null; } return ComponentName.unflattenFromString(nearbyComponent); } @VisibleForTesting protected @Nullable ComponentName getEditSharingComponent() { String editorPackage = getApplicationContext().getString(R.string.config_systemImageEditor); if (editorPackage == null || TextUtils.isEmpty(editorPackage)) { return null; } return ComponentName.unflattenFromString(editorPackage); } @VisibleForTesting protected TargetInfo getEditSharingTarget(Intent originalIntent) { final ComponentName cn = getEditSharingComponent(); final Intent resolveIntent = new Intent(originalIntent); // Retain only URI permission grant flags if present. Other flags may prevent the scene // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION, // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed. resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS); resolveIntent.setComponent(cn); resolveIntent.setAction(Intent.ACTION_EDIT); String originalAction = originalIntent.getAction(); if (Intent.ACTION_SEND.equals(originalAction)) { if (resolveIntent.getData() == null) { Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM, android.net.Uri.class); if (uri != null) { String mimeType = getContentResolver().getType(uri); resolveIntent.setDataAndType(uri, mimeType); } } } else { Log.e(TAG, originalAction + " is not supported."); return null; } final ResolveInfo ri = getPackageManager().resolveActivity( resolveIntent, PackageManager.GET_META_DATA); if (ri == null || ri.activityInfo == null) { Log.e(TAG, "Device-specified image edit component (" + cn + ") not available"); return null; } final DisplayResolveInfo dri = new DisplayResolveInfo( originalIntent, ri, getString(R.string.screenshot_edit), "", resolveIntent, null); dri.setDisplayIcon(getDrawable(R.drawable.ic_screenshot_edit)); return dri; } @VisibleForTesting protected TargetInfo getNearbySharingTarget(Intent originalIntent) { final ComponentName cn = getNearbySharingComponent(); if (cn == null) return null; final Intent resolveIntent = new Intent(originalIntent); resolveIntent.setComponent(cn); final ResolveInfo ri = getPackageManager().resolveActivity( resolveIntent, PackageManager.GET_META_DATA); if (ri == null || ri.activityInfo == null) { Log.e(TAG, "Device-specified nearby sharing component (" + cn + ") not available"); return null; } // Allow the nearby sharing component to provide a more appropriate icon and label // for the chip. CharSequence name = null; Drawable icon = null; final Bundle metaData = ri.activityInfo.metaData; if (metaData != null) { try { final Resources pkgRes = getPackageManager().getResourcesForActivity(cn); final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY); name = pkgRes.getString(nameResId); final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY); icon = pkgRes.getDrawable(resId); } catch (Resources.NotFoundException ex) { } catch (NameNotFoundException ex) { } } if (TextUtils.isEmpty(name)) { name = ri.loadLabel(getPackageManager()); } if (icon == null) { icon = ri.loadIcon(getPackageManager()); } final DisplayResolveInfo dri = new DisplayResolveInfo( originalIntent, ri, name, "", resolveIntent, null); dri.setDisplayIcon(icon); return dri; } private Button createActionButton(Drawable icon, CharSequence title, View.OnClickListener r) { Button b = (Button) LayoutInflater.from(this).inflate(R.layout.chooser_action_button, null); if (icon != null) { final int size = getResources() .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size); icon.setBounds(0, 0, size, size); b.setCompoundDrawablesRelative(icon, null, null, null); } b.setText(title); b.setOnClickListener(r); return b; } private Button createCopyButton() { final Button b = createActionButton( getDrawable(R.drawable.ic_menu_copy_material), getString(R.string.copy), this::onCopyButtonClicked); b.setId(R.id.chooser_copy_button); return b; } private @Nullable Button createNearbyButton(Intent originalIntent) { final TargetInfo ti = getNearbySharingTarget(originalIntent); if (ti == null) return null; final Button b = createActionButton( ti.getDisplayIcon(this), ti.getDisplayLabel(), (View unused) -> { // Log share completion via nearby getChooserActivityLogger().logShareTargetSelected( SELECTION_TYPE_NEARBY, "", -1, false); // Action bar is user-independent, always start as primary safelyStartActivityAsUser(ti, getPersonalProfileUserHandle()); finish(); } ); b.setId(R.id.chooser_nearby_button); return b; } private @Nullable Button createEditButton(Intent originalIntent) { final TargetInfo ti = getEditSharingTarget(originalIntent); if (ti == null) return null; final Button b = createActionButton( ti.getDisplayIcon(this), ti.getDisplayLabel(), (View unused) -> { // Log share completion via edit getChooserActivityLogger().logShareTargetSelected( SELECTION_TYPE_EDIT, "", -1, false); View firstImgView = getFirstVisibleImgPreviewView(); // Action bar is user-independent, always start as primary if (firstImgView == null) { safelyStartActivityAsUser(ti, getPersonalProfileUserHandle()); finish(); } else { ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( this, firstImgView, IMAGE_EDITOR_SHARED_ELEMENT); safelyStartActivityAsUser( ti, getPersonalProfileUserHandle(), options.toBundle()); startFinishAnimation(); } } ); b.setId(R.id.chooser_edit_button); return b; } @Nullable private View getFirstVisibleImgPreviewView() { View firstImage = findViewById(R.id.content_preview_image_1_large); return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null; } private void addActionButton(ViewGroup parent, Button b) { if (b == null) return; final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT ); final int gap = getResources().getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2; lp.setMarginsRelative(gap, 0, gap, 0); parent.addView(b, lp); } private ViewGroup displayContentPreview(@ContentPreviewType int previewType, Intent targetIntent, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = null; switch (previewType) { case CONTENT_PREVIEW_TEXT: layout = displayTextContentPreview(targetIntent, layoutInflater, parent); break; case CONTENT_PREVIEW_IMAGE: layout = displayImageContentPreview(targetIntent, layoutInflater, parent); break; case CONTENT_PREVIEW_FILE: layout = displayFileContentPreview(targetIntent, layoutInflater, parent); break; default: Log.e(TAG, "Unexpected content preview type: " + previewType); } if (layout != null) { adjustPreviewWidth(getResources().getConfiguration().orientation, layout); } if (previewType != CONTENT_PREVIEW_IMAGE) { mEnterTransitionAnimationDelegate.markImagePreviewReady(); } return layout; } private ViewGroup displayTextContentPreview(Intent targetIntent, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_text, parent, false); final ViewGroup actionRow = (ViewGroup) contentPreviewLayout.findViewById(R.id.chooser_action_row); addActionButton(actionRow, createCopyButton()); if (shouldNearbyShareBeIncludedAsActionButton()) { addActionButton(actionRow, createNearbyButton(targetIntent)); } CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); if (sharingText == null) { contentPreviewLayout.findViewById(R.id.content_preview_text_layout).setVisibility( View.GONE); } else { TextView textView = contentPreviewLayout.findViewById(R.id.content_preview_text); textView.setText(sharingText); } String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); if (TextUtils.isEmpty(previewTitle)) { contentPreviewLayout.findViewById(R.id.content_preview_title_layout).setVisibility( View.GONE); } else { TextView previewTitleView = contentPreviewLayout.findViewById( R.id.content_preview_title); previewTitleView.setText(previewTitle); ClipData previewData = targetIntent.getClipData(); Uri previewThumbnail = null; if (previewData != null) { if (previewData.getItemCount() > 0) { ClipData.Item previewDataItem = previewData.getItemAt(0); previewThumbnail = previewDataItem.getUri(); } } ImageView previewThumbnailView = contentPreviewLayout.findViewById( R.id.content_preview_thumbnail); if (!validForContentPreview(previewThumbnail)) { previewThumbnailView.setVisibility(View.GONE); } else { mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false); mPreviewCoord.loadUriIntoView(R.id.content_preview_thumbnail, previewThumbnail, 0); } } return contentPreviewLayout; } private ViewGroup displayImageContentPreview(Intent targetIntent, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_image, parent, false); ViewGroup imagePreview = contentPreviewLayout.findViewById(R.id.content_preview_image_area); final ViewGroup actionRow = (ViewGroup) contentPreviewLayout.findViewById(R.id.chooser_action_row); //TODO: addActionButton(actionRow, createCopyButton()); if (shouldNearbyShareBeIncludedAsActionButton()) { addActionButton(actionRow, createNearbyButton(targetIntent)); } addActionButton(actionRow, createEditButton(targetIntent)); mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false); String action = targetIntent.getAction(); if (Intent.ACTION_SEND.equals(action)) { Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM, android.net.Uri.class); if (!validForContentPreview(uri)) { imagePreview.setVisibility(View.GONE); return contentPreviewLayout; } imagePreview.findViewById(R.id.content_preview_image_1_large) .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, uri, 0); } else { ContentResolver resolver = getContentResolver(); List uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, android.net.Uri.class); List imageUris = new ArrayList<>(); for (Uri uri : uris) { if (validForContentPreview(uri) && isImageType(resolver.getType(uri))) { imageUris.add(uri); } } if (imageUris.size() == 0) { Log.i(TAG, "Attempted to display image preview area with zero" + " available images detected in EXTRA_STREAM list"); imagePreview.setVisibility(View.GONE); return contentPreviewLayout; } imagePreview.findViewById(R.id.content_preview_image_1_large) .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, imageUris.get(0), 0); if (imageUris.size() == 2) { mPreviewCoord.loadUriIntoView(R.id.content_preview_image_2_large, imageUris.get(1), 0); } else if (imageUris.size() > 2) { mPreviewCoord.loadUriIntoView(R.id.content_preview_image_2_small, imageUris.get(1), 0); mPreviewCoord.loadUriIntoView(R.id.content_preview_image_3_small, imageUris.get(2), imageUris.size() - 3); } } return contentPreviewLayout; } private static class FileInfo { public final String name; public final boolean hasThumbnail; FileInfo(String name, boolean hasThumbnail) { this.name = name; this.hasThumbnail = hasThumbnail; } } /** * 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 FileInfo extractFileInfo(Uri uri, ContentResolver resolver) { String fileName = null; boolean hasThumbnail = false; try (Cursor cursor = queryResolver(resolver, uri)) { if (cursor != null && cursor.getCount() > 0) { int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE); int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS); cursor.moveToFirst(); if (nameIndex != -1) { fileName = cursor.getString(nameIndex); } else if (titleIndex != -1) { fileName = cursor.getString(titleIndex); } if (flagsIndex != -1) { hasThumbnail = (cursor.getInt(flagsIndex) & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0; } } } catch (SecurityException | NullPointerException e) { logContentPreviewWarning(uri); } if (TextUtils.isEmpty(fileName)) { fileName = uri.getPath(); int index = fileName.lastIndexOf('/'); if (index != -1) { fileName = fileName.substring(index + 1); } } return new FileInfo(fileName, hasThumbnail); } private void logContentPreviewWarning(Uri uri) { // The ContentResolver already logs the exception. Log something more informative. Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If " + "desired, consider using Intent#createChooser to launch the ChooserActivity, " + "and set your Intent's clipData and flags in accordance with that method's " + "documentation"); } private ViewGroup displayFileContentPreview(Intent targetIntent, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_file, parent, false); final ViewGroup actionRow = (ViewGroup) contentPreviewLayout.findViewById(R.id.chooser_action_row); //TODO(b/120417119): addActionButton(actionRow, createCopyButton()); if (shouldNearbyShareBeIncludedAsActionButton()) { addActionButton(actionRow, createNearbyButton(targetIntent)); } String action = targetIntent.getAction(); if (Intent.ACTION_SEND.equals(action)) { Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM, android.net.Uri.class); if (!validForContentPreview(uri)) { contentPreviewLayout.setVisibility(View.GONE); return contentPreviewLayout; } loadFileUriIntoView(uri, contentPreviewLayout); } else { List uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, android.net.Uri.class); uris = uris.stream() .filter(ChooserActivity::validForContentPreview) .collect(Collectors.toList()); int uriCount = uris.size(); if (uriCount == 0) { contentPreviewLayout.setVisibility(View.GONE); Log.i(TAG, "Appears to be no uris available in EXTRA_STREAM, removing " + "preview area"); return contentPreviewLayout; } else if (uriCount == 1) { loadFileUriIntoView(uris.get(0), contentPreviewLayout); } else { FileInfo fileInfo = extractFileInfo(uris.get(0), getContentResolver()); int remUriCount = uriCount - 1; Map arguments = new HashMap<>(); arguments.put(PLURALS_COUNT, remUriCount); arguments.put(PLURALS_FILE_NAME, fileInfo.name); String fileName = PluralsMessageFormatter.format( getResources(), arguments, R.string.file_count); TextView fileNameView = contentPreviewLayout.findViewById( R.id.content_preview_filename); fileNameView.setText(fileName); View thumbnailView = contentPreviewLayout.findViewById( R.id.content_preview_file_thumbnail); thumbnailView.setVisibility(View.GONE); ImageView fileIconView = contentPreviewLayout.findViewById( R.id.content_preview_file_icon); fileIconView.setVisibility(View.VISIBLE); fileIconView.setImageResource(R.drawable.ic_file_copy); } } return contentPreviewLayout; } private void loadFileUriIntoView(final Uri uri, final View parent) { FileInfo fileInfo = extractFileInfo(uri, getContentResolver()); TextView fileNameView = parent.findViewById(R.id.content_preview_filename); fileNameView.setText(fileInfo.name); if (fileInfo.hasThumbnail) { mPreviewCoord = new ContentPreviewCoordinator(parent, false); mPreviewCoord.loadUriIntoView(R.id.content_preview_file_thumbnail, uri, 0); } else { View thumbnailView = parent.findViewById(R.id.content_preview_file_thumbnail); thumbnailView.setVisibility(View.GONE); ImageView fileIconView = parent.findViewById(R.id.content_preview_file_icon); fileIconView.setVisibility(View.VISIBLE); fileIconView.setImageResource(R.drawable.chooser_file_generic); } } /** * Indicate if the incoming content URI should be allowed. * * @param uri the uri to test * @return true if the URI is allowed for content preview */ private static boolean validForContentPreview(Uri uri) throws SecurityException { if (uri == null) { return false; } int userId = getUserIdFromUri(uri, UserHandle.USER_CURRENT); if (userId != UserHandle.USER_CURRENT && userId != UserHandle.myUserId()) { Log.e(TAG, "dropped invalid content URI belonging to user " + userId); return false; } return true; } @VisibleForTesting protected boolean isImageType(String mimeType) { return mimeType != null && mimeType.startsWith("image/"); } @ContentPreviewType private int findPreferredContentPreview(Uri uri, ContentResolver resolver) { if (uri == null) { return CONTENT_PREVIEW_TEXT; } String mimeType = resolver.getType(uri); return isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE; } /** * In {@link android.content.Intent#getType}, the app may specify a very general * mime-type that broadly covers all data being shared, such as {@literal *}/* * when sending an image and text. We therefore should inspect each item for the * the preferred type, in order of IMAGE, FILE, TEXT. */ @ContentPreviewType private int findPreferredContentPreview(Intent targetIntent, ContentResolver resolver) { String action = targetIntent.getAction(); if (Intent.ACTION_SEND.equals(action)) { Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM, android.net.Uri.class); return findPreferredContentPreview(uri, resolver); } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { List uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, android.net.Uri.class); if (uris == null || uris.isEmpty()) { return CONTENT_PREVIEW_TEXT; } for (Uri uri : uris) { // Defaulting to file preview when there are mixed image/file types is // preferable, as it shows the user the correct number of items being shared if (findPreferredContentPreview(uri, resolver) == CONTENT_PREVIEW_FILE) { return CONTENT_PREVIEW_FILE; } } return CONTENT_PREVIEW_IMAGE; } return CONTENT_PREVIEW_TEXT; } 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 (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroy(); mRefinementResultReceiver = null; } mChooserHandler.removeAllMessages(); if (mPreviewCoord != null) mPreviewCoord.cancelLoads(); mChooserMultiProfilePagerAdapter.getActiveListAdapter().destroyAppPredictor(); if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) { mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor(); } mPersonalAppPredictor = null; mWorkAppPredictor = null; } @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { Intent result = defIntent; if (mReplacementExtras != null) { final Bundle replExtras = mReplacementExtras.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 (mChosenComponentSender != null) { final ComponentName target = cti.getResolvedComponentName(); if (target != null) { final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target); try { mChosenComponentSender.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 (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( /* origTarget */ null, Lists.newArrayList(mCallerChooserTargets), TARGET_TYPE_DEFAULT, /* directShareShortcutInfoCache */ null); } } @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 modifyTargetIntent(Intent in) { if (isSendAction(in)) { in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); } } @Override protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { if (mRefinementIntentSender != null) { final Intent fillIn = new Intent(); final List sourceIntents = target.getAllSourceIntents(); if (!sourceIntents.isEmpty()) { fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0)); if (sourceIntents.size() > 1) { final Intent[] alts = new Intent[sourceIntents.size() - 1]; for (int i = 1, N = sourceIntents.size(); i < N; i++) { alts[i - 1] = sourceIntents.get(i); } fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts); } if (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroy(); } mRefinementResultReceiver = new RefinementResultReceiver(this, target, null); fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, mRefinementResultReceiver); try { mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null); return false; } catch (SendIntentException e) { Log.e(TAG, "Refinement IntentSender failed to send", e); } } } updateModelAndChooserCounts(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 instanceof NotSelectableTargetInfo) { return; } final long selectionCost = System.currentTimeMillis() - mChooserShownTime; if (targetInfo instanceof MultiDisplayResolveInfo) { MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; if (!mti.hasSelected()) { ChooserStackedAppDialogFragment f = new ChooserStackedAppDialogFragment(); Bundle b = new Bundle(); // Add userHandle based badge to the stackedAppDialogBox. b.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY, getResolveInfoUserHandle( targetInfo.getResolveInfo(), mChooserMultiProfilePagerAdapter.getCurrentUserHandle())); b.putObject(ChooserStackedAppDialogFragment.MULTI_DRI_KEY, mti); b.putInt(ChooserStackedAppDialogFragment.WHICH_KEY, which); f.setArguments(b); f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); return; } } super.startSelected(which, always, filtered); if (currentListAdapter.getCount() > 0) { // Log the index of which type of target the user picked. // Lower values mean the ranking was better. int cat = 0; int value = which; int directTargetAlsoRanked = -1; int numCallerProvided = 0; HashedStringCache.HashResult directTargetHashed = null; switch (currentListAdapter.getPositionTargetType(which)) { case ChooserListAdapter.TARGET_SERVICE: cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET; // Log the package name + target name to answer the question if most users // share to mostly the same person or to a bunch of different people. ChooserTarget target = currentListAdapter.getChooserTargetForValue(value); directTargetHashed = HashedStringCache.getInstance().hashString( this, TAG, target.getComponentName().getPackageName() + target.getTitle().toString(), mMaxHashSaltDays); SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo; directTargetAlsoRanked = getRankedPosition(selectableTargetInfo); if (mCallerChooserTargets != null) { numCallerProvided = mCallerChooserTargets.length; } getChooserActivityLogger().logShareTargetSelected( SELECTION_TYPE_SERVICE, targetInfo.getResolveInfo().activityInfo.processName, value, selectableTargetInfo.isPinned() ); break; case ChooserListAdapter.TARGET_CALLER: case ChooserListAdapter.TARGET_STANDARD: cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET; value -= currentListAdapter.getSurfacedTargetInfo().size(); numCallerProvided = currentListAdapter.getCallerTargetCount(); getChooserActivityLogger().logShareTargetSelected( SELECTION_TYPE_APP, targetInfo.getResolveInfo().activityInfo.processName, value, targetInfo.isPinned() ); break; case ChooserListAdapter.TARGET_STANDARD_AZ: // A-Z targets are unranked standard targets; we use -1 to mark that they // are from the alphabetical pool. value = -1; cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET; getChooserActivityLogger().logShareTargetSelected( SELECTION_TYPE_STANDARD, targetInfo.getResolveInfo().activityInfo.processName, value, false ); break; } if (cat != 0) { LogMaker targetLogMaker = new LogMaker(cat).setSubtype(value); if (directTargetHashed != null) { targetLogMaker.addTaggedData( MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString); targetLogMaker.addTaggedData( MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN, directTargetHashed.saltGeneration); targetLogMaker.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION, directTargetAlsoRanked); } targetLogMaker.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, numCallerProvided); getMetricsLogger().write(targetLogMaker); } if (mIsSuccessfullySelected) { if (DEBUG) { Log.d(TAG, "User Selection Time Cost is " + selectionCost); Log.d(TAG, "position of selected app/service/caller is " + Integer.toString(value)); } MetricsLogger.histogram(null, "user_selection_cost_for_smart_sharing", (int) selectionCost); MetricsLogger.histogram(null, "app_position_for_smart_sharing", value); } } } private int getRankedPosition(SelectableTargetInfo targetInfo) { String targetPackageName = targetInfo.getChooserTarget().getComponentName().getPackageName(); ChooserListAdapter currentListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); int maxRankedResults = Math.min(currentListAdapter.mDisplayList.size(), MAX_LOG_RANK_POSITION); for (int i = 0; i < maxRankedResults; i++) { if (currentListAdapter.mDisplayList.get(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 IntentFilter getTargetIntentFilter() { try { final Intent intent = getTargetIntent(); String dataString = intent.getDataString(); if (intent.getType() == null) { if (!TextUtils.isEmpty(dataString)) { return new IntentFilter(intent.getAction(), dataString); } Log.e(TAG, "Failed to get target intent filter: intent data and type are null"); return null; } IntentFilter intentFilter = new IntentFilter(intent.getAction(), intent.getType()); List contentUris = new ArrayList<>(); if (Intent.ACTION_SEND.equals(intent.getAction())) { Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM, android.net.Uri.class); if (uri != null) { contentUris.add(uri); } } else { List uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, android.net.Uri.class); if (uris != null) { contentUris.addAll(uris); } } for (Uri uri : contentUris) { intentFilter.addDataScheme(uri.getScheme()); intentFilter.addDataAuthority(uri.getAuthority(), null); intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL); } return intentFilter; } catch (Exception e) { Log.e(TAG, "Failed to get target intent filter", e); return null; } } @VisibleForTesting protected void queryDirectShareTargets( ChooserListAdapter adapter, boolean skipAppPredictionService) { mQueriedSharingShortcutsTimeMs = System.currentTimeMillis(); UserHandle userHandle = adapter.getUserHandle(); if (!skipAppPredictionService) { AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle); if (appPredictor != null) { appPredictor.requestPredictionUpdate(); return; } } // Default to just querying ShortcutManager if AppPredictor not present. final IntentFilter filter = getTargetIntentFilter(); if (filter == null) { return; } AsyncTask.execute(() -> { Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */); ShortcutManager sm = (ShortcutManager) selectedProfileContext .getSystemService(Context.SHORTCUT_SERVICE); List resultList = sm.getShareTargets(filter); sendShareShortcutInfoList(resultList, adapter, null, userHandle); }); } /** * Returns {@code false} if {@code userHandle} is the work profile and it's either * in quiet mode or not running. */ private boolean shouldQueryShortcutManager(UserHandle userHandle) { if (!shouldShowTabs()) { return true; } if (!getWorkProfileUserHandle().equals(userHandle)) { return true; } if (!isUserRunning(userHandle)) { return false; } if (!isUserUnlocked(userHandle)) { return false; } if (isQuietModeEnabled(userHandle)) { return false; } return true; } private void sendShareShortcutInfoList( List resultList, ChooserListAdapter chooserListAdapter, @Nullable List appTargets, UserHandle userHandle) { if (appTargets != null && appTargets.size() != resultList.size()) { throw new RuntimeException("resultList and appTargets must have the same size." + " resultList.size()=" + resultList.size() + " appTargets.size()=" + appTargets.size()); } Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */); for (int i = resultList.size() - 1; i >= 0; i--) { final String packageName = resultList.get(i).getTargetComponent().getPackageName(); if (!isPackageEnabled(selectedProfileContext, packageName)) { resultList.remove(i); if (appTargets != null) { appTargets.remove(i); } } } // If |appTargets| is not null, results are from AppPredictionService and already sorted. final int shortcutType = (appTargets == null ? TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER : TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path // for direct share targets. After ShareSheet is refactored we should use the // ShareShortcutInfos directly. List resultRecords = new ArrayList<>(); for (int i = 0; i < chooserListAdapter.getDisplayResolveInfoCount(); i++) { DisplayResolveInfo displayResolveInfo = chooserListAdapter.getDisplayResolveInfo(i); List matchingShortcuts = filterShortcutsByTargetComponentName( resultList, displayResolveInfo.getResolvedComponentName()); if (matchingShortcuts.isEmpty()) { continue; } List chooserTargets = convertToChooserTarget( matchingShortcuts, resultList, appTargets, shortcutType); ServiceResultInfo resultRecord = new ServiceResultInfo( displayResolveInfo, chooserTargets, userHandle); resultRecords.add(resultRecord); } sendShortcutManagerShareTargetResults( shortcutType, resultRecords.toArray(new ServiceResultInfo[0])); } private List filterShortcutsByTargetComponentName( List allShortcuts, ComponentName requiredTarget) { List matchingShortcuts = new ArrayList<>(); for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) { if (requiredTarget.equals(shortcut.getTargetComponent())) { matchingShortcuts.add(shortcut); } } return matchingShortcuts; } @VisibleForTesting protected void sendShortcutManagerShareTargetResults( int shortcutType, ServiceResultInfo[] results) { final Message msg = Message.obtain(); msg.what = ChooserHandler.SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS; msg.obj = results; msg.arg1 = shortcutType; mChooserHandler.sendMessage(msg); } private boolean isPackageEnabled(Context context, String packageName) { if (TextUtils.isEmpty(packageName)) { return false; } ApplicationInfo appInfo; try { appInfo = context.getPackageManager().getApplicationInfo(packageName, 0); } catch (NameNotFoundException e) { return false; } if (appInfo != null && appInfo.enabled && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) { return true; } return false; } /** * Converts a list of ShareShortcutInfos to ChooserTargets. * @param matchingShortcuts List of shortcuts, all from the same package, that match the current * share intent filter. * @param allShortcuts List of all the shortcuts from all the packages on the device that are * returned for the current sharing action. * @param allAppTargets List of AppTargets. Null if the results are not from prediction service. * @param shortcutType One of the values TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER or * TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE * @return A list of ChooserTargets sorted by score in descending order. */ @VisibleForTesting @NonNull public List convertToChooserTarget( @NonNull List matchingShortcuts, @NonNull List allShortcuts, @Nullable List allAppTargets, @ShareTargetType int shortcutType) { // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted // list instead of the actual rank value when converting a rank to a score. List scoreList = new ArrayList<>(); if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) { for (int i = 0; i < matchingShortcuts.size(); i++) { int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank(); if (!scoreList.contains(shortcutRank)) { scoreList.add(shortcutRank); } } Collections.sort(scoreList); } List chooserTargetList = new ArrayList<>(matchingShortcuts.size()); for (int i = 0; i < matchingShortcuts.size(); i++) { ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo(); int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i)); float score; if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) { // Incoming results are ordered. Create a score based on index in the original list. score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f); } else { // Create a score based on the rank of the shortcut. int rankIndex = scoreList.indexOf(shortcutInfo.getRank()); score = Math.max(1.0f - (0.01f * rankIndex), 0.0f); } Bundle extras = new Bundle(); extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId()); ChooserTarget chooserTarget = new ChooserTarget( shortcutInfo.getLabel(), null, // Icon will be loaded later if this target is selected to be shown. score, matchingShortcuts.get(i).getTargetComponent().clone(), extras); chooserTargetList.add(chooserTarget); if (mDirectShareAppTargetCache != null && allAppTargets != null) { mDirectShareAppTargetCache.put(chooserTarget, allAppTargets.get(indexInAllShortcuts)); } if (mDirectShareShortcutInfoCache != null) { mDirectShareShortcutInfoCache.put(chooserTarget, shortcutInfo); } } // Sort ChooserTargets by score in descending order Comparator byScore = (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore()); Collections.sort(chooserTargetList, byScore); return chooserTargetList; } private void logDirectShareTargetReceived(int logCategory) { final int apiLatency = (int) (System.currentTimeMillis() - mQueriedSharingShortcutsTimeMs); getMetricsLogger().write(new LogMaker(logCategory).setSubtype(apiLatency)); } void updateModelAndChooserCounts(TargetInfo info) { if (info != null && info instanceof MultiDisplayResolveInfo) { info = ((MultiDisplayResolveInfo) info).getSelectedTarget(); } if (info != null) { sendClickToAppPredictor(info); final ResolveInfo ri = info.getResolveInfo(); Intent targetIntent = getTargetIntent(); if (ri != null && ri.activityInfo != null && targetIntent != null) { ChooserListAdapter currentListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); if (currentListAdapter != null) { sendImpressionToAppPredictor(info, currentListAdapter); currentListAdapter.updateModel(info); currentListAdapter.updateChooserCounts( ri.activityInfo.packageName, targetIntent.getAction(), ri.userHandle); } if (DEBUG) { Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName); Log.d(TAG, "Action to be updated is " + targetIntent.getAction()); } } else if (DEBUG) { Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo"); } } mIsSuccessfullySelected = true; } private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) { AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled( mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); if (directShareAppPredictor == null) { return; } // Send DS target impression info to AppPredictor, only when user chooses app share. if (targetInfo instanceof ChooserTargetInfo) { return; } List surfacedTargetInfo = adapter.getSurfacedTargetInfo(); List targetIds = new ArrayList<>(); for (ChooserTargetInfo chooserTargetInfo : surfacedTargetInfo) { ChooserTarget chooserTarget = chooserTargetInfo.getChooserTarget(); ComponentName componentName = chooserTarget.getComponentName(); if (mDirectShareShortcutInfoCache.containsKey(chooserTarget)) { String shortcutId = mDirectShareShortcutInfoCache.get(chooserTarget).getId(); targetIds.add(new AppTargetId( String.format("%s/%s/%s", shortcutId, componentName.flattenToString(), SHORTCUT_TARGET))); } } directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds); } private void sendClickToAppPredictor(TargetInfo targetInfo) { AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled( mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); if (directShareAppPredictor == null) { return; } if (!(targetInfo instanceof ChooserTargetInfo)) { return; } ChooserTarget chooserTarget = ((ChooserTargetInfo) targetInfo).getChooserTarget(); AppTarget appTarget = null; if (mDirectShareAppTargetCache != null) { appTarget = mDirectShareAppTargetCache.get(chooserTarget); } // This is a direct share click that was provided by the APS if (appTarget != null) { directShareAppPredictor.notifyAppTargetEvent( new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE) .build()); } } @Nullable private AppPredictor createAppPredictor(UserHandle userHandle) { if (!mIsAppPredictorComponentAvailable) { return null; } if (getPersonalProfileUserHandle().equals(userHandle)) { if (mPersonalAppPredictor != null) { return mPersonalAppPredictor; } } else { if (mWorkAppPredictor != null) { return mWorkAppPredictor; } } // TODO(b/148230574): Currently AppPredictor fetches only the same-profile app targets. // Make AppPredictor work cross-profile. Context contextAsUser = createContextAsUser(userHandle, 0 /* flags */); final IntentFilter filter = getTargetIntentFilter(); Bundle extras = new Bundle(); extras.putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, filter); populateTextContent(extras); AppPredictionContext appPredictionContext = new AppPredictionContext.Builder(contextAsUser) .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE) .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT) .setExtras(extras) .build(); AppPredictionManager appPredictionManager = contextAsUser .getSystemService(AppPredictionManager.class); AppPredictor appPredictionSession = appPredictionManager.createAppPredictionSession( appPredictionContext); if (getPersonalProfileUserHandle().equals(userHandle)) { mPersonalAppPredictor = appPredictionSession; } else { mWorkAppPredictor = appPredictionSession; } return appPredictionSession; } private void populateTextContent(Bundle extras) { final Intent intent = getTargetIntent(); String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); extras.putString(SHARED_TEXT_KEY, sharedText); } /** * This will return an app predictor if it is enabled for direct share sorting * and if one exists. Otherwise, it returns null. * @param userHandle */ @Nullable private AppPredictor getAppPredictorForDirectShareIfEnabled(UserHandle userHandle) { return ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS && !ActivityManager.isLowRamDeviceStatic() ? createAppPredictor(userHandle) : null; } /** * This will return an app predictor if it is enabled for share activity sorting * and if one exists. Otherwise, it returns null. */ @Nullable private AppPredictor getAppPredictorForShareActivitiesIfEnabled(UserHandle userHandle) { // We cannot use APS service when clone profile is present as APS service cannot sort // cross profile targets as of now. return USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES && getCloneProfileUserHandle() == null ? createAppPredictor(userHandle) : null; } void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) { if (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroy(); mRefinementResultReceiver = null; } if (selectedTarget == null) { Log.e(TAG, "Refinement result intent did not match any known targets; canceling"); } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) { Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget + " cannot match refined source intent " + matchingIntent); } else { TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0); if (super.onTargetSelected(clonedTarget, false)) { updateModelAndChooserCounts(clonedTarget); finish(); return; } } onRefinementCanceled(); } void onRefinementCanceled() { if (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroy(); mRefinementResultReceiver = null; } finish(); } boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) { final List targetIntents = target.getAllSourceIntents(); for (int i = 0, N = targetIntents.size(); i < N; i++) { final Intent targetIntent = targetIntents.get(i); if (targetIntent.filterEquals(matchingIntent)) { return true; } } return false; } /** * Sort intents alphabetically based on display label. */ static class AzInfoComparator implements Comparator { Comparator mComparator; AzInfoComparator(Context context) { Collator collator = Collator .getInstance(context.getResources().getConfiguration().locale); // Adding two stage comparator, first stage compares using displayLabel, next stage // compares using resolveInfo.userHandle mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator) .thenComparingInt(displayResolveInfo -> getResolveInfoUserHandle( displayResolveInfo.getResolveInfo(), // TODO: User resolveInfo.userHandle, once its available. UserHandle.SYSTEM).getIdentifier()); } @Override public int compare( DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) { return mComparator.compare(lhsp, rhsp); } } protected MetricsLogger getMetricsLogger() { if (mMetricsLogger == null) { mMetricsLogger = new MetricsLogger(); } return mMetricsLogger; } protected ChooserActivityLogger getChooserActivityLogger() { if (mChooserActivityLogger == null) { mChooserActivityLogger = new ChooserActivityLoggerImpl(); } return mChooserActivityLogger; } public class ChooserListController extends ResolverListController { public ChooserListController(Context context, PackageManager pm, Intent targetIntent, String referrerPackageName, int launchedFromUid, UserHandle userId, AbstractResolverComparator resolverComparator, UserHandle queryIntentsAsUser) { super(context, pm, targetIntent, referrerPackageName, launchedFromUid, userId, resolverComparator, queryIntentsAsUser); } @Override boolean isComponentFiltered(ComponentName name) { if (mFilteredComponentNames == null) { return false; } for (ComponentName filteredComponentName : mFilteredComponentNames) { if (name.equals(filteredComponentName)) { return true; } } return false; } @Override public boolean isComponentPinned(ComponentName name) { return false; } @Override public boolean isFixedAtTop(ComponentName name) { return name != null && name.equals(getNearbySharingComponent()) && shouldNearbyShareBeFirstInRankedRow(); } } @VisibleForTesting public ChooserGridAdapter createChooserGridAdapter(Context context, List payloadIntents, Intent[] initialIntents, List rList, boolean filterLastUsed, UserHandle userHandle) { ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents, initialIntents, rList, filterLastUsed, userHandle); ResolverAppPredictorCallback appPredictorCallbackWrapper = createAppPredictorCallback(chooserListAdapter); AppPredictor.Callback appPredictorCallback = appPredictorCallbackWrapper.asCallback(); AppPredictor appPredictor = setupAppPredictorForUser(userHandle, appPredictorCallback); chooserListAdapter.setAppPredictor(appPredictor); chooserListAdapter.setAppPredictorCallback( appPredictorCallback, appPredictorCallbackWrapper); return new ChooserGridAdapter(chooserListAdapter); } @VisibleForTesting public ChooserListAdapter createChooserListAdapter(Context context, List payloadIntents, Intent[] initialIntents, List rList, boolean filterLastUsed, UserHandle userHandle) { UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() && userHandle.equals(getPersonalProfileUserHandle()) ? getCloneProfileUserHandle() : userHandle; return new ChooserListAdapter(context, payloadIntents, initialIntents, rList, filterLastUsed, createListController(userHandle), this, this, context.getPackageManager(), getChooserActivityLogger(), initialIntentsUserSpace); } @VisibleForTesting protected ResolverListController createListController(UserHandle userHandle) { AppPredictor appPredictor = getAppPredictorForShareActivitiesIfEnabled(userHandle); AbstractResolverComparator resolverComparator; if (appPredictor != null) { resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(), getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger()); } else { resolverComparator = new ResolverRankerServiceResolverComparator( this, getTargetIntent(), getReferrerPackageName(), null, getChooserActivityLogger(), getResolverRankerServiceUserHandleList(userHandle)); } UserHandle queryIntentsUser = getQueryIntentsUser(userHandle); return new ChooserListController( this, mPm, getTargetIntent(), getReferrerPackageName(), mLaunchedFromUid, userHandle, resolverComparator, queryIntentsUser == null ? userHandle : queryIntentsUser); } @VisibleForTesting protected Bitmap loadThumbnail(Uri uri, Size size) { if (uri == null || size == null) { return null; } try { return getContentResolver().loadThumbnail(uri, size, null); } catch (IOException | NullPointerException | SecurityException ex) { logContentPreviewWarning(uri); } return null; } static final class PlaceHolderTargetInfo extends NotSelectableTargetInfo { public Drawable getDisplayIcon(Context context) { AnimatedVectorDrawable avd = (AnimatedVectorDrawable) context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder); avd.start(); // Start animation after generation return avd; } } protected static final class EmptyTargetInfo extends NotSelectableTargetInfo { public EmptyTargetInfo() {} public Drawable getDisplayIcon(Context context) { return null; } } 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.consumeLayoutRequest() || 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(R.id.content_preview_container); if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) { offset += stickyContentPreview.getHeight(); } if (shouldShowTabs()) { offset += findViewById(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_WORK}, if the given user handle matches work user handle. * Returns {@link #PROFILE_PERSONAL}, otherwise. **/ private int getProfileForUser(UserHandle currentUserHandle) { if (currentUserHandle.equals(getWorkProfileUserHandle())) { return PROFILE_WORK; } // We return personal profile, as it is the default when there is no work profile, personal // profile represents rootUser, clonedUser & secondaryUser, covering all use cases. return PROFILE_PERSONAL; } private ViewGroup getActiveEmptyStateView() { int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage(); return mChooserMultiProfilePagerAdapter.getItem(currentPage).getEmptyStateView(); } static class BaseChooserTargetComparator implements Comparator { @Override public int compare(ChooserTarget lhs, ChooserTarget rhs) { // Descending order return (int) Math.signum(rhs.getScore() - lhs.getScore()); } } @Override // ResolverListCommunicator public void onHandlePackagesChanged(ResolverListAdapter listAdapter) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged(); super.onHandlePackagesChanged(listAdapter); } @Override // SelectableTargetInfoCommunicator public ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info) { return mChooserMultiProfilePagerAdapter.getActiveListAdapter().makePresentationGetter(info); } @Override // SelectableTargetInfoCommunicator public Intent getReferrerFillInIntent() { return mReferrerFillInIntent; } @Override // ChooserListCommunicator public int getMaxRankedTargets() { return mMaxTargetsPerRow; } @Override // ChooserListCommunicator public void sendListViewUpdateMessage(UserHandle userHandle) { Message msg = Message.obtain(); msg.what = ChooserHandler.LIST_VIEW_UPDATE_MESSAGE; msg.obj = userHandle; mChooserHandler.sendMessageDelayed(msg, mListViewUpdateDelayMs); } @Override public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { setupScrollListener(); ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; if (chooserListAdapter.getUserHandle() .equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) { mChooserMultiProfilePagerAdapter.getActiveAdapterView() .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()); mChooserMultiProfilePagerAdapter .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage()); } if (chooserListAdapter.mDisplayList == null || chooserListAdapter.mDisplayList.isEmpty()) { chooserListAdapter.notifyDataSetChanged(); } else { chooserListAdapter.updateAlphabeticalList(); } if (rebuildComplete) { getChooserActivityLogger().logSharesheetAppLoadComplete(); maybeQueryAdditionalPostProcessingTargets(chooserListAdapter); mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET); } } private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) { // don't support direct share on low ram devices if (ActivityManager.isLowRamDeviceStatic()) { return; } // no need to query direct share for work profile when its locked or disabled if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) { return; } if (ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) { if (DEBUG) { Log.d(TAG, "querying direct share targets from ShortcutManager"); } queryDirectShareTargets(chooserListAdapter, false); } } @VisibleForTesting protected boolean isUserRunning(UserHandle userHandle) { UserManager userManager = getSystemService(UserManager.class); return userManager.isUserRunning(userHandle); } @VisibleForTesting protected boolean isUserUnlocked(UserHandle userHandle) { UserManager userManager = getSystemService(UserManager.class); return userManager.isUserUnlocked(userHandle); } @VisibleForTesting protected boolean isQuietModeEnabled(UserHandle userHandle) { UserManager userManager = getSystemService(UserManager.class); return userManager.isQuietModeEnabled(userHandle); } private void setupScrollListener() { if (mResolverDrawerLayout == null) { return; } int elevatedViewResId = shouldShowTabs() ? R.id.tabs : 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); } }); } @Override // ChooserListCommunicator public boolean isSendAction(Intent targetIntent) { if (targetIntent == null) { return false; } String action = targetIntent.getAction(); if (action == null) { return false; } if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { return true; } return false; } /** * 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 || shouldShowStickyContentPreviewWhenEmpty()) && shouldShowContentPreview(); } /** * This method could be used to override the default behavior when we hide the sticky preview * area when the current tab doesn't have any items. * * @return {@code true} if we want to show the sticky content preview area even if the tab for * the current user is empty */ protected boolean shouldShowStickyContentPreviewWhenEmpty() { return false; } @Override public boolean shouldShowContentPreview() { return isSendAction(getTargetIntent()); } @Override public boolean shouldShowServiceTargets() { return shouldShowContentPreview() && !ActivityManager.isLowRamDeviceStatic(); } 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(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(R.id.content_preview_container); contentPreviewContainer.setVisibility(View.VISIBLE); } private boolean isStickyContentPreviewShowing() { ViewGroup contentPreviewContainer = findViewById(R.id.content_preview_container); return contentPreviewContainer.getVisibility() == View.VISIBLE; } private void hideStickyContentPreview() { if (!isStickyContentPreviewShowing()) { return; } ViewGroup contentPreviewContainer = findViewById(R.id.content_preview_container); contentPreviewContainer.setVisibility(View.GONE); } private void logActionShareWithPreview() { Intent targetIntent = getTargetIntent(); int previewType = findPreferredContentPreview(targetIntent, getContentResolver()); getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW) .setSubtype(previewType)); } 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; } abstract static class ViewHolderBase extends RecyclerView.ViewHolder { private int mViewType; ViewHolderBase(View itemView, int viewType) { super(itemView); this.mViewType = viewType; } int getViewType() { return mViewType; } } /** * Used to bind types of individual item including * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL}, * {@link ChooserGridAdapter#VIEW_TYPE_CONTENT_PREVIEW}, * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE}, * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}. */ final class ItemViewHolder extends ViewHolderBase { ResolverListAdapter.ViewHolder mWrappedViewHolder; int mListPosition = ChooserListAdapter.NO_POSITION; ItemViewHolder(View itemView, boolean isClickable, int viewType) { super(itemView, viewType); mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView); if (isClickable) { itemView.setOnClickListener(v -> startSelected(mListPosition, false/* always */, true/* filterd */)); } } } /** * Add a footer to the list, to support scrolling behavior below the navbar. */ static final class FooterViewHolder extends ViewHolderBase { FooterViewHolder(View itemView, int viewType) { super(itemView, viewType); } } /** * 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(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(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); } } } /** * Adapter for all types of items and targets in ShareSheet. * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the * row level by this adapter but not on the item level. Individual targets within the row are * handled by {@link ChooserListAdapter} */ @VisibleForTesting public final class ChooserGridAdapter extends RecyclerView.Adapter { private ChooserListAdapter mChooserListAdapter; private final LayoutInflater mLayoutInflater; private DirectShareViewHolder mDirectShareViewHolder; private int mChooserTargetWidth = 0; private boolean mShowAzLabelIfPoss; private boolean mLayoutRequested = false; private int mFooterHeight = 0; private static final int VIEW_TYPE_DIRECT_SHARE = 0; private static final int VIEW_TYPE_NORMAL = 1; private static final int VIEW_TYPE_CONTENT_PREVIEW = 2; private static final int VIEW_TYPE_PROFILE = 3; private static final int VIEW_TYPE_AZ_LABEL = 4; private static final int VIEW_TYPE_CALLER_AND_RANK = 5; private static final int VIEW_TYPE_FOOTER = 6; private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20; ChooserGridAdapter(ChooserListAdapter wrappedAdapter) { super(); mChooserListAdapter = wrappedAdapter; mLayoutInflater = LayoutInflater.from(ChooserActivity.this); mShowAzLabelIfPoss = getNumSheetExpansions() < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL; wrappedAdapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { super.onChanged(); notifyDataSetChanged(); } @Override public void onInvalidated() { super.onInvalidated(); notifyDataSetChanged(); } }); } public void setFooterHeight(int height) { mFooterHeight = height; } /** * Calculate the chooser target width to maximize space per item * * @param width The new row width to use for recalculation * @return true if the view width has changed */ public boolean calculateChooserTargetWidth(int width) { if (width == 0) { return false; } // Limit width to the maximum width of the chooser activity int maxWidth = getResources().getDimensionPixelSize(R.dimen.chooser_width); width = Math.min(maxWidth, width); int newWidth = width / mMaxTargetsPerRow; if (newWidth != mChooserTargetWidth) { mChooserTargetWidth = newWidth; return true; } return false; } /** * Hides the list item content preview. *

Not to be confused with the sticky content preview which is above the * personal and work tabs. */ public void hideContentPreview() { mLayoutRequested = true; notifyDataSetChanged(); } public boolean consumeLayoutRequest() { boolean oldValue = mLayoutRequested; mLayoutRequested = false; return oldValue; } public int getRowCount() { return (int) ( getSystemRowCount() + getProfileRowCount() + getServiceTargetRowCount() + getCallerAndRankedTargetRowCount() + getAzLabelRowCount() + Math.ceil( (float) mChooserListAdapter.getAlphaTargetCount() / mMaxTargetsPerRow) ); } /** * Whether the "system" row of targets is displayed. * This area includes the content preview (if present) and action row. */ public int getSystemRowCount() { // For the tabbed case we show the sticky content preview above the tabs, // please refer to shouldShowStickyContentPreview if (shouldShowTabs()) { return 0; } if (!shouldShowContentPreview()) { return 0; } if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) { return 0; } return 1; } public int getProfileRowCount() { if (shouldShowTabs()) { return 0; } return mChooserListAdapter.getOtherProfile() == null ? 0 : 1; } public int getFooterRowCount() { return 1; } public int getCallerAndRankedTargetRowCount() { return (int) Math.ceil( ((float) mChooserListAdapter.getCallerTargetCount() + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow); } // There can be at most one row in the listview, that is internally // a ViewGroup with 2 rows public int getServiceTargetRowCount() { return shouldShowServiceTargets() ? 1 : 0; } public int getAzLabelRowCount() { // Only show a label if the a-z list is showing return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0; } @Override public int getItemCount() { return (int) ( getSystemRowCount() + getProfileRowCount() + getServiceTargetRowCount() + getCallerAndRankedTargetRowCount() + getAzLabelRowCount() + mChooserListAdapter.getAlphaTargetCount() + getFooterRowCount() ); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_CONTENT_PREVIEW: return new ItemViewHolder(createContentPreviewView(parent), false, viewType); case VIEW_TYPE_PROFILE: return new ItemViewHolder(createProfileView(parent), false, viewType); case VIEW_TYPE_AZ_LABEL: return new ItemViewHolder(createAzLabelView(parent), false, viewType); case VIEW_TYPE_NORMAL: return new ItemViewHolder( mChooserListAdapter.createView(parent), true, viewType); case VIEW_TYPE_DIRECT_SHARE: case VIEW_TYPE_CALLER_AND_RANK: return createItemGroupViewHolder(viewType, parent); case VIEW_TYPE_FOOTER: Space sp = new Space(parent.getContext()); sp.setLayoutParams(new RecyclerView.LayoutParams( LayoutParams.MATCH_PARENT, mFooterHeight)); return new FooterViewHolder(sp, viewType); default: // Since we catch all possible viewTypes above, no chance this is being called. return null; } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { int viewType = ((ViewHolderBase) holder).getViewType(); switch (viewType) { case VIEW_TYPE_DIRECT_SHARE: case VIEW_TYPE_CALLER_AND_RANK: bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder); break; case VIEW_TYPE_NORMAL: bindItemViewHolder(position, (ItemViewHolder) holder); break; default: } } @Override public int getItemViewType(int position) { int count; int countSum = (count = getSystemRowCount()); if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW; countSum += (count = getProfileRowCount()); if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE; countSum += (count = getServiceTargetRowCount()); if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE; countSum += (count = getCallerAndRankedTargetRowCount()); if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK; countSum += (count = getAzLabelRowCount()); if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL; if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER; return VIEW_TYPE_NORMAL; } public int getTargetType(int position) { return mChooserListAdapter.getPositionTargetType(getListPosition(position)); } private View createProfileView(ViewGroup parent) { View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false); mProfileView = profileRow.findViewById(R.id.profile_button); mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); updateProfileViewButton(); return profileRow; } private View createAzLabelView(ViewGroup parent) { return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false); } private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) { final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth, MeasureSpec.EXACTLY); int columnCount = holder.getColumnCount(); final boolean isDirectShare = holder instanceof DirectShareViewHolder; for (int i = 0; i < columnCount; i++) { final View v = mChooserListAdapter.createView(holder.getRowByIndex(i)); final int column = i; v.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startSelected(holder.getItemIndex(column), false, true); } }); holder.addView(i, v); // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll = // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be // done before measuring. if (isDirectShare) { final ViewHolder vh = (ViewHolder) v.getTag(); vh.text.setLines(2); vh.text.setHorizontallyScrolling(false); vh.text2.setVisibility(View.GONE); } // Force height to be a given so we don't have visual disruption during scaling. v.measure(exactSpec, spec); setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight()); } final ViewGroup viewGroup = holder.getViewGroup(); // Pre-measure and fix height so we can scale later. holder.measure(); setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight()); if (isDirectShare) { DirectShareViewHolder dsvh = (DirectShareViewHolder) holder; setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight()); setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight()); } viewGroup.setTag(holder); return holder; } private void setViewBounds(View view, int widthPx, int heightPx) { LayoutParams lp = view.getLayoutParams(); if (lp == null) { lp = new LayoutParams(widthPx, heightPx); view.setLayoutParams(lp); } else { lp.height = heightPx; lp.width = widthPx; } } ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) { if (viewType == VIEW_TYPE_DIRECT_SHARE) { ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate( R.layout.chooser_row_direct_share, parent, false); ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parentGroup, false); ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parentGroup, false); parentGroup.addView(row1); parentGroup.addView(row2); mDirectShareViewHolder = new DirectShareViewHolder(parentGroup, Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType, mChooserMultiProfilePagerAdapter::getActiveListAdapter); loadViewsIntoGroup(mDirectShareViewHolder); return mDirectShareViewHolder; } else { ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parent, false); ItemGroupViewHolder holder = new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType); loadViewsIntoGroup(holder); return holder; } } /** * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from * showing on top of the AZ list if the AZ label is visible. All other types are placed into * their own row as determined by their target type, and dividers are added in the list to * separate each type. */ int getRowType(int rowPosition) { // Merge caller and ranked standard into a single row int positionType = mChooserListAdapter.getPositionTargetType(rowPosition); if (positionType == ChooserListAdapter.TARGET_CALLER) { return ChooserListAdapter.TARGET_STANDARD; } // If an the A-Z label is shown, prevent a separator from appearing by making the A-Z // row type the same as the suggestion row type if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) { return ChooserListAdapter.TARGET_STANDARD; } return positionType; } void bindItemViewHolder(int position, ItemViewHolder holder) { View v = holder.itemView; int listPosition = getListPosition(position); holder.mListPosition = listPosition; mChooserListAdapter.bindView(listPosition, v); } void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) { final ViewGroup viewGroup = (ViewGroup) holder.itemView; int start = getListPosition(position); int startType = getRowType(start); int columnCount = holder.getColumnCount(); int end = start + columnCount - 1; while (getRowType(end) != startType && end >= start) { end--; } if (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) { final TextView textView = viewGroup.findViewById(R.id.chooser_row_text_option); if (textView.getVisibility() != View.VISIBLE) { textView.setAlpha(0.0f); textView.setVisibility(View.VISIBLE); textView.setText(R.string.chooser_no_direct_share_targets); ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f); fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f)); float translationInPx = getResources().getDimensionPixelSize( R.dimen.chooser_row_text_option_translate); textView.setTranslationY(translationInPx); ValueAnimator translateAnim = ObjectAnimator.ofFloat(textView, "translationY", 0.0f); translateAnim.setInterpolator(new DecelerateInterpolator(1.0f)); AnimatorSet animSet = new AnimatorSet(); animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS); animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS); animSet.playTogether(fadeAnim, translateAnim); animSet.start(); } } for (int i = 0; i < columnCount; i++) { final View v = holder.getView(i); if (start + i <= end) { holder.setViewVisibility(i, View.VISIBLE); holder.setItemIndex(i, start + i); mChooserListAdapter.bindView(holder.getItemIndex(i), v); } else { holder.setViewVisibility(i, View.INVISIBLE); } } } int getListPosition(int position) { position -= getSystemRowCount() + getProfileRowCount(); final int serviceCount = mChooserListAdapter.getServiceTargetCount(); final int serviceRows = (int) Math.ceil((float) serviceCount / getMaxRankedTargets()); if (position < serviceRows) { return position * mMaxTargetsPerRow; } position -= serviceRows; final int callerAndRankedCount = mChooserListAdapter.getCallerTargetCount() + mChooserListAdapter.getRankedTargetCount(); final int callerAndRankedRows = getCallerAndRankedTargetRowCount(); if (position < callerAndRankedRows) { return serviceCount + position * mMaxTargetsPerRow; } position -= getAzLabelRowCount() + callerAndRankedRows; return callerAndRankedCount + serviceCount + position; } public void handleScroll(View v, int y, int oldy) { boolean canExpandDirectShare = canExpandDirectShare(); if (mDirectShareViewHolder != null && canExpandDirectShare) { mDirectShareViewHolder.handleScroll( mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy, mMaxTargetsPerRow); } } /** * Only expand direct share area if there is a minimum number of targets. */ private boolean canExpandDirectShare() { // Do not enable until we have confirmed more apps are using sharing shortcuts // Check git history for enablement logic return false; } public ChooserListAdapter getListAdapter() { return mChooserListAdapter; } boolean shouldCellSpan(int position) { return getItemViewType(position) == VIEW_TYPE_NORMAL; } void updateDirectShareExpansion() { if (mDirectShareViewHolder == null || !canExpandDirectShare()) { return; } RecyclerView activeAdapterView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); if (mResolverDrawerLayout.isCollapsed()) { mDirectShareViewHolder.collapse(activeAdapterView); } else { mDirectShareViewHolder.expand(activeAdapterView); } } } /** * Used to bind types for group of items including: * {@link ChooserGridAdapter#VIEW_TYPE_DIRECT_SHARE}, * and {@link ChooserGridAdapter#VIEW_TYPE_CALLER_AND_RANK}. */ abstract static class ItemGroupViewHolder extends ViewHolderBase { protected int mMeasuredRowHeight; private int[] mItemIndices; protected final View[] mCells; private final int mColumnCount; ItemGroupViewHolder(int cellCount, View itemView, int viewType) { super(itemView, viewType); this.mCells = new View[cellCount]; this.mItemIndices = new int[cellCount]; this.mColumnCount = cellCount; } abstract ViewGroup addView(int index, View v); abstract ViewGroup getViewGroup(); abstract ViewGroup getRowByIndex(int index); abstract ViewGroup getRow(int rowNumber); abstract void setViewVisibility(int i, int visibility); public int getColumnCount() { return mColumnCount; } public void measure() { final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); getViewGroup().measure(spec, spec); mMeasuredRowHeight = getViewGroup().getMeasuredHeight(); } public int getMeasuredRowHeight() { return mMeasuredRowHeight; } public void setItemIndex(int itemIndex, int listIndex) { mItemIndices[itemIndex] = listIndex; } public int getItemIndex(int itemIndex) { return mItemIndices[itemIndex]; } public View getView(int index) { return mCells[index]; } } static class SingleRowViewHolder extends ItemGroupViewHolder { private final ViewGroup mRow; SingleRowViewHolder(ViewGroup row, int cellCount, int viewType) { super(cellCount, row, viewType); this.mRow = row; } public ViewGroup getViewGroup() { return mRow; } public ViewGroup getRowByIndex(int index) { return mRow; } public ViewGroup getRow(int rowNumber) { if (rowNumber == 0) return mRow; return null; } public ViewGroup addView(int index, View v) { mRow.addView(v); mCells[index] = v; return mRow; } public void setViewVisibility(int i, int visibility) { getView(i).setVisibility(visibility); } } static class DirectShareViewHolder extends ItemGroupViewHolder { private final ViewGroup mParent; private final List mRows; private int mCellCountPerRow; private boolean mHideDirectShareExpansion = false; private int mDirectShareMinHeight = 0; private int mDirectShareCurrHeight = 0; private int mDirectShareMaxHeight = 0; private final boolean[] mCellVisibility; private final Supplier mListAdapterSupplier; DirectShareViewHolder(ViewGroup parent, List rows, int cellCountPerRow, int viewType, Supplier listAdapterSupplier) { super(rows.size() * cellCountPerRow, parent, viewType); this.mParent = parent; this.mRows = rows; this.mCellCountPerRow = cellCountPerRow; this.mCellVisibility = new boolean[rows.size() * cellCountPerRow]; Arrays.fill(mCellVisibility, true); this.mListAdapterSupplier = listAdapterSupplier; } public ViewGroup addView(int index, View v) { ViewGroup row = getRowByIndex(index); row.addView(v); mCells[index] = v; return row; } public ViewGroup getViewGroup() { return mParent; } public ViewGroup getRowByIndex(int index) { return mRows.get(index / mCellCountPerRow); } public ViewGroup getRow(int rowNumber) { return mRows.get(rowNumber); } public void measure() { final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); getRow(0).measure(spec, spec); getRow(1).measure(spec, spec); mDirectShareMinHeight = getRow(0).getMeasuredHeight(); mDirectShareCurrHeight = mDirectShareCurrHeight > 0 ? mDirectShareCurrHeight : mDirectShareMinHeight; mDirectShareMaxHeight = 2 * mDirectShareMinHeight; } public int getMeasuredRowHeight() { return mDirectShareCurrHeight; } public int getMinRowHeight() { return mDirectShareMinHeight; } public void setViewVisibility(int i, int visibility) { final View v = getView(i); if (visibility == View.VISIBLE) { mCellVisibility[i] = true; v.setVisibility(visibility); v.setAlpha(1.0f); } else if (visibility == View.INVISIBLE && mCellVisibility[i]) { mCellVisibility[i] = false; ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f); fadeAnim.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS); fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f)); fadeAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { v.setVisibility(View.INVISIBLE); } }); fadeAnim.start(); } } public void handleScroll(RecyclerView view, int y, int oldy, int maxTargetsPerRow) { // only exit early if fully collapsed, otherwise onListRebuilt() with shifting // targets can lock us into an expanded mode boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight; if (notExpanded) { if (mHideDirectShareExpansion) { return; } // only expand if we have more than maxTargetsPerRow, and delay that decision // until they start to scroll ChooserListAdapter adapter = mListAdapterSupplier.get(); int validTargets = adapter.getSelectableServiceTargetCount(); if (validTargets <= maxTargetsPerRow) { mHideDirectShareExpansion = true; return; } } int yDiff = (int) ((oldy - y) * DIRECT_SHARE_EXPANSION_RATE); int prevHeight = mDirectShareCurrHeight; int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight); newHeight = Math.max(newHeight, mDirectShareMinHeight); yDiff = newHeight - prevHeight; updateDirectShareRowHeight(view, yDiff, newHeight); } void expand(RecyclerView view) { updateDirectShareRowHeight(view, mDirectShareMaxHeight - mDirectShareCurrHeight, mDirectShareMaxHeight); } void collapse(RecyclerView view) { updateDirectShareRowHeight(view, mDirectShareMinHeight - mDirectShareCurrHeight, mDirectShareMinHeight); } private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) { if (view == null || view.getChildCount() == 0 || yDiff == 0) { return; } // locate the item to expand, and offset the rows below that one boolean foundExpansion = false; for (int i = 0; i < view.getChildCount(); i++) { View child = view.getChildAt(i); if (foundExpansion) { child.offsetTopAndBottom(yDiff); } else { if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) { int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(), MeasureSpec.EXACTLY); int heightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY); child.measure(widthSpec, heightSpec); child.getLayoutParams().height = child.getMeasuredHeight(); child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getTop() + child.getMeasuredHeight()); foundExpansion = true; } } } if (foundExpansion) { mDirectShareCurrHeight = newHeight; } } } /** * Shortcuts grouped by application. */ @VisibleForTesting public static class ServiceResultInfo { public final DisplayResolveInfo originalTarget; public final List resultTargets; public final UserHandle userHandle; public ServiceResultInfo(DisplayResolveInfo ot, List rt, UserHandle userHandle) { originalTarget = ot; resultTargets = rt; this.userHandle = userHandle; } } static class ChooserTargetRankingInfo { public final List scores; public final UserHandle userHandle; ChooserTargetRankingInfo(List chooserTargetScores, UserHandle userHandle) { this.scores = chooserTargetScores; this.userHandle = userHandle; } } static class RefinementResultReceiver extends ResultReceiver { private ChooserActivity mChooserActivity; private TargetInfo mSelectedTarget; public RefinementResultReceiver(ChooserActivity host, TargetInfo target, Handler handler) { super(handler); mChooserActivity = host; mSelectedTarget = target; } @Override protected void onReceiveResult(int resultCode, Bundle resultData) { if (mChooserActivity == null) { Log.e(TAG, "Destroyed RefinementResultReceiver received a result"); return; } if (resultData == null) { Log.e(TAG, "RefinementResultReceiver received null resultData"); return; } switch (resultCode) { case RESULT_CANCELED: mChooserActivity.onRefinementCanceled(); break; case RESULT_OK: Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT); if (intentParcelable instanceof Intent) { mChooserActivity.onRefinementResult(mSelectedTarget, (Intent) intentParcelable); } else { Log.e(TAG, "RefinementResultReceiver received RESULT_OK but no Intent" + " in resultData with key Intent.EXTRA_INTENT"); } break; default: Log.w(TAG, "Unknown result code " + resultCode + " sent to RefinementResultReceiver"); break; } } public void destroy() { mChooserActivity = null; mSelectedTarget = null; } } /** * Used internally to round image corners while obeying view padding. */ public static class RoundedRectImageView extends ImageView { private int mRadius = 0; private Path mPath = new Path(); private Paint mOverlayPaint = new Paint(0); private Paint mRoundRectPaint = new Paint(0); private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private String mExtraImageCount = null; public RoundedRectImageView(Context context) { super(context); } public RoundedRectImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius); mOverlayPaint.setColor(0x99000000); mOverlayPaint.setStyle(Paint.Style.FILL); mRoundRectPaint.setColor(context.getResources().getColor(R.color.chooser_row_divider)); mRoundRectPaint.setStyle(Paint.Style.STROKE); mRoundRectPaint.setStrokeWidth(context.getResources() .getDimensionPixelSize(R.dimen.chooser_preview_image_border)); mTextPaint.setColor(Color.WHITE); mTextPaint.setTextSize(context.getResources() .getDimensionPixelSize(R.dimen.chooser_preview_image_font_size)); mTextPaint.setTextAlign(Paint.Align.CENTER); } private void updatePath(int width, int height) { mPath.reset(); int imageWidth = width - getPaddingRight() - getPaddingLeft(); int imageHeight = height - getPaddingBottom() - getPaddingTop(); mPath.addRoundRect(getPaddingLeft(), getPaddingTop(), imageWidth, imageHeight, mRadius, mRadius, Path.Direction.CW); } /** * Sets the corner radius on all corners * * param radius 0 for no radius, > 0 for a visible corner radius */ public void setRadius(int radius) { mRadius = radius; updatePath(getWidth(), getHeight()); } /** * Display an overlay with extra image count on 3rd image */ public void setExtraImageCount(int count) { if (count > 0) { this.mExtraImageCount = "+" + count; } else { this.mExtraImageCount = null; } } @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldWidth, oldHeight); updatePath(width, height); } @Override protected void onDraw(Canvas canvas) { if (mRadius != 0) { canvas.clipPath(mPath); } super.onDraw(canvas); int x = getPaddingLeft(); int y = getPaddingRight(); int width = getWidth() - getPaddingRight() - getPaddingLeft(); int height = getHeight() - getPaddingBottom() - getPaddingTop(); if (mExtraImageCount != null) { canvas.drawRect(x, y, width, height, mOverlayPaint); int xPos = canvas.getWidth() / 2; int yPos = (int) ((canvas.getHeight() / 2.0f) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f)); canvas.drawText(mExtraImageCount, xPos, yPos, mTextPaint); } canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint); } } /** * A helper class to track app's readiness for the scene transition animation. * The app is ready when both the image is laid out and the drawer offset is calculated. */ private class EnterTransitionAnimationDelegate implements View.OnLayoutChangeListener { private boolean mPreviewReady = false; private boolean mOffsetCalculated = false; void postponeTransition() { postponeEnterTransition(); } void markImagePreviewReady() { if (!mPreviewReady) { mPreviewReady = true; maybeStartListenForLayout(); } } void markOffsetCalculated() { if (!mOffsetCalculated) { mOffsetCalculated = true; maybeStartListenForLayout(); } } private void maybeStartListenForLayout() { if (mPreviewReady && mOffsetCalculated && mResolverDrawerLayout != null) { if (mResolverDrawerLayout.isInLayout()) { startPostponedEnterTransition(); } else { mResolverDrawerLayout.addOnLayoutChangeListener(this); mResolverDrawerLayout.requestLayout(); } } } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { v.removeOnLayoutChangeListener(this); startPostponedEnterTransition(); } } /** * 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().logShareheetProfileChanged(); } private boolean shouldNearbyShareBeFirstInRankedRow() { return ActivityManager.isLowRamDeviceStatic() && mIsNearbyShareFirstTargetInRankedApp; } private boolean shouldNearbyShareBeIncludedAsActionButton() { return !shouldNearbyShareBeFirstInRankedRow(); } }