diff options
Diffstat (limited to 'java')
5 files changed, 626 insertions, 370 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java new file mode 100644 index 00000000..1fe55890 --- /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 34390770..a2f2bbde 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -32,13 +32,10 @@ import android.annotation.Nullable; 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.IntentSender.SendIntentException; 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.MultiDisplayResolveInfo; 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.AppPredictionServiceResolverComparator; 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.content.PackageMonitor; 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 @@ public class ChooserActivity extends ResolverActivity implements | 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 @@ public class ChooserActivity extends ResolverActivity implements private ChooserRequestParameters mChooserRequest; private FeatureFlagRepository mFeatureFlagRepository; + private ChooserActionFactory mChooserActionFactory; private ChooserContentPreviewUi mChooserContentPreviewUi; private boolean mShouldDisplayLandscape; @@ -274,11 +263,14 @@ public class ChooserActivity extends ResolverActivity implements 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 @@ public class ChooserActivity extends ResolverActivity implements 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 @@ public class ChooserActivity extends ResolverActivity implements mEnterTransitionAnimationDelegate.postponeTransition(); } + @VisibleForTesting + protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { + return ChooserIntegratedDeviceComponents.get(this); + } + @Override protected int appliedThemeResId() { return R.style.Theme_DeviceDefault_Chooser; @@ -607,51 +637,6 @@ public class ChooserActivity extends ResolverActivity implements 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 @@ public class ChooserActivity extends ResolverActivity implements 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 @@ public class ChooserActivity extends ResolverActivity implements 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 @@ public class ChooserActivity extends ResolverActivity implements } } - 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 00000000..9b124c20 --- /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 2b67b273..83a0e2e1 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -71,6 +71,8 @@ public class ChooserRequestParameters { 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 class ChooserRequestParameters { 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 @@ public class ChooserRequestParameters { 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 @@ public class ChooserRequestParameters { return getTargetIntent().getType(); } + public String getReferrerPackageName() { + return mReferrerPackageName; + } + @Nullable public CharSequence getTitle() { return mTitleSpec.first; @@ -245,6 +257,10 @@ public class ChooserRequestParameters { 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 a47014e8..17084e1c 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.CrossProfileI 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 @@ public class ChooserWrapperActivity } @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 @@ public class ChooserWrapperActivity } @Override - public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) { + public void safelyStartActivity(TargetInfo cti) { if (sOverrides.onSafelyStartCallback != null && sOverrides.onSafelyStartCallback.apply(cti)) { return; |