diff options
Diffstat (limited to 'java')
5 files changed, 589 insertions, 249 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 776d34a9..89a9833f 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -133,7 +133,6 @@ 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; @@ -154,13 +153,9 @@ import java.util.function.Supplier; * */ public class ChooserActivity extends ResolverActivity implements - ChooserListAdapter.ChooserListCommunicator { + ResolverListAdapter.ResolverListCommunicator { private static final String TAG = "ChooserActivity"; - private boolean mShouldDisplayLandscape; - - 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 @@ -169,7 +164,6 @@ public class ChooserActivity extends ResolverActivity implements 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. @@ -216,9 +210,6 @@ public class ChooserActivity extends ResolverActivity implements 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, @@ -246,14 +237,26 @@ public class ChooserActivity extends ResolverActivity implements | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; - private Bundle mReplacementExtras; - private IntentSender mChosenComponentSender; - private IntentSender mRefinementIntentSender; - private RefinementResultReceiver mRefinementResultReceiver; - private ChooserTarget[] mCallerChooserTargets; - private ArrayList<ComponentName> mFilteredComponentNames; + /* TODO: this is `nullable` *primarily* because we have to defer the assignment til onCreate(). + * We make the only assignment there, and *expect* it to be ready by the time we ever use it -- + * someday if we move all the usage to a component with a narrower lifecycle (something that + * matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we + * should be able to make this assignment as "final." Unfortunately, for now we also have + * a vestigial design where ChooserActivity.onCreate() can invalidate a request, but it still + * has to call up to ResolverActivity.onCreate() before closing, and the base method delegates + * back down to other methods in ChooserActivity that aren't really relevant if we're closing + * (and so they'd normally want to assume it was a valid "creation," with non-null parameters). + * Any client null checks are workarounds for this condition that can be removed once that + * design is cleaned up. */ + @Nullable + private ChooserRequestParameters mChooserRequest; - private Intent mReferrerFillInIntent; + private boolean mShouldDisplayLandscape; + // statsd logger wrapper + protected ChooserActivityLogger mChooserActivityLogger; + + @Nullable + private RefinementResultReceiver mRefinementResultReceiver; private long mChooserShownTime; protected boolean mIsSuccessfullySelected; @@ -265,6 +268,7 @@ public class ChooserActivity extends ResolverActivity implements private static final int MAX_LOG_RANK_POSITION = 12; + // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters. private static final int MAX_EXTRA_INITIAL_INTENTS = 2; private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; @@ -291,6 +295,8 @@ public class ChooserActivity extends ResolverActivity implements private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>(); + public ChooserActivity() {} + private void setupPreDrawForSharedElementTransition(View v) { v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override @@ -319,134 +325,42 @@ public class ChooserActivity extends ResolverActivity implements getChooserActivityLogger().logSharesheetTriggered(); - 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); + try { + mChooserRequest = new ChooserRequestParameters( + getIntent(), getReferrer(), getNearbySharingComponent()); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Caller provided invalid Chooser request parameters", e); finish(); super.onCreate(null); return; } - final Intent target = (Intent) targetParcelable; - modifyTargetIntent(target); - Parcelable[] targetsParcelable - = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS); - if (targetsParcelable != null) { - Intent[] additionalTargets = new Intent[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]; - additionalTargets[i] = additionalTarget; - modifyTargetIntent(additionalTarget); - } - setAdditionalTargets(additionalTargets); - } - mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS); + setAdditionalTargets(mChooserRequest.getAdditionalTargets()); - // Do not allow the title to be changed when sharing content - CharSequence title = 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); - mRefinementIntentSender = intent.getParcelableExtra( - Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); setSafeForwardingMode(true); mPinnedSharedPrefs = getPinnedSharedPrefs(this); - mFilteredComponentNames = new ArrayList<>(); - try { - ComponentName[] exclodedComponents = intent.getParcelableArrayExtra( - Intent.EXTRA_EXCLUDE_COMPONENTS, - ComponentName.class); - if (exclodedComponents != null) { - Collections.addAll(mFilteredComponentNames, exclodedComponents); - } - } catch (ClassCastException e) { - Log.e(TAG, "Excluded components must be of type ComponentName[]", e); - } - - // Exclude Nearby from main list if chip is present, to avoid duplication - ComponentName nearby = getNearbySharingComponent(); - if (nearby != null) { - mFilteredComponentNames.add(nearby); - } - - 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)); - IntentFilter targetIntentFilter = getTargetIntentFilter(target); + setRetainInOnStop(mChooserRequest.shouldRetainInOnStop()); + createProfileRecords( new AppPredictorFactory( getApplicationContext(), - target.getStringExtra(Intent.EXTRA_TEXT), - targetIntentFilter), - targetIntentFilter); + mChooserRequest.getSharedText(), + mChooserRequest.getTargetIntentFilter()), + mChooserRequest.getTargetIntentFilter()); + + super.onCreate( + savedInstanceState, + mChooserRequest.getTargetIntent(), + mChooserRequest.getTitle(), + mChooserRequest.getDefaultTitleResource(), + mChooserRequest.getInitialIntents(), + /* rList: List<ResolveInfo> = */ null, + /* supportsAlwaysUseOption = */ false); mPreviewCoordinator = new ChooserContentPreviewCoordinator( mBackgroundThreadPoolExecutor, @@ -454,23 +368,21 @@ public class ChooserActivity extends ResolverActivity implements this::hideContentPreview, this::setupPreDrawForSharedElementTransition); - 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_SHARESHEET_MIMETYPE, mChooserRequest.getTargetType()) .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)) { + if (mChooserRequest.isSendActionTarget()) { mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll); } @@ -499,13 +411,14 @@ public class ChooserActivity extends ResolverActivity implements getChooserActivityLogger().logShareStarted( FrameworkStatsLog.SHARESHEET_STARTED, getReferrerPackageName(), - target.getType(), - mCallerChooserTargets == null ? 0 : mCallerChooserTargets.length, - initialIntents == null ? 0 : initialIntents.length, + mChooserRequest.getTargetType(), + mChooserRequest.getCallerChooserTargets().size(), + (mChooserRequest.getInitialIntents() == null) + ? 0 : mChooserRequest.getInitialIntents().length, isWorkProfile(), ChooserContentPreviewUi.findPreferredContentPreview( getTargetIntent(), getContentResolver(), this::isImageType), - target.getAction() + mChooserRequest.getTargetAction() ); setEnterSharedElementCallback(new SharedElementCallback() { @@ -607,7 +520,7 @@ public class ChooserActivity extends ResolverActivity implements @Override protected EmptyStateProvider createBlockerEmptyStateProvider() { - final boolean isSendAction = isSendAction(getTargetIntent()); + final boolean isSendAction = mChooserRequest.isSendActionTarget(); final EmptyState noWorkToPersonalEmptyState = new DevicePolicyBlockerEmptyState( @@ -1194,9 +1107,14 @@ public class ChooserActivity extends ResolverActivity implements @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { + if (mChooserRequest == null) { + return defIntent; + } + Intent result = defIntent; - if (mReplacementExtras != null) { - final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName); + if (mChooserRequest.getReplacementExtras() != null) { + final Bundle replExtras = + mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName); if (replExtras != null) { result = new Intent(defIntent); result.putExtras(replExtras); @@ -1217,12 +1135,13 @@ public class ChooserActivity extends ResolverActivity implements @Override public void onActivityStarted(TargetInfo cti) { - if (mChosenComponentSender != null) { + if (mChooserRequest.getChosenComponentSender() != 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); + mChooserRequest.getChosenComponentSender().sendIntent( + this, Activity.RESULT_OK, fillIn, null, null); } catch (IntentSender.SendIntentException e) { Slog.e(TAG, "Unable to launch supplied IntentSender to report " + "the chosen component: " + e); @@ -1233,10 +1152,14 @@ public class ChooserActivity extends ResolverActivity implements @Override public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) { - if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) { + if (mChooserRequest == null) { + return; + } + + if (mChooserRequest.getCallerChooserTargets().size() > 0) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( /* origTarget */ null, - Lists.newArrayList(mCallerChooserTargets), + mChooserRequest.getCallerChooserTargets(), TARGET_TYPE_DEFAULT, /* directShareShortcutInfoCache */ Collections.emptyMap(), /* directShareAppTargetCache */ Collections.emptyMap()); @@ -1277,8 +1200,8 @@ public class ChooserActivity extends ResolverActivity implements // TODO: implement these type-conditioned behaviors polymorphically, and consider moving // the logic into `ChooserTargetActionsDialogFragment.show()`. boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned(); - IntentFilter intentFilter = - targetInfo.isSelectableTargetInfo() ? getTargetIntentFilter() : null; + IntentFilter intentFilter = targetInfo.isSelectableTargetInfo() + ? mChooserRequest.getTargetIntentFilter() : null; String shortcutTitle = targetInfo.isSelectableTargetInfo() ? targetInfo.getDisplayLabel().toString() : null; String shortcutIdKey = targetInfo.getDirectShareShortcutId(); @@ -1293,16 +1216,9 @@ public class ChooserActivity extends ResolverActivity implements intentFilter); } - 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) { + if (mChooserRequest.getRefinementIntentSender() != null) { final Intent fillIn = new Intent(); final List<Intent> sourceIntents = target.getAllSourceIntents(); if (!sourceIntents.isEmpty()) { @@ -1321,7 +1237,8 @@ public class ChooserActivity extends ResolverActivity implements fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, mRefinementResultReceiver); try { - mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null); + mChooserRequest.getRefinementIntentSender().sendIntent( + this, 0, fillIn, null, null); return false; } catch (SendIntentException e) { Log.e(TAG, "Refinement IntentSender failed to send", e); @@ -1372,9 +1289,7 @@ public class ChooserActivity extends ResolverActivity implements directTargetHashed = targetInfo.getHashedTargetIdForMetrics(this); directTargetAlsoRanked = getRankedPosition(targetInfo); - if (mCallerChooserTargets != null) { - numCallerProvided = mCallerChooserTargets.length; - } + numCallerProvided = mChooserRequest.getCallerChooserTargets().size(); getChooserActivityLogger().logShareTargetSelected( SELECTION_TYPE_SERVICE, targetInfo.getResolveInfo().activityInfo.processName, @@ -1686,7 +1601,7 @@ public class ChooserActivity extends ResolverActivity implements @Override boolean isComponentFiltered(ComponentName name) { - return mFilteredComponentNames != null && mFilteredComponentNames.contains(name); + return mChooserRequest.getFilteredComponentNames().contains(name); } @Override @@ -1696,19 +1611,35 @@ public class ChooserActivity extends ResolverActivity implements } @VisibleForTesting - public ChooserGridAdapter createChooserGridAdapter(Context context, - List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed, UserHandle userHandle) { - ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents, - initialIntents, rList, filterLastUsed, - createListController(userHandle)); + public ChooserGridAdapter createChooserGridAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + UserHandle userHandle) { + ChooserListAdapter chooserListAdapter = createChooserListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + createListController(userHandle), + mChooserRequest, + mMaxTargetsPerRow); return new ChooserGridAdapter(chooserListAdapter); } @VisibleForTesting - public ChooserListAdapter createChooserListAdapter(Context context, - List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed, ResolverListController resolverListController) { + public ChooserListAdapter createChooserListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + ResolverListController resolverListController, + ChooserRequestParameters chooserRequest, + int maxTargetsPerRow) { return new ChooserListAdapter( context, payloadIntents, @@ -1718,7 +1649,9 @@ public class ChooserActivity extends ResolverActivity implements resolverListController, this, context.getPackageManager(), - getChooserActivityLogger()); + getChooserActivityLogger(), + chooserRequest, + maxTargetsPerRow); } @VisibleForTesting @@ -1949,16 +1882,6 @@ public class ChooserActivity extends ResolverActivity implements super.onHandlePackagesChanged(listAdapter); } - @Override // SelectableTargetInfoCommunicator - public Intent getReferrerFillInIntent() { - return mReferrerFillInIntent; - } - - @Override // ChooserListCommunicator - public int getMaxRankedTargets() { - return mMaxTargetsPerRow; - } - @Override public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { setupScrollListener(); @@ -2093,24 +2016,6 @@ public class ChooserActivity extends ResolverActivity implements }); } - @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, @@ -2132,7 +2037,7 @@ public class ChooserActivity extends ResolverActivity implements * @return true if we want to show the content preview area */ protected boolean shouldShowContentPreview() { - return isSendAction(getTargetIntent()); + return (mChooserRequest != null) && mChooserRequest.isSendActionTarget(); } private void updateStickyContentPreview() { @@ -2762,7 +2667,7 @@ public class ChooserActivity extends ResolverActivity implements position -= getSystemRowCount() + getProfileRowCount(); final int serviceCount = mChooserListAdapter.getServiceTargetCount(); - final int serviceRows = (int) Math.ceil((float) serviceCount / getMaxRankedTargets()); + final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow); if (position < serviceRows) { return position * mMaxTargetsPerRow; } diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index e31bf2ab..b18d2718 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -83,7 +83,9 @@ public class ChooserListAdapter extends ResolverListAdapter { /** {@link #getBaseScore} */ public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; - private final ChooserListCommunicator mChooserListCommunicator; + private final ChooserRequestParameters mChooserRequest; + private final int mMaxRankedTargets; + private final ChooserActivityLogger mChooserActivityLogger; private final Map<TargetInfo, AsyncTask> mIconLoaders = new HashMap<>(); @@ -136,15 +138,19 @@ public class ChooserListAdapter extends ResolverListAdapter { List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, - ChooserListCommunicator chooserListCommunicator, + ResolverListCommunicator resolverListCommunicator, PackageManager packageManager, - ChooserActivityLogger chooserActivityLogger) { + ChooserActivityLogger chooserActivityLogger, + ChooserRequestParameters chooserRequest, + int maxRankedTargets) { // Don't send the initial intents through the shared ResolverActivity path, // we want to separate them into a different section. super(context, payloadIntents, null, rList, filterLastUsed, - resolverListController, chooserListCommunicator, false); + resolverListController, resolverListCommunicator, false); + + mChooserRequest = chooserRequest; + mMaxRankedTargets = maxRankedTargets; - mChooserListCommunicator = chooserListCommunicator; mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context); createPlaceHolders(); mChooserActivityLogger = chooserActivityLogger; @@ -221,13 +227,13 @@ public class ChooserListAdapter extends ResolverListAdapter { Log.d(TAG, "clearing queryTargets on package change"); } createPlaceHolders(); - mChooserListCommunicator.onHandlePackagesChanged(this); + mResolverListCommunicator.onHandlePackagesChanged(this); } private void createPlaceHolders() { mServiceTargets.clear(); - for (int i = 0; i < mChooserListCommunicator.getMaxRankedTargets(); i++) { + for (int i = 0; i < mMaxRankedTargets; ++i) { mServiceTargets.add(mPlaceHolderTargetInfo); } } @@ -367,8 +373,9 @@ public class ChooserListAdapter extends ResolverListAdapter { @Override public int getUnfilteredCount() { int appTargets = super.getUnfilteredCount(); - if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) { - appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets(); + if (appTargets > mMaxRankedTargets) { + // TODO: what does this condition mean? + appTargets = appTargets + mMaxRankedTargets; } return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount(); } @@ -392,9 +399,8 @@ public class ChooserListAdapter extends ResolverListAdapter { } public int getServiceTargetCount() { - if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent()) - && !ActivityManager.isLowRamDeviceStatic()) { - return Math.min(mServiceTargets.size(), mChooserListCommunicator.getMaxRankedTargets()); + if (mChooserRequest.isSendActionTarget() && !ActivityManager.isLowRamDeviceStatic()) { + return Math.min(mServiceTargets.size(), mMaxRankedTargets); } return 0; @@ -403,15 +409,14 @@ public class ChooserListAdapter extends ResolverListAdapter { int getAlphaTargetCount() { int groupedCount = mSortedList.size(); int ungroupedCount = mCallerTargets.size() + mDisplayList.size(); - return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0; + return (ungroupedCount > mMaxRankedTargets) ? groupedCount : 0; } /** * Fetch ranked app target count */ public int getRankedTargetCount() { - int spacesAvailable = - mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount(); + int spacesAvailable = mMaxRankedTargets - getCallerTargetCount(); return Math.min(spacesAvailable, super.getCount()); } @@ -508,8 +513,8 @@ public class ChooserListAdapter extends ResolverListAdapter { protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) { // Checks if this info is already listed in callerTargets. for (TargetInfo existingInfo : mCallerTargets) { - if (mResolverListCommunicator - .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) { + if (mResolverListCommunicator.resolveInfoMatch( + dri.getResolveInfo(), existingInfo.getResolveInfo())) { return false; } } @@ -520,9 +525,8 @@ public class ChooserListAdapter extends ResolverListAdapter { * Fetch surfaced direct share target info */ public List<TargetInfo> getSurfacedTargetInfo() { - int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets(); return mServiceTargets.subList(0, - Math.min(maxSurfacedTargets, getSelectableServiceTargetCount())); + Math.min(mMaxRankedTargets, getSelectableServiceTargetCount())); } @@ -550,9 +554,9 @@ public class ChooserListAdapter extends ResolverListAdapter { directShareToShortcutInfos, directShareToAppTargets, mContext.createContextAsUser(getUserHandle(), 0), - mChooserListCommunicator.getTargetIntent(), - mChooserListCommunicator.getReferrerFillInIntent(), - mChooserListCommunicator.getMaxRankedTargets(), + mChooserRequest.getTargetIntent(), + mChooserRequest.getReferrerFillInIntent(), + mMaxRankedTargets, mServiceTargets); if (isUpdated) { notifyDataSetChanged(); @@ -616,8 +620,7 @@ public class ChooserListAdapter extends ResolverListAdapter { protected List<ResolvedComponentInfo> doInBackground( List<ResolvedComponentInfo>... params) { Trace.beginSection("ChooserListAdapter#SortingTask"); - mResolverListController.topK(params[0], - mChooserListCommunicator.getMaxRankedTargets()); + mResolverListController.topK(params[0], mMaxRankedTargets); Trace.endSection(); return params[0]; } @@ -625,7 +628,7 @@ public class ChooserListAdapter extends ResolverListAdapter { protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { processSortedList(sortedComponents, doPostProcessing); if (doPostProcessing) { - mChooserListCommunicator.updateProfileViewButton(); + mResolverListCommunicator.updateProfileViewButton(); notifyDataSetChanged(); } } @@ -633,21 +636,6 @@ public class ChooserListAdapter extends ResolverListAdapter { } /** - * Necessary methods to communicate between {@link ChooserListAdapter} - * and {@link ChooserActivity}. - */ - interface ChooserListCommunicator extends ResolverListCommunicator { - - int getMaxRankedTargets(); - - boolean isSendAction(Intent targetIntent); - - Intent getTargetIntent(); - - Intent getReferrerFillInIntent(); - } - - /** * Loads direct share targets icons. */ @VisibleForTesting diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java new file mode 100644 index 00000000..81481bf1 --- /dev/null +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.PatternMatcher; +import android.service.chooser.ChooserTarget; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import com.google.common.collect.ImmutableList; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utility to parse and validate parameters from the client-supplied {@link Intent} that launched + * the Sharesheet {@link ChooserActivity}. The validated parameters are stored as immutable ivars. + * + * TODO: field nullability in this class reflects legacy use, and typically would indicate that the + * client's intent didn't provide the respective data. In some cases we may be able to provide + * defaults instead of nulls -- especially for methods that return nullable lists or arrays, if the + * client code could instead handle empty collections equally well. + * + * TODO: some of these fields (especially getTargetIntent() and any other getters that delegate to + * it internally) differ from the legacy model because they're computed directly from the initial + * Chooser intent, where in the past they've been relayed up to ResolverActivity and then retrieved + * through methods on the base class. The base always seems to return them exactly as they were + * provided, so this should be safe -- and clients can reasonably switch to retrieving through these + * parameters instead. For now, the other convention is still used in some places. Ideally we'd like + * to normalize on a single source of truth, but we'll have to clean up the delegation up to the + * resolver (or perhaps this needs to be a subclass of some `ResolverRequestParameters` class?). + */ +public class ChooserRequestParameters { + private static final String TAG = "ChooserActivity"; + + private static final int LAUNCH_FLAGS_FOR_SEND_ACTION = + Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; + + private final Intent mTarget; + private final Pair<CharSequence, Integer> mTitleSpec; + private final Intent mReferrerFillInIntent; + private final ImmutableList<ComponentName> mFilteredComponentNames; + private final ImmutableList<ChooserTarget> mCallerChooserTargets; + private final boolean mRetainInOnStop; + + @Nullable + private final ImmutableList<Intent> mAdditionalTargets; + + @Nullable + private final Bundle mReplacementExtras; + + @Nullable + private final ImmutableList<Intent> mInitialIntents; + + @Nullable + private final IntentSender mChosenComponentSender; + + @Nullable + private final IntentSender mRefinementIntentSender; + + @Nullable + private final String mSharedText; + + @Nullable + private final IntentFilter mTargetIntentFilter; + + public ChooserRequestParameters( + final Intent clientIntent, + final Uri referrer, + @Nullable final ComponentName nearbySharingComponent) { + final Intent requestedTarget = parseTargetIntentExtra( + clientIntent.getParcelableExtra(Intent.EXTRA_INTENT)); + mTarget = intentWithModifiedLaunchFlags(requestedTarget); + + mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent( + clientIntent, Intent.EXTRA_ALTERNATE_INTENTS); + + mReplacementExtras = clientIntent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS); + + mTitleSpec = makeTitleSpec( + clientIntent.getCharSequenceExtra(Intent.EXTRA_TITLE), + isSendAction(mTarget.getAction())); + + mInitialIntents = intentsWithModifiedLaunchFlagsFromExtraIfPresent( + clientIntent, Intent.EXTRA_INITIAL_INTENTS); + + mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, referrer); + + mChosenComponentSender = clientIntent.getParcelableExtra( + Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER); + mRefinementIntentSender = clientIntent.getParcelableExtra( + Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); + + mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent); + + mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent); + + mRetainInOnStop = clientIntent.getBooleanExtra( + ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false); + + mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT); + + mTargetIntentFilter = getTargetIntentFilter(mTarget); + } + + public Intent getTargetIntent() { + return mTarget; + } + + @Nullable + public String getTargetAction() { + return getTargetIntent().getAction(); + } + + public boolean isSendActionTarget() { + return isSendAction(getTargetAction()); + } + + @Nullable + public String getTargetType() { + return getTargetIntent().getType(); + } + + @Nullable + public CharSequence getTitle() { + return mTitleSpec.first; + } + + public int getDefaultTitleResource() { + return mTitleSpec.second; + } + + public Intent getReferrerFillInIntent() { + return mReferrerFillInIntent; + } + + public ImmutableList<ComponentName> getFilteredComponentNames() { + return mFilteredComponentNames; + } + + public ImmutableList<ChooserTarget> getCallerChooserTargets() { + return mCallerChooserTargets; + } + + /** + * Whether the {@link ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested. + */ + public boolean shouldRetainInOnStop() { + return mRetainInOnStop; + } + + /** + * TODO: this returns a nullable array for convenience, but if the legacy APIs can be + * refactored, returning {@link mAdditionalTargets} directly is simpler and safer. + */ + @Nullable + public Intent[] getAdditionalTargets() { + return (mAdditionalTargets == null) ? null : mAdditionalTargets.toArray(new Intent[0]); + } + + @Nullable + public Bundle getReplacementExtras() { + return mReplacementExtras; + } + + /** + * TODO: this returns a nullable array for convenience, but if the legacy APIs can be + * refactored, returning {@link mInitialIntents} directly is simpler and safer. + */ + @Nullable + public Intent[] getInitialIntents() { + return (mInitialIntents == null) ? null : mInitialIntents.toArray(new Intent[0]); + } + + @Nullable + public IntentSender getChosenComponentSender() { + return mChosenComponentSender; + } + + @Nullable + public IntentSender getRefinementIntentSender() { + return mRefinementIntentSender; + } + + @Nullable + public String getSharedText() { + return mSharedText; + } + + @Nullable + public IntentFilter getTargetIntentFilter() { + return mTargetIntentFilter; + } + + private static boolean isSendAction(@Nullable String action) { + return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)); + } + + private static Intent parseTargetIntentExtra(@Nullable Parcelable targetParcelable) { + if (targetParcelable instanceof Uri) { + try { + targetParcelable = Intent.parseUri(targetParcelable.toString(), + Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("Failed to parse EXTRA_INTENT from URI", ex); + } + } + + if (!(targetParcelable instanceof Intent)) { + throw new IllegalArgumentException( + "EXTRA_INTENT is neither an Intent nor a Uri: " + targetParcelable); + } + + return ((Intent) targetParcelable); + } + + private static Intent intentWithModifiedLaunchFlags(Intent intent) { + if (isSendAction(intent.getAction())) { + intent.addFlags(LAUNCH_FLAGS_FOR_SEND_ACTION); + } + return intent; + } + + /** + * Build a pair of values specifying the title to use from the client request. The first + * ({@link CharSequence}) value is the client-specified title, if there was one and their + * requested target <em>wasn't</em> a send action; otherwise it is null. The second value is + * the resource ID of a default title string; this is nonzero only if the first value is null. + * + * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate()}, or + * create a real type (not {@link Pair}) to express the semantics described in this comment. + */ + private static Pair<CharSequence, Integer> makeTitleSpec( + @Nullable CharSequence requestedTitle, boolean hasSendActionTarget) { + if (hasSendActionTarget && (requestedTitle != null)) { + // Do not allow the title to be changed when sharing content + 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."); + requestedTitle = null; + } + + int defaultTitleRes = + (requestedTitle == null) ? com.android.internal.R.string.chooseActivity : 0; + + return Pair.create(requestedTitle, defaultTitleRes); + } + + private static ImmutableList<ComponentName> getFilteredComponentNames( + Intent clientIntent, @Nullable ComponentName nearbySharingComponent) { + Stream<ComponentName> filteredComponents = streamParcelableArrayExtra( + clientIntent, Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class, true, true); + + if (nearbySharingComponent != null) { + // Exclude Nearby from main list if chip is present, to avoid duplication. + // TODO: we don't have an explicit guarantee that the chip will be displayed just + // because we have a non-null component; that's ultimately determined by the preview + // layout. Maybe we can make that decision further upstream? + filteredComponents = Stream.concat( + filteredComponents, Stream.of(nearbySharingComponent)); + } + + return filteredComponents.collect(toImmutableList()); + } + + private static ImmutableList<ChooserTarget> parseCallerTargetsFromClientIntent( + Intent clientIntent) { + return + streamParcelableArrayExtra( + clientIntent, Intent.EXTRA_CHOOSER_TARGETS, ChooserTarget.class, true, true) + .collect(toImmutableList()); + } + + private static <T> Collector<T, ?, ImmutableList<T>> toImmutableList() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); + } + + @Nullable + private static ImmutableList<Intent> intentsWithModifiedLaunchFlagsFromExtraIfPresent( + Intent clientIntent, String extra) { + Stream<Intent> intents = + streamParcelableArrayExtra(clientIntent, extra, Intent.class, true, false); + if (intents == null) { + return null; + } + return intents + .map(ChooserRequestParameters::intentWithModifiedLaunchFlags) + .collect(toImmutableList()); + } + + /** + * Make a {@link Stream} of the {@link Parcelable} objects given in the provided {@link Intent} + * as the optional parcelable array extra with key {@code extra}. The stream elements, if any, + * are all of the type specified by {@code clazz}. + * + * @param intent The intent that may contain the optional extras. + * @param extra The extras key to identify the parcelable array. + * @param clazz A class that is assignable from any elements in the result stream. + * @param warnOnTypeError Whether to log a warning (and ignore) if the client extra doesn't have + * the required type. If false, throw an {@link IllegalArgumentException} if the extra is + * non-null but can't be assigned to variables of type {@code T}. + * @param streamEmptyIfNull Whether to return an empty stream if the optional extra isn't + * present in the intent (or if it had the wrong type, but {@link warnOnTypeError} is true). + * If false, return null in these cases, and only return an empty stream if the intent + * explicitly provided an empty array for the specified extra. + */ + @Nullable + private static <T extends Parcelable> Stream<T> streamParcelableArrayExtra( + final Intent intent, + String extra, + @NonNull Class<T> clazz, + boolean warnOnTypeError, + boolean streamEmptyIfNull) { + T[] result = null; + + try { + result = getParcelableArrayExtraIfPresent(intent, extra, clazz); + } catch (IllegalArgumentException e) { + if (warnOnTypeError) { + Log.w(TAG, "Ignoring client-requested " + extra, e); + } else { + throw e; + } + } + + if (result != null) { + return Arrays.stream(result); + } else if (streamEmptyIfNull) { + return Stream.empty(); + } else { + return null; + } + } + + /** + * If the specified {@code extra} is provided in the {@code intent}, cast it to type {@code T[]} + * or throw an {@code IllegalArgumentException} if the cast fails. If the {@code extra} isn't + * present in the {@code intent}, return null. + */ + @Nullable + private static <T extends Parcelable> T[] getParcelableArrayExtraIfPresent( + final Intent intent, String extra, @NonNull Class<T> clazz) throws + IllegalArgumentException { + if (!intent.hasExtra(extra)) { + return null; + } + + T[] castResult = intent.getParcelableArrayExtra(extra, clazz); + if (castResult == null) { + Parcelable[] actualExtrasArray = intent.getParcelableArrayExtra(extra); + if (actualExtrasArray != null) { + throw new IllegalArgumentException( + String.format( + "%s is not of type %s[]: %s", + extra, + clazz.getSimpleName(), + Arrays.toString(actualExtrasArray))); + } else if (intent.getParcelableExtra(extra) != null) { + throw new IllegalArgumentException( + String.format( + "%s is not of type %s[] (or any array type): %s", + extra, + clazz.getSimpleName(), + intent.getParcelableExtra(extra))); + } else { + throw new IllegalArgumentException( + String.format( + "%s is not of type %s (or any Parcelable type): %s", + extra, + clazz.getSimpleName(), + intent.getExtras().get(extra))); + } + } + + return castResult; + } + + private static IntentFilter getTargetIntentFilter(final Intent intent) { + try { + 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<Uri> contentUris = new ArrayList<>(); + if (Intent.ACTION_SEND.equals(intent.getAction())) { + Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) { + contentUris.add(uri); + } + } else { + List<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + 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; + } + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt index bcb6c240..d054e7fa 100644 --- a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt @@ -45,10 +45,6 @@ class ChooserListAdapterTest { } private val context = InstrumentationRegistry.getInstrumentation().getContext() private val resolverListController = mock<ResolverListController>() - private val chooserListCommunicator = mock<ChooserListAdapter.ChooserListCommunicator> { - whenever(maxRankedTargets).thenReturn(0) - whenever(targetIntent).thenReturn(mock()) - } private val chooserActivityLogger = mock<ChooserActivityLogger>() private fun createChooserListAdapter( @@ -60,9 +56,11 @@ class ChooserListAdapterTest { emptyList(), false, resolverListController, - chooserListCommunicator, + mock(), packageManager, chooserActivityLogger, + mock(), + 0 ) { override fun createLoadDirectShareIconTask( info: SelectableTargetInfo diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 56e583bb..fe448d63 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -66,9 +66,15 @@ public class ChooserWrapperActivity } @Override - public ChooserListAdapter createChooserListAdapter(Context context, List<Intent> payloadIntents, - Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, - ResolverListController resolverListController) { + public ChooserListAdapter createChooserListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + ResolverListController resolverListController, + ChooserRequestParameters chooserRequest, + int maxTargetsPerRow) { PackageManager packageManager = sOverrides.packageManager == null ? context.getPackageManager() : sOverrides.packageManager; @@ -81,7 +87,9 @@ public class ChooserWrapperActivity resolverListController, this, packageManager, - getChooserActivityLogger()); + getChooserActivityLogger(), + chooserRequest, + maxTargetsPerRow); } @Override |