Extract ChooserActionFactory.
This was a sizable chunk of code dedicated to one logical set of
responsibilities, so it's nice to separate.
Test: `atest IntentResolverUnitTests`
Bug: 202167050
Change-Id: I3b033e975afeee66e33da38d0dc0eeba768d0ed4
Merged-In: I3b033e975afeee66e33da38d0dc0eeba768d0ed4
(cherry picked from commit 6f3ea1e9310afb2a56c1491148802e1b6154a094)
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
new file mode 100644
index 0000000..1fe5589
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) 2023 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.Nullable;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.service.chooser.ChooserAction;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application
+ * requirements of Sharesheet / {@link ChooserActivity}.
+ */
+public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory {
+ /** Delegate interface to launch activities when the actions are selected. */
+ public interface ActionActivityStarter {
+ /**
+ * Request an activity launch for the provided target. Implementations may choose to exit
+ * the current activity when the target is launched.
+ */
+ void safelyStartActivityAsPersonalProfileUser(TargetInfo info);
+
+ /**
+ * Request an activity launch for the provided target, optionally employing the specified
+ * shared element transition. Implementations may choose to exit the current activity when
+ * the target is launched.
+ */
+ default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo info, View sharedElement, String sharedElementName) {
+ safelyStartActivityAsPersonalProfileUser(info);
+ }
+ }
+
+ private static final String TAG = "ChooserActions";
+
+ private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+ private 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 String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
+ private final Context mContext;
+ private final String mCopyButtonLabel;
+ private final Drawable mCopyButtonDrawable;
+ private final Runnable mOnCopyButtonClicked;
+ private final TargetInfo mEditSharingTarget;
+ private final Runnable mOnEditButtonClicked;
+ private final TargetInfo mNearbySharingTarget;
+ private final Runnable mOnNearbyButtonClicked;
+ private final ImmutableList<ChooserAction> mCustomActions;
+ private final PendingIntent mReselectionIntent;
+ private final Consumer<Boolean> mExcludeSharedTextAction;
+ private final Consumer</* @Nullable */ Integer> mFinishCallback;
+
+ /**
+ * @param context
+ * @param chooserRequest data about the invocation of the current Sharesheet session.
+ * @param featureFlagRepository feature flags that may control the eligibility of some actions.
+ * @param integratedDeviceComponents info about other components that are available on this
+ * device to implement the supported action types.
+ * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
+ * setting is updated. The argument is whether the shared text is to be excluded.
+ * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image
+ * View in the Sharesheet UI, if any, or null.
+ * @param activityStarter a delegate to launch activities when actions are selected.
+ * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was
+ * completed).
+ */
+ public ChooserActionFactory(
+ Context context,
+ ChooserRequestParameters chooserRequest,
+ FeatureFlagRepository featureFlagRepository,
+ ChooserIntegratedDeviceComponents integratedDeviceComponents,
+ ChooserActivityLogger logger,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ this(
+ context,
+ context.getString(com.android.internal.R.string.copy),
+ context.getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
+ makeOnCopyRunnable(
+ context,
+ chooserRequest.getTargetIntent(),
+ chooserRequest.getReferrerPackageName(),
+ finishCallback,
+ logger),
+ getEditSharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ makeOnEditRunnable(
+ getEditSharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ firstVisibleImageQuery,
+ activityStarter,
+ logger),
+ getNearbySharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ makeOnNearbyShareRunnable(
+ getNearbySharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ activityStarter,
+ finishCallback,
+ logger),
+ chooserRequest.getChooserActions(),
+ (featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)
+ ? chooserRequest.getModifyShareAction() : null),
+ onUpdateSharedTextIsExcluded,
+ finishCallback);
+ }
+
+ @VisibleForTesting
+ ChooserActionFactory(
+ Context context,
+ String copyButtonLabel,
+ Drawable copyButtonDrawable,
+ Runnable onCopyButtonClicked,
+ TargetInfo editSharingTarget,
+ Runnable onEditButtonClicked,
+ TargetInfo nearbySharingTarget,
+ Runnable onNearbyButtonClicked,
+ List<ChooserAction> customActions,
+ @Nullable PendingIntent reselectionIntent,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ mContext = context;
+ mCopyButtonLabel = copyButtonLabel;
+ mCopyButtonDrawable = copyButtonDrawable;
+ mOnCopyButtonClicked = onCopyButtonClicked;
+ mEditSharingTarget = editSharingTarget;
+ mOnEditButtonClicked = onEditButtonClicked;
+ mNearbySharingTarget = nearbySharingTarget;
+ mOnNearbyButtonClicked = onNearbyButtonClicked;
+ mCustomActions = ImmutableList.copyOf(customActions);
+ mReselectionIntent = reselectionIntent;
+ mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
+ mFinishCallback = finishCallback;
+ }
+
+ /** Create an action that copies the share content to the clipboard. */
+ @Override
+ public ActionRow.Action createCopyButton() {
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_copy_button,
+ mCopyButtonLabel,
+ mCopyButtonDrawable,
+ mOnCopyButtonClicked);
+ }
+
+ /** Create an action that opens the share content in a system-default editor. */
+ @Override
+ @Nullable
+ public ActionRow.Action createEditButton() {
+ if (mEditSharingTarget == null) {
+ return null;
+ }
+
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_edit_button,
+ mEditSharingTarget.getDisplayLabel(),
+ mEditSharingTarget.getDisplayIconHolder().getDisplayIcon(),
+ mOnEditButtonClicked);
+ }
+
+ /** Create a "Share to Nearby" action. */
+ @Override
+ @Nullable
+ public ActionRow.Action createNearbyButton() {
+ if (mNearbySharingTarget == null) {
+ return null;
+ }
+
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_nearby_button,
+ mNearbySharingTarget.getDisplayLabel(),
+ mNearbySharingTarget.getDisplayIconHolder().getDisplayIcon(),
+ mOnNearbyButtonClicked);
+ }
+
+ /** Create custom actions */
+ @Override
+ public List<ActionRow.Action> createCustomActions() {
+ return mCustomActions.stream()
+ .map(target -> createCustomAction(mContext, target, mFinishCallback))
+ .filter(action -> action != null)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Provides a share modification action, if any.
+ */
+ @Override
+ @Nullable
+ public Runnable getModifyShareAction() {
+ return (mReselectionIntent == null) ? null : createReselectionRunnable(mReselectionIntent);
+ }
+
+ private Runnable createReselectionRunnable(PendingIntent pendingIntent) {
+ return () -> {
+ try {
+ pendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "Payload reselection action has been cancelled");
+ }
+ // TODO: add reporting
+ mFinishCallback.accept(Activity.RESULT_OK);
+ };
+ }
+
+ /**
+ * <p>
+ * Creates an exclude-text action that can be called when the user changes shared text
+ * status in the Media + Text preview.
+ * </p>
+ * <p>
+ * <code>true</code> argument value indicates that the text should be excluded.
+ * </p>
+ */
+ @Override
+ public Consumer<Boolean> getExcludeSharedTextAction() {
+ return mExcludeSharedTextAction;
+ }
+
+ private static Runnable makeOnCopyRunnable(
+ Context context,
+ Intent targetIntent,
+ String referrerPackageName,
+ Consumer<Integer> finishCallback,
+ ChooserActivityLogger logger) {
+ return () -> {
+ if (targetIntent == null) {
+ finishCallback.accept(null);
+ return;
+ }
+
+ 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);
+
+ if (extraText != null) {
+ clipData = ClipData.newPlainText(null, extraText);
+ } else if (extraStream != null) {
+ clipData = ClipData.newUri(context.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<Uri> streams = targetIntent.getParcelableArrayListExtra(
+ Intent.EXTRA_STREAM);
+ clipData = ClipData.newUri(context.getContentResolver(), null, streams.get(0));
+ for (int i = 1; i < streams.size(); i++) {
+ clipData.addItem(
+ context.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) context.getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
+
+ logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+ finishCallback.accept(Activity.RESULT_OK);
+ };
+ }
+
+ private static TargetInfo getEditSharingTarget(
+ Context context,
+ Intent originalIntent,
+ ChooserIntegratedDeviceComponents integratedComponents) {
+ final ComponentName editorComponent = integratedComponents.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(editorComponent);
+ 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);
+ if (uri != null) {
+ String mimeType = context.getContentResolver().getType(uri);
+ resolveIntent.setDataAndType(uri, mimeType);
+ }
+ }
+ } else {
+ Log.e(TAG, originalAction + " is not supported.");
+ return null;
+ }
+ final ResolveInfo ri = context.getPackageManager().resolveActivity(
+ resolveIntent, PackageManager.GET_META_DATA);
+ if (ri == null || ri.activityInfo == null) {
+ Log.e(TAG, "Device-specified editor (" + editorComponent + ") not available");
+ return null;
+ }
+
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ri,
+ context.getString(com.android.internal.R.string.screenshot_edit),
+ "",
+ resolveIntent,
+ null);
+ dri.getDisplayIconHolder().setDisplayIcon(
+ context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+ return dri;
+ }
+
+ private static Runnable makeOnEditRunnable(
+ TargetInfo editSharingTarget,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ ChooserActivityLogger logger) {
+ return () -> {
+ // Log share completion via edit.
+ logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_EDIT);
+
+ View firstImageView = null;
+ try {
+ firstImageView = firstVisibleImageQuery.call();
+ } catch (Exception e) { /* ignore */ }
+ // Action bar is user-independent; always start as primary.
+ if (firstImageView == null) {
+ activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget);
+ } else {
+ activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT);
+ }
+ };
+ }
+
+ private static TargetInfo getNearbySharingTarget(
+ Context context,
+ Intent originalIntent,
+ ChooserIntegratedDeviceComponents integratedComponents) {
+ final ComponentName cn = integratedComponents.getNearbySharingComponent();
+ if (cn == null) return null;
+
+ final Intent resolveIntent = new Intent(originalIntent);
+ resolveIntent.setComponent(cn);
+ final ResolveInfo ri = context.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 = context.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 (NameNotFoundException | Resources.NotFoundException ex) { /* ignore */ }
+ }
+ if (TextUtils.isEmpty(name)) {
+ name = ri.loadLabel(context.getPackageManager());
+ }
+ if (icon == null) {
+ icon = ri.loadIcon(context.getPackageManager());
+ }
+
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent, ri, name, "", resolveIntent, null);
+ dri.getDisplayIconHolder().setDisplayIcon(icon);
+ return dri;
+ }
+
+ private static Runnable makeOnNearbyShareRunnable(
+ TargetInfo nearbyShareTarget,
+ ActionActivityStarter activityStarter,
+ Consumer<Integer> finishCallback,
+ ChooserActivityLogger logger) {
+ return () -> {
+ logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_NEARBY);
+ // Action bar is user-independent; always start as primary.
+ activityStarter.safelyStartActivityAsPersonalProfileUser(nearbyShareTarget);
+ };
+ }
+
+ @Nullable
+ private static ActionRow.Action createCustomAction(
+ Context context, ChooserAction action, Consumer<Integer> finishCallback) {
+ Drawable icon = action.getIcon().loadDrawable(context);
+ if (icon == null && TextUtils.isEmpty(action.getLabel())) {
+ return null;
+ }
+ return new ActionRow.Action(
+ action.getLabel(),
+ icon,
+ () -> {
+ try {
+ action.getAction().send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
+ }
+ // TODO: add reporting
+ finishCallback.accept(Activity.RESULT_OK);
+ }
+ );
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 3439077..a2f2bbd 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -32,13 +32,10 @@
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
-import android.app.PendingIntent;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
-import android.content.ClipData;
-import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
@@ -49,31 +46,24 @@
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
-import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Insets;
-import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
-import android.os.PatternMatcher;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.provider.DeviceConfig;
-import android.provider.Settings;
-import android.service.chooser.ChooserAction;
import android.service.chooser.ChooserTarget;
-import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -100,7 +90,6 @@
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
-import com.android.intentresolver.flags.Flags;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.grid.DirectShareViewHolder;
import com.android.intentresolver.model.AbstractResolverComparator;
@@ -108,7 +97,6 @@
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
-import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -116,8 +104,6 @@
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
-import com.google.common.collect.ImmutableList;
-
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -210,6 +196,8 @@
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+ private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+
/* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
* only assignment there, and expect it to be ready by the time we ever use it --
* someday if we move all the usage to a component with a narrower lifecycle (something that
@@ -220,6 +208,7 @@
private ChooserRequestParameters mChooserRequest;
private FeatureFlagRepository mFeatureFlagRepository;
+ private ChooserActionFactory mChooserActionFactory;
private ChooserContentPreviewUi mChooserContentPreviewUi;
private boolean mShouldDisplayLandscape;
@@ -274,11 +263,14 @@
getChooserActivityLogger().logSharesheetTriggered();
mFeatureFlagRepository = createFeatureFlagRepository();
+ mIntegratedDeviceComponents = getIntegratedDeviceComponents();
+
try {
mChooserRequest = new ChooserRequestParameters(
getIntent(),
+ getReferrerPackageName(),
getReferrer(),
- getNearbySharingComponent(),
+ mIntegratedDeviceComponents,
mFeatureFlagRepository);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
@@ -286,6 +278,39 @@
super_onCreate(null);
return;
}
+
+ mChooserActionFactory = new ChooserActionFactory(
+ this,
+ mChooserRequest,
+ mFeatureFlagRepository,
+ mIntegratedDeviceComponents,
+ getChooserActivityLogger(),
+ (isExcluded) -> mExcludeSharedText = isExcluded,
+ this::getFirstVisibleImgPreviewView,
+ new ChooserActionFactory.ActionActivityStarter() {
+ @Override
+ public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
+ safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle());
+ finish();
+ }
+
+ @Override
+ public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo targetInfo, View sharedElement, String sharedElementName) {
+ ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+ ChooserActivity.this, sharedElement, sharedElementName);
+ safelyStartActivityAsUser(
+ targetInfo, getPersonalProfileUserHandle(), options.toBundle());
+ startFinishAnimation();
+ }
+ },
+ (status) -> {
+ if (status != null) {
+ setResult(status);
+ }
+ finish();
+ });
+
mChooserContentPreviewUi = new ChooserContentPreviewUi(mFeatureFlagRepository);
setAdditionalTargets(mChooserRequest.getAdditionalTargets());
@@ -368,6 +393,11 @@
mEnterTransitionAnimationDelegate.postponeTransition();
}
+ @VisibleForTesting
+ protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+ return ChooserIntegratedDeviceComponents.get(this);
+ }
+
@Override
protected int appliedThemeResId() {
return R.style.Theme_DeviceDefault_Chooser;
@@ -607,51 +637,6 @@
updateProfileViewButton();
}
- private void onCopyButtonClicked() {
- 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);
-
- 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<Uri> streams = targetIntent.getParcelableArrayListExtra(
- Intent.EXTRA_STREAM);
- 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());
-
- getChooserActivityLogger().logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
-
- setResult(RESULT_OK);
- finish();
- }
- }
-
@Override
protected void onResume() {
super.onResume();
@@ -728,64 +713,12 @@
int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
targetIntent, getContentResolver(), this::isImageType);
- ChooserContentPreviewUi.ActionFactory actionFactory =
- new ChooserContentPreviewUi.ActionFactory() {
- @Override
- public ActionRow.Action createCopyButton() {
- return ChooserActivity.this.createCopyAction();
- }
-
- @Nullable
- @Override
- public ActionRow.Action createEditButton() {
- return ChooserActivity.this.createEditAction(targetIntent);
- }
-
- @Nullable
- @Override
- public ActionRow.Action createNearbyButton() {
- return ChooserActivity.this.createNearbyAction(targetIntent);
- }
-
- @Override
- public List<ActionRow.Action> createCustomActions() {
- ImmutableList<ChooserAction> customActions =
- mChooserRequest.getChooserActions();
- List<ActionRow.Action> actions = new ArrayList<>(customActions.size());
- for (ChooserAction customAction : customActions) {
- ActionRow.Action action = createCustomAction(customAction);
- if (action != null) {
- actions.add(action);
- }
- }
- return actions;
- }
-
- @Nullable
- @Override
- public Runnable getModifyShareAction() {
- if (!mFeatureFlagRepository
- .isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) {
- return null;
- }
- PendingIntent reselectionAction = mChooserRequest.getModifyShareAction();
- return reselectionAction == null
- ? null
- : createReselectionRunnable(reselectionAction);
- }
-
- @Override
- public Consumer<Boolean> getExcludeSharedTextAction() {
- return (isExcluded) -> mExcludeSharedText = isExcluded;
- }
- };
-
ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
previewType,
targetIntent,
getResources(),
getLayoutInflater(),
- actionFactory,
+ mChooserActionFactory,
parent,
imageLoader,
mEnterTransitionAnimationDelegate,
@@ -799,211 +732,6 @@
return layout;
}
- @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);
- 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 = DisplayResolveInfo.newDisplayResolveInfo(
- originalIntent,
- ri,
- getString(com.android.internal.R.string.screenshot_edit),
- "",
- resolveIntent,
- null);
- dri.getDisplayIconHolder().setDisplayIcon(
- getDrawable(com.android.internal.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 = DisplayResolveInfo.newDisplayResolveInfo(
- originalIntent, ri, name, "", resolveIntent, null);
- dri.getDisplayIconHolder().setDisplayIcon(icon);
- return dri;
- }
-
- private ActionRow.Action createCopyAction() {
- return new ActionRow.Action(
- com.android.internal.R.id.chooser_copy_button,
- getString(com.android.internal.R.string.copy),
- getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
- this::onCopyButtonClicked);
- }
-
- @Nullable
- private ActionRow.Action createNearbyAction(Intent originalIntent) {
- final TargetInfo ti = getNearbySharingTarget(originalIntent);
- if (ti == null) {
- return null;
- }
-
- return new ActionRow.Action(
- com.android.internal.R.id.chooser_nearby_button,
- ti.getDisplayLabel(),
- ti.getDisplayIconHolder().getDisplayIcon(),
- () -> {
- getChooserActivityLogger().logActionSelected(
- ChooserActivityLogger.SELECTION_TYPE_NEARBY);
- // Action bar is user-independent, always start as primary
- safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
- finish();
- });
- }
-
- @Nullable
- private ActionRow.Action createEditAction(Intent originalIntent) {
- final TargetInfo ti = getEditSharingTarget(originalIntent);
- if (ti == null) {
- return null;
- }
-
- return new ActionRow.Action(
- com.android.internal.R.id.chooser_edit_button,
- ti.getDisplayLabel(),
- ti.getDisplayIconHolder().getDisplayIcon(),
- () -> {
- // Log share completion via edit
- getChooserActivityLogger().logActionSelected(
- ChooserActivityLogger.SELECTION_TYPE_EDIT);
- 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();
- }
- }
- );
- }
-
- @Nullable
- private ActionRow.Action createCustomAction(ChooserAction action) {
- Drawable icon = action.getIcon().loadDrawable(this);
- if (icon == null && TextUtils.isEmpty(action.getLabel())) {
- return null;
- }
- return new ActionRow.Action(
- action.getLabel(),
- icon,
- () -> {
- try {
- action.getAction().send();
- } catch (PendingIntent.CanceledException e) {
- Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
- }
- // TODO: add reporting
- setResult(RESULT_OK);
- finish();
- }
- );
- }
-
- private Runnable createReselectionRunnable(PendingIntent pendingIntent) {
- return () -> {
- try {
- pendingIntent.send();
- } catch (PendingIntent.CanceledException e) {
- Log.d(TAG, "Payload reselection action has been cancelled");
- }
- // TODO: add reporting
- setResult(RESULT_OK);
- finish();
- };
- }
-
@Nullable
private View getFirstVisibleImgPreviewView() {
View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large);
@@ -1315,45 +1043,6 @@
}
}
- private IntentFilter getTargetIntentFilter() {
- return getTargetIntentFilter(getTargetIntent());
- }
-
- private 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;
- }
- }
-
private void logDirectShareTargetReceived(UserHandle forUser) {
ProfileRecord profileRecord = getProfileRecord(forUser);
if (profileRecord == null) {
diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
new file mode 100644
index 0000000..9b124c2
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper to look up the components available on this device to handle assorted built-in actions
+ * like "Edit" that may be displayed for certain content/preview types. The components are queried
+ * when this record is instantiated, and are then immutable for a given instance.
+ *
+ * Because this describes the app's external execution environment, test methods may prefer to
+ * provide explicit values to override the default lookup logic.
+ */
+public final class ChooserIntegratedDeviceComponents {
+ @Nullable
+ private final ComponentName mEditSharingComponent;
+
+ @Nullable
+ private final ComponentName mNearbySharingComponent;
+
+ /** Look up the integrated components available on this device. */
+ public static ChooserIntegratedDeviceComponents get(Context context) {
+ return new ChooserIntegratedDeviceComponents(
+ getEditSharingComponent(context),
+ getNearbySharingComponent(context));
+ }
+
+ @VisibleForTesting
+ ChooserIntegratedDeviceComponents(
+ ComponentName editSharingComponent, ComponentName nearbySharingComponent) {
+ mEditSharingComponent = editSharingComponent;
+ mNearbySharingComponent = nearbySharingComponent;
+ }
+
+ public ComponentName getEditSharingComponent() {
+ return mEditSharingComponent;
+ }
+
+ public ComponentName getNearbySharingComponent() {
+ return mNearbySharingComponent;
+ }
+
+ private static ComponentName getEditSharingComponent(Context context) {
+ String editorComponent = context.getApplicationContext().getString(
+ R.string.config_systemImageEditor);
+ return TextUtils.isEmpty(editorComponent)
+ ? null : ComponentName.unflattenFromString(editorComponent);
+ }
+
+ private static ComponentName getNearbySharingComponent(Context context) {
+ String nearbyComponent = Settings.Secure.getString(
+ context.getContentResolver(), Settings.Secure.NEARBY_SHARING_COMPONENT);
+ return TextUtils.isEmpty(nearbyComponent)
+ ? null : ComponentName.unflattenFromString(nearbyComponent);
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
index 2b67b27..83a0e2e 100644
--- a/java/src/com/android/intentresolver/ChooserRequestParameters.java
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -71,6 +71,8 @@
Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
private final Intent mTarget;
+ private final ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+ private final String mReferrerPackageName;
private final Pair<CharSequence, Integer> mTitleSpec;
private final Intent mReferrerFillInIntent;
private final ImmutableList<ComponentName> mFilteredComponentNames;
@@ -102,13 +104,18 @@
public ChooserRequestParameters(
final Intent clientIntent,
+ String referrerPackageName,
final Uri referrer,
- @Nullable final ComponentName nearbySharingComponent,
+ ChooserIntegratedDeviceComponents integratedDeviceComponents,
FeatureFlagRepository featureFlags) {
final Intent requestedTarget = parseTargetIntentExtra(
clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
mTarget = intentWithModifiedLaunchFlags(requestedTarget);
+ mIntegratedDeviceComponents = integratedDeviceComponents;
+
+ mReferrerPackageName = referrerPackageName;
+
mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
clientIntent, Intent.EXTRA_ALTERNATE_INTENTS);
@@ -128,7 +135,8 @@
mRefinementIntentSender = clientIntent.getParcelableExtra(
Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
- mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent);
+ mFilteredComponentNames = getFilteredComponentNames(
+ clientIntent, mIntegratedDeviceComponents.getNearbySharingComponent());
mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent);
@@ -165,6 +173,10 @@
return getTargetIntent().getType();
}
+ public String getReferrerPackageName() {
+ return mReferrerPackageName;
+ }
+
@Nullable
public CharSequence getTitle() {
return mTitleSpec.first;
@@ -245,6 +257,10 @@
return mTargetIntentFilter;
}
+ public ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+ return mIntegratedDeviceComponents;
+ }
+
private static boolean isSendAction(@Nullable String action) {
return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));
}
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index a47014e..17084e1 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -37,7 +37,6 @@
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
import com.android.intentresolver.chooser.DisplayResolveInfo;
-import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.grid.ChooserGridAdapter;
@@ -120,15 +119,13 @@
}
@Override
- protected ComponentName getNearbySharingComponent() {
- // an arbitrary pre-installed activity that handles this type of intent
- return ComponentName.unflattenFromString("com.google.android.apps.messaging/"
- + "com.google.android.apps.messaging.ui.conversationlist.ShareIntentActivity");
- }
-
- @Override
- protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
- return NotSelectableTargetInfo.newEmptyTargetInfo();
+ protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+ return new ChooserIntegratedDeviceComponents(
+ /* editSharingComponent=*/ null,
+ // An arbitrary pre-installed activity that handles this type of intent:
+ /* nearbySharingComponent=*/ new ComponentName(
+ "com.google.android.apps.messaging",
+ ".ui.conversationlist.ShareIntentActivity"));
}
@Override
@@ -172,7 +169,7 @@
}
@Override
- public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) {
+ public void safelyStartActivity(TargetInfo cti) {
if (sOverrides.onSafelyStartCallback != null
&& sOverrides.onSafelyStartCallback.apply(cti)) {
return;