From 49b65f54be53ec48d53a550e783759100e8812dc Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Mon, 12 Dec 2022 09:17:29 -0800 Subject: Add Chooser custom actions Add Chooser custom action support under a compile-time flag. Bug: 262278109 Test: manual testing of the basic functionality Test: manual custom actions testing with a test app Test: atest IntentResolverUnitTests (with the both flag values) Change-Id: Ib6f6b46aa4f693a544e0e52a6d1a3e63ba57b162 --- .../android/intentresolver/ChooserActivity.java | 47 ++++++++++++++++- .../intentresolver/ChooserContentPreviewUi.java | 27 ++++++++-- .../intentresolver/ChooserRequestParameters.java | 23 +++++++- .../UnbundledChooserActivityTest.java | 61 ++++++++++++++++++++++ 4 files changed, 152 insertions(+), 6 deletions(-) (limited to 'java') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index ceab62b2..55904fc1 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -32,6 +32,7 @@ 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; @@ -70,6 +71,7 @@ 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; @@ -112,6 +114,8 @@ 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.io.IOException; import java.lang.annotation.Retention; @@ -158,6 +162,7 @@ public class ChooserActivity extends ResolverActivity implements private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon"; private static final boolean DEBUG = true; + static final boolean ENABLE_CUSTOM_ACTIONS = false; public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; private static final String SHORTCUT_TARGET = "shortcut_target"; @@ -265,7 +270,7 @@ public class ChooserActivity extends ResolverActivity implements try { mChooserRequest = new ChooserRequestParameters( - getIntent(), getReferrer(), getNearbySharingComponent()); + getIntent(), getReferrer(), getNearbySharingComponent(), ENABLE_CUSTOM_ACTIONS); } catch (IllegalArgumentException e) { Log.e(TAG, "Caller provided invalid Chooser request parameters", e); finish(); @@ -732,6 +737,20 @@ public class ChooserActivity extends ResolverActivity implements public ActionRow.Action createNearbyButton() { return ChooserActivity.this.createNearbyAction(targetIntent); } + + @Override + public List createCustomActions() { + ImmutableList customActions = + mChooserRequest.getChooserActions(); + List actions = new ArrayList<>(customActions.size()); + for (ChooserAction customAction : customActions) { + ActionRow.Action action = createCustomAction(customAction); + if (action != null) { + actions.add(action); + } + } + return actions; + } }; ViewGroup layout = ChooserContentPreviewUi.displayContentPreview( @@ -740,7 +759,9 @@ public class ChooserActivity extends ResolverActivity implements getResources(), getLayoutInflater(), actionFactory, - R.layout.chooser_action_row, + ENABLE_CUSTOM_ACTIONS + ? R.layout.scrollable_chooser_action_row + : R.layout.chooser_action_row, parent, previewCoordinator, mEnterTransitionAnimationDelegate::markImagePreviewReady, @@ -927,6 +948,28 @@ public class ChooserActivity extends ResolverActivity implements ); } + @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(); + } + ); + } + @Nullable private View getFirstVisibleImgPreviewView() { View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large); diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java index ff88e5e1..daded28b 100644 --- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java @@ -102,6 +102,9 @@ public final class ChooserContentPreviewUi { /** Create an "Share to Nearby" action. */ @Nullable ActionRow.Action createNearbyButton(); + + /** Create custom actions */ + List createCustomActions(); } /** @@ -187,12 +190,15 @@ public final class ChooserContentPreviewUi { ImageMimeTypeClassifier imageClassifier) { ViewGroup layout = null; + List customActions = actionFactory.createCustomActions(); switch (previewType) { case CONTENT_PREVIEW_TEXT: layout = displayTextContentPreview( targetIntent, layoutInflater, - createTextPreviewActions(actionFactory), + createActions( + createTextPreviewActions(actionFactory), + customActions), parent, previewCoord, actionRowLayout); @@ -201,7 +207,9 @@ public final class ChooserContentPreviewUi { layout = displayImageContentPreview( targetIntent, layoutInflater, - createImagePreviewActions(actionFactory), + createActions( + createImagePreviewActions(actionFactory), + customActions), parent, previewCoord, onTransitionTargetReady, @@ -214,7 +222,9 @@ public final class ChooserContentPreviewUi { targetIntent, resources, layoutInflater, - createFilePreviewActions(actionFactory), + createActions( + createFilePreviewActions(actionFactory), + customActions), parent, previewCoord, contentResolver, @@ -227,6 +237,17 @@ public final class ChooserContentPreviewUi { return layout; } + private static List createActions( + List systemActions, List customActions) { + ArrayList actions = + new ArrayList<>(systemActions.size() + customActions.size()); + actions.addAll(systemActions); + if (ChooserActivity.ENABLE_CUSTOM_ACTIONS) { + actions.addAll(customActions); + } + return actions; + } + private static Cursor queryResolver(ContentResolver resolver, Uri uri) { return resolver.query(uri, null, null, null, null); } diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 81481bf1..a7e543a5 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -26,6 +26,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.os.PatternMatcher; +import android.service.chooser.ChooserAction; import android.service.chooser.ChooserTarget; import android.text.TextUtils; import android.util.Log; @@ -70,6 +71,7 @@ public class ChooserRequestParameters { private final Intent mReferrerFillInIntent; private final ImmutableList mFilteredComponentNames; private final ImmutableList mCallerChooserTargets; + private final ImmutableList mChooserActions; private final boolean mRetainInOnStop; @Nullable @@ -96,7 +98,8 @@ public class ChooserRequestParameters { public ChooserRequestParameters( final Intent clientIntent, final Uri referrer, - @Nullable final ComponentName nearbySharingComponent) { + @Nullable final ComponentName nearbySharingComponent, + boolean extractCustomActions) { final Intent requestedTarget = parseTargetIntentExtra( clientIntent.getParcelableExtra(Intent.EXTRA_INTENT)); mTarget = intentWithModifiedLaunchFlags(requestedTarget); @@ -130,6 +133,10 @@ public class ChooserRequestParameters { mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT); mTargetIntentFilter = getTargetIntentFilter(mTarget); + + mChooserActions = extractCustomActions + ? getChooserActions(clientIntent) + : ImmutableList.of(); } public Intent getTargetIntent() { @@ -171,6 +178,10 @@ public class ChooserRequestParameters { return mCallerChooserTargets; } + public ImmutableList getChooserActions() { + return mChooserActions; + } + /** * Whether the {@link ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested. */ @@ -300,6 +311,16 @@ public class ChooserRequestParameters { .collect(toImmutableList()); } + private static ImmutableList getChooserActions(Intent intent) { + return streamParcelableArrayExtra( + intent, + Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, + ChooserAction.class, + true, + true) + .collect(toImmutableList()); + } + private static Collector> toImmutableList() { return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index af2557ef..c2d3f21c 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -55,13 +55,16 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.PendingIntent; import android.app.usage.UsageStatsManager; +import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -69,6 +72,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager.ShareShortcutInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -79,6 +83,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; import android.provider.DeviceConfig; +import android.service.chooser.ChooserAction; import android.service.chooser.ChooserTarget; import android.util.HashedStringCache; import android.util.Pair; @@ -117,6 +122,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; import java.util.function.Function; @@ -1664,6 +1670,61 @@ public class UnbundledChooserActivityTest { is(callerTargetLabel)); } + @Test + public void testLaunchWithCustomAction() throws InterruptedException { + if (!ChooserActivity.ENABLE_CUSTOM_ACTIONS) { + return; + } + List resolvedComponentInfos = createResolvedComponentsForTest(2); + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))) + .thenReturn(resolvedComponentInfos); + + Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); + final String customActionLabel = "Custom Action"; + final String testAction = "test-broadcast-receiver-action"; + Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + chooserIntent.putExtra( + Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, + new ChooserAction[] { + new ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + customActionLabel, + PendingIntent.getBroadcast( + testContext, + 123, + new Intent(testAction), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT)) + .build() + }); + // Start activity + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + final CountDownLatch broadcastInvoked = new CountDownLatch(1); + BroadcastReceiver testReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + broadcastInvoked.countDown(); + } + }; + testContext.registerReceiver(testReceiver, new IntentFilter(testAction)); + + try { + onView(withText(customActionLabel)).perform(click()); + broadcastInvoked.await(); + } finally { + testContext.unregisterReceiver(testReceiver); + } + } + @Test public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException { updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4); -- cgit v1.2.3-59-g8ed1b