diff options
| author | 2023-02-13 23:38:29 +0000 | |
|---|---|---|
| committer | 2023-02-17 14:53:32 +0000 | |
| commit | f1576aeb415af53da75ca7dbc9413b04c43dee54 (patch) | |
| tree | ad97ba368e1124b66c558fedd97655f86709d143 /java/src | |
| parent | 9a733ee98ed889b80c6182fa95fa123c8bbdb7b7 (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.java | 172 | ||||
| -rw-r--r-- | java/src/com/android/intentresolver/ChooserRefinementManager.java | 215 |
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); + } +} |