summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author Joshua Trask <joshtrask@google.com> 2023-02-13 23:38:29 +0000
committer Joshua Trask <joshtrask@google.com> 2023-02-17 14:53:32 +0000
commitf1576aeb415af53da75ca7dbc9413b04c43dee54 (patch)
treead97ba368e1124b66c558fedd97655f86709d143 /java/src
parent9a733ee98ed889b80c6182fa95fa123c8bbdb7b7 (diff)
Extract a component to handle refinement.
This is in advance of any possible bug-fixes related to b/262805893 (which may probably be accompanied by additional unit tests..) Test: `atest IntentResolverUnitTests` Bug: 202167050 Change-Id: I4c8d20522236559ff99b6e11a7c1a3a0fcbbd17d Merged-In: I4c8d20522236559ff99b6e11a7c1a3a0fcbbd17d (cherry picked from commit c07d3f064db9cf715e36e9b6d4c1cd516e2258ce)
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java172
-rw-r--r--java/src/com/android/intentresolver/ChooserRefinementManager.java215
2 files changed, 236 insertions, 151 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index a2f2bbde..65c72fda 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -42,7 +42,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
-import android.content.IntentSender.SendIntentException;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
@@ -54,10 +53,6 @@ import android.graphics.Insets;
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.ResultReceiver;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
@@ -207,6 +202,8 @@ public class ChooserActivity extends ResolverActivity implements
@Nullable
private ChooserRequestParameters mChooserRequest;
+ private ChooserRefinementManager mRefinementManager;
+
private FeatureFlagRepository mFeatureFlagRepository;
private ChooserActionFactory mChooserActionFactory;
private ChooserContentPreviewUi mChooserContentPreviewUi;
@@ -215,9 +212,6 @@ public class ChooserActivity extends ResolverActivity implements
// statsd logger wrapper
protected ChooserActivityLogger mChooserActivityLogger;
- @Nullable
- private RefinementResultReceiver mRefinementResultReceiver;
-
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
@@ -311,6 +305,20 @@ public class ChooserActivity extends ResolverActivity implements
finish();
});
+ mRefinementManager = new ChooserRefinementManager(
+ this,
+ mChooserRequest.getRefinementIntentSender(),
+ (validatedRefinedTarget) -> {
+ maybeRemoveSharedText(validatedRefinedTarget);
+ if (super.onTargetSelected(validatedRefinedTarget, false)) {
+ finish();
+ }
+ },
+ () -> {
+ mRefinementManager.destroy();
+ finish();
+ });
+
mChooserContentPreviewUi = new ChooserContentPreviewUi(mFeatureFlagRepository);
setAdditionalTargets(mChooserRequest.getAdditionalTargets());
@@ -777,9 +785,9 @@ public class ChooserActivity extends ResolverActivity implements
mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
}
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- mRefinementResultReceiver = null;
+ if (mRefinementManager != null) { // TODO: null-checked in case of early-destroy, or skip?
+ mRefinementManager.destroy();
+ mRefinementManager = null;
}
mBackgroundThreadPoolExecutor.shutdownNow();
@@ -903,32 +911,8 @@ public class ChooserActivity extends ResolverActivity implements
@Override
protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
- if (mChooserRequest.getRefinementIntentSender() != null) {
- final Intent fillIn = new Intent();
- final List<Intent> sourceIntents = target.getAllSourceIntents();
- if (!sourceIntents.isEmpty()) {
- fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0));
- if (sourceIntents.size() > 1) {
- final Intent[] alts = new Intent[sourceIntents.size() - 1];
- for (int i = 1, N = sourceIntents.size(); i < N; i++) {
- alts[i - 1] = sourceIntents.get(i);
- }
- fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts);
- }
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- }
- mRefinementResultReceiver = new RefinementResultReceiver(this, target, null);
- fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER,
- mRefinementResultReceiver.copyForSending());
- try {
- mChooserRequest.getRefinementIntentSender().sendIntent(
- this, 0, fillIn, null, null);
- return false;
- } catch (SendIntentException e) {
- Log.e(TAG, "Refinement IntentSender failed to send", e);
- }
- }
+ if (mRefinementManager.maybeHandleSelection(target)) {
+ return false;
}
updateModelAndChooserCounts(target);
maybeRemoveSharedText(target);
@@ -1157,47 +1141,6 @@ public class ChooserActivity extends ResolverActivity implements
return (record == null) ? null : record.appPredictor;
}
- void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) {
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- mRefinementResultReceiver = null;
- }
- if (selectedTarget == null) {
- Log.e(TAG, "Refinement result intent did not match any known targets; canceling");
- } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) {
- Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget
- + " cannot match refined source intent " + matchingIntent);
- } else {
- TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0);
- maybeRemoveSharedText(clonedTarget);
- if (super.onTargetSelected(clonedTarget, false)) {
- updateModelAndChooserCounts(clonedTarget);
- finish();
- return;
- }
- }
- onRefinementCanceled();
- }
-
- void onRefinementCanceled() {
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- mRefinementResultReceiver = null;
- }
- finish();
- }
-
- boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) {
- final List<Intent> targetIntents = target.getAllSourceIntents();
- for (int i = 0, N = targetIntents.size(); i < N; i++) {
- final Intent targetIntent = targetIntents.get(i);
- if (targetIntent.filterEquals(matchingIntent)) {
- return true;
- }
- }
- return false;
- }
-
/**
* Sort intents alphabetically based on display label.
*/
@@ -1892,79 +1835,6 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- static class ChooserTargetRankingInfo {
- public final List<AppTarget> scores;
- public final UserHandle userHandle;
-
- ChooserTargetRankingInfo(List<AppTarget> chooserTargetScores,
- UserHandle userHandle) {
- this.scores = chooserTargetScores;
- this.userHandle = userHandle;
- }
- }
-
- static class RefinementResultReceiver extends ResultReceiver {
- private ChooserActivity mChooserActivity;
- private TargetInfo mSelectedTarget;
-
- public RefinementResultReceiver(ChooserActivity host, TargetInfo target,
- Handler handler) {
- super(handler);
- mChooserActivity = host;
- mSelectedTarget = target;
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (mChooserActivity == null) {
- Log.e(TAG, "Destroyed RefinementResultReceiver received a result");
- return;
- }
- if (resultData == null) {
- Log.e(TAG, "RefinementResultReceiver received null resultData");
- return;
- }
-
- switch (resultCode) {
- case RESULT_CANCELED:
- mChooserActivity.onRefinementCanceled();
- break;
- case RESULT_OK:
- Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT);
- if (intentParcelable instanceof Intent) {
- mChooserActivity.onRefinementResult(mSelectedTarget,
- (Intent) intentParcelable);
- } else {
- Log.e(TAG, "RefinementResultReceiver received RESULT_OK but no Intent"
- + " in resultData with key Intent.EXTRA_INTENT");
- }
- break;
- default:
- Log.w(TAG, "Unknown result code " + resultCode
- + " sent to RefinementResultReceiver");
- break;
- }
- }
-
- public void destroy() {
- mChooserActivity = null;
- mSelectedTarget = null;
- }
-
- /**
- * Apps can't load this class directly, so we need a regular ResultReceiver copy for
- * sending. Obtain this by parceling and unparceling (one weird trick).
- */
- ResultReceiver copyForSending() {
- Parcel parcel = Parcel.obtain();
- writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
- ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
- parcel.recycle();
- return receiverForSending;
- }
- }
-
/**
* Used in combination with the scene transition when launching the image editor
*/
diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java
new file mode 100644
index 00000000..5997bfed
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java
@@ -0,0 +1,215 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.android.intentresolver.chooser.TargetInfo;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Helper class to manage Sharesheet's "refinement" flow, where callers supply a "refinement
+ * activity" that will be invoked when a target is selected, allowing the calling app to add
+ * additional extras and other refinements (subject to {@link Intent#filterEquals()}), e.g., to
+ * convert the format of the payload, or lazy-download some data that was deferred in the original
+ * call).
+ *
+ * TODO(b/262805893): this currently requires the result to be a refinement of <em>the best</em>
+ * match for the user's selected target among the initially-provided source intents (according to
+ * their originally-provided priority order). In order to support alternate formats/actions, we
+ * should instead require it to refine <em>any</em> of the source intents -- presumably, the first
+ * in priority order that matches according to {@link Intent#filterEquals()}.
+ */
+public final class ChooserRefinementManager {
+ private static final String TAG = "ChooserRefinement";
+
+ @Nullable
+ private final IntentSender mRefinementIntentSender;
+
+ private final Context mContext;
+ private final Consumer<TargetInfo> mOnSelectionRefined;
+ private final Runnable mOnRefinementCancelled;
+
+ @Nullable
+ private RefinementResultReceiver mRefinementResultReceiver;
+
+ public ChooserRefinementManager(
+ Context context,
+ @Nullable IntentSender refinementIntentSender,
+ Consumer<TargetInfo> onSelectionRefined,
+ Runnable onRefinementCancelled) {
+ mContext = context;
+ mRefinementIntentSender = refinementIntentSender;
+ mOnSelectionRefined = onSelectionRefined;
+ mOnRefinementCancelled = onRefinementCancelled;
+ }
+
+ /**
+ * Delegate the user's {@code selectedTarget} 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(TargetInfo selectedTarget) {
+ if (mRefinementIntentSender == null) {
+ return false;
+ }
+ if (selectedTarget.getAllSourceIntents().isEmpty()) {
+ return false;
+ }
+
+ destroy(); // Terminate any prior sessions.
+ mRefinementResultReceiver = new RefinementResultReceiver(
+ refinedIntent -> {
+ destroy();
+ TargetInfo refinedTarget = getValidRefinedTarget(selectedTarget, refinedIntent);
+ if (refinedTarget != null) {
+ mOnSelectionRefined.accept(refinedTarget);
+ } else {
+ mOnRefinementCancelled.run();
+ }
+ },
+ mOnRefinementCancelled);
+
+ Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, selectedTarget);
+ try {
+ mRefinementIntentSender.sendIntent(mContext, 0, refinementRequest, null, null);
+ return true;
+ } catch (SendIntentException e) {
+ Log.e(TAG, "Refinement IntentSender failed to send", e);
+ }
+ return false;
+ }
+
+ /** Clean up any ongoing refinement session. */
+ public void destroy() {
+ if (mRefinementResultReceiver != null) {
+ mRefinementResultReceiver.destroy();
+ mRefinementResultReceiver = null;
+ }
+ }
+
+ private static Intent makeRefinementRequest(
+ RefinementResultReceiver resultReceiver, TargetInfo originalTarget) {
+ final Intent fillIn = new Intent();
+ final List<Intent> sourceIntents = originalTarget.getAllSourceIntents();
+ fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0));
+ if (sourceIntents.size() > 1) {
+ fillIn.putExtra(
+ Intent.EXTRA_ALTERNATE_INTENTS,
+ sourceIntents.subList(1, sourceIntents.size()).toArray());
+ }
+ fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, resultReceiver.copyForSending());
+ return fillIn;
+ }
+
+ private static class RefinementResultReceiver extends ResultReceiver {
+ private final Consumer<Intent> mOnSelectionRefined;
+ private final Runnable mOnRefinementCancelled;
+
+ private boolean mDestroyed;
+
+ RefinementResultReceiver(
+ Consumer<Intent> onSelectionRefined,
+ Runnable onRefinementCancelled) {
+ super(/* handler=*/ null);
+ mOnSelectionRefined = onSelectionRefined;
+ mOnRefinementCancelled = onRefinementCancelled;
+ }
+
+ public void destroy() {
+ mDestroyed = true;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (mDestroyed) {
+ Log.e(TAG, "Destroyed RefinementResultReceiver received a result");
+ return;
+ }
+ if (resultData == null) {
+ Log.e(TAG, "RefinementResultReceiver received null resultData");
+ // TODO: treat as cancellation?
+ return;
+ }
+
+ switch (resultCode) {
+ case Activity.RESULT_CANCELED:
+ mOnRefinementCancelled.run();
+ break;
+ case Activity.RESULT_OK:
+ Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT);
+ if (intentParcelable instanceof Intent) {
+ mOnSelectionRefined.accept((Intent) intentParcelable);
+ } else {
+ Log.e(TAG, "No valid Intent.EXTRA_INTENT in 'OK' refinement result data");
+ }
+ break;
+ default:
+ Log.w(TAG, "Received unknown refinement result " + resultCode);
+ break;
+ }
+ }
+
+ /**
+ * Apps can't load this class directly, so we need a regular ResultReceiver copy for
+ * sending. Obtain this by parceling and unparceling (one weird trick).
+ */
+ ResultReceiver copyForSending() {
+ Parcel parcel = Parcel.obtain();
+ writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return receiverForSending;
+ }
+ }
+
+ private static TargetInfo getValidRefinedTarget(
+ TargetInfo originalTarget, Intent proposedRefinement) {
+ if (originalTarget == null) {
+ // TODO: this legacy log message doesn't seem to describe the real condition we just
+ // checked; probably this method should never be invoked with a null target.
+ Log.e(TAG, "Refinement result intent did not match any known targets; canceling");
+ return null;
+ }
+ if (!checkProposalRefinesSourceIntent(originalTarget, proposedRefinement)) {
+ Log.e(TAG, "Refinement " + proposedRefinement + " has no match in " + originalTarget);
+ return null;
+ }
+ return originalTarget.cloneFilledIn(proposedRefinement, 0); // TODO: select the right base.
+ }
+
+ // TODO: return the actual match, to use as the base that we fill in? Or, if that's handled by
+ // `TargetInfo.cloneFilledIn()`, just let it be nullable (it already is?) and don't bother doing
+ // this pre-check.
+ private static boolean checkProposalRefinesSourceIntent(
+ TargetInfo originalTarget, Intent proposedMatch) {
+ return originalTarget.getAllSourceIntents().stream().anyMatch(proposedMatch::filterEquals);
+ }
+}