diff options
author | Paul Sowden <paulsowden@google.com> | 2024-04-02 21:58:38 -0700 |
---|---|---|
committer | Copybara-Service <copybara-worker@google.com> | 2024-04-02 21:59:16 -0700 |
commit | 246f462a1c0c051b2e117bf0dfe6a6a8a3d71165 (patch) | |
tree | fbbd6fc55280464a9a1aa0e717d08755e08c2b07 | |
parent | 36c75fb8a7c511b00def718ae4d7df0c562a52b8 (diff) | |
download | robolectric-246f462a1c0c051b2e117bf0dfe6a6a8a3d71165.tar.gz |
Adds support for exercising predictive back animations
Implements an API on `ShadowWindowManagerGlobal` to facilitate testing
predictive back animations (and it can also verify app behavior with the
cancelled motion events and exclusion rects too).
PiperOrigin-RevId: 621395157
5 files changed, 484 insertions, 0 deletions
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java index 8bc4a049d..28185849a 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java @@ -2,22 +2,37 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.app.Activity; import android.content.ClipData; +import android.graphics.Rect; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Looper; +import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.view.View.DragShadowBuilder; +import android.view.View.OnTouchListener; +import android.view.ViewConfiguration; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; +import android.window.OnBackInvokedDispatcher; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import java.util.ArrayList; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; +import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) @@ -60,6 +75,158 @@ public class ShadowWindowManagerGlobalTest { assertThat(decorView.getWindowVisibility()).isEqualTo(View.VISIBLE); } + @SuppressWarnings("MemberName") // In lieu of parameterization. + private void startPredictiveBackGesture_callsBackCallbackMethods(@BackEvent.SwipeEdge int edge) { + ShadowApplication.setEnableOnBackInvokedCallback(true); + float touchSlop = + ViewConfiguration.get(ApplicationProvider.getApplicationContext()).getScaledTouchSlop(); + try (ActivityController<ActivityWithBackCallback> controller = + Robolectric.buildActivity(ActivityWithBackCallback.class)) { + Activity activity = controller.setup().get(); + TestBackAnimationCallback backInvokedCallback = new TestBackAnimationCallback(); + activity + .getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, backInvokedCallback); + + float moveByX = (edge == BackEvent.EDGE_LEFT ? 1 : -1) * touchSlop * 2; + try (ShadowWindowManagerGlobal.PredictiveBackGesture backGesture = + ShadowWindowManagerGlobal.startPredictiveBackGesture(edge)) { + backGesture.moveBy(moveByX, 0f); + } + + assertThat(backInvokedCallback.onBackStarted).isNotNull(); + assertThat(backInvokedCallback.onBackProgressed).isNotEmpty(); + assertThat(Iterables.getLast(backInvokedCallback.onBackProgressed).getTouchX()) + .isEqualTo(backInvokedCallback.onBackStarted.getTouchX() + moveByX); + assertThat(Iterables.getLast(backInvokedCallback.onBackProgressed).getProgress()) + .isGreaterThan(0); + assertThat(backInvokedCallback.onBackInvokedCalled).isTrue(); + } + } + + @Test + @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startPredictiveBackGesture_leftEdge_callsBackCallbackMethods() { + startPredictiveBackGesture_callsBackCallbackMethods(BackEvent.EDGE_LEFT); + } + + @Test + @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startPredictiveBackGesture_rightEdge_callsBackCallbackMethods() { + startPredictiveBackGesture_callsBackCallbackMethods(BackEvent.EDGE_RIGHT); + } + + @Test + @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startPredictiveBackGesture_cancel_callbackIsCancelled() { + ShadowApplication.setEnableOnBackInvokedCallback(true); + try (ActivityController<ActivityWithBackCallback> controller = + Robolectric.buildActivity(ActivityWithBackCallback.class)) { + Activity activity = controller.setup().get(); + TestBackAnimationCallback backInvokedCallback = new TestBackAnimationCallback(); + activity + .getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, backInvokedCallback); + + try (ShadowWindowManagerGlobal.PredictiveBackGesture backGesture = + ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT)) { + backGesture.cancel(); + } + + assertThat(backInvokedCallback.onBackStarted).isNotNull(); + assertThat(backInvokedCallback.onBackCancelledCalled).isTrue(); + } + } + + @Test + @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startPredictiveBackGesture_withExclusion_isNotCalled() { + ShadowApplication.setEnableOnBackInvokedCallback(true); + Display display = ShadowDisplay.getDefaultDisplay(); + try (ActivityController<ActivityWithBackCallback> controller = + Robolectric.buildActivity(ActivityWithBackCallback.class)) { + Activity activity = controller.setup().get(); + TestBackAnimationCallback backInvokedCallback = new TestBackAnimationCallback(); + activity + .getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, backInvokedCallback); + // Exclude the entire display. + activity + .findViewById(android.R.id.content) + .setSystemGestureExclusionRects( + ImmutableList.of(new Rect(0, 0, display.getWidth(), display.getHeight()))); + + ShadowWindowManagerGlobal.PredictiveBackGesture backGesture = + ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT); + + assertThat(backGesture).isNull(); + assertThat(backInvokedCallback.onBackStarted).isNull(); + } + } + + @Test + @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startPredictiveBackGesture_cancelledTouchEventsDispatchedToWindow() { + ShadowApplication.setEnableOnBackInvokedCallback(true); + try (ActivityController<ActivityWithBackCallback> controller = + Robolectric.buildActivity(ActivityWithBackCallback.class)) { + Activity activity = controller.setup().get(); + List<MotionEvent> touchEvents = new ArrayList<>(); + activity + .getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, new TestBackAnimationCallback()); + activity + .findViewById(android.R.id.content) + .setOnTouchListener( + new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + touchEvents.add(event); + return true; + } + }); + + ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT).close(); + + assertThat(touchEvents).isNotEmpty(); + assertThat(touchEvents.get(0).getAction()).isEqualTo(MotionEvent.ACTION_DOWN); + assertThat(Iterables.getLast(touchEvents).getAction()).isEqualTo(MotionEvent.ACTION_CANCEL); + } + } + + @Test + @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startPredictiveBackGesture_invalidPosition_throwsIllegalArgumentException() { + ShadowApplication.setEnableOnBackInvokedCallback(true); + assertThrows( + IllegalArgumentException.class, + () -> ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT, -1f)); + } + + @Test + @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startPredictiveBackGesture_alreadyOngoing_throwsIllegalStateException() { + ShadowApplication.setEnableOnBackInvokedCallback(true); + try (ActivityController<ActivityWithBackCallback> controller = + Robolectric.buildActivity(ActivityWithBackCallback.class)) { + Activity activity = controller.setup().get(); + activity + .getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, new TestBackAnimationCallback()); + + ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT); + + assertThrows( + IllegalStateException.class, + () -> ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT)); + } + } + static final class DragActivity extends Activity { @Override protected void onCreate(@Nullable Bundle bundle) { @@ -81,4 +248,33 @@ public class ShadowWindowManagerGlobalTest { setContentView(contentView); } } + + public static final class ActivityWithBackCallback extends Activity {} + + private static final class TestBackAnimationCallback implements OnBackAnimationCallback { + @Nullable public BackEvent onBackStarted; + public List<BackEvent> onBackProgressed = new ArrayList<>(); + public boolean onBackInvokedCalled = false; + public boolean onBackCancelledCalled = false; + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + onBackStarted = backEvent; + } + + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + onBackProgressed.add(backEvent); + } + + @Override + public void onBackInvoked() { + onBackInvokedCalled = true; + } + + @Override + public void onBackCancelled() { + onBackCancelledCalled = true; + } + } } diff --git a/robolectric/src/test/resources/AndroidManifest.xml b/robolectric/src/test/resources/AndroidManifest.xml index b677954f2..27a3dc9b3 100644 --- a/robolectric/src/test/resources/AndroidManifest.xml +++ b/robolectric/src/test/resources/AndroidManifest.xml @@ -81,6 +81,9 @@ <activity android:name="org.robolectric.shadows.ShadowPackageManagerTest$ActivityWithConfigChanges" android:configChanges="screenLayout|orientation"/> + <activity android:name="org.robolectric.shadows.ShadowWindowManagerGlobalTest$ActivityWithBackCallback" + android:enableOnBackInvokedCallback="true" /> + <activity android:name="org.robolectric.shadows.ShadowActivityTest$LabelTestActivity1" /> <activity android:name="org.robolectric.shadows.ShadowActivityTest$LabelTestActivity2" android:label="@string/activity_name"/> diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java index b726386c8..950193699 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java @@ -16,6 +16,7 @@ import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; @@ -391,4 +392,19 @@ public class ShadowApplication extends ShadowContextWrapper { ShadowContextImpl shadowContext = Shadow.extract(realApplication.getBaseContext()); shadowContext.setSystemService(key, service); } + + /** + * Enables or disables predictive back for the current application. + * + * <p>This is the equivalent of specifying {code android:enableOnBackInvokedCallback} on the + * {@code <application>} tag in the Android manifest. + */ + public static void setEnableOnBackInvokedCallback(boolean isEnabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ShadowWindowOnBackInvokedDispatcher.setEnablePredictiveBack(isEnabled); + RuntimeEnvironment.getApplication() + .getApplicationInfo() + .setEnableOnBackInvokedCallback(isEnabled); + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java index 4f988c39f..5d63e093c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java @@ -2,22 +2,36 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.P; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.Math.max; +import static java.lang.Math.round; import static org.robolectric.shadows.ShadowView.useRealGraphics; import static org.robolectric.util.reflector.Reflector.reflector; +import android.annotation.FloatRange; import android.annotation.Nullable; import android.app.Instrumentation; import android.content.ClipData; import android.content.Context; +import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; +import android.util.Log; import android.view.IWindowManager; import android.view.IWindowSession; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.WindowManagerGlobal; +import android.window.BackEvent; +import android.window.BackMotionEvent; +import android.window.OnBackInvokedCallbackInfo; +import java.io.Closeable; import java.lang.reflect.Proxy; import java.util.List; import org.robolectric.RuntimeEnvironment; @@ -72,6 +86,199 @@ public class ShadowWindowManagerGlobal { windowSessionDelegate.lastDragClipData = null; } + /** + * Ongoing predictive back gesture. + * + * <p>Start a predictive back gesture by calling {@link + * ShadowWindowManagerGlobal#startPredictiveBackGesture}. One or more drag progress events can be + * dispatched by calling {@link #moveBy}. The gesture must be ended by either calling {@link + * #cancel()} or {@link #close()}, if {@link #cancel()} is called a subsequent call to {@link + * close()} will do nothing to allow using the gesture in a try with resources statement: + * + * <pre> + * try (PredictiveBackGesture backGesture = + * ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT)) { + * backGesture.moveBy(10, 10); + * } + * </pre> + */ + public static final class PredictiveBackGesture implements Closeable { + @BackEvent.SwipeEdge private final int edge; + private final int displayWidth; + private final float startTouchX; + private final float progressThreshold; + private float touchX; + private float touchY; + private boolean isCancelled; + private boolean isFinished; + + private PredictiveBackGesture( + @BackEvent.SwipeEdge int edge, int displayWidth, float touchX, float touchY) { + this.edge = edge; + this.displayWidth = displayWidth; + this.progressThreshold = + ViewConfiguration.get(RuntimeEnvironment.getApplication()).getScaledTouchSlop(); + this.startTouchX = touchX; + this.touchX = touchX; + this.touchY = touchY; + } + + /** Dispatches drag progress for a predictive back gesture. */ + public void moveBy(float dx, float dy) { + checkState(!isCancelled && !isFinished); + try { + touchX += dx; + touchY += dy; + ShadowWindowManagerGlobal.windowSessionDelegate + .onBackInvokedCallbackInfo + .getCallback() + .onBackProgressed( + BackMotionEvents.newBackMotionEvent(edge, touchX, touchY, caclulateProgress())); + ShadowLooper.idleMainLooper(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Cancels the back gesture. */ + public void cancel() { + checkState(!isCancelled && !isFinished); + isCancelled = true; + try { + ShadowWindowManagerGlobal.windowSessionDelegate + .onBackInvokedCallbackInfo + .getCallback() + .onBackCancelled(); + ShadowWindowManagerGlobal.windowSessionDelegate.currentPredictiveBackGesture = null; + ShadowLooper.idleMainLooper(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Ends the back gesture. If the back gesture has not been cancelled by calling {@link + * #cancel()} then the back handler is invoked. + * + * <p>Callers should always call either {@link #cancel()} or {@link #close()}. It is recommended + * to use the result of {@link ShadowWindowManagerGlobal#startPredictiveBackGesture} in a try + * with resources. + */ + @Override + public void close() { + checkState(!isFinished); + isFinished = true; + if (!isCancelled) { + try { + ShadowWindowManagerGlobal.windowSessionDelegate + .onBackInvokedCallbackInfo + .getCallback() + .onBackInvoked(); + ShadowWindowManagerGlobal.windowSessionDelegate.currentPredictiveBackGesture = null; + ShadowLooper.idleMainLooper(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + + private float caclulateProgress() { + // The real implementation anchors the progress on the start x and resets it each time the + // threshold is lost, it also calculates a linear and non linear progress area. This + // implementation is much simpler. + int direction = (edge == BackEvent.EDGE_LEFT ? 1 : -1); + float draggableWidth = + (edge == BackEvent.EDGE_LEFT ? displayWidth - startTouchX : startTouchX) + - progressThreshold; + return max((((touchX - startTouchX) * direction) - progressThreshold) / draggableWidth, 0f); + } + } + + /** + * Starts a predictive back gesture in the center of the edge. See {@link + * #startPredictiveBackGesture(int, float)}. + */ + @Nullable + public static PredictiveBackGesture startPredictiveBackGesture(@BackEvent.SwipeEdge int edge) { + return startPredictiveBackGesture(edge, 0.5f); + } + + /** + * Starts a predictive back gesture. + * + * <p>If no active activity with a back pressed callback that supports animations is registered + * then null will be returned. See {@link PredictiveBackGesture}. + * + * <p>See {@link ShadowApplication#setEnableOnBackInvokedCallback}. + * + * @param position The position on edge of the window + */ + @Nullable + public static PredictiveBackGesture startPredictiveBackGesture( + @BackEvent.SwipeEdge int edge, @FloatRange(from = 0f, to = 1f) float position) { + checkArgument(position >= 0f && position <= 1f, "Invalid position: %s.", position); + checkState( + windowSessionDelegate.currentPredictiveBackGesture == null, + "Current predictive back gesture in progress."); + if (windowSessionDelegate.onBackInvokedCallbackInfo == null + || !windowSessionDelegate.onBackInvokedCallbackInfo.isAnimationCallback()) { + return null; + } else { + try { + // Exclusion rects are sent to the window session by posting so idle the looper first. + ShadowLooper.idleMainLooper(); + int touchSlop = + ViewConfiguration.get(RuntimeEnvironment.getApplication()).getScaledTouchSlop(); + int displayWidth = ShadowDisplay.getDefaultDisplay().getWidth(); + float deltaX = (edge == BackEvent.EDGE_LEFT ? 1 : -1) * touchSlop / 2f; + float downX = (edge == BackEvent.EDGE_LEFT ? 0 : displayWidth) + deltaX; + float downY = ShadowDisplay.getDefaultDisplay().getHeight() * position; + if (windowSessionDelegate.systemGestureExclusionRects != null) { + // TODO: The rects should be offset based on the window's position in the display, most + // windows should be full screen which makes this naive logic work ok. + for (Rect rect : windowSessionDelegate.systemGestureExclusionRects) { + if (rect.contains(round(downX), round(downY))) { + return null; + } + } + } + // A predictive back gesture starts as a user swipe which the window will receive the start + // of the gesture before it gets intercepted by the window manager. + MotionEvent downEvent = + MotionEvent.obtain( + /* downTime= */ SystemClock.uptimeMillis(), + /* eventTime= */ SystemClock.uptimeMillis(), + MotionEvent.ACTION_DOWN, + downX, + downY, + /* metaState= */ 0); + MotionEvent moveEvent = MotionEvent.obtain(downEvent); + moveEvent.setAction(MotionEvent.ACTION_MOVE); + moveEvent.offsetLocation(deltaX, 0); + MotionEvent cancelEvent = MotionEvent.obtain(moveEvent); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL); + ShadowUiAutomation.injectInputEvent(downEvent); + ShadowUiAutomation.injectInputEvent(moveEvent); + ShadowUiAutomation.injectInputEvent(cancelEvent); + windowSessionDelegate + .onBackInvokedCallbackInfo + .getCallback() + .onBackStarted( + BackMotionEvents.newBackMotionEvent( + edge, downX + 2 * deltaX, downY, /* progress= */ 0)); + ShadowLooper.idleMainLooper(); + PredictiveBackGesture backGesture = + new PredictiveBackGesture(edge, displayWidth, downX + 2 * deltaX, downY); + windowSessionDelegate.currentPredictiveBackGesture = backGesture; + return backGesture; + } catch (RemoteException e) { + Log.e("ShadowWindowManagerGlobal", "Failed to start back gesture", e); + return null; + } + } + } + + @SuppressWarnings("unchecked") // Cast args to IWindowSession methods @Implementation protected static synchronized IWindowSession getWindowSession() { if (windowSession == null) { @@ -99,6 +306,13 @@ public class ShadowWindowManagerGlobal { case "setInTouchMode": windowSessionDelegate.setInTouchMode((boolean) args[0]); return null; + case "setOnBackInvokedCallbackInfo": + windowSessionDelegate.onBackInvokedCallbackInfo = + (OnBackInvokedCallbackInfo) args[1]; + return null; + case "reportSystemGestureExclusionChanged": + windowSessionDelegate.systemGestureExclusionRects = (List<Rect>) args[1]; + return null; default: return ReflectionHelpers.defaultValueForType( method.getReturnType().getName()); @@ -155,6 +369,9 @@ public class ShadowWindowManagerGlobal { // TODO: Default to touch mode always. private boolean inTouchMode = useRealGraphics(); @Nullable protected ClipData lastDragClipData; + @Nullable private OnBackInvokedCallbackInfo onBackInvokedCallbackInfo; + @Nullable private List<Rect> systemGestureExclusionRects; + @Nullable private PredictiveBackGesture currentPredictiveBackGesture; protected int getAddFlags() { int res = 0; @@ -196,4 +413,20 @@ public class ShadowWindowManagerGlobal { throw new AssertionError("Missing ClipData param"); } } + + private static class BackMotionEvents { + private BackMotionEvents() {} + + static BackMotionEvent newBackMotionEvent( + @BackEvent.SwipeEdge int edge, float touchX, float touchY, float progress) { + return new BackMotionEvent( + touchX, + touchY, + progress, + /* velocityX= */ 0f, + /* velocityY= */ 0f, + edge, + /* departingAnimationTarget= */ null); + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowOnBackInvokedDispatcher.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowOnBackInvokedDispatcher.java new file mode 100644 index 000000000..98b199d1f --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowOnBackInvokedDispatcher.java @@ -0,0 +1,36 @@ +package org.robolectric.shadows; + +import android.os.Build; +import android.window.WindowOnBackInvokedDispatcher; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; +import org.robolectric.util.ReflectionHelpers; + +/** Shadow for {@link WindowOnBackInvokedDispatcher}. */ +@Implements( + value = WindowOnBackInvokedDispatcher.class, + minSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, + isInAndroidSdk = false) +public class ShadowWindowOnBackInvokedDispatcher { + private static final boolean ENABLE_PREDICTIVE_BACK_DEFAULT = + RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + ? ReflectionHelpers.getStaticField( + WindowOnBackInvokedDispatcher.class, "ENABLE_PREDICTIVE_BACK") + : false; + + static void setEnablePredictiveBack(boolean isEnabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ReflectionHelpers.setStaticField( + WindowOnBackInvokedDispatcher.class, "ENABLE_PREDICTIVE_BACK", isEnabled); + } + } + + @Resetter + public static void reset() { + ReflectionHelpers.setStaticField( + WindowOnBackInvokedDispatcher.class, + "ENABLE_PREDICTIVE_BACK", + ENABLE_PREDICTIVE_BACK_DEFAULT); + } +} |