summaryrefslogtreecommitdiff
path: root/java/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java305
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java68
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java441
3 files changed, 574 insertions, 240 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;
+ }
+ }
+}