diff options
| author | 2023-02-04 06:33:12 +0000 | |
|---|---|---|
| committer | 2023-02-04 06:33:12 +0000 | |
| commit | 9f5489d8213f4ff6259c43b076d9538db83180d0 (patch) | |
| tree | 9a79937c80767008627cd3c4d69ea9a5476c8e30 /java | |
| parent | fb86bd0412c7a771ff4e314eaa35fcfc68c6c568 (diff) | |
| parent | ede473e7fca58b254e4ed2890309458167c68e1f (diff) | |
Add optional text exclusion from media + text share am: 89697b484b am: ede473e7fc
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/IntentResolver/+/21266095
Change-Id: I99841de8b034150fadd92a30d9d8608dd0234d2b
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'java')
11 files changed, 309 insertions, 9 deletions
diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml index 6af0af11..80c12e6c 100644 --- a/java/res/layout/chooser_grid_preview_image.xml +++ b/java/res/layout/chooser_grid_preview_image.xml @@ -24,6 +24,14 @@ android:orientation="vertical" android:background="?android:attr/colorBackground"> + <CheckBox + android:id="@+id/include_text_action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:layout_marginEnd="@dimen/chooser_edge_margin_normal" + android:visibility="gone" /> + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 59179504..24604ed3 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -108,4 +108,13 @@ <string name="select_images">Select Images</string> <!-- Tittle for a button. Launches client-provided content reselection action. --> <string name="select_text">Select Text</string> + + <!-- Title for a button. Excludes a text from the shared content (a media and a text). --> + <string name="exclude_text">Exclude text</string> + <!-- Title for a button. Adds back a (previously excluded) text into the shared content. --> + <string name="include_text">Include text</string> + <!-- Title for a button. Excludes a web link from the shared content (a media and a text). --> + <string name="exclude_link">Exclude link</string> + <!-- Title for a button. Adds back a (previously excluded) web link into the shared content. --> + <string name="include_link">Include link</string> </resources> diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index e741b06c..3a7d4e68 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -262,6 +262,8 @@ public class ChooserActivity extends ResolverActivity implements private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>(); + private boolean mExcludeSharedText = false; + public ChooserActivity() {} @Override @@ -771,6 +773,11 @@ public class ChooserActivity extends ResolverActivity implements ? null : createReselectionRunnable(reselectionAction); } + + @Override + public Consumer<Boolean> getExcludeSharedTextAction() { + return (isExcluded) -> mExcludeSharedText = isExcluded; + } }; ViewGroup layout = mChooserContentPreviewUi.displayContentPreview( @@ -1196,6 +1203,7 @@ public class ChooserActivity extends ResolverActivity implements } } updateModelAndChooserCounts(target); + maybeRemoveSharedText(target); return super.onTargetSelected(target, alwaysCheck); } @@ -1384,6 +1392,27 @@ public class ChooserActivity extends ResolverActivity implements mIsSuccessfullySelected = true; } + private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) { + Intent targetIntent = targetInfo.getTargetIntent(); + if (targetIntent == null) { + return; + } + Intent originalTargetIntent = new Intent(mChooserRequest.getTargetIntent()); + // Our TargetInfo implementations add associated component to the intent, let's do the same + // for the sake of the comparison below. + if (targetIntent.getComponent() != null) { + originalTargetIntent.setComponent(targetIntent.getComponent()); + } + // Use filterEquals as a way to check that the primary intent is in use (and not an + // alternative one). For example, an app is sharing an image and a link with mime type + // "image/png" and provides an alternative intent to share only the link with mime type + // "text/uri". Should there be a target that accepts only the latter, the alternative intent + // will be used and we don't want to exclude the link from it. + if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) { + targetIntent.removeExtra(Intent.EXTRA_TEXT); + } + } + private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) { // Send DS target impression info to AppPredictor, only when user chooses app share. if (targetInfo.isChooserTargetInfo()) { @@ -1451,6 +1480,7 @@ public class ChooserActivity extends ResolverActivity implements + " cannot match refined source intent " + matchingIntent); } else { TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0); + maybeRemoveSharedText(clonedTarget); if (super.onTargetSelected(clonedTarget, false)) { updateModelAndChooserCounts(clonedTarget); finish(); diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java index 9bef3553..91abd9d0 100644 --- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java @@ -40,6 +40,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.view.animation.DecelerateInterpolator; +import android.widget.CheckBox; import android.widget.ImageView; import android.widget.TextView; @@ -59,6 +60,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.stream.Collectors; /** @@ -100,6 +102,17 @@ public final class ChooserContentPreviewUi { */ @Nullable Runnable getReselectionAction(); + + /** + * <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> + */ + Consumer<Boolean> getExcludeSharedTextAction(); } /** @@ -222,7 +235,8 @@ public final class ChooserContentPreviewUi { transitionElementStatusCallback, contentResolver, imageClassifier, - actionRowLayout); + actionRowLayout, + actionFactory); break; case CONTENT_PREVIEW_FILE: layout = displayFileContentPreview( @@ -360,7 +374,8 @@ public final class ChooserContentPreviewUi { TransitionElementStatusCallback transitionElementStatusCallback, ContentResolver contentResolver, ImageMimeTypeClassifier imageClassifier, - @LayoutRes int actionRowLayout) { + @LayoutRes int actionRowLayout, + ActionFactory actionFactory) { ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_image, parent, false); ImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); @@ -391,7 +406,8 @@ public final class ChooserContentPreviewUi { setTextInImagePreviewVisibility( contentPreviewLayout, - targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT)); + targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), + actionFactory); imagePreview.setTransitionElementStatusCallback(transitionElementStatusCallback); imagePreview.setImages(imageUris, imageLoader); @@ -399,20 +415,36 @@ public final class ChooserContentPreviewUi { } private void setTextInImagePreviewVisibility( - ViewGroup contentPreview, CharSequence text) { + ViewGroup contentPreview, CharSequence text, ActionFactory actionFactory) { int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) && !TextUtils.isEmpty(text) ? View.VISIBLE : View.GONE; - TextView textView = contentPreview + final TextView textView = contentPreview .requireViewById(com.android.internal.R.id.content_preview_text); + CheckBox actionView = contentPreview + .requireViewById(R.id.include_text_action); textView.setVisibility(visibility); - int linkMask = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(text.toString()) - ? Linkify.WEB_URLS - : 0; - textView.setAutoLinkMask(linkMask); + boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(text.toString()); + textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); textView.setText(text); + + if (visibility == View.VISIBLE) { + final int[] actionLabels = isLink + ? new int[] { R.string.include_link, R.string.exclude_link } + : new int[] { R.string.include_text, R.string.exclude_text }; + final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction(); + actionView.setChecked(true); + actionView.setText(actionLabels[1]); + shareTextAction.accept(false); + actionView.setOnCheckedChangeListener((view, isChecked) -> { + view.setText(actionLabels[isChecked ? 1 : 0]); + textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + shareTextAction.accept(!isChecked); + }); + } + actionView.setVisibility(visibility); } private static List<ActionRow.Action> createImagePreviewActions( diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java index 1b729c0e..4bbf59d8 100644 --- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java @@ -224,6 +224,11 @@ public class DisplayResolveInfo implements TargetInfo { return false; } + @Override + public Intent getTargetIntent() { + return mResolvedIntent; + } + public boolean isSuspended() { return mIsSuspended; } diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java index e4cec887..0d79e5d5 100644 --- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java @@ -119,4 +119,9 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo { public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { return mTargetInfos.get(mSelected).startAsUser(activity, options, user); } + + @Override + public Intent getTargetIntent() { + return mTargetInfos.get(mSelected).getTargetIntent(); + } } diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java index d6333374..9a2c971f 100644 --- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java @@ -16,6 +16,7 @@ package com.android.intentresolver.chooser; +import android.annotation.Nullable; import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -101,6 +102,12 @@ public abstract class NotSelectableTargetInfo extends ChooserTargetInfo { return false; } + @Nullable + @Override + public Intent getTargetIntent() { + return null; + } + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { return false; } diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java index 3ab50175..ca778233 100644 --- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java @@ -346,6 +346,12 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { return mActivityStarter.startAsUser(activity, options, user); } + @Nullable + @Override + public Intent getTargetIntent() { + return mBaseIntentToSend; + } + @Override public ResolveInfo getResolveInfo() { return mResolveInfo; diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java index 72dd1b0b..7dcf66b2 100644 --- a/java/src/com/android/intentresolver/chooser/TargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java @@ -88,6 +88,12 @@ public interface TargetInfo { Intent getResolvedIntent(); /** + * Get the target intent, the one that will be used with one of the <code>start</code> methods. + * @return the intent with target will be launced with. + */ + @Nullable Intent getTargetIntent(); + + /** * Get the resolved component name that represents this target. Note that this may not * be the component that will be directly launched by calling one of the <code>start</code> * methods provided; this is the component that will be credited with the launch. This may be diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java index fb928e09..6807bfd6 100644 --- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java +++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java @@ -41,6 +41,14 @@ public class ResolverDataProvider { createResolverIntent(i), createResolveInfo(i, UserHandle.USER_CURRENT)); } + static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfo( + ComponentName componentName, Intent intent) { + return new ResolverActivity.ResolvedComponentInfo( + componentName, + intent, + createResolveInfo(componentName, UserHandle.USER_CURRENT)); + } + static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) { return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), createResolverIntent(i), createResolveInfo(i, USER_SOMEONE_ELSE)); @@ -64,6 +72,13 @@ public class ResolverDataProvider { return resolveInfo; } + public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) { + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = createActivityInfo(componentName); + resolveInfo.targetUserId = userId; + return resolveInfo; + } + static ActivityInfo createActivityInfo(int i) { ActivityInfo ai = new ActivityInfo(); ai.name = "activity_name" + i; @@ -75,6 +90,18 @@ public class ResolverDataProvider { return ai; } + static ActivityInfo createActivityInfo(ComponentName componentName) { + ActivityInfo ai = new ActivityInfo(); + ai.name = componentName.getClassName(); + ai.packageName = componentName.getPackageName(); + ai.enabled = true; + ai.exported = true; + ai.permission = null; + ai.applicationInfo = createApplicationInfo(); + ai.applicationInfo.packageName = componentName.getPackageName(); + return ai; + } + static ApplicationInfo createApplicationInfo() { ApplicationInfo ai = new ApplicationInfo(); ai.name = "app_name"; diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 249dca62..c90f0b63 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -128,6 +128,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; @@ -756,6 +757,170 @@ public class UnbundledChooserActivityTest { } @Test + public void testImagePlusTextSharing_ExcludeText() { + ChooserActivityOverrideData.getInstance().featureFlagRepository = + new TestFeatureFlagRepository( + Collections.singletonMap(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, true)); + Intent sendIntent = createSendImageIntent( + Uri.parse("android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240)); + ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().isImageType = true; + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT")) + ); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))) + .thenReturn(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + launchedIntentRef.set(targetInfo.getTargetIntent()); + return true; + }; + + onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(launchedIntentRef.get().hasExtra(Intent.EXTRA_TEXT)).isFalse(); + } + + @Test + public void testImagePlusTextSharing_RemoveAndAddBackText() { + ChooserActivityOverrideData.getInstance().featureFlagRepository = + new TestFeatureFlagRepository( + Collections.singletonMap(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, true)); + Intent sendIntent = createSendImageIntent( + Uri.parse("android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240)); + ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().isImageType = true; + final String text = "https://google.com/search?q=google"; + sendIntent.putExtra(Intent.EXTRA_TEXT, text); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT")) + ); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))) + .thenReturn(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + onView(withId(R.id.include_text_action)) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + launchedIntentRef.set(targetInfo.getTargetIntent()); + return true; + }; + + onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text); + } + + @Test + public void testImagePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { + ChooserActivityOverrideData.getInstance().featureFlagRepository = + new TestFeatureFlagRepository( + Collections.singletonMap(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, true)); + Intent sendIntent = createSendImageIntent( + Uri.parse("android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240)); + ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().isImageType = true; + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + Intent alternativeIntent = createSendTextIntent(); + final String text = "alternative intent"; + alternativeIntent.putExtra(Intent.EXTRA_TEXT, text); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + alternativeIntent) + ); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))) + .thenReturn(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + launchedIntentRef.set(targetInfo.getTargetIntent()); + return true; + }; + + onView(withText(resolvedComponentInfos.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text); + } + + @Test public void copyTextToClipboard() throws Exception { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); |