summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--aconfig/FeatureFlags.aconfig10
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java130
-rw-r--r--java/src/com/android/intentresolver/ChooserRefinementManager.java117
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt20
4 files changed, 221 insertions, 56 deletions
diff --git a/aconfig/FeatureFlags.aconfig b/aconfig/FeatureFlags.aconfig
index 02f1c872..4d787ea2 100644
--- a/aconfig/FeatureFlags.aconfig
+++ b/aconfig/FeatureFlags.aconfig
@@ -42,3 +42,13 @@ flag {
description: "Enable private profile support"
bug: "328029692"
}
+
+flag {
+ name: "refine_system_actions"
+ namespace: "intentresolver"
+ description: "This flag enables sending system actions to the caller refinement flow"
+ bug: "331206205"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 9328baf5..56873302 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -98,6 +98,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
+import com.android.intentresolver.ChooserRefinementManager.RefinementType;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -569,23 +570,52 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
mRefinementManager.getRefinementCompletion().observe(this, completion -> {
if (completion.consume()) {
- TargetInfo targetInfo = completion.getTargetInfo();
- // targetInfo is non-null if the refinement process was successful.
- if (targetInfo != null) {
- maybeRemoveSharedText(targetInfo);
-
- // We already block suspended targets from going to refinement, and we probably
- // can't recover a Chooser session if that's the reason the refined target fails
- // to launch now. Fire-and-forget the refined launch; ignore the return value
- // and just make sure the Sharesheet session gets cleaned up regardless.
- final ResolveInfo ri = targetInfo.getResolveInfo();
- final Intent intent1 = targetInfo.getResolvedIntent();
-
- safelyStartActivity(targetInfo);
-
- // Rely on the ActivityManager to pop up a dialog regarding app suspension
- // and return false
- targetInfo.isSuspended();
+ if (completion.getRefinedIntent() == null) {
+ finish();
+ return;
+ }
+
+ // Prepare to regenerate our "system actions" based on the refined intent.
+ // TODO: optimize if needed. `TARGET_INFO` cases don't require a new action
+ // factory at all. And if we break up `ChooserActionFactory`, we could avoid
+ // resolving a new editor intent unless we're handling an `EDIT_ACTION`.
+ ChooserActionFactory refinedActionFactory =
+ createChooserActionFactory(completion.getRefinedIntent());
+ switch (completion.getType()) {
+ case TARGET_INFO: {
+ TargetInfo refinedTarget = completion
+ .getOriginalTargetInfo()
+ .tryToCloneWithAppliedRefinement(
+ completion.getRefinedIntent());
+ if (refinedTarget == null) {
+ Log.e(TAG, "Failed to apply refinement to any matching source intent");
+ } else {
+ maybeRemoveSharedText(refinedTarget);
+
+ // We already block suspended targets from going to refinement, and we
+ // probably can't recover a Chooser session if that's the reason the
+ // refined target fails to launch now. Fire-and-forget the refined
+ // launch, and make sure Sharesheet gets cleaned up regardless of the
+ // outcome of that launch.launch; ignore
+
+ safelyStartActivity(refinedTarget);
+ }
+ }
+ break;
+
+ case COPY_ACTION: {
+ if (refinedActionFactory.getCopyButtonRunnable() != null) {
+ refinedActionFactory.getCopyButtonRunnable().run();
+ }
+ }
+ break;
+
+ case EDIT_ACTION: {
+ if (refinedActionFactory.getEditButtonRunnable() != null) {
+ refinedActionFactory.getEditButtonRunnable().run();
+ }
+ }
+ break;
}
finish();
@@ -598,12 +628,15 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
mRequest.getTargetIntent(),
mRequest.getAdditionalContentUri(),
mChooserServiceFeatureFlags.chooserPayloadToggling());
+ ChooserContentPreviewUi.ActionFactory actionFactory =
+ decorateActionFactoryWithRefinement(
+ createChooserActionFactory(mRequest.getTargetIntent()));
mChooserContentPreviewUi = new ChooserContentPreviewUi(
getCoroutineScope(getLifecycle()),
previewViewModel.getPreviewDataProvider(),
mRequest.getTargetIntent(),
previewViewModel.getImageLoader(),
- createChooserActionFactory(),
+ actionFactory,
createModifyShareActionFactory(),
mEnterTransitionAnimationDelegate,
new HeadlineGeneratorImpl(this),
@@ -2110,10 +2143,67 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return PreviewViewModel.Companion.getFactory();
}
- private ChooserActionFactory createChooserActionFactory() {
+ private ChooserContentPreviewUi.ActionFactory decorateActionFactoryWithRefinement(
+ ChooserContentPreviewUi.ActionFactory originalFactory) {
+ if (!mFeatureFlags.refineSystemActions()) {
+ return originalFactory;
+ }
+
+ return new ChooserContentPreviewUi.ActionFactory() {
+ @Override
+ @Nullable
+ public Runnable getEditButtonRunnable() {
+ return () -> {
+ if (!mRefinementManager.maybeHandleSelection(
+ RefinementType.EDIT_ACTION,
+ List.of(mRequest.getTargetIntent()),
+ null,
+ mRequest.getRefinementIntentSender(),
+ getApplication(),
+ getMainThreadHandler())) {
+ originalFactory.getEditButtonRunnable().run();
+ }
+ };
+ }
+
+ @Override
+ @Nullable
+ public Runnable getCopyButtonRunnable() {
+ return () -> {
+ if (!mRefinementManager.maybeHandleSelection(
+ RefinementType.COPY_ACTION,
+ List.of(mRequest.getTargetIntent()),
+ null,
+ mRequest.getRefinementIntentSender(),
+ getApplication(),
+ getMainThreadHandler())) {
+ originalFactory.getCopyButtonRunnable().run();
+ }
+ };
+ }
+
+ @Override
+ public List<ActionRow.Action> createCustomActions() {
+ return originalFactory.createCustomActions();
+ }
+
+ @Override
+ @Nullable
+ public ActionRow.Action getModifyShareAction() {
+ return originalFactory.getModifyShareAction();
+ }
+
+ @Override
+ public Consumer<Boolean> getExcludeSharedTextAction() {
+ return originalFactory.getExcludeSharedTextAction();
+ }
+ };
+ }
+
+ private ChooserActionFactory createChooserActionFactory(Intent targetIntent) {
return new ChooserActionFactory(
this,
- mRequest.getTargetIntent(),
+ targetIntent,
mRequest.getLaunchedFromPackage(),
mRequest.getChooserActions(),
mImageEditor,
diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java
index 79484240..5c828a8e 100644
--- a/java/src/com/android/intentresolver/ChooserRefinementManager.java
+++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java
@@ -59,22 +59,58 @@ public final class ChooserRefinementManager extends ViewModel {
private boolean mConfigurationChangeInProgress = false;
/**
+ * The types of selections that may be sent to refinement.
+ *
+ * The refinement flow results in a refined intent, but the interpretation of that intent
+ * depends on the type of selection that prompted the refinement.
+ */
+ public enum RefinementType {
+ TARGET_INFO, // A normal (`TargetInfo`) target.
+
+ // System actions derived from the refined intent (from `ChooserActionFactory`).
+ COPY_ACTION,
+ EDIT_ACTION
+ }
+
+ /**
* A token for the completion of a refinement process that can be consumed exactly once.
*/
public static class RefinementCompletion {
private TargetInfo mTargetInfo;
private boolean mConsumed;
+ private final RefinementType mType;
- RefinementCompletion(TargetInfo targetInfo) {
- mTargetInfo = targetInfo;
+ @Nullable
+ private final TargetInfo mOriginalTargetInfo;
+
+ @Nullable
+ private final Intent mRefinedIntent;
+
+ RefinementCompletion(
+ @Nullable RefinementType type,
+ @Nullable TargetInfo originalTargetInfo,
+ @Nullable Intent refinedIntent) {
+ mType = type;
+ mOriginalTargetInfo = originalTargetInfo;
+ mRefinedIntent = refinedIntent;
+ }
+
+ public RefinementType getType() {
+ return mType;
+ }
+
+ @Nullable
+ public TargetInfo getOriginalTargetInfo() {
+ return mOriginalTargetInfo;
}
/**
* @return The output of the completed refinement process. Null if the process was aborted
* or failed.
*/
- public TargetInfo getTargetInfo() {
- return mTargetInfo;
+ @Nullable
+ public Intent getRefinedIntent() {
+ return mRefinedIntent;
}
/**
@@ -105,14 +141,11 @@ public final class ChooserRefinementManager extends ViewModel {
* @return true if the selection should wait for a now-started refinement flow, or false if it
* can proceed by the default (non-refinement) logic.
*/
- public boolean maybeHandleSelection(TargetInfo selectedTarget,
- IntentSender refinementIntentSender, Application application, Handler mainHandler) {
- if (refinementIntentSender == null) {
- return false;
- }
- if (selectedTarget.getAllSourceIntents().isEmpty()) {
- return false;
- }
+ public boolean maybeHandleSelection(
+ TargetInfo selectedTarget,
+ IntentSender refinementIntentSender,
+ Application application,
+ Handler mainHandler) {
if (selectedTarget.isSuspended()) {
// We expect all launches to fail for this target, so don't make the user go through the
// refinement flow first. Besides, the default (non-refinement) handling displays a
@@ -121,27 +154,57 @@ public final class ChooserRefinementManager extends ViewModel {
return false;
}
+ return maybeHandleSelection(
+ RefinementType.TARGET_INFO,
+ selectedTarget.getAllSourceIntents(),
+ selectedTarget,
+ refinementIntentSender,
+ application,
+ mainHandler);
+ }
+
+ /**
+ * Delegate the user's selection of targets (with one or more matching {@code sourceIntents} to
+ * the refinement flow, if possible.
+ * @return true if the selection should wait for a now-started refinement flow, or false if it
+ * can proceed by the default (non-refinement) logic.
+ */
+ public boolean maybeHandleSelection(
+ RefinementType refinementType,
+ List<Intent> sourceIntents,
+ @Nullable TargetInfo originalTargetInfo,
+ IntentSender refinementIntentSender,
+ Application application,
+ Handler mainHandler) {
+ // Our requests have a non-null `originalTargetInfo` in exactly the
+ // cases when `refinementType == TARGET_INFO`.
+ assert ((originalTargetInfo == null) == (refinementType == RefinementType.TARGET_INFO));
+
+ if (refinementIntentSender == null) {
+ return false;
+ }
+ if (sourceIntents.isEmpty()) {
+ return false;
+ }
+
destroy(); // Terminate any prior sessions.
mRefinementResultReceiver = new RefinementResultReceiver(
+ refinementType,
refinedIntent -> {
destroy();
-
- TargetInfo refinedTarget =
- selectedTarget.tryToCloneWithAppliedRefinement(refinedIntent);
- if (refinedTarget != null) {
- mRefinementCompletion.setValue(new RefinementCompletion(refinedTarget));
- } else {
- Log.e(TAG, "Failed to apply refinement to any matching source intent");
- mRefinementCompletion.setValue(new RefinementCompletion(null));
- }
+ mRefinementCompletion.setValue(
+ new RefinementCompletion(
+ refinementType, originalTargetInfo, refinedIntent));
},
() -> {
destroy();
- mRefinementCompletion.setValue(new RefinementCompletion(null));
+ mRefinementCompletion.setValue(
+ new RefinementCompletion(
+ refinementType, originalTargetInfo, null));
},
mainHandler);
- Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, selectedTarget);
+ Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, sourceIntents);
try {
refinementIntentSender.sendIntent(application, 0, refinementRequest, null, null);
return true;
@@ -167,7 +230,7 @@ public final class ChooserRefinementManager extends ViewModel {
// into a valid Chooser session, so we'll treat it as a cancellation instead.
Log.w(TAG, "Chooser resumed while awaiting refinement result; aborting");
destroy();
- mRefinementCompletion.setValue(new RefinementCompletion(null));
+ mRefinementCompletion.setValue(new RefinementCompletion(null, null, null));
}
}
}
@@ -187,9 +250,8 @@ public final class ChooserRefinementManager extends ViewModel {
}
private static Intent makeRefinementRequest(
- RefinementResultReceiver resultReceiver, TargetInfo originalTarget) {
+ RefinementResultReceiver resultReceiver, List<Intent> sourceIntents) {
final Intent fillIn = new Intent();
- final List<Intent> sourceIntents = originalTarget.getAllSourceIntents();
fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0));
final int sourceIntentCount = sourceIntents.size();
if (sourceIntentCount > 1) {
@@ -204,16 +266,19 @@ public final class ChooserRefinementManager extends ViewModel {
}
private static class RefinementResultReceiver extends ResultReceiver {
+ private final RefinementType mType;
private final Consumer<Intent> mOnSelectionRefined;
private final Runnable mOnRefinementCancelled;
private boolean mDestroyed;
RefinementResultReceiver(
+ RefinementType type,
Consumer<Intent> onSelectionRefined,
Runnable onRefinementCancelled,
Handler handler) {
super(handler);
+ mType = type;
mOnSelectionRefined = onSelectionRefined;
mOnRefinementCancelled = onRefinementCancelled;
}
diff --git a/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt b/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
index 61ac0c21..16c917b0 100644
--- a/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
@@ -29,8 +29,8 @@ import androidx.lifecycle.Observer
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.intentresolver.ChooserRefinementManager.RefinementCompletion
+import com.android.intentresolver.ChooserRefinementManager.RefinementType
import com.android.intentresolver.chooser.ImmutableTargetInfo
-import com.android.intentresolver.chooser.TargetInfo
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -55,15 +55,15 @@ class ChooserRefinementManagerTest {
object : Observer<RefinementCompletion> {
val failureCountDown = CountDownLatch(1)
val successCountDown = CountDownLatch(1)
- var latestTargetInfo: TargetInfo? = null
+ var latestRefinedIntent: Intent? = null
override fun onChanged(completion: RefinementCompletion) {
if (completion.consume()) {
- val targetInfo = completion.targetInfo
- if (targetInfo == null) {
+ val refinedIntent = completion.refinedIntent
+ if (refinedIntent == null) {
failureCountDown.countDown()
} else {
- latestTargetInfo = targetInfo
+ latestRefinedIntent = refinedIntent
successCountDown.countDown()
}
}
@@ -115,8 +115,7 @@ class ChooserRefinementManagerTest {
receiver?.send(Activity.RESULT_OK, bundle)
assertThat(completionObserver.successCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue()
- assertThat(completionObserver.latestTargetInfo?.resolvedIntent?.action)
- .isEqualTo(Intent.ACTION_VIEW)
+ assertThat(completionObserver.latestRefinedIntent?.action).isEqualTo(Intent.ACTION_VIEW)
}
@Test
@@ -231,10 +230,11 @@ class ChooserRefinementManagerTest {
@Test
fun testRefinementCompletion() {
- val refinementCompletion = RefinementCompletion(exampleTargetInfo)
- assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo)
+ val refinementCompletion =
+ RefinementCompletion(RefinementType.TARGET_INFO, exampleTargetInfo, null)
+ assertThat(refinementCompletion.originalTargetInfo).isEqualTo(exampleTargetInfo)
assertThat(refinementCompletion.consume()).isTrue()
- assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo)
+ assertThat(refinementCompletion.originalTargetInfo).isEqualTo(exampleTargetInfo)
// can only consume once.
assertThat(refinementCompletion.consume()).isFalse()