diff options
author | Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> | 2024-04-15 04:27:31 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2024-04-15 04:27:31 +0000 |
commit | b3b17db94476c25225589165fbf53ec938f5b6dc (patch) | |
tree | f11775470d93f8e96cf44f5466f7a7c10a0edc9c /shadows/framework | |
parent | 300f6ab8d044ea63c3fbe4b53fc0ca726f8cf573 (diff) | |
parent | acf14fc6bfb8c1c9a7cbb748bf841c4a2613258f (diff) | |
download | robolectric-b3b17db94476c25225589165fbf53ec938f5b6dc.tar.gz |
Merge changes from topic "merge" into main
* changes:
Cleanup vs upstream.
Merge branch 'upstream-google' of sso://android.googlesource.com/platform/external/robolectric into update
Diffstat (limited to 'shadows/framework')
11 files changed, 452 insertions, 50 deletions
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/InformationElementBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/InformationElementBuilder.java new file mode 100644 index 000000000..493244daa --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/InformationElementBuilder.java @@ -0,0 +1,50 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.R; + +import android.net.wifi.ScanResult.InformationElement; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.robolectric.RuntimeEnvironment; + +/** Builder for {@link InformationElement}. */ +public class InformationElementBuilder { + private int id; + private int idExt; + private byte[] bytes; + + private InformationElementBuilder() {} + + public static InformationElementBuilder newBuilder() { + return new InformationElementBuilder(); + } + + @CanIgnoreReturnValue + public InformationElementBuilder setId(int id) { + this.id = id; + return this; + } + + @CanIgnoreReturnValue + public InformationElementBuilder setIdExt(int idExt) { + this.idExt = idExt; + return this; + } + + @CanIgnoreReturnValue + public InformationElementBuilder setBytes(byte[] bytes) { + this.bytes = bytes; + return this; + } + + public InformationElement build() { + if (RuntimeEnvironment.getApiLevel() > R) { + return new InformationElement(this.id, this.idExt, this.bytes); + } else { + InformationElement info = new InformationElement(); + info.id = this.id; + info.idExt = this.idExt; + info.bytes = this.bytes; + return info; + } + } +} 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/ShadowFileIntegrityManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFileIntegrityManager.java new file mode 100644 index 000000000..f00259319 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFileIntegrityManager.java @@ -0,0 +1,28 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.R; + +import android.security.FileIntegrityManager; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +/** Shadow for {@link FileIntegrityManager}. */ +@Implements(value = FileIntegrityManager.class, minSdk = R) +public class ShadowFileIntegrityManager { + + private boolean isApkVeritySupported = true; + + /** Sets the value of {@link #isApkVeritySupported}. */ + public void setIsApkVeritySupported(boolean isApkVeritySupported) { + this.isApkVeritySupported = isApkVeritySupported; + } + + /** + * Returns {@code true} by default, or can be changed by {@link + * #setIsApkVeritySupported(boolean)}. + */ + @Implementation + protected boolean isApkVeritySupported() { + return isApkVeritySupported; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java deleted file mode 100644 index 76fc9a172..000000000 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.robolectric.shadows; - -import android.net.wifi.ScanResult; -import android.os.Build.VERSION_CODES; -import org.robolectric.annotation.Implements; - -/** Shadow for {@link android.net.wifi.ScanResult.InformationElement}. */ -@Implements(value = ScanResult.InformationElement.class, minSdk = VERSION_CODES.R) -public class ShadowInformationElement { - /** - * A builder for creating ShadowInformationElement objects. Use build() to return the - * InformationElement object. - */ - public static class Builder { - private final ScanResult.InformationElement informationElement; - - public Builder() { - informationElement = new ScanResult.InformationElement(); - } - - public Builder setId(int id) { - informationElement.id = id; - return this; - } - - public Builder setIdExt(int idExt) { - informationElement.idExt = idExt; - return this; - } - - public Builder setBytes(byte[] bytes) { - informationElement.bytes = bytes; - return this; - } - - public ScanResult.InformationElement build() { - return informationElement; - } - } -} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java index e33bc335b..fe74b37f1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java @@ -82,6 +82,7 @@ import android.os.storage.IStorageManager; import android.permission.ILegacyPermissionManager; import android.permission.IPermissionManager; import android.safetycenter.ISafetyCenterManager; +import android.security.IFileIntegrityService; import android.speech.IRecognitionServiceManager; import android.uwb.IUwbAdapter; import android.view.IWindowManager; @@ -242,6 +243,7 @@ public class ShadowServiceManager { addBinderService(binderServices, Context.TETHERING_SERVICE, ITetheringConnector.class); addBinderService(binderServices, "telephony.registry", ITelephonyRegistry.class); addBinderService(binderServices, Context.PLATFORM_COMPAT_SERVICE, IPlatformCompat.class); + addBinderService(binderServices, Context.FILE_INTEGRITY_SERVICE, IFileIntegrityService.class); } if (RuntimeEnvironment.getApiLevel() >= S) { addBinderService(binderServices, "permissionmgr", IPermissionManager.class); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java index 4c8c369fa..bdf1d08bd 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java @@ -153,6 +153,7 @@ public class ShadowTelephonyManager { private static final Map<Integer, String> simCountryIsoMap = Collections.synchronizedMap(new LinkedHashMap<>()); private int simCarrierId; + private int simSpecificCarrierId; private CharSequence simCarrierIdName; private int carrierIdFromSimMccMnc; private String subscriberId; @@ -177,6 +178,7 @@ public class ShadowTelephonyManager { private VisualVoicemailSmsFilterSettings visualVoicemailSmsFilterSettings; private static volatile boolean emergencyCallbackMode; private static Map<Integer, List<EmergencyNumber>> emergencyNumbersList; + private static volatile boolean isDataRoamingEnabled; /** * Should be {@link TelephonyManager.BootstrapAuthenticationCallback} but this object was @@ -1226,6 +1228,16 @@ public class ShadowTelephonyManager { this.simCarrierId = simCarrierId; } + @Implementation(minSdk = Q) + protected int getSimSpecificCarrierId() { + return simSpecificCarrierId; + } + + /** Sets the value to be returned by {@link #getSimSpecificCarrierId()}. */ + public void setSimSpecificCarrierId(int simSpecificCarrierId) { + this.simSpecificCarrierId = simSpecificCarrierId; + } + @Implementation(minSdk = P) protected CharSequence getSimCarrierIdName() { return simCarrierIdName; @@ -1633,4 +1645,21 @@ public class ShadowTelephonyManager { } return ImmutableMap.of(); } + + /** + * Implementation for {@link TelephonyManager#isDataRoamingEnabled}. + * + * @return False by default, unless set with {@link #setDataRoamingEnabled(boolean)}. + */ + @Implementation(minSdk = Q) + protected boolean isDataRoamingEnabled() { + checkReadPhoneStatePermission(); + return isDataRoamingEnabled; + } + + /** Sets the value to be returned by {@link #isDataRoamingEnabled()} */ + @Implementation(minSdk = Q) + protected void setDataRoamingEnabled(boolean isDataRoamingEnabled) { + ShadowTelephonyManager.isDataRoamingEnabled = isDataRoamingEnabled; + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTranslationManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTranslationManager.java index ed33b5928..9cd316944 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTranslationManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTranslationManager.java @@ -11,7 +11,11 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; /** Shadow for {@link TranslationManager}. */ -@Implements(value = TranslationManager.class, minSdk = VERSION_CODES.S) +@Implements( + value = TranslationManager.class, + minSdk = VERSION_CODES.S, + isInAndroidSdk = false // turn off shadowOf generation + ) public class ShadowTranslationManager { private final Table<Integer, Integer, ImmutableSet<TranslationCapability>> onDeviceTranslationCapabilities = HashBasedTable.create(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java index 78a96ec31..1fef4d09d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java @@ -76,6 +76,7 @@ public class ShadowUserManager { public static final int FLAG_PROFILE = UserInfo.FLAG_PROFILE; public static final int FLAG_FULL = UserInfo.FLAG_FULL; public static final int FLAG_SYSTEM = UserInfo.FLAG_SYSTEM; + public static final int FLAG_MAIN = UserInfo.FLAG_MAIN; private static int maxSupportedUsers = DEFAULT_MAX_SUPPORTED_USERS; private static boolean isMultiUserSupported = false; @@ -131,13 +132,10 @@ public class ShadowUserManager { private int nextUserId = DEFAULT_SECONDARY_USER_ID; - // TODO: use UserInfo.FLAG_MAIN when available - private static final int FLAG_MAIN = 0x00004000; - public UserManagerState() { int id = UserHandle.USER_SYSTEM; String name = "system_user"; - int flags = UserInfo.FLAG_PRIMARY | UserInfo.FLAG_ADMIN | FLAG_MAIN; + int flags = UserInfo.FLAG_PRIMARY | UserInfo.FLAG_ADMIN | UserInfo.FLAG_MAIN; userSerialNumbers.put(id, (long) id); // Start the user as shut down. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java index 20749faea..86c2bd02e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java @@ -520,4 +520,3 @@ public class ShadowViewRootImpl { WindowInsets getWindowInsets(boolean forceConstruct); } } - 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..456609dea 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,38 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +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.RemoteAnimationTarget; 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; @@ -26,15 +42,13 @@ import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; /** Shadow for {@link WindowManagerGlobal}. */ @SuppressWarnings("unused") // Unused params are implementations of Android SDK methods. -@Implements( - value = WindowManagerGlobal.class, - isInAndroidSdk = false, - looseSignatures = true) +@Implements(value = WindowManagerGlobal.class, isInAndroidSdk = false, looseSignatures = true) public class ShadowWindowManagerGlobal { private static WindowSessionDelegate windowSessionDelegate = new WindowSessionDelegate(); private static IWindowSession windowSession; @@ -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,67 @@ public class ShadowWindowManagerGlobal { throw new AssertionError("Missing ClipData param"); } } + + @ForType(BackMotionEvent.class) + interface BackMotionEventReflector { + @Constructor + BackMotionEvent newBackMotionEvent( + float touchX, + float touchY, + float progress, + float velocityX, + float velocityY, + int swipeEdge, + RemoteAnimationTarget departingAnimationTarget); + + @Constructor + BackMotionEvent newBackMotionEventV( + float touchX, + float touchY, + float progress, + float velocityX, + float velocityY, + boolean triggerBack, + int swipeEdge, + RemoteAnimationTarget departingAnimationTarget); + } + + private static class BackMotionEvents { + private BackMotionEvents() {} + + static BackMotionEvent newBackMotionEvent( + @BackEvent.SwipeEdge int edge, float touchX, float touchY, float progress) { + if (RuntimeEnvironment.getApiLevel() >= UPSIDE_DOWN_CAKE) { + try { + return reflector(BackMotionEventReflector.class) + .newBackMotionEventV( + touchX, + touchY, + progress, + 0f, // velocity x + 0f, // velocity y + Boolean.FALSE, // trigger back + edge, // swipe edge + null); + } catch (Throwable t) { + if (NoSuchMethodException.class.isInstance(t) || AssertionError.class.isInstance(t)) { + // fall through, assuming (perhaps falsely?) this exception is thrown by reflector(), + // and not the method reflected in to. + } else { + if (RuntimeException.class.isInstance(t)) { + throw (RuntimeException) t; + } else { + throw new RuntimeException(t); + } + } + } + } + return reflector(BackMotionEventReflector.class) + .newBackMotionEvent( + touchX, touchY, progress, 0f, // velocity x + 0f, // velocity y + edge, // swipe edge + 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); + } +} |