From 7cc1eb3e660decca55d42105835d7935ef444613 Mon Sep 17 00:00:00 2001 From: 1 Date: Mon, 1 May 2023 21:09:50 +0000 Subject: 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 --- .../android/intentresolver/ChooserActivity.java | 46 ++++---- .../intentresolver/ChooserRefinementManager.java | 117 ++++++++++++++------- 2 files changed, 104 insertions(+), 59 deletions(-) (limited to 'java/src') 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 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 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 mRefinementCompletion = new MutableLiveData<>(); + + public LiveData 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; -- cgit v1.2.3-59-g8ed1b