summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2022-12-12 09:17:29 -0800
committer Andrey Epin <ayepin@google.com> 2023-01-05 10:18:51 -0800
commit49b65f54be53ec48d53a550e783759100e8812dc (patch)
treea9c9a5ba553f0fddaae5a74b6dec9641d6c18881 /java
parentc317e6a26b75da0a693cbb14f673ecdd7152c433 (diff)
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
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java47
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewUi.java27
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java23
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java61
4 files changed, 152 insertions, 6 deletions
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<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;
+ }
};
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,
@@ -928,6 +949,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);
return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
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<ActionRow.Action> createCustomActions();
}
/**
@@ -187,12 +190,15 @@ public final class ChooserContentPreviewUi {
ImageMimeTypeClassifier imageClassifier) {
ViewGroup layout = null;
+ List<ActionRow.Action> 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<ActionRow.Action> createActions(
+ List<ActionRow.Action> systemActions, List<ActionRow.Action> customActions) {
+ ArrayList<ActionRow.Action> 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<ComponentName> mFilteredComponentNames;
private final ImmutableList<ChooserTarget> mCallerChooserTargets;
+ private final ImmutableList<ChooserAction> 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<ChooserAction> 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<ChooserAction> getChooserActions(Intent intent) {
+ return streamParcelableArrayExtra(
+ intent,
+ Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS,
+ ChooserAction.class,
+ true,
+ true)
+ .collect(toImmutableList());
+ }
+
private static <T> Collector<T, ?, ImmutableList<T>> 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;
@@ -1665,6 +1671,61 @@ public class UnbundledChooserActivityTest {
}
@Test
+ public void testLaunchWithCustomAction() throws InterruptedException {
+ if (!ChooserActivity.ENABLE_CUSTOM_ACTIONS) {
+ return;
+ }
+ List<ResolvedComponentInfo> 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);
givenAppTargets(/* appCount= */ 16);