aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Sowden <paulsowden@google.com>2024-04-02 21:58:38 -0700
committerCopybara-Service <copybara-worker@google.com>2024-04-02 21:59:16 -0700
commit246f462a1c0c051b2e117bf0dfe6a6a8a3d71165 (patch)
treefbbd6fc55280464a9a1aa0e717d08755e08c2b07
parent36c75fb8a7c511b00def718ae4d7df0c562a52b8 (diff)
downloadrobolectric-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
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java196
-rw-r--r--robolectric/src/test/resources/AndroidManifest.xml3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java16
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java233
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowOnBackInvokedDispatcher.java36
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);
+ }
+}