diff options
| author | 2023-05-01 21:09:50 +0000 | |
|---|---|---|
| committer | 2023-05-08 16:36:53 +0000 | |
| commit | 7cc1eb3e660decca55d42105835d7935ef444613 (patch) | |
| tree | a12ceb303f0069199d52c88f3d3b2b011ef96648 /java/src | |
| parent | d572ef6c2ba73880edecd66c86835e78763d9400 (diff) | |
Use a ViewState to preserve refinement state
Refinement manager keeps its own state across config changes so that it
can receive callbacks on the ResultReceiver. ChooserActivity can watch
for the state of refinement completion and launch the refined TargetInfo
if refinement succeeds, otherwise just finish().
If onResume happens when refinement is in progress, ChooserActivity will
finish, as this is expected to be abandonment of the refinement process
without a result. But, onResume also happens during a config change
during refinement, and the activity must not finish in those cases. The
logic for this is contained within ChooserRefinementManager, with
ChooserActivity only needing to pass along a couple of lifecycle events.
Add a bunch of test coverage to new and old functionality.
Also fix some kotlin nullability errors in TargetInfo tests that sysui
studio was complaining about.
Bug: 279514914
Test: atest ChooserRefinementManagerTest
Test: manually test the refinement flow in Photos, both completing and
cancelling the process, rotating at various points.
Change-Id: I2b22155894e29b062c78b4af20e8fe0683d40bea
Diffstat (limited to 'java/src')
| -rw-r--r-- | java/src/com/android/intentresolver/ChooserActivity.java | 46 | ||||
| -rw-r--r-- | java/src/com/android/intentresolver/ChooserRefinementManager.java | 117 |
2 files changed, 104 insertions, 59 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 7f55f78f..8067876f 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -72,6 +72,7 @@ import android.view.animation.LinearInterpolator; import android.widget.TextView; import androidx.annotation.MainThread; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; @@ -250,20 +251,25 @@ public class ChooserActivity extends ResolverActivity implements return; } - mRefinementManager = new ChooserRefinementManager( - this, - mChooserRequest.getRefinementIntentSender(), - (validatedRefinedTarget) -> { - maybeRemoveSharedText(validatedRefinedTarget); + 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. - super.onTargetSelected(validatedRefinedTarget, false); - finish(); - }, - this::finish); + ChooserActivity.super.onTargetSelected(targetInfo, false); + } + + finish(); + } + }); mChooserContentPreviewUi = new ChooserContentPreviewUi( mChooserRequest.getTargetIntent(), @@ -611,14 +617,7 @@ public class ChooserActivity extends ResolverActivity implements Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); maybeCancelFinishAnimation(); - if (mRefinementManager.isAwaitingRefinementResult()) { - // This can happen if the refinement activity terminates without ever sending a response - // to our `ResultReceiver`. We're probably not prepared to return the user into a valid - // Chooser session, so we'll treat it as a cancellation instead. - Log.w(TAG, "Chooser resumed while awaiting refinement result; aborting"); - mRefinementManager.destroy(); - finish(); - } + mRefinementManager.onActivityResume(); } @Override @@ -730,6 +729,8 @@ public class ChooserActivity extends ResolverActivity implements @Override protected void onStop() { super.onStop(); + mRefinementManager.onActivityStop(isChangingConfigurations()); + if (maybeCancelFinishAnimation()) { finish(); } @@ -743,11 +744,6 @@ public class ChooserActivity extends ResolverActivity implements mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); } - if (mRefinementManager != null) { // TODO: null-checked in case of early-destroy, or skip? - mRefinementManager.destroy(); - mRefinementManager = null; - } - mBackgroundThreadPoolExecutor.shutdownNow(); destroyProfileRecords(); @@ -873,7 +869,11 @@ public class ChooserActivity extends ResolverActivity implements @Override protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { - if (mRefinementManager.maybeHandleSelection(target)) { + if (mRefinementManager.maybeHandleSelection( + target, + mChooserRequest.getRefinementIntentSender(), + getApplication(), + getMainThreadHandler())) { return false; } updateModelAndChooserCounts(target); diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java index 8d7b1aac..2ebe48a6 100644 --- a/java/src/com/android/intentresolver/ChooserRefinementManager.java +++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java @@ -19,16 +19,19 @@ package com.android.intentresolver; import android.annotation.Nullable; import android.annotation.UiThread; import android.app.Activity; -import android.content.Context; +import android.app.Application; import android.content.Intent; import android.content.IntentSender; -import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.ResultReceiver; import android.util.Log; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + import com.android.intentresolver.chooser.TargetInfo; import java.util.List; @@ -42,38 +45,51 @@ import java.util.function.Consumer; * call). */ @UiThread -public final class ChooserRefinementManager { +public final class ChooserRefinementManager extends ViewModel { 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 // Non-null only during an active refinement session. private RefinementResultReceiver mRefinementResultReceiver; - public ChooserRefinementManager( - Context context, - @Nullable IntentSender refinementIntentSender, - Consumer<TargetInfo> onSelectionRefined, - Runnable onRefinementCancelled) { - mContext = context; - mRefinementIntentSender = refinementIntentSender; - mOnSelectionRefined = onSelectionRefined; - mOnRefinementCancelled = onRefinementCancelled; - } + private boolean mConfigurationChangeInProgress = false; /** - * @return whether a refinement session has been initiated (i.e., an earlier call to - * {@link #maybeHandleSelection(TargetInfo)} returned true), and isn't yet complete. The session - * is complete if the refinement activity calls {@link ResultReceiver#onResultReceived()} (with - * any result), or if it's cancelled on our side by {@link ChooserRefinementManager#destroy()}. + * A token for the completion of a refinement process that can be consumed exactly once. */ - public boolean isAwaitingRefinementResult() { - return (mRefinementResultReceiver != null); + public static class RefinementCompletion { + private TargetInfo mTargetInfo; + private boolean mConsumed; + + RefinementCompletion(TargetInfo targetInfo) { + mTargetInfo = targetInfo; + } + + /** + * @return The output of the completed refinement process. Null if the process was aborted + * or failed. + */ + public TargetInfo getTargetInfo() { + return mTargetInfo; + } + + /** + * Mark this event as consumed if it wasn't already. + * + * @return true if this had not already been consumed. + */ + public boolean consume() { + if (!mConsumed) { + mConsumed = true; + return true; + } + return false; + } + } + + private MutableLiveData<RefinementCompletion> mRefinementCompletion = new MutableLiveData<>(); + + public LiveData<RefinementCompletion> getRefinementCompletion() { + return mRefinementCompletion; } /** @@ -81,8 +97,9 @@ public final class ChooserRefinementManager { * @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) { + public boolean maybeHandleSelection(TargetInfo selectedTarget, + IntentSender refinementIntentSender, Application application, Handler mainHandler) { + if (refinementIntentSender == null) { return false; } if (selectedTarget.getAllSourceIntents().isEmpty()) { @@ -100,33 +117,61 @@ public final class ChooserRefinementManager { mRefinementResultReceiver = new RefinementResultReceiver( refinedIntent -> { destroy(); + TargetInfo refinedTarget = selectedTarget.tryToCloneWithAppliedRefinement(refinedIntent); if (refinedTarget != null) { - mOnSelectionRefined.accept(refinedTarget); + mRefinementCompletion.setValue(new RefinementCompletion(refinedTarget)); } else { Log.e(TAG, "Failed to apply refinement to any matching source intent"); - mOnRefinementCancelled.run(); + mRefinementCompletion.setValue(new RefinementCompletion(null)); } }, () -> { destroy(); - mOnRefinementCancelled.run(); + mRefinementCompletion.setValue(new RefinementCompletion(null)); }, - mContext.getMainThreadHandler()); + mainHandler); Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, selectedTarget); try { - mRefinementIntentSender.sendIntent(mContext, 0, refinementRequest, null, null); + refinementIntentSender.sendIntent(application, 0, refinementRequest, null, null); return true; - } catch (SendIntentException e) { + } catch (IntentSender.SendIntentException e) { Log.e(TAG, "Refinement IntentSender failed to send", e); } - return false; + return true; + } + + /** ChooserActivity has stopped */ + public void onActivityStop(boolean configurationChanging) { + mConfigurationChangeInProgress = configurationChanging; + } + + /** ChooserActivity has resumed */ + public void onActivityResume() { + if (mConfigurationChangeInProgress) { + mConfigurationChangeInProgress = false; + } else { + if (mRefinementResultReceiver != null) { + // This can happen if the refinement activity terminates without ever sending a + // response to our `ResultReceiver`. We're probably not prepared to return the user + // 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)); + } + } + } + + @Override + protected void onCleared() { + // App lifecycle over, time to clean up. + destroy(); } /** Clean up any ongoing refinement session. */ - public void destroy() { + private void destroy() { if (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroyReceiver(); mRefinementResultReceiver = null; |