From 491e1d3c1669a5ab219ebe1d806eaf734ea84b13 Mon Sep 17 00:00:00 2001 From: Googler Date: Mon, 2 Oct 2023 15:35:55 -0700 Subject: [PATCH 01/33] Partial support for native ImageReader. Supports ImageReader native creation as well as getting the last/next image. There is no support for Support.lockCanvas nor for ImageReader.OnImageAvailableListener. The current support only works for S+. Added ShadowPublicFormatUtils to support ImageReader in T and above. PiperOrigin-RevId: 570199936 --- .../nativeruntime/ImageReaderNatives.java | 77 +++++++++++ .../ImageReaderSurfaceImageNatives.java | 47 +++++++ .../interceptors/AndroidInterceptors.java | 40 +++++- .../interceptors/AndroidInterceptorsTest.java | 39 ++++++ .../shadows/GraphicsShadowPicker.java | 6 +- .../shadows/ShadowNativeImageReader.java | 127 ++++++++++++++++++ .../ShadowNativeImageReaderSurfaceImage.java | 65 +++++++++ .../shadows/ShadowNativeSurface.java | 4 +- .../shadows/ShadowPublicFormatUtils.java | 34 +++++ 9 files changed, 436 insertions(+), 3 deletions(-) create mode 100644 nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderNatives.java create mode 100644 nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderSurfaceImageNatives.java create mode 100644 sandbox/src/test/java/org/robolectric/interceptors/AndroidInterceptorsTest.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReader.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReaderSurfaceImage.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowPublicFormatUtils.java diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderNatives.java new file mode 100644 index 00000000000..2b7035b863f --- /dev/null +++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderNatives.java @@ -0,0 +1,77 @@ +package org.robolectric.nativeruntime; + +import android.media.Image; +import android.media.ImageReader; +import android.view.Surface; + +/** + * Native methods for {@link ImageReader} JNI registration. + * + *

Native method signatures are derived from + * + *

+ * API 33 (T, Android 13)
+ * https://cs.android.com/android/platform/superproject/+/android-13.0.0_r1:frameworks/base/media/java/android/media/ImageReader.java
+ *
+ * API 31/32 (S, S_V2, Android 12)
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/media/java/android/media/ImageReader.java
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/media/jni/android_media_ImageReader.cpp
+ *
+ * API 30 (R, Android 11)
+ * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/media/java/android/media/ImageReader.java
+ *
+ * API 29 (Q, Android 10)
+ * https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/media/java/android/media/ImageReader.java
+ * 
+ */ +public final class ImageReaderNatives { + + // Name must match gImageReaderClassInfo fields in android_media_ImageReader.cpp + public long mNativeContext; + + // Name must match gImageReaderClassInfo fields in android_media_ImageReader.cpp + public static void postEventFromNative(Object o) { + throw new IllegalStateException("ImageReaderNatives.postEventFromNative is not implemented"); + } + + /** Returned by nativeImageSetup when acquiring the image was successful. */ + public static final int ACQUIRE_SUCCESS = 0; + + /** + * Returned by nativeImageSetup when we couldn't acquire the buffer, because there were no buffers + * available to acquire. + */ + public static final int ACQUIRE_NO_BUFS = 1; + + /** + * Returned by nativeImageSetup when we couldn't acquire the buffer because the consumer has + * already acquired {@code maxImages} and cannot acquire more than that. + */ + public static final int ACQUIRE_MAX_IMAGES = 2; + + public synchronized native void nativeInit( + Object weakSelf, int w, int h, int fmt, int maxImgs, long consumerUsage); // Q-S only + + public synchronized native void nativeClose(); // Q+ + + public synchronized native void nativeReleaseImage(Image i); // Q+ + + public synchronized native Surface nativeGetSurface(); // Q+ + + public synchronized native int nativeDetachImage(Image i); // Q+ + + public synchronized native void nativeDiscardFreeBuffers(); // Q+ + + /** + * @return A return code {@code ACQUIRE_*} + * @see #ACQUIRE_SUCCESS + * @see #ACQUIRE_NO_BUFS + * @see #ACQUIRE_MAX_IMAGES + */ + public synchronized native int nativeImageSetup(Image i); // Q-S, U, excluding T + + /** We use a class initializer to allow the native code to cache some field offsets. */ + public static native void nativeClassInit(); // Q+ + + public ImageReaderNatives() {} +} diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderSurfaceImageNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderSurfaceImageNatives.java new file mode 100644 index 00000000000..80847e0f220 --- /dev/null +++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageReaderSurfaceImageNatives.java @@ -0,0 +1,47 @@ +package org.robolectric.nativeruntime; + +import android.media.ImageReader; + +/** + * Native methods for {@link ImageReader} JNI registration. + * + *

Native method signatures are derived from: + * + *

+ * API 31/32 (S, S_V2, Android 12, all above)
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/media/java/android/media/ImageReader.java
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/media/jni/android_media_ImageReader.cpp
+ *
+ * API 30 (R, Android 11)
+ * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/media/java/android/media/ImageReader.java
+ *
+ * API 29 (Q, Android 10)
+ * https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/media/java/android/media/ImageReader.java
+ * 
+ */ +public final class ImageReaderSurfaceImageNatives { + + public synchronized native /*SurfacePlane[]*/ Object[] nativeCreatePlanes( + int numPlanes, int readerFormat, long readerUsage); // S+, not Q or R + + public synchronized native int nativeGetWidth(); + + public synchronized native int nativeGetHeight(); + + public synchronized native int nativeGetFormat(int readerFormat); + + /** + * RNG-specific native trampoline methods to invoke the native member functions on the proper + * SurfaceImage object reference. + */ + public static native Object[] nativeSurfaceImageCreatePlanes( + Object realSurfaceImage, int numPlanes, int readerFormat, long readerUsage); + + public static native int nativeSurfaceImageGetWidth(Object realSurfaceImage); + + public static native int nativeSurfaceImageGetHeight(Object realSurfaceImage); + + public static native int nativeSurfaceImageGetFormat(Object realSurfaceImage, int readerFormat); + + public ImageReaderSurfaceImageNatives() {} +} diff --git a/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java b/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java index 219b8fcf63f..156713d0671 100644 --- a/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java +++ b/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java @@ -18,6 +18,7 @@ import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.net.Socket; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -46,7 +47,8 @@ public static Collection all() { new FileDescriptorInterceptor(), new NoOpInterceptor(), new SocketInterceptor(), - new ReferenceRefersToInterceptor())); + new ReferenceRefersToInterceptor(), + new NioUtilsFreeDirectBufferInterceptor())); if (Util.getJavaVersion() >= 9) { interceptors.add(new CleanerInterceptor()); @@ -492,4 +494,40 @@ public MethodHandle getMethodHandle(String methodName, MethodType type) getClass(), METHOD, methodType(boolean.class, Reference.class, Object.class)); } } + + /** + * AndroidInterceptor for NioUtils.freeDirectBuffer. + * + *

This method is invoked by ImageReader.java. The class is not part of the JDK. + */ + public static class NioUtilsFreeDirectBufferInterceptor extends Interceptor { + private static final String METHOD = "freeDirectBuffer"; + + public NioUtilsFreeDirectBufferInterceptor() { + super(new MethodRef("java.nio.NioUtils", METHOD)); + } + + static void freeDirectBuffer(ByteBuffer buffer) { + // Following the layoutlib/java/NioUtils_Delegate.java implementation, this is a no-op: "it + // does not seem we have to do anything in here as we are only referencing the existing native + // buffer and do not perform any allocation on creation." + // + // Note: for the interceptor to work, this method _must_ be present even though it doesn't + // do anything. Otherwise the bytecode can't reference it at runtime. + } + + @Override + public Function handle(MethodSignature methodSignature) { + return (theClass, value, params) -> { + freeDirectBuffer((ByteBuffer) value); + return null; + }; + } + + @Override + public MethodHandle getMethodHandle(String methodName, MethodType type) + throws NoSuchMethodException, IllegalAccessException { + return lookup.findStatic(getClass(), METHOD, methodType(void.class, ByteBuffer.class)); + } + } } diff --git a/sandbox/src/test/java/org/robolectric/interceptors/AndroidInterceptorsTest.java b/sandbox/src/test/java/org/robolectric/interceptors/AndroidInterceptorsTest.java new file mode 100644 index 00000000000..fa263b5affa --- /dev/null +++ b/sandbox/src/test/java/org/robolectric/interceptors/AndroidInterceptorsTest.java @@ -0,0 +1,39 @@ +package org.robolectric.interceptors; + +import static com.google.common.truth.Truth.assertThat; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.robolectric.internal.bytecode.Interceptor; +import org.robolectric.internal.bytecode.Interceptors; +import org.robolectric.internal.bytecode.MethodSignature; +import org.robolectric.util.Function; + +@RunWith(JUnit4.class) +public class AndroidInterceptorsTest { + @Test + public void testNioUtilsFreeDirectBufferInterceptor() + throws NoSuchMethodException, IllegalAccessException { + + Interceptors interceptors = new Interceptors(AndroidInterceptors.all()); + Interceptor interceptor = interceptors.findInterceptor("java.nio.NioUtils", "freeDirectBuffer"); + assertThat(interceptor).isNotNull(); + + MethodHandle methodHandle = + interceptor.getMethodHandle( + "freeDirectBuffer", + MethodType.methodType(void.class, new Class[] {ByteBuffer.class})); + assertThat(methodHandle).isNotNull(); + + Function function = + interceptor.handle( + MethodSignature.parse("java.nio.NioUtils/freeDirectBuffer(Ljava.nio.ByteBuffer;)V")); + assertThat(function).isNotNull(); + // Actual invocation is a no-op. + function.call(/* theClass= */ null, ByteBuffer.allocate(0), /* params= */ null); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/GraphicsShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/GraphicsShadowPicker.java index 8916375dcaa..abef23460f8 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/GraphicsShadowPicker.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/GraphicsShadowPicker.java @@ -22,11 +22,15 @@ public GraphicsShadowPicker( @Override public Class pickShadowClass() { - if (RuntimeEnvironment.getApiLevel() >= O + if (RuntimeEnvironment.getApiLevel() >= getMinApiLevel() && ConfigurationRegistry.get(GraphicsMode.Mode.class) == Mode.NATIVE) { return nativeShadowClass; } else { return legacyShadowClass; } } + + protected int getMinApiLevel() { + return O; + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReader.java new file mode 100644 index 00000000000..72d5d0cf61a --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReader.java @@ -0,0 +1,127 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.S_V2; + +import android.media.Image; +import android.media.ImageReader; +import android.view.Surface; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.ReflectorObject; +import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; +import org.robolectric.nativeruntime.ImageReaderNatives; +import org.robolectric.shadows.ShadowNativeImageReader.Picker; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.T; +import org.robolectric.versioning.AndroidVersions.U; + +/** Shadow for {@link ImageReader} that is backed by native code */ +@Implements( + value = ImageReader.class, + minSdk = Q, + looseSignatures = true, + isInAndroidSdk = false, + shadowPicker = Picker.class) +public class ShadowNativeImageReader { + + @ReflectorObject private ImageReaderReflector imageReaderReflector; + private final ImageReaderNatives natives = new ImageReaderNatives(); + + @Implementation(maxSdk = S_V2) + protected synchronized void nativeInit( + Object weakSelf, int w, int h, int fmt, int maxImgs, long consumerUsage) { + natives.nativeInit(weakSelf, w, h, fmt, maxImgs, consumerUsage); + imageReaderReflector.setMemberNativeContext(natives.mNativeContext); + } + + @Implementation(minSdk = T.SDK_INT) + protected synchronized void nativeInit( + Object weakSelf, + int w, + int h, + int maxImgs, + long consumerUsage, + int hardwareBufferFormat, + int dataSpace) { + // Up to S, "fmt" is a PublicFormat (JNI), aka ImageFormat.format (java), which is then + // split into a hal format + data space in the JNI code. + // In T+, the hal format and data space are provided directly instead. + // However the format values overlap and the conversion is merely a cast. + // Reference: android12/.../frameworks/base/libs/hostgraphics/PublicFormat.cpp + int fmt = hardwareBufferFormat; + natives.nativeInit(weakSelf, w, h, fmt, maxImgs, consumerUsage); + imageReaderReflector.setMemberNativeContext(natives.mNativeContext); + } + + @Implementation + protected void nativeClose() { + natives.nativeClose(); + } + + @Implementation + protected void nativeReleaseImage(Image i) { + natives.nativeReleaseImage(i); + } + + @Implementation + protected Surface nativeGetSurface() { + return natives.nativeGetSurface(); + } + + @Implementation(maxSdk = S_V2) + protected int nativeDetachImage(Image i) { + return natives.nativeDetachImage(i); + } + + @Implementation + protected void nativeDiscardFreeBuffers() { + natives.nativeDiscardFreeBuffers(); + } + + /** + * @return A return code {@code ACQUIRE_*} + */ + @Implementation(maxSdk = S_V2) + protected int nativeImageSetup(Image i) { + return natives.nativeImageSetup(i); + } + + @Implementation(minSdk = T.SDK_INT, maxSdk = T.SDK_INT) + protected int nativeImageSetup(Image i, boolean legacyValidateImageFormat) { + return natives.nativeImageSetup(i); + } + + @Implementation(minSdk = U.SDK_INT) + protected Object nativeImageSetup(Object i) { + // Note: reverted to Q-S API + return natives.nativeImageSetup((Image) i); + } + + /** We use a class initializer to allow the native code to cache some field offsets. */ + @Implementation + protected static void nativeClassInit() { + DefaultNativeRuntimeLoader.injectAndLoad(); + ImageReaderNatives.nativeClassInit(); + } + + @ForType(ImageReader.class) + interface ImageReaderReflector { + @Accessor("mNativeContext") + void setMemberNativeContext(long mNativeContext); + } + + /** Shadow picker for {@link ImageReader}. */ + public static final class Picker extends GraphicsShadowPicker { + public Picker() { + super(ShadowImageReader.class, ShadowNativeImageReader.class); + } + + @Override + protected int getMinApiLevel() { + return S; + } + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReaderSurfaceImage.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReaderSurfaceImage.java new file mode 100644 index 00000000000..ec0e8318868 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageReaderSurfaceImage.java @@ -0,0 +1,65 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.nativeruntime.ImageReaderSurfaceImageNatives; +import org.robolectric.shadows.ShadowImageReader.ShadowSurfaceImage; + +/** Shadow for {@code ImageReader.SurfaceImage} that is backed by native code. */ +@Implements( + className = "android.media.ImageReader$SurfaceImage", + minSdk = Q, + looseSignatures = true, + isInAndroidSdk = false, + shadowPicker = ShadowNativeImageReaderSurfaceImage.Picker.class) +public class ShadowNativeImageReaderSurfaceImage { + + @RealObject private Object realSurfaceImage; + + @Implementation(maxSdk = R) + protected synchronized /*SurfacePlane[]*/ Object nativeCreatePlanes( + /*int*/ Object numPlanes, /*int*/ Object readerFormat) { + return ImageReaderSurfaceImageNatives.nativeSurfaceImageCreatePlanes( + realSurfaceImage, (int) numPlanes, (int) readerFormat, /* readerUsage= */ 0); + } + + @Implementation(minSdk = S) + protected synchronized /*SurfacePlane[]*/ Object nativeCreatePlanes( + /*int*/ Object numPlanes, /*int*/ Object readerFormat, /*long*/ Object readerUsage) { + return ImageReaderSurfaceImageNatives.nativeSurfaceImageCreatePlanes( + realSurfaceImage, (int) numPlanes, (int) readerFormat, (long) readerUsage); + } + + @Implementation + protected synchronized int nativeGetWidth() { + return ImageReaderSurfaceImageNatives.nativeSurfaceImageGetWidth(realSurfaceImage); + } + + @Implementation + protected synchronized int nativeGetHeight() { + return ImageReaderSurfaceImageNatives.nativeSurfaceImageGetHeight(realSurfaceImage); + } + + @Implementation + protected synchronized int nativeGetFormat(int readerFormat) { + return ImageReaderSurfaceImageNatives.nativeSurfaceImageGetFormat( + realSurfaceImage, readerFormat); + } + + /** Shadow picker for {@code ImageReader.SurfaceImage}. */ + public static final class Picker extends GraphicsShadowPicker { + public Picker() { + super(ShadowSurfaceImage.class, ShadowNativeImageReaderSurfaceImage.class); + } + + @Override + protected int getMinApiLevel() { + return S; + } + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSurface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSurface.java index fe789981a36..97556fcffa4 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSurface.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSurface.java @@ -51,7 +51,9 @@ protected static long nativeGetFromBlastBufferQueue( @Implementation protected static long nativeLockCanvas(long nativeObject, Canvas canvas, Rect dirty) throws OutOfResourcesException { - return SurfaceNatives.nativeLockCanvas(nativeObject, canvas, dirty); + // Do not call the nativeLockCanvas method. It is not implemented, and calling it can + // only result in a native crash (unlocking the canvas wipes out this Surface native object). + throw new UnsupportedOperationException("Not implemented yet"); } @Implementation diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPublicFormatUtils.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPublicFormatUtils.java new file mode 100644 index 00000000000..915390b8a9d --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPublicFormatUtils.java @@ -0,0 +1,34 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.TIRAMISU; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +/** + * Shadow for private class PublicFormatUtils. + * + *

It converts between the "legacy" Image "public format" (S) and the newer "hal format" (T). + * + *

Reference: + * https://cs.android.com/android/platform/superproject/+/android-13.0.0_r1:frameworks/base/media/java/android/media/PublicFormatUtils.java + * https://cs.android.com/android/platform/superproject/+/android-13.0.0_r1:frameworks/base/libs/hostgraphics/PublicFormat.cpp + */ +@Implements(className = "android.media.PublicFormatUtils", minSdk = TIRAMISU) +public class ShadowPublicFormatUtils { + + @Implementation + protected static int getHalFormat(int imageFormat) { + return imageFormat; + } + + @Implementation + protected static int getHalDataspace(int imageFormat) { + return 0; + } + + @Implementation + protected static int getPublicFormat(int imageFormat, int dataspace) { + return imageFormat; + } +} From 1204891df5d72cb08e2ce79d72908fe9fc69b9f1 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Tue, 3 Oct 2023 11:03:13 -0700 Subject: [PATCH 02/33] Internal PiperOrigin-RevId: 570439306 --- integration_tests/androidx_test/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/androidx_test/src/main/AndroidManifest.xml b/integration_tests/androidx_test/src/main/AndroidManifest.xml index 9c365acea81..b6a6ab9d39c 100644 --- a/integration_tests/androidx_test/src/main/AndroidManifest.xml +++ b/integration_tests/androidx_test/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + From 435f754519869e1ddee4464c4917d3c94a26cdad Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 5 Oct 2023 02:10:29 -0700 Subject: [PATCH 03/33] Support dispatchMediaKeyEvent in ShadowAudioManager Before this change, `AudioManager` routed key events to `MediaSessionManager`, just like in production. `MediaSessionManager` then forwarded the events to the platform's MediaSessionService which does not exist in Robolectric. PiperOrigin-RevId: 570946087 --- .../shadows/ShadowAudioManagerTest.java | 23 ++++++++++++++++ .../shadows/ShadowAudioManager.java | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java index 14985625451..9b441067544 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; @@ -28,6 +29,7 @@ import android.media.AudioSystem; import android.media.MediaRecorder.AudioSource; import android.media.audiopolicy.AudioPolicy; +import android.view.KeyEvent; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; @@ -1301,6 +1303,27 @@ public void getDirectPlaybackSupport_withShadowAudioSystemReset_returnsOffloadNo assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_NOT_SUPPORTED); } + @Test + @Config(minSdk = KITKAT) + public void dispatchMediaKeyEvent_recordsEvent() { + KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); + + audioManager.dispatchMediaKeyEvent(keyEvent); + + assertThat(shadowOf(audioManager).getDispatchedMediaKeyEvents()).containsExactly(keyEvent); + } + + @Test + @Config(minSdk = KITKAT) + public void clearDispatchedMediaKeyEvents_clearsDispatchedEvents() { + audioManager.dispatchMediaKeyEvent( + new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)); + + shadowOf(audioManager).clearDispatchedMediaKeyEvents(); + + assertThat(shadowOf(audioManager).getDispatchedMediaKeyEvents()).isEmpty(); + } + private static AudioDeviceInfo createAudioDevice(int type) throws ReflectiveOperationException { AudioDeviceInfo info = Shadow.newInstanceOf(AudioDeviceInfo.class); Field portField = AudioDeviceInfo.class.getDeclaredField("mPort"); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java index 5f2d4fd3cf3..3c6f1f65c1e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; @@ -26,6 +27,7 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.Parcel; +import android.view.KeyEvent; import com.android.internal.util.Preconditions; import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -95,6 +97,7 @@ public class ShadowAudioManager { private List outputDevices = new ArrayList<>(); private List availableCommunicationDevices = new ArrayList<>(); private AudioDeviceInfo communicationDevice = null; + private final List dispatchedMediaKeyEvents = new ArrayList<>(); public ShadowAudioManager() { for (int stream : ALL_STREAMS) { @@ -864,6 +867,29 @@ private static void writeMono16BitAudioFormatToParcel(Parcel p) { p.writeInt(0); // mChannelIndexMask } + /** + * Sends a simulated key event for a media button. + * + *

Instead of sending the media event to the media system service, from where it would be + * routed to a media app, this shadow method only records the events to be verified through {@link + * #getDispatchedMediaKeyEvents()}. + */ + @Implementation(minSdk = KITKAT) + protected void dispatchMediaKeyEvent(KeyEvent keyEvent) { + if (keyEvent == null) { + throw new NullPointerException("keyEvent is null"); + } + dispatchedMediaKeyEvents.add(keyEvent); + } + + public List getDispatchedMediaKeyEvents() { + return new ArrayList<>(dispatchedMediaKeyEvents); + } + + public void clearDispatchedMediaKeyEvents() { + dispatchedMediaKeyEvents.clear(); + } + public static class AudioFocusRequest { public final AudioManager.OnAudioFocusChangeListener listener; public final int streamType; From 2147482e91fd64946c2ad23e6768d23ae239b308 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Fri, 6 Oct 2023 13:10:30 -0700 Subject: [PATCH 04/33] Compile robolectric third party shadows against android U. This is a precursor change to supporting android U. For #8404 PiperOrigin-RevId: 571416264 --- buildSrc/src/main/groovy/AndroidSdk.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/AndroidSdk.groovy b/buildSrc/src/main/groovy/AndroidSdk.groovy index 2d8fb1c3849..29e5cf616e4 100644 --- a/buildSrc/src/main/groovy/AndroidSdk.groovy +++ b/buildSrc/src/main/groovy/AndroidSdk.groovy @@ -15,12 +15,12 @@ class AndroidSdk implements Comparable { static final S = new AndroidSdk(31, "12", "7732740"); static final S_V2 = new AndroidSdk(32, "12.1", "8229987"); static final TIRAMISU = new AndroidSdk(33, "13", "9030017"); - + static final U = new AndroidSdk(34, "14", "10818077"); static final List ALL_SDKS = [ KITKAT, LOLLIPOP, LOLLIPOP_MR1, M, N, N_MR1, O, O_MR1, P, Q, R, S, S_V2, - TIRAMISU + TIRAMISU, U ] static final MAX_SDK = Collections.max(ALL_SDKS) From c90c030288d92590d6dbdc24cfb74ca9380afda4 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Fri, 6 Oct 2023 14:13:07 -0700 Subject: [PATCH 05/33] Move Theme GC test to MemoryLeaksTest This test has been flaky, The goal is to make it more reliable. PiperOrigin-RevId: 571431835 --- integration_tests/memoryleaks/build.gradle | 1 + .../memoryleaks/BaseMemoryLeaksTest.java | 22 +++++++++ .../robolectric/shadows/ShadowThemeTest.java | 45 ------------------- 3 files changed, 23 insertions(+), 45 deletions(-) diff --git a/integration_tests/memoryleaks/build.gradle b/integration_tests/memoryleaks/build.gradle index 1655a15e198..070f70bdb8c 100644 --- a/integration_tests/memoryleaks/build.gradle +++ b/integration_tests/memoryleaks/build.gradle @@ -31,5 +31,6 @@ dependencies { testImplementation project(":robolectric") testImplementation libs.junit4 testImplementation libs.guava.testlib + testImplementation libs.guava.testlib testImplementation libs.androidx.fragment } diff --git a/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java b/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java index 3e990d13395..0e144b5225a 100644 --- a/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java +++ b/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java @@ -5,13 +5,16 @@ import android.app.Activity; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.os.Looper; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentContainerView; +import com.google.common.testing.GcFinalization; import java.lang.ref.WeakReference; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicLong; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -19,6 +22,7 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; +import org.robolectric.res.android.Registries; import org.robolectric.util.ReflectionHelpers; /** @@ -140,5 +144,23 @@ public void typedArrayData() { }); } + @Test + @Config(sdk = 29) + public void themeObjectInNativeObjectRegistry() { + final AtomicLong themeId = new AtomicLong(0); + assertNotLeaking( + () -> { + Theme theme = RuntimeEnvironment.getApplication().getResources().newTheme(); + long nativeId = + ReflectionHelpers.getField(ReflectionHelpers.getField(theme, "mThemeImpl"), "mTheme"); + themeId.set(nativeId); + return theme; + }); + + // Also wait for the theme to be cleared from the registry. + GcFinalization.awaitDone( + () -> Registries.NATIVE_THEME9_REGISTRY.peekNativeObject(themeId.get()) == null); + } + public abstract void assertNotLeaking(Callable potentiallyLeakingCallable); } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowThemeTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowThemeTest.java index c7730ecfac2..ff07540d82b 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowThemeTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowThemeTest.java @@ -9,7 +9,6 @@ import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.ColorDrawable; -import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.util.AttributeSet; import android.util.Xml; @@ -17,11 +16,6 @@ import android.widget.Button; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.lang.ref.WeakReference; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Supplier; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -29,10 +23,7 @@ import org.robolectric.R; import org.robolectric.Robolectric; import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; -import org.robolectric.res.android.Registries; import org.robolectric.shadows.testing.TestActivity; -import org.robolectric.util.ReflectionHelpers; import org.xmlpull.v1.XmlPullParser; @RunWith(AndroidJUnit4.class) @@ -297,42 +288,6 @@ public void shouldApplyFromStyleAttribute() { } } - @Test - @Config(minSdk = VERSION_CODES.N) - public void shouldFreeNativeObjectInRegistry() { - final AtomicLong themeId = new AtomicLong(0); - Supplier themeSupplier = - () -> { - Theme theme = resources.newTheme(); - long nativeId = - ReflectionHelpers.getField(ReflectionHelpers.getField(theme, "mThemeImpl"), "mTheme"); - themeId.set(nativeId); - return theme; - }; - - WeakReference weakRef = new WeakReference<>(themeSupplier.get()); - awaitFinalized(weakRef); - assertThat(Registries.NATIVE_THEME9_REGISTRY.peekNativeObject(themeId.get())).isNull(); - } - - private static void awaitFinalized(WeakReference weakRef) { - final CountDownLatch latch = new CountDownLatch(1); - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); - while (System.nanoTime() < deadline) { - if (weakRef.get() == null) { - return; - } - try { - System.gc(); - latch.await(100, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - } - } - - //////////////////////////// - private XmlResourceParser getFirstElementAttrSet(int resId) throws Exception { XmlResourceParser xml = resources.getXml(resId); assertThat(xml.next()).isEqualTo(XmlPullParser.START_DOCUMENT); From 41fa23dded937c89d09b56998bb304c2660ea98b Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Fri, 6 Oct 2023 23:41:05 -0700 Subject: [PATCH 06/33] Update themeObjectInNativeObjectRegistry to be minSdk=N It was temporarily set to Q for debugging purposes, but this test is designed to work on N and above. PiperOrigin-RevId: 571520417 --- .../integrationtests/memoryleaks/BaseMemoryLeaksTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java b/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java index 0e144b5225a..2361cbc1c83 100644 --- a/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java +++ b/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java @@ -1,5 +1,6 @@ package org.robolectric.integrationtests.memoryleaks; +import static android.os.Build.VERSION_CODES.N; import static org.robolectric.Shadows.shadowOf; import android.app.Activity; @@ -145,7 +146,7 @@ public void typedArrayData() { } @Test - @Config(sdk = 29) + @Config(minSdk = N) public void themeObjectInNativeObjectRegistry() { final AtomicLong themeId = new AtomicLong(0); assertNotLeaking( From 33bcb7eab27280cd0a4cd55c79f22d09137fbb22 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Sun, 8 Oct 2023 12:30:26 -0700 Subject: [PATCH 07/33] Add shadows for Android U. This commit adds shadows and test APIS for new-in-U functionality: - Managing devicepolicy - android.graphics.Gainmap - InputManagerGlobal refactoring - PixelCopy.ofWindow - PowerManager setExemptFromLowPowerStandby and newLowPowerStandbyPortsLock - WearableSensingManager - VirtualDeviceManager - WifiManager localConnectionFailureListener PiperOrigin-RevId: 571765381 --- .../shadows/ShadowGainmapTest.java | 123 ++++++++++ .../shadows/ShadowPowerManagerTest.java | 126 ++++++++++ .../ShadowVirtualDeviceManagerTest.java | 145 ++++++++++++ .../ShadowWearableSensingManagerTest.java | 74 ++++++ .../shadows/ShadowWifiManagerTest.java | 165 +++++++++++++ .../shadows/DevicePolicyStateBuilder.java | 34 +++ .../shadows/EnforcingAdminFactory.java | 47 ++++ .../robolectric/shadows/PolicyKeyFactory.java | 16 ++ .../shadows/PolicyStateBuilder.java | 58 +++++ .../shadows/PolicyValueFactory.java | 24 ++ .../shadows/ShadowDevicePolicyManager.java | 20 +- .../robolectric/shadows/ShadowGainmap.java | 219 ++++++++++++++++++ .../shadows/ShadowInputManagerGlobal.java | 66 ++++++ .../ShadowNfcFrameworkInitializer.java | 33 +++ .../robolectric/shadows/ShadowPixelCopy.java | 85 +++++++ .../shadows/ShadowPowerManager.java | 110 ++++++++- .../shadows/ShadowServiceManager.java | 7 + .../shadows/ShadowSurfaceSyncGroup.java | 47 ++++ .../shadows/ShadowVirtualDeviceManager.java | 172 ++++++++++++++ .../shadows/ShadowVirtualDeviceParams.java | 33 +++ .../shadows/ShadowVirtualSensor.java | 39 ++++ .../shadows/ShadowWearableSensingManager.java | 67 ++++++ .../shadows/ShadowWifiManager.java | 42 ++++ 23 files changed, 1743 insertions(+), 9 deletions(-) create mode 100644 robolectric/src/test/java/org/robolectric/shadows/ShadowGainmapTest.java create mode 100644 robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java create mode 100644 robolectric/src/test/java/org/robolectric/shadows/ShadowWearableSensingManagerTest.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/DevicePolicyStateBuilder.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/EnforcingAdminFactory.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/PolicyKeyFactory.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/PolicyStateBuilder.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/PolicyValueFactory.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowGainmap.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManagerGlobal.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcFrameworkInitializer.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceSyncGroup.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualSensor.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowWearableSensingManager.java diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowGainmapTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowGainmapTest.java new file mode 100644 index 00000000000..91fdde33ac4 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowGainmapTest.java @@ -0,0 +1,123 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import android.graphics.Bitmap; +import android.graphics.Gainmap; +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Unit test for {@link ShadowGainmap}. */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = UPSIDE_DOWN_CAKE) +public class ShadowGainmapTest { + + @Test + public void testGainmap_getSetRatioMin() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + gainmap.setRatioMin(1, 2, 3); + assertThat(gainmap.getRatioMin()).isEqualTo(new float[] {1, 2, 3}); + } + + @Test + public void testGainmap_getSetRatioMax() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + gainmap.setRatioMax(1, 2, 3); + assertThat(gainmap.getRatioMax()).isEqualTo(new float[] {1, 2, 3}); + } + + @Test + public void testGainmap_getSetGamma() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + gainmap.setGamma(1, 2, 3); + assertThat(gainmap.getGamma()).isEqualTo(new float[] {1, 2, 3}); + } + + @Test + public void testGainmap_getSetEpsilonSdr() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + gainmap.setEpsilonSdr(1, 2, 3); + assertThat(gainmap.getEpsilonSdr()).isEqualTo(new float[] {1, 2, 3}); + } + + @Test + public void testGainmap_getSetEpsilonHdr() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + gainmap.setEpsilonHdr(1, 2, 3); + assertThat(gainmap.getEpsilonHdr()).isEqualTo(new float[] {1, 2, 3}); + } + + @Test + public void testGainmap_getSetDisplayRatioForFullHdr() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + gainmap.setDisplayRatioForFullHdr(5.0f); + assertThat(gainmap.getDisplayRatioForFullHdr()).isEqualTo(5.0f); + } + + @Test + public void testGainmap_getSetMinDisplayRatioForHdrTransition() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + gainmap.setDisplayRatioForFullHdr(5.0f); + assertThat(gainmap.getDisplayRatioForFullHdr()).isEqualTo(5.0f); + } + + @Test + public void testGainmap_writeToParcel() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmap); + + Parcel parcel = Parcel.obtain(); + gainmap.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); + Gainmap parcelGainmap = Gainmap.CREATOR.createFromParcel(parcel); + assertTrue(bitmap.sameAs(parcelGainmap.getGainmapContents())); + } + + // TODO: move this to ShadowBitmapTest once U is supported there + @Test + public void testBitmap_writeToParcel_with_Gainmap() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Bitmap bitmapGainmapContents = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(bitmapGainmapContents); + bitmap.setGainmap(gainmap); + assertThat(bitmap.hasGainmap()).isTrue(); + + Parcel parcel = Parcel.obtain(); + bitmap.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); + Bitmap parcelBitmap = Bitmap.CREATOR.createFromParcel(parcel); + assertThat(parcelBitmap.getGainmap()).isNotNull(); + } + + @Test + public void setGainmap_recycledBitmap() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); + bitmap.recycle(); + assertThrows(IllegalStateException.class, () -> bitmap.setGainmap(gainmap)); + } + + @Test + public void hasGainmap_recycledBitmap() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Gainmap gainmap = new Gainmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); + bitmap.setGainmap(gainmap); + bitmap.recycle(); + assertThrows(IllegalStateException.class, bitmap::hasGainmap); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java index 3c5781720bc..f001fecffe6 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java @@ -10,6 +10,9 @@ import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.PowerManager.LowPowerStandbyPortDescription.MATCH_PORT_REMOTE; +import static android.os.PowerManager.LowPowerStandbyPortDescription.PROTOCOL_TCP; +import static android.os.PowerManager.LowPowerStandbyPortDescription.PROTOCOL_UDP; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.Assert.fail; @@ -19,9 +22,12 @@ import android.content.Context; import android.content.Intent; import android.os.PowerManager; +import android.os.PowerManager.LowPowerStandbyPortDescription; +import android.os.PowerManager.LowPowerStandbyPortsLock; import android.os.WorkSource; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import com.google.common.truth.Correspondence; import java.time.Duration; import org.junit.Before; @@ -30,6 +36,8 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowPowerManager.ShadowLowPowerStandbyPortsLock; +import org.robolectric.versioning.AndroidVersions.U; @RunWith(AndroidJUnit4.class) public class ShadowPowerManagerTest { @@ -608,4 +616,122 @@ public void isDeviceLightIdleMode_shouldGetAndSet() { shadowPowerManager.setIsDeviceLightIdleMode(false); assertThat(powerManager.isDeviceLightIdleMode()).isFalse(); } + + @Test + @Config(minSdk = U.SDK_INT) + public void setLowPowerStandbySupported() { + ShadowPowerManager shadowPowerManager = Shadow.extract(powerManager); + shadowPowerManager.setLowPowerStandbySupported(true); + assertThat(powerManager.isLowPowerStandbySupported()).isTrue(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void setLowPowerStandbyEnabled() { + ShadowPowerManager shadowPowerManager = Shadow.extract(powerManager); + shadowPowerManager.setLowPowerStandbySupported(true); + shadowPowerManager.setLowPowerStandbyEnabled(true); + assertThat(powerManager.isLowPowerStandbyEnabled()).isTrue(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void setLowPowerStandbyEnabled_notSupported() { + ShadowPowerManager shadowPowerManager = Shadow.extract(powerManager); + shadowPowerManager.setLowPowerStandbySupported(false); + shadowPowerManager.setLowPowerStandbyEnabled(true); + assertThat(powerManager.isLowPowerStandbyEnabled()).isFalse(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void isAllowedInLowPowerStandby() { + ShadowPowerManager shadowPowerManager = Shadow.extract(powerManager); + shadowPowerManager.addAllowedInLowPowerStandby("hello world"); + assertThat(powerManager.isAllowedInLowPowerStandby("hello world")).isTrue(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void isAllowedInLowPowerStandby_notSupported() { + ShadowPowerManager shadowPowerManager = Shadow.extract(powerManager); + shadowPowerManager.setLowPowerStandbySupported(false); + assertThat(powerManager.isAllowedInLowPowerStandby("hello world")).isTrue(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void isExemptFromLowPowerStandby() { + ShadowPowerManager shadowPowerManager = Shadow.extract(powerManager); + shadowPowerManager.setExemptFromLowPowerStandby(true); + assertThat(powerManager.isExemptFromLowPowerStandby()).isTrue(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void isExemptFromLowPowerStandby_notSupported() { + ShadowPowerManager shadowPowerManager = Shadow.extract(powerManager); + shadowPowerManager.setLowPowerStandbySupported(false); + assertThat(powerManager.isExemptFromLowPowerStandby()).isTrue(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void newLowPowerStandbyPortsLock_setsPorts() { + LowPowerStandbyPortDescription port1 = + new LowPowerStandbyPortDescription(PROTOCOL_TCP, MATCH_PORT_REMOTE, 42); + LowPowerStandbyPortDescription port2 = + new LowPowerStandbyPortDescription(PROTOCOL_UDP, MATCH_PORT_REMOTE, 314); + ImmutableList ports = ImmutableList.of(port1, port2); + + LowPowerStandbyPortsLock lock = powerManager.newLowPowerStandbyPortsLock(ports); + + ShadowLowPowerStandbyPortsLock shadowLock = + (ShadowLowPowerStandbyPortsLock) Shadow.extract(lock); + assertThat(shadowLock.getPorts()).isEqualTo(ports); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void shadowLowPowerStandbyPortsLock_getAcquireCount() { + LowPowerStandbyPortDescription defaultPort = + new LowPowerStandbyPortDescription(PROTOCOL_TCP, MATCH_PORT_REMOTE, 42); + ImmutableList portDescriptions = ImmutableList.of(defaultPort); + + LowPowerStandbyPortsLock lock = powerManager.newLowPowerStandbyPortsLock(portDescriptions); + ShadowLowPowerStandbyPortsLock shadowLock = + (ShadowLowPowerStandbyPortsLock) Shadow.extract(lock); + lock.acquire(); + lock.acquire(); + assertThat(shadowLock.getAcquireCount()).isEqualTo(2); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void shadowLowPowerStandbyPortsLock_acquire_held() { + LowPowerStandbyPortDescription defaultPort = + new LowPowerStandbyPortDescription(PROTOCOL_TCP, MATCH_PORT_REMOTE, 42); + ImmutableList portDescriptions = ImmutableList.of(defaultPort); + + LowPowerStandbyPortsLock lock = powerManager.newLowPowerStandbyPortsLock(portDescriptions); + ShadowLowPowerStandbyPortsLock shadowLock = + (ShadowLowPowerStandbyPortsLock) Shadow.extract(lock); + lock.acquire(); + assertThat(shadowLock.isAcquired()).isTrue(); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void shadowLowPowerStandbyPortsLock_acquire_released() { + LowPowerStandbyPortDescription defaultPort = + new LowPowerStandbyPortDescription(PROTOCOL_TCP, MATCH_PORT_REMOTE, 42); + ImmutableList portDescriptions = ImmutableList.of(defaultPort); + + LowPowerStandbyPortsLock lock = powerManager.newLowPowerStandbyPortsLock(portDescriptions); + ShadowLowPowerStandbyPortsLock shadowLock = + (ShadowLowPowerStandbyPortsLock) Shadow.extract(lock); + lock.acquire(); + lock.release(); + assertThat(shadowLock.isAcquired()).isFalse(); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java new file mode 100644 index 00000000000..f89c3b80cb2 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java @@ -0,0 +1,145 @@ +package org.robolectric.shadows; + +import static android.hardware.Sensor.TYPE_ACCELEROMETER; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.PendingIntent; +import android.companion.virtual.VirtualDeviceManager; +import android.companion.virtual.VirtualDeviceManager.VirtualDevice; +import android.companion.virtual.VirtualDeviceParams; +import android.companion.virtual.sensor.VirtualSensorCallback; +import android.companion.virtual.sensor.VirtualSensorConfig; +import android.content.Context; +import android.content.Intent; +import java.time.Duration; +import java.util.function.IntConsumer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowVirtualDeviceManager.ShadowVirtualDevice; + +/** Unit test for ShadowVirtualDeviceManager and ShadowVirtualDevice. */ +@Config(minSdk = UPSIDE_DOWN_CAKE) +@RunWith(RobolectricTestRunner.class) +public class ShadowVirtualDeviceManagerTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private Context context; + private VirtualDeviceManager virtualDeviceManager; + @Mock private IntConsumer mockCallback; + + @Before + public void setUp() throws Exception { + virtualDeviceManager = + (VirtualDeviceManager) + getApplicationContext().getSystemService(Context.VIRTUAL_DEVICE_SERVICE); + context = getApplicationContext(); + } + + @Test + public void testCreateVirtualDevice() { + assertThat(virtualDeviceManager.getVirtualDevices()).isEmpty(); + virtualDeviceManager.createVirtualDevice( + 0, new VirtualDeviceParams.Builder().setName("foo").build()); + + assertThat(virtualDeviceManager.getVirtualDevices()).hasSize(1); + assertThat(virtualDeviceManager.getVirtualDevices().get(0).getName()).isEqualTo("foo"); + } + + @Test + public void testGetDevicePolicy() { + VirtualDevice virtualDevice = + virtualDeviceManager.createVirtualDevice( + 0, + new VirtualDeviceParams.Builder() + .setDevicePolicy( + VirtualDeviceParams.POLICY_TYPE_SENSORS, + VirtualDeviceParams.DEVICE_POLICY_CUSTOM) + .build()); + + assertThat( + virtualDeviceManager.getDevicePolicy( + virtualDevice.getDeviceId(), VirtualDeviceParams.POLICY_TYPE_SENSORS)) + .isEqualTo(VirtualDeviceParams.DEVICE_POLICY_CUSTOM); + } + + @Test + public void testLaunchIntentOnDevice() { + PendingIntent pendingIntent = + PendingIntent.getActivity(context, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE); + VirtualDevice virtualDevice = + virtualDeviceManager.createVirtualDevice(0, new VirtualDeviceParams.Builder().build()); + ShadowVirtualDevice shadowVirtualDevice = Shadow.extract(virtualDevice); + shadowVirtualDevice.setPendingIntentCallbackResultCode( + VirtualDeviceManager.LAUNCH_FAILURE_NO_ACTIVITY); + + virtualDevice.launchPendingIntent(0, pendingIntent, context.getMainExecutor(), mockCallback); + Robolectric.flushForegroundThreadScheduler(); + + verify(mockCallback).accept(VirtualDeviceManager.LAUNCH_FAILURE_NO_ACTIVITY); + assertThat(shadowVirtualDevice.getLastLaunchedPendingIntent()).isEqualTo(pendingIntent); + } + + @Test + public void testGetVirtualSensorList() { + VirtualDevice virtualDevice = + virtualDeviceManager.createVirtualDevice( + 0, + new VirtualDeviceParams.Builder() + .setDevicePolicy( + VirtualDeviceParams.POLICY_TYPE_SENSORS, + VirtualDeviceParams.DEVICE_POLICY_CUSTOM) + .addVirtualSensorConfig( + new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, "accel").build()) + .setVirtualSensorCallback( + context.getMainExecutor(), mock(VirtualSensorCallback.class)) + .build()); + + assertThat(virtualDevice.getVirtualSensorList()).hasSize(1); + assertThat(virtualDevice.getVirtualSensorList().get(0).getName()).isEqualTo("accel"); + assertThat(virtualDevice.getVirtualSensorList().get(0).getType()).isEqualTo(TYPE_ACCELEROMETER); + assertThat(virtualDevice.getVirtualSensorList().get(0).getDeviceId()) + .isEqualTo(virtualDevice.getDeviceId()); + } + + @Test + public void testGetSensorCallbacks() { + VirtualSensorCallback mockVirtualSensorCallback = mock(VirtualSensorCallback.class); + VirtualDevice virtualDevice = + virtualDeviceManager.createVirtualDevice( + 0, + new VirtualDeviceParams.Builder() + .setDevicePolicy( + VirtualDeviceParams.POLICY_TYPE_SENSORS, + VirtualDeviceParams.DEVICE_POLICY_CUSTOM) + .addVirtualSensorConfig( + new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, "accel").build()) + .setVirtualSensorCallback(context.getMainExecutor(), mockVirtualSensorCallback) + .build()); + + ShadowVirtualDevice shadowDevice = Shadow.extract(virtualDevice); + ShadowVirtualDeviceParams shadowParams = Shadow.extract(shadowDevice.getParams()); + VirtualSensorCallback retrievedCallback = shadowParams.getVirtualSensorCallback(); + + retrievedCallback.onConfigurationChanged( + virtualDevice.getVirtualSensorList().get(0), true, Duration.ZERO, Duration.ZERO); + + assertThat(retrievedCallback).isNotNull(); + verify(mockVirtualSensorCallback).onConfigurationChanged(any(), eq(true), any(), any()); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWearableSensingManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWearableSensingManagerTest.java new file mode 100644 index 00000000000..bb24fc8c9e6 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWearableSensingManagerTest.java @@ -0,0 +1,74 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; + +import android.app.wearable.WearableSensingManager; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.SharedMemory; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.function.Consumer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +/** Unit test for ShadowWearableSensingManager. */ +@Config(minSdk = UPSIDE_DOWN_CAKE) +@RunWith(RobolectricTestRunner.class) +public class ShadowWearableSensingManagerTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private Consumer callback; + + @Test + public void provideDataStream() throws Exception { + WearableSensingManager wearableSensingManager = + (WearableSensingManager) + getApplicationContext().getSystemService(Context.WEARABLE_SENSING_SERVICE); + ShadowWearableSensingManager shadowWearableSensingManager = + Shadow.extract(wearableSensingManager); + + ParcelFileDescriptor[] descriptors = ParcelFileDescriptor.createPipe(); + shadowWearableSensingManager.setProvideDataStreamResult( + WearableSensingManager.STATUS_ACCESS_DENIED); + + wearableSensingManager.provideDataStream( + descriptors[0], MoreExecutors.directExecutor(), callback); + + verify(callback).accept(WearableSensingManager.STATUS_ACCESS_DENIED); + assertThat(shadowWearableSensingManager.getLastParcelFileDescriptor()) + .isSameInstanceAs(descriptors[0]); + } + + @Test + public void provideData() throws Exception { + WearableSensingManager wearableSensingManager = + (WearableSensingManager) + getApplicationContext().getSystemService(Context.WEARABLE_SENSING_SERVICE); + ShadowWearableSensingManager shadowWearableSensingManager = + Shadow.extract(wearableSensingManager); + + PersistableBundle persistableBundle = new PersistableBundle(); + SharedMemory sharedMemory = SharedMemory.create("name", 100); + shadowWearableSensingManager.setProvideDataResult(WearableSensingManager.STATUS_ACCESS_DENIED); + + wearableSensingManager.provideData( + persistableBundle, sharedMemory, MoreExecutors.directExecutor(), callback); + + verify(callback).accept(WearableSensingManager.STATUS_ACCESS_DENIED); + assertThat(shadowWearableSensingManager.getLastDataBundle()) + .isSameInstanceAs(persistableBundle); + assertThat(shadowWearableSensingManager.getLastSharedMemory()).isSameInstanceAs(sharedMemory); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java index 26acedbb868..6563db768e2 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.net.ConnectivityManager; import android.net.DhcpInfo; +import android.net.MacAddress; import android.net.NetworkInfo; import android.net.wifi.ScanResult; import android.net.wifi.SoftApConfiguration; @@ -34,8 +35,10 @@ import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.AddNetworkResult; +import android.net.wifi.WifiManager.LocalOnlyConnectionFailureListener; import android.net.wifi.WifiManager.MulticastLock; import android.net.wifi.WifiManager.PnoScanResultsCallback; +import android.net.wifi.WifiNetworkSpecifier; import android.net.wifi.WifiSsid; import android.net.wifi.WifiUsabilityStatsEntry; import android.os.Build; @@ -43,6 +46,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; @@ -52,7 +56,9 @@ import org.mockito.ArgumentCaptor; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; +import org.robolectric.versioning.AndroidVersions.U; @RunWith(AndroidJUnit4.class) public class ShadowWifiManagerTest { @@ -1183,6 +1189,165 @@ public void networksFoundFromPnoScan_noMatchingSsid_availableCallbackNotInvoked( assertThat(callback.incomingScanResults).isEmpty(); } + @Test + @Config(minSdk = U.SDK_INT) + public void addLocalOnlyConnectionFailureListener_nullExecutor_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> + wifiManager.addLocalOnlyConnectionFailureListener( + /* executor= */ null, /* listener= */ (unused1, unused2) -> {})); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void addLocalOnlyConnectionFailureListener_nullListener_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> + wifiManager.addLocalOnlyConnectionFailureListener( + Executors.newSingleThreadExecutor(), /* listener= */ null)); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void + removeLocalOnlyConnectionFailureListener_nullListener_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> wifiManager.removeLocalOnlyConnectionFailureListener(/* listener= */ null)); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void triggerLocalConnectionFailure_callbackTriggered() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + TestFailureListener listener = new TestFailureListener(); + WifiNetworkSpecifier wifiNetworkSpecifier = + new WifiNetworkSpecifier.Builder() + .setSsid("icanhazinternet") + .setBssid(MacAddress.fromString("01:92:83:74:65:AB")) + .setWpa3Passphrase("r3@l gud pa$$w0rd") + .build(); + + wifiManager.addLocalOnlyConnectionFailureListener(executor, listener); + ((ShadowWifiManager) Shadow.extract(wifiManager)) + .triggerLocalConnectionFailure( + wifiNetworkSpecifier, WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + executor.shutdown(); + + IncomingFailure incomingFailure = listener.incomingFailures.take(); + assertThat(incomingFailure.wifiNetworkSpecifier).isEqualTo(wifiNetworkSpecifier); + assertThat(incomingFailure.failureReason) + .isEqualTo(WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void triggerLocalConnectionFailure_multipleCallbacksRegistered_allCallbacksTriggered() + throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + TestFailureListener listener1 = new TestFailureListener(); + TestFailureListener listener2 = new TestFailureListener(); + WifiNetworkSpecifier wifiNetworkSpecifier = + new WifiNetworkSpecifier.Builder() + .setSsid("icanhazinternet") + .setBssid(MacAddress.fromString("01:92:83:74:65:AB")) + .setWpa3Passphrase("r3@l gud pa$$w0rd") + .build(); + + wifiManager.addLocalOnlyConnectionFailureListener(executor, listener1); + wifiManager.addLocalOnlyConnectionFailureListener(executor, listener2); + ((ShadowWifiManager) Shadow.extract(wifiManager)) + .triggerLocalConnectionFailure( + wifiNetworkSpecifier, WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + executor.shutdown(); + + IncomingFailure incomingFailure = listener1.incomingFailures.take(); + assertThat(incomingFailure.wifiNetworkSpecifier).isEqualTo(wifiNetworkSpecifier); + assertThat(incomingFailure.failureReason) + .isEqualTo(WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + incomingFailure = listener2.incomingFailures.take(); + assertThat(incomingFailure.wifiNetworkSpecifier).isEqualTo(wifiNetworkSpecifier); + assertThat(incomingFailure.failureReason) + .isEqualTo(WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void + triggerLocalConnectionFailure_multipleCallbacksRegisteredOnDifferentExecutors_allCallbacksTriggered() + throws Exception { + ExecutorService executor1 = Executors.newSingleThreadExecutor(); + ExecutorService executor2 = Executors.newSingleThreadExecutor(); + TestFailureListener listener1 = new TestFailureListener(); + TestFailureListener listener2 = new TestFailureListener(); + WifiNetworkSpecifier wifiNetworkSpecifier = + new WifiNetworkSpecifier.Builder() + .setSsid("icanhazinternet") + .setBssid(MacAddress.fromString("01:92:83:74:65:AB")) + .setWpa3Passphrase("r3@l gud pa$$w0rd") + .build(); + + wifiManager.addLocalOnlyConnectionFailureListener(executor1, listener1); + wifiManager.addLocalOnlyConnectionFailureListener(executor2, listener2); + ((ShadowWifiManager) Shadow.extract(wifiManager)) + .triggerLocalConnectionFailure( + wifiNetworkSpecifier, WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + executor1.shutdown(); + executor2.shutdown(); + + IncomingFailure incomingFailure = listener1.incomingFailures.take(); + assertThat(incomingFailure.wifiNetworkSpecifier).isEqualTo(wifiNetworkSpecifier); + assertThat(incomingFailure.failureReason) + .isEqualTo(WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + incomingFailure = listener2.incomingFailures.take(); + assertThat(incomingFailure.wifiNetworkSpecifier).isEqualTo(wifiNetworkSpecifier); + assertThat(incomingFailure.failureReason) + .isEqualTo(WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + } + + @Test + @Config(minSdk = U.SDK_INT) + public void triggerLocalConnectionFailure_listenerRemovedBeforeTrigger_callbackNotInvoked() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + TestFailureListener listener = new TestFailureListener(); + WifiNetworkSpecifier wifiNetworkSpecifier = + new WifiNetworkSpecifier.Builder() + .setSsid("icanhazinternet") + .setBssid(MacAddress.fromString("01:92:83:74:65:AB")) + .setWpa3Passphrase("r3@l gud pa$$w0rd") + .build(); + + wifiManager.addLocalOnlyConnectionFailureListener(executor, listener); + wifiManager.removeLocalOnlyConnectionFailureListener(listener); + ((ShadowWifiManager) Shadow.extract(wifiManager)) + .triggerLocalConnectionFailure( + wifiNetworkSpecifier, WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION); + executor.shutdown(); + + assertThat(listener.incomingFailures).isEmpty(); + } + + private static final class IncomingFailure { + private final WifiNetworkSpecifier wifiNetworkSpecifier; + private final int failureReason; + + IncomingFailure(WifiNetworkSpecifier wifiNetworkSpecifier, int failureReason) { + this.wifiNetworkSpecifier = wifiNetworkSpecifier; + this.failureReason = failureReason; + } + } + + private static final class TestFailureListener implements LocalOnlyConnectionFailureListener { + private final BlockingQueue incomingFailures = new LinkedBlockingQueue<>(); + + @Override + public void onConnectionFailed(WifiNetworkSpecifier wifiNetworkSpecifier, int i) { + incomingFailures.add(new IncomingFailure(wifiNetworkSpecifier, i)); + } + } + private class TestPnoScanResultsCallback implements PnoScanResultsCallback { LinkedBlockingQueue> incomingScanResults = new LinkedBlockingQueue<>(); LinkedBlockingQueue successfulRegistrations = new LinkedBlockingQueue<>(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/DevicePolicyStateBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/DevicePolicyStateBuilder.java new file mode 100644 index 00000000000..8f736d348f4 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/DevicePolicyStateBuilder.java @@ -0,0 +1,34 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.admin.DevicePolicyState; +import android.app.admin.PolicyKey; +import android.app.admin.PolicyState; +import android.os.UserHandle; +import java.util.Map; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Factory for {@link DevicePolicyState} */ +public class DevicePolicyStateBuilder { + private DevicePolicyStateBuilder() {} + + /** Return a real instance of {@link DevicePolicyState} */ + public static DevicePolicyState create(Map>> policies) { + DevicePolicyState devicePolicyState = + reflector(DevicePolicyStateReflector.class).newDevicePolicyState(); + reflector(DevicePolicyStateReflector.class, devicePolicyState).setPolicy(policies); + return devicePolicyState; + } + + @ForType(DevicePolicyState.class) + private interface DevicePolicyStateReflector { + @Constructor + DevicePolicyState newDevicePolicyState(); + + @Accessor("mPolicies") + void setPolicy(Map>> policies); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/EnforcingAdminFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/EnforcingAdminFactory.java new file mode 100644 index 00000000000..c30974b93f1 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/EnforcingAdminFactory.java @@ -0,0 +1,47 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.admin.Authority; +import android.app.admin.EnforcingAdmin; +import android.os.UserHandle; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Factory for {@link EnforcingAdmin} */ +public class EnforcingAdminFactory { + private EnforcingAdminFactory() {} + + /** + * Return an {@link EnforcingAdmin} which can enforce a certain policy + * + * @param userHandle The {@link UserHandle} on which the admin is installed on. + * @param packageName The package name of the admin. + * @param authority The {@link Authority} on which the admin is acting on, e.g. DPC, DeviceAdmin, + * etc.. + */ + public static EnforcingAdmin create( + UserHandle userHandle, String packageName, Authority authority) { + EnforcingAdmin enforcingAdmin = reflector(EnforcingAdminReflector.class).newEnforcingAdmin(); + reflector(EnforcingAdminReflector.class, enforcingAdmin).setUserHandle(userHandle); + reflector(EnforcingAdminReflector.class, enforcingAdmin).setPackageName(packageName); + reflector(EnforcingAdminReflector.class, enforcingAdmin).setAuthority(authority); + return enforcingAdmin; + } + + @ForType(EnforcingAdmin.class) + private interface EnforcingAdminReflector { + @Constructor + EnforcingAdmin newEnforcingAdmin(); + + @Accessor("mUserHandle") + void setUserHandle(UserHandle userHandle); + + @Accessor("mPackageName") + void setPackageName(String packageName); + + @Accessor("mAuthority") + void setAuthority(Authority authority); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/PolicyKeyFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/PolicyKeyFactory.java new file mode 100644 index 00000000000..259930f07eb --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/PolicyKeyFactory.java @@ -0,0 +1,16 @@ +package org.robolectric.shadows; + +import android.app.admin.NoArgsPolicyKey; +import android.app.admin.PolicyKey; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; + +/** Factory for {@link PolicyKey} */ +public class PolicyKeyFactory { + private PolicyKeyFactory() {} + + public static PolicyKey create(String identifier) { + return ReflectionHelpers.callConstructor( + NoArgsPolicyKey.class, ClassParameter.from(String.class, identifier)); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/PolicyStateBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/PolicyStateBuilder.java new file mode 100644 index 00000000000..ad035bf153e --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/PolicyStateBuilder.java @@ -0,0 +1,58 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.admin.EnforcingAdmin; +import android.app.admin.PolicyState; +import android.app.admin.PolicyValue; +import java.util.LinkedHashMap; +import java.util.Map; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Builder for {@link PolicyState} */ +public class PolicyStateBuilder { + private Map> policiesSetByAdmins = + new LinkedHashMap>(); + private PolicyValue currentResolvedPolicy; + + private PolicyStateBuilder() {} + + private PolicyState policyState = reflector(PolicyStateReflector.class).newPolicyState(); + + public static PolicyStateBuilder newBuilder() { + return new PolicyStateBuilder(); + } + + /** Set the policy state for the {@link EnforcingAdmin}. */ + public PolicyStateBuilder setPolicy(EnforcingAdmin enforcingAdmin, PolicyValue value) { + this.policiesSetByAdmins.put(enforcingAdmin, value); + return this; + } + + /** Set the current resolved policy value. */ + public PolicyStateBuilder setCurrentResolvedPolicy(PolicyValue currentResolvedPolicy) { + this.currentResolvedPolicy = currentResolvedPolicy; + return this; + } + + public PolicyState build() { + reflector(PolicyStateReflector.class, policyState).setPoliciesSetByAdmins(policiesSetByAdmins); + reflector(PolicyStateReflector.class, policyState) + .setCurrentResolvedPolicy(currentResolvedPolicy); + return policyState; + } + + @ForType(PolicyState.class) + private interface PolicyStateReflector { + @Constructor + PolicyState newPolicyState(); + + @Accessor("mPoliciesSetByAdmins") + void setPoliciesSetByAdmins(Map> policiesSetByAdmins); + + @Accessor("mCurrentResolvedPolicy") + void setCurrentResolvedPolicy(PolicyValue currentResolvedPolicy); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/PolicyValueFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/PolicyValueFactory.java new file mode 100644 index 00000000000..1a9b84535d9 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/PolicyValueFactory.java @@ -0,0 +1,24 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.admin.PolicyValue; +import android.app.admin.StringPolicyValue; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** A Factory class representing {@link StringPolicyValue} */ +public class PolicyValueFactory { + private PolicyValueFactory() {} + + /** Return a real instance of StringPolicyValue */ + public static PolicyValue create() { + return reflector(PolicyValueReflector.class).newStringPolicyValue(); + } + + @ForType(StringPolicyValue.class) + private interface PolicyValueReflector { + @Constructor + PolicyValue newStringPolicyValue(); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java index 486d9e492aa..2b587d1c1b8 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java @@ -34,6 +34,7 @@ import android.app.admin.DevicePolicyManager.NearbyStreamingPolicy; import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.app.admin.DevicePolicyManager.UserProvisioningState; +import android.app.admin.DevicePolicyState; import android.app.admin.IDevicePolicyManager; import android.app.admin.SystemUpdatePolicy; import android.content.ComponentName; @@ -70,8 +71,9 @@ import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.shadow.api.Shadow; +import org.robolectric.versioning.AndroidVersions.U; -@Implements(DevicePolicyManager.class) +@Implements(value = DevicePolicyManager.class, looseSignatures = true) @SuppressLint("NewApi") public class ShadowDevicePolicyManager { /** @@ -154,8 +156,10 @@ public class ShadowDevicePolicyManager { private final Map userProvisioningStatesMap = new HashMap<>(); @Nullable private PersistableBundle lastTransferOwnershipBundle; + private Object /* DevicePolicyState */ devicePolicyState; private @RealObject DevicePolicyManager realObject; + private static class PackageAndPermission { public PackageAndPermission(String packageName, String permission) { @@ -1582,4 +1586,18 @@ public void setPolicyManagedProfiles(List policyManagedProfiles) { public int getUserProvisioningStateForUser(int userId) { return userProvisioningStatesMap.getOrDefault(userId, DevicePolicyManager.STATE_USER_UNMANAGED); } + + /** Return a stub value set by {@link #setDevicePolicyState(DevicePolicyState policyState)} */ + @Implementation(minSdk = U.SDK_INT) + protected Object getDevicePolicyState() { + return devicePolicyState; + } + + /** + * Set the {@link DevicePolicyState} which can be constructed from {@link + * DevicePolicyStateBuilder} + */ + public void setDevicePolicyState(Object policyState) { + devicePolicyState = policyState; + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowGainmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowGainmap.java new file mode 100644 index 00000000000..0fdbf25f3da --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowGainmap.java @@ -0,0 +1,219 @@ +package org.robolectric.shadows; + +import android.graphics.Bitmap; +import android.graphics.Gainmap; +import android.os.Parcel; +import android.os.Parcelable; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.res.android.NativeObjRegistry; +import org.robolectric.versioning.AndroidVersions.U; + +/** Fake implementation for Gainmap class. */ +@Implements( + value = Gainmap.class, + minSdk = U.SDK_INT, + // TODO: remove when minimum supported compileSdk is >= 34 + isInAndroidSdk = false) +public class ShadowGainmap { + + @RealObject Gainmap realGainmap; + + static final NativeObjRegistry nativeObjectRegistry = + new NativeObjRegistry<>(NativeGainmap.class); + + @Implementation + protected static long nCreateEmpty() { + return nativeObjectRegistry.register(new NativeGainmap()); + } + + @Implementation + protected static void nSetBitmap(long ptr, Bitmap bitmap) { + getNativeGainmap(ptr).bitmap = bitmap; + } + + @Implementation + protected static void nSetRatioMin(long ptr, float r, float g, float b) { + getNativeGainmap(ptr).ratioMin = new float[] {r, g, b}; + } + + @Implementation + protected static void nGetRatioMin(long ptr, float[] components) { + components[0] = getNativeGainmap(ptr).ratioMin[0]; + components[1] = getNativeGainmap(ptr).ratioMin[1]; + components[2] = getNativeGainmap(ptr).ratioMin[2]; + } + + @Implementation + protected static void nSetRatioMax(long ptr, float r, float g, float b) { + getNativeGainmap(ptr).ratioMax = new float[] {r, g, b}; + } + + @Implementation + protected static void nGetRatioMax(long ptr, float[] components) { + components[0] = getNativeGainmap(ptr).ratioMax[0]; + components[1] = getNativeGainmap(ptr).ratioMax[1]; + components[2] = getNativeGainmap(ptr).ratioMax[2]; + } + + @Implementation + protected static void nSetGamma(long ptr, float r, float g, float b) { + getNativeGainmap(ptr).gamma = new float[] {r, g, b}; + } + + @Implementation + protected static void nGetGamma(long ptr, float[] components) { + components[0] = getNativeGainmap(ptr).gamma[0]; + components[1] = getNativeGainmap(ptr).gamma[1]; + components[2] = getNativeGainmap(ptr).gamma[2]; + } + + @Implementation + protected static void nSetEpsilonSdr(long ptr, float r, float g, float b) { + getNativeGainmap(ptr).epsilonSdr = new float[] {r, g, b}; + } + + @Implementation + protected static void nGetEpsilonSdr(long ptr, float[] components) { + components[0] = getNativeGainmap(ptr).epsilonSdr[0]; + components[1] = getNativeGainmap(ptr).epsilonSdr[1]; + components[2] = getNativeGainmap(ptr).epsilonSdr[2]; + } + + @Implementation + protected static void nSetEpsilonHdr(long ptr, float r, float g, float b) { + getNativeGainmap(ptr).epsilonHdr = new float[] {r, g, b}; + } + + @Implementation + protected static void nGetEpsilonHdr(long ptr, float[] components) { + components[0] = getNativeGainmap(ptr).epsilonHdr[0]; + components[1] = getNativeGainmap(ptr).epsilonHdr[1]; + components[2] = getNativeGainmap(ptr).epsilonHdr[2]; + } + + @Implementation + protected static void nSetDisplayRatioHdr(long ptr, float max) { + getNativeGainmap(ptr).displayRatioHdr = max; + } + + @Implementation + protected static float nGetDisplayRatioHdr(long ptr) { + return getNativeGainmap(ptr).displayRatioHdr; + } + + @Implementation + protected static void nSetDisplayRatioSdr(long ptr, float min) { + getNativeGainmap(ptr).displayRatioSdr = min; + } + + @Implementation + protected static float nGetDisplayRatioSdr(long ptr) { + return getNativeGainmap(ptr).displayRatioSdr; + } + + @Implementation + protected static void nWriteGainmapToParcel(long ptr, Parcel dest) { + if (dest == null) { + return; + } + // write gainmap to parcel + // ratio min + dest.writeFloat(getNativeGainmap(ptr).ratioMin[0]); + dest.writeFloat(getNativeGainmap(ptr).ratioMin[1]); + dest.writeFloat(getNativeGainmap(ptr).ratioMin[2]); + // ratio max + dest.writeFloat(getNativeGainmap(ptr).ratioMax[0]); + dest.writeFloat(getNativeGainmap(ptr).ratioMax[1]); + dest.writeFloat(getNativeGainmap(ptr).ratioMax[2]); + // gamma + dest.writeFloat(getNativeGainmap(ptr).gamma[0]); + dest.writeFloat(getNativeGainmap(ptr).gamma[1]); + dest.writeFloat(getNativeGainmap(ptr).gamma[2]); + // epsilonsdr + dest.writeFloat(getNativeGainmap(ptr).epsilonSdr[0]); + dest.writeFloat(getNativeGainmap(ptr).epsilonSdr[1]); + dest.writeFloat(getNativeGainmap(ptr).epsilonSdr[2]); + // epsilonhdr + dest.writeFloat(getNativeGainmap(ptr).epsilonHdr[0]); + dest.writeFloat(getNativeGainmap(ptr).epsilonHdr[1]); + dest.writeFloat(getNativeGainmap(ptr).epsilonHdr[2]); + // display ratio sdr + dest.writeFloat(getNativeGainmap(ptr).displayRatioSdr); + // display ratio hdr + dest.writeFloat(getNativeGainmap(ptr).displayRatioHdr); + // base image type + // TODO: Figure out how to get the BaseImageType + dest.writeInt(0); + // type + // TODO: Figure out how to get the Type + dest.writeInt(0); + } + + @Implementation + protected static void nReadGainmapFromParcel(long ptr, Parcel dest) { + if (dest == null) { + return; + } + // write gainmap to parcel + // ratio min + getNativeGainmap(ptr).ratioMin[0] = dest.readFloat(); + getNativeGainmap(ptr).ratioMin[1] = dest.readFloat(); + getNativeGainmap(ptr).ratioMin[2] = dest.readFloat(); + // ratio max + getNativeGainmap(ptr).ratioMax[0] = dest.readFloat(); + getNativeGainmap(ptr).ratioMax[1] = dest.readFloat(); + getNativeGainmap(ptr).ratioMax[2] = dest.readFloat(); + // gamma + getNativeGainmap(ptr).gamma[0] = dest.readFloat(); + getNativeGainmap(ptr).gamma[1] = dest.readFloat(); + getNativeGainmap(ptr).gamma[2] = dest.readFloat(); + // epsilonsdr + getNativeGainmap(ptr).epsilonSdr[0] = dest.readFloat(); + getNativeGainmap(ptr).epsilonSdr[1] = dest.readFloat(); + getNativeGainmap(ptr).epsilonSdr[2] = dest.readFloat(); + // epsilonhdr + getNativeGainmap(ptr).epsilonHdr[0] = dest.readFloat(); + getNativeGainmap(ptr).epsilonHdr[1] = dest.readFloat(); + getNativeGainmap(ptr).epsilonHdr[2] = dest.readFloat(); + // display ratio sdr + getNativeGainmap(ptr).displayRatioSdr = dest.readFloat(); + // display ratio hdr + getNativeGainmap(ptr).displayRatioHdr = dest.readFloat(); + // base image type (unused in java) + dest.readInt(); + // type (unused in java) + dest.readInt(); + } + + private static NativeGainmap getNativeGainmap(long ptr) { + return nativeObjectRegistry.getNativeObject(ptr); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Gainmap createFromParcel(Parcel in) { + in.setDataPosition(0); + Gainmap gm = new Gainmap(in.readTypedObject(Bitmap.CREATOR)); + return gm; + } + + @Override + public Gainmap[] newArray(int size) { + return new Gainmap[size]; + } + }; + + private static class NativeGainmap { + public float[] ratioMin = new float[3]; + public float[] ratioMax = new float[3]; + public float[] gamma = new float[3]; + public float[] epsilonSdr = new float[3]; + public float[] epsilonHdr = new float[3]; + public float displayRatioSdr = 1.0f; + public float displayRatioHdr = 1.0f; + public Bitmap bitmap = null; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManagerGlobal.java new file mode 100644 index 00000000000..c3f6341daf0 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManagerGlobal.java @@ -0,0 +1,66 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.hardware.input.InputManagerGlobal; +import android.util.SparseArray; +import android.view.InputDevice; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.U; + +/** Shadow for new InputManagerGlobal introduced in android U. */ +@Implements(value = InputManagerGlobal.class, isInAndroidSdk = false, minSdk = U.SDK_INT) +public class ShadowInputManagerGlobal { + + @RealObject InputManagerGlobal realInputManager; + + /** Used in {@link InputDevice#getDeviceIds()} */ + @Implementation + protected int[] getInputDeviceIds() { + return new int[0]; + } + + @Implementation + protected void populateInputDevicesLocked() throws ClassNotFoundException { + if (ReflectionHelpers.getField(realInputManager, "mInputDevicesChangedListener") == null) { + ReflectionHelpers.setField( + realInputManager, + "mInputDevicesChangedListener", + ReflectionHelpers.callConstructor( + Class.forName( + "android.hardware.input.InputManagerGlobal$InputDevicesChangedListener"))); + } + + if (getInputDevices() == null) { + final int[] ids = realInputManager.getInputDeviceIds(); + + SparseArray inputDevices = new SparseArray<>(); + for (int i = 0; i < ids.length; i++) { + inputDevices.put(ids[i], null); + } + setInputDevices(inputDevices); + } + } + + private SparseArray getInputDevices() { + return reflector(InputManagerReflector.class, realInputManager).getInputDevices(); + } + + private void setInputDevices(SparseArray devices) { + reflector(InputManagerReflector.class, realInputManager).setInputDevices(devices); + } + + @ForType(InputManagerGlobal.class) + interface InputManagerReflector { + @Accessor("mInputDevices") + SparseArray getInputDevices(); + + @Accessor("mInputDevices") + void setInputDevices(SparseArray devices); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcFrameworkInitializer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcFrameworkInitializer.java new file mode 100644 index 00000000000..47c22dae710 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcFrameworkInitializer.java @@ -0,0 +1,33 @@ +package org.robolectric.shadows; + +import android.nfc.NfcFrameworkInitializer; +import android.nfc.NfcServiceManager; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; +import org.robolectric.versioning.AndroidVersions.U; + +/** + * Shadow for new NfcFrameworkInitializer class in U. + * + *

Real android will initialize this class on app startup. That doesn't happen in Robolectric, + * and besides seems wasteful to always do so. This shadow exists to lazy load the + * NfcServiceManager. + */ +@Implements(value = NfcFrameworkInitializer.class, isInAndroidSdk = false, minSdk = U.SDK_INT) +public class ShadowNfcFrameworkInitializer { + private static NfcServiceManager nfcServiceManager = null; + + @Implementation + protected static NfcServiceManager getNfcServiceManager() { + if (nfcServiceManager == null) { + nfcServiceManager = new NfcServiceManager(); + } + return nfcServiceManager; + } + + @Resetter + public static void reset() { + nfcServiceManager = null; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java index 82f61654919..21a4e6f0bb3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java @@ -12,17 +12,26 @@ import android.os.Looper; import android.view.PixelCopy; import android.view.PixelCopy.OnPixelCopyFinishedListener; +import android.view.PixelCopy.Result; import android.view.Surface; import android.view.SurfaceView; import android.view.View; +import android.view.ViewRootImpl; import android.view.Window; import android.view.WindowManagerGlobal; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.concurrent.Executor; +import java.util.function.Consumer; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowWindowManagerGlobal.WindowManagerGlobalReflector; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; +import org.robolectric.util.reflector.Static; +import org.robolectric.versioning.AndroidVersions.U; /** * Shadow for PixelCopy that uses View.draw to create screenshots. The real PixelCopy performs a @@ -113,6 +122,30 @@ protected static void request( alertFinished(listener, listenerThread, PixelCopy.SUCCESS); } + @Implementation(minSdk = U.SDK_INT) + protected static void request( + PixelCopy.Request request, Executor callbackExecutor, Consumer listener) { + RequestReflector requestReflector = reflector(RequestReflector.class, request); + OnPixelCopyFinishedListener legacyListener = + new OnPixelCopyFinishedListener() { + @Override + public void onPixelCopyFinished(int copyResult) { + listener.accept( + reflector(ResultReflector.class) + .newResult(copyResult, request.getDestinationBitmap())); + } + }; + Rect adjustedSrcRect = + reflector(PixelCopyReflector.class) + .adjustSourceRectForInsets(requestReflector.getSourceInsets(), request.getSourceRect()); + PixelCopy.request( + requestReflector.getSource(), + adjustedSrcRect, + request.getDestinationBitmap(), + legacyListener, + new Handler(Looper.getMainLooper())); + } + private static View findViewForSurface(Surface source) { for (View windowView : reflector(WindowManagerGlobalReflector.class, WindowManagerGlobal.getInstance()) @@ -161,4 +194,56 @@ private static Bitmap validateBitmap(Bitmap bitmap) { } return bitmap; } + + @Implements(value = PixelCopy.Request.Builder.class, minSdk = U.SDK_INT, isInAndroidSdk = false) + public static class ShadowPixelCopyRequestBuilder { + + // TODO(brettchabot): remove once robolectric has proper support for initializing a Surface + // for now, this copies Android implementation and just omits the valid surface check + @Implementation + protected static PixelCopy.Request.Builder ofWindow(View source) { + if (source == null || !source.isAttachedToWindow()) { + throw new IllegalArgumentException("View must not be null & must be attached to window"); + } + final Rect insets = new Rect(); + Surface surface = null; + final ViewRootImpl root = source.getViewRootImpl(); + if (root != null) { + surface = root.mSurface; + insets.set(root.mWindowAttributes.surfaceInsets); + } + PixelCopy.Request request = reflector(RequestReflector.class).newRequest(surface, insets); + return reflector(BuilderReflector.class).newBuilder(request); + } + } + + @ForType(PixelCopy.class) + private interface PixelCopyReflector { + @Static + Rect adjustSourceRectForInsets(Rect insets, Rect srcRect); + } + + @ForType(PixelCopy.Request.Builder.class) + private interface BuilderReflector { + @Constructor + PixelCopy.Request.Builder newBuilder(PixelCopy.Request request); + } + + @ForType(PixelCopy.Request.class) + private interface RequestReflector { + @Constructor + PixelCopy.Request newRequest(Surface surface, Rect insets); + + @Accessor("mSource") + Surface getSource(); + + @Accessor("mSourceInsets") + Rect getSourceInsets(); + } + + @ForType(PixelCopy.Result.class) + private interface ResultReflector { + @Constructor + PixelCopy.Result newResult(int copyResult, Bitmap bitmap); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java index a5191fe7cb7..9bce4cca6b4 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java @@ -12,21 +12,24 @@ import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static com.google.common.base.Preconditions.checkState; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toCollection; import static org.robolectric.util.reflector.Reflector.reflector; -import android.Manifest.permission; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.Build.VERSION_CODES; import android.os.PowerManager; +import android.os.PowerManager.LowPowerStandbyPortDescription; +import android.os.PowerManager.LowPowerStandbyPortsLock; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.os.WorkSource; @@ -82,6 +85,11 @@ public class ShadowPowerManager { private static PowerManager.WakeLock latestWakeLock; + private boolean lowPowerStandbyEnabled = false; + private boolean lowPowerStandbySupported = false; + private boolean exemptFromLowPowerStandby = false; + private final Set allowedFeatures = new HashSet(); + @Implementation protected PowerManager.WakeLock newWakeLock(int flags, String tag) { PowerManager.WakeLock wl = Shadow.newInstanceOf(PowerManager.WakeLock.class); @@ -369,8 +377,6 @@ public void setIsRebootingUserspaceSupported(boolean supported) { * #setAmbientDisplayAvailable(boolean)}. */ @Implementation(minSdk = R) - @SystemApi - @RequiresPermission(permission.READ_DREAM_STATE) protected boolean isAmbientDisplayAvailable() { return isAmbientDisplayAvailable; } @@ -384,8 +390,6 @@ protected boolean isAmbientDisplayAvailable() { * ambient display for the given token. */ @Implementation(minSdk = R) - @SystemApi - @RequiresPermission(permission.WRITE_DREAM_STATE) protected void suppressAmbientDisplay(String token, boolean suppress) { String suppressionToken = Binder.getCallingUid() + "_" + token; if (suppress) { @@ -400,8 +404,6 @@ protected void suppressAmbientDisplay(String token, boolean suppress) { * token. */ @Implementation(minSdk = R) - @SystemApi - @RequiresPermission(permission.READ_DREAM_STATE) protected boolean isAmbientDisplaySuppressed() { return !ambientDisplaySuppressionTokens.isEmpty(); } @@ -411,7 +413,6 @@ protected boolean isAmbientDisplaySuppressed() { * false} by default. */ @Implementation(minSdk = R) - @SystemApi protected boolean isRebootingUserspaceSupported() { return isRebootingUserspaceSupported; } @@ -565,6 +566,99 @@ private Context getContext() { } } + @Implementation(minSdk = TIRAMISU) + protected boolean isLowPowerStandbySupported() { + return lowPowerStandbySupported; + } + + @TargetApi(TIRAMISU) + public void setLowPowerStandbySupported(boolean lowPowerStandbySupported) { + this.lowPowerStandbySupported = lowPowerStandbySupported; + } + + @Implementation(minSdk = TIRAMISU) + protected boolean isLowPowerStandbyEnabled() { + return lowPowerStandbySupported && lowPowerStandbyEnabled; + } + + @Implementation(minSdk = TIRAMISU) + protected void setLowPowerStandbyEnabled(boolean lowPowerStandbyEnabled) { + this.lowPowerStandbyEnabled = lowPowerStandbyEnabled; + } + + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected boolean isAllowedInLowPowerStandby(String feature) { + if (!lowPowerStandbySupported) { + return true; + } + return allowedFeatures.contains(feature); + } + + @TargetApi(UPSIDE_DOWN_CAKE) + public void addAllowedInLowPowerStandby(String feature) { + allowedFeatures.add(feature); + } + + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected boolean isExemptFromLowPowerStandby() { + if (!lowPowerStandbySupported) { + return true; + } + return exemptFromLowPowerStandby; + } + + @TargetApi(UPSIDE_DOWN_CAKE) + public void setExemptFromLowPowerStandby(boolean exemptFromLowPowerStandby) { + this.exemptFromLowPowerStandby = exemptFromLowPowerStandby; + } + + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected Object /* LowPowerStandbyPortsLock */ newLowPowerStandbyPortsLock( + List ports) { + PowerManager.LowPowerStandbyPortsLock lock = + Shadow.newInstanceOf(PowerManager.LowPowerStandbyPortsLock.class); + ((ShadowLowPowerStandbyPortsLock) Shadow.extract(lock)).setPorts(ports); + return (Object) lock; + } + + /** Shadow of {@link LowPowerStandbyPortsLock} to allow testing state. */ + @Implements( + value = PowerManager.LowPowerStandbyPortsLock.class, + minSdk = UPSIDE_DOWN_CAKE, + isInAndroidSdk = false) + public static class ShadowLowPowerStandbyPortsLock { + private List ports; + private boolean isAcquired = false; + private int acquireCount = 0; + + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected void acquire() { + isAcquired = true; + acquireCount++; + } + + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected void release() { + isAcquired = false; + } + + public boolean isAcquired() { + return isAcquired; + } + + public int getAcquireCount() { + return acquireCount; + } + + public void setPorts(List ports) { + this.ports = ports; + } + + public List getPorts() { + return ports; + } + } + /** Reflector interface for {@link PowerManager}'s internals. */ @ForType(PowerManager.class) private interface ReflectorPowerManager { 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 17b4ba10d00..b2ad1e3f87e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java @@ -13,6 +13,7 @@ import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import android.accounts.IAccountManager; import android.app.IAlarmManager; @@ -31,10 +32,12 @@ import android.app.trust.ITrustManager; import android.app.usage.IStorageStatsManager; import android.app.usage.IUsageStatsManager; +import android.app.wearable.IWearableSensingManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.IBluetooth; import android.bluetooth.IBluetoothManager; import android.companion.ICompanionDeviceManager; +import android.companion.virtual.IVirtualDeviceManager; import android.content.Context; import android.content.IClipboard; import android.content.IRestrictionsManager; @@ -218,6 +221,10 @@ public class ShadowServiceManager { addBinderService(Context.SAFETY_CENTER_SERVICE, ISafetyCenterManager.class); addBinderService(Context.STATUS_BAR_SERVICE, IStatusBar.class); } + if (RuntimeEnvironment.getApiLevel() >= UPSIDE_DOWN_CAKE) { + addBinderService(Context.VIRTUAL_DEVICE_SERVICE, IVirtualDeviceManager.class); + addBinderService(Context.WEARABLE_SENSING_SERVICE, IWearableSensingManager.class); + } } /** diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceSyncGroup.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceSyncGroup.java new file mode 100644 index 00000000000..999bedd378f --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceSyncGroup.java @@ -0,0 +1,47 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.HandlerThread; +import android.window.SurfaceSyncGroup; +import com.google.common.util.concurrent.Uninterruptibles; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; +import org.robolectric.util.reflector.Static; +import org.robolectric.versioning.AndroidVersions.U; + +/** Shadow for new SurfaceSyncGroup introduced in android U. */ +@Implements( + value = SurfaceSyncGroup.class, + minSdk = U.SDK_INT, + // TODO: remove when minimum supported compileSdk is >= 34 + isInAndroidSdk = false) +public class ShadowSurfaceSyncGroup { + + @Resetter + public static void reset() { + if (RuntimeEnvironment.getApiLevel() > TIRAMISU) { + HandlerThread hThread = reflector(SurfaceSyncGroupReflector.class).getHandlerThread(); + if (hThread != null) { + hThread.quit(); + Uninterruptibles.joinUninterruptibly(hThread); + reflector(SurfaceSyncGroupReflector.class).setHandlerThread(null); + } + } + } + + @ForType(SurfaceSyncGroup.class) + private interface SurfaceSyncGroupReflector { + @Accessor("sHandlerThread") + @Static + HandlerThread getHandlerThread(); + + @Accessor("sHandlerThread") + @Static + void setHandlerThread(HandlerThread t); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java new file mode 100644 index 00000000000..6c2f1a3a287 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java @@ -0,0 +1,172 @@ +package org.robolectric.shadows; + +import static android.companion.virtual.VirtualDeviceManager.LAUNCH_SUCCESS; + +import android.annotation.NonNull; +import android.app.PendingIntent; +import android.companion.virtual.IVirtualDeviceManager; +import android.companion.virtual.VirtualDevice; +import android.companion.virtual.VirtualDeviceManager; +import android.companion.virtual.VirtualDeviceParams; +import android.companion.virtual.sensor.VirtualSensor; +import android.content.Context; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; +import java.util.stream.Collectors; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.Resetter; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.versioning.AndroidVersions.U; + +/** Shadow for VirtualDeviceManager. */ +@Implements( + value = VirtualDeviceManager.class, + minSdk = U.SDK_INT, + + // TODO: remove when minimum supported compileSdk is >= 34 + isInAndroidSdk = false) +public class ShadowVirtualDeviceManager { + + private final List mVirtualDevices = new ArrayList<>(); + private Context context; + private IVirtualDeviceManager service; + + @Implementation + protected void __constructor__(IVirtualDeviceManager service, Context context) { + this.context = context; + this.service = service; + } + + @Implementation + protected VirtualDeviceManager.VirtualDevice createVirtualDevice( + int associationId, VirtualDeviceParams params) { + VirtualDeviceManager.VirtualDevice device = + ReflectionHelpers.callConstructor( + VirtualDeviceManager.VirtualDevice.class, + ClassParameter.from(IVirtualDeviceManager.class, service), + ClassParameter.from(Context.class, context), + ClassParameter.from(int.class, associationId), + ClassParameter.from(VirtualDeviceParams.class, params)); + mVirtualDevices.add(device); + return device; + } + + @Implementation + protected List getVirtualDevices() { + return mVirtualDevices.stream() + .map( + virtualDevice -> + new VirtualDevice( + virtualDevice.getDeviceId(), + ((ShadowVirtualDevice) Shadow.extract(virtualDevice)).getParams().getName())) + .collect(Collectors.toList()); + } + + @Implementation + protected int getDevicePolicy(int deviceId, int policyType) { + return mVirtualDevices.stream() + .filter(virtualDevice -> virtualDevice.getDeviceId() == deviceId) + .findFirst() + .map( + virtualDevice -> + ((ShadowVirtualDevice) Shadow.extract(virtualDevice)) + .getParams() + .getDevicePolicy(policyType)) + .orElse(VirtualDeviceParams.DEVICE_POLICY_DEFAULT); + } + + /** Shadow for inner class VirtualDeviceManager.VirtualDevice. */ + @Implements( + value = VirtualDeviceManager.VirtualDevice.class, + minSdk = U.SDK_INT, + // TODO: remove when minimum supported compileSdk is >= 34 + isInAndroidSdk = false) + public static class ShadowVirtualDevice { + private static final AtomicInteger nextDeviceId = new AtomicInteger(1); + + @RealObject VirtualDeviceManager.VirtualDevice realVirtualDevice; + private VirtualDeviceParams params; + private int deviceId; + private PendingIntent pendingIntent; + private Integer pendingIntentResultCode = LAUNCH_SUCCESS; + + @Implementation + protected void __constructor__( + IVirtualDeviceManager service, + Context context, + int associationId, + VirtualDeviceParams params) { + Shadow.invokeConstructor( + VirtualDeviceManager.VirtualDevice.class, + realVirtualDevice, + ClassParameter.from(IVirtualDeviceManager.class, service), + ClassParameter.from(Context.class, context), + ClassParameter.from(int.class, associationId), + ClassParameter.from(VirtualDeviceParams.class, params)); + this.params = params; + this.deviceId = nextDeviceId.getAndIncrement(); + } + + @Implementation + protected int getDeviceId() { + return deviceId; + } + + /** Prevents a NPE when calling .close() on a VirtualDevice in unit tests. */ + @Implementation + protected void close() {} + + public VirtualDeviceParams getParams() { + return params; + } + + @Implementation + protected List getVirtualSensorList() { + if (params.getVirtualSensorConfigs() == null) { + return new ArrayList<>(); + } + + return params.getVirtualSensorConfigs().stream() + .map( + config -> { + VirtualSensor sensor = + new VirtualSensor( + config.hashCode(), config.getType(), config.getName(), null, null); + ShadowVirtualSensor shadowSensor = Shadow.extract(sensor); + shadowSensor.setDeviceId(deviceId); + return sensor; + }) + .collect(Collectors.toList()); + } + + @Implementation + protected void launchPendingIntent( + int displayId, + @NonNull PendingIntent pendingIntent, + @NonNull Executor executor, + @NonNull IntConsumer listener) { + this.pendingIntent = pendingIntent; + executor.execute(() -> listener.accept(pendingIntentResultCode)); + } + + public void setPendingIntentCallbackResultCode(int resultCode) { + this.pendingIntentResultCode = resultCode; + } + + public PendingIntent getLastLaunchedPendingIntent() { + return pendingIntent; + } + + @Resetter + public static void reset() { + nextDeviceId.set(1); + } + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java new file mode 100644 index 00000000000..dc372e05427 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java @@ -0,0 +1,33 @@ +package org.robolectric.shadows; + +import android.companion.virtual.VirtualDeviceParams; +import android.companion.virtual.sensor.VirtualSensorCallback; +import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.versioning.AndroidVersions.U; + +/** Shadow for VirtualDeviceParams. */ +@Implements( + value = VirtualDeviceParams.class, + minSdk = U.SDK_INT, + // TODO: remove when minimum supported compileSdk is >= 34 + isInAndroidSdk = false) +public class ShadowVirtualDeviceParams { + + @RealObject VirtualDeviceParams realObject; + + public VirtualSensorCallback getVirtualSensorCallback() { + return realObject.getVirtualSensorCallback() == null + ? null + : ReflectionHelpers.getField(realObject.getVirtualSensorCallback(), "mCallback"); + } + + public VirtualSensorDirectChannelCallback getVirtualSensorDirectChannelCallback() { + return realObject.getVirtualSensorCallback() == null + ? null + : ReflectionHelpers.getField( + realObject.getVirtualSensorCallback(), "mDirectChannelCallback"); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualSensor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualSensor.java new file mode 100644 index 00000000000..9a745d9ee0d --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualSensor.java @@ -0,0 +1,39 @@ +package org.robolectric.shadows; + +import android.companion.virtual.sensor.VirtualSensor; +import android.companion.virtual.sensor.VirtualSensorEvent; +import java.util.ArrayList; +import java.util.List; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.versioning.AndroidVersions.U; + +/** Shadow for VirtualSensor. */ +@Implements( + value = VirtualSensor.class, + minSdk = U.SDK_INT, + // TODO: remove when minimum supported compileSdk is >= 34 + isInAndroidSdk = false) +public class ShadowVirtualSensor { + + private int deviceId = 0; + private final List sentEvents = new ArrayList<>(); + + @Implementation + protected int getDeviceId() { + return deviceId; + } + + @Implementation + protected void sendEvent(VirtualSensorEvent event) { + sentEvents.add(event); + } + + public List getSentEvents() { + return sentEvents; + } + + void setDeviceId(int deviceId) { + this.deviceId = deviceId; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWearableSensingManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWearableSensingManager.java new file mode 100644 index 00000000000..9713374b8ab --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWearableSensingManager.java @@ -0,0 +1,67 @@ +package org.robolectric.shadows; + +import android.app.wearable.WearableSensingManager; +import android.app.wearable.WearableSensingManager.StatusCode; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.SharedMemory; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.versioning.AndroidVersions.U; + +/** Shadow for VirtualDeviceManager. */ +@Implements( + value = WearableSensingManager.class, + minSdk = U.SDK_INT, + // TODO: remove when minimum supported compileSdk is >= 34 + isInAndroidSdk = false) +public class ShadowWearableSensingManager { + + private @StatusCode Integer provideDataStreamResult = WearableSensingManager.STATUS_SUCCESS; + private @StatusCode Integer provideDataResult = WearableSensingManager.STATUS_SUCCESS; + private PersistableBundle lastDataBundle; + private SharedMemory lastSharedMemory; + private ParcelFileDescriptor lastParcelFileDescriptor; + + @Implementation + protected void provideDataStream( + ParcelFileDescriptor parcelFileDescriptor, + Executor executor, + Consumer statusConsumer) { + lastParcelFileDescriptor = parcelFileDescriptor; + executor.execute(() -> statusConsumer.accept(provideDataStreamResult)); + } + + @Implementation + protected void provideData( + PersistableBundle data, + SharedMemory sharedMemory, + Executor executor, + @StatusCode Consumer statusConsumer) { + lastDataBundle = data; + lastSharedMemory = sharedMemory; + executor.execute(() -> statusConsumer.accept(provideDataResult)); + } + + public void setProvideDataStreamResult(@StatusCode Integer provideDataStreamResult) { + this.provideDataStreamResult = provideDataStreamResult; + } + + public void setProvideDataResult(@StatusCode Integer provideDataResult) { + this.provideDataResult = provideDataResult; + } + + public ParcelFileDescriptor getLastParcelFileDescriptor() { + return lastParcelFileDescriptor; + } + + public PersistableBundle getLastDataBundle() { + return lastDataBundle; + } + + public SharedMemory getLastSharedMemory() { + return lastSharedMemory; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java index a5c7000674e..72c0009312b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java @@ -7,6 +7,7 @@ import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static java.util.stream.Collectors.toList; import android.app.admin.DevicePolicyManager; @@ -21,7 +22,9 @@ import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.AddNetworkResult; +import android.net.wifi.WifiManager.LocalOnlyConnectionFailureListener; import android.net.wifi.WifiManager.MulticastLock; +import android.net.wifi.WifiNetworkSpecifier; import android.net.wifi.WifiSsid; import android.net.wifi.WifiUsabilityStatsEntry; import android.os.Binder; @@ -42,6 +45,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import org.robolectric.RuntimeEnvironment; @@ -89,6 +93,44 @@ public class ShadowWifiManager { private final Object pnoRequestLock = new Object(); private PnoScanRequest outstandingPnoScanRequest = null; + private final ConcurrentMap + localOnlyConnectionFailureListenerExecutorMap = new ConcurrentHashMap<>(); + + /** + * Simulates a connection failure for a specified local network connection. + * + * @param specifier the {@link WifiNetworkSpecifier} describing the local network connection + * attempt + * @param failureReason the reason for the network connection failure. This should be one of the + * values specified in {@code WifiManager#STATUS_LOCAL_ONLY_CONNECTION_FAILURE_*} + */ + public void triggerLocalConnectionFailure(WifiNetworkSpecifier specifier, int failureReason) { + localOnlyConnectionFailureListenerExecutorMap.forEach( + (failureListener, executor) -> + executor.execute(() -> failureListener.onConnectionFailed(specifier, failureReason))); + } + + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected void addLocalOnlyConnectionFailureListener( + Executor executor, LocalOnlyConnectionFailureListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener cannot be null"); + } + if (executor == null) { + throw new IllegalArgumentException("Executor cannot be null"); + } + localOnlyConnectionFailureListenerExecutorMap.putIfAbsent(listener, executor); + } + + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected void removeLocalOnlyConnectionFailureListener( + LocalOnlyConnectionFailureListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener cannot be null"); + } + localOnlyConnectionFailureListenerExecutorMap.remove(listener); + } + @Implementation protected boolean setWifiEnabled(boolean wifiEnabled) { checkAccessWifiStatePermission(); From f803c316e68ba804d0317c71f9e0754842ae8d15 Mon Sep 17 00:00:00 2001 From: Googler Date: Mon, 9 Oct 2023 16:04:22 -0700 Subject: [PATCH 08/33] Adds shadow for UserManager#isUserForeground. Adds an implementation and shadow for setting whether or not the current user is a foreground user. PiperOrigin-RevId: 572065800 --- .../shadows/ShadowUserManagerTest.java | 23 +++++++++++++++++++ .../shadows/ShadowUserManager.java | 11 +++++++++ 2 files changed, 34 insertions(+) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java index 9845a594635..5c83ff1464c 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java @@ -1126,6 +1126,29 @@ public void someUserHasAccount() { assertThat(userManager.someUserHasAccount(SEED_ACCOUNT_NAME, SEED_ACCOUNT_TYPE)).isFalse(); } + @Test + @Config(minSdk = S) + public void isUserForeground_defaultValue_returnsTrue() { + assertThat(userManager.isUserForeground()).isTrue(); + } + + @Test + @Config(minSdk = S) + public void isUserForeground_overridden_returnsNewValue() { + shadowOf(userManager).setUserForeground(false); + + assertThat(userManager.isUserForeground()).isFalse(); + } + + @Test + @Config(minSdk = S) + public void isUserForeground_valueToggled_returnsLatestValue() { + shadowOf(userManager).setUserForeground(false); + shadowOf(userManager).setUserForeground(true); + + assertThat(userManager.isUserForeground()).isTrue(); + } + // Create user handle from parcel since UserHandle.of() was only added in later APIs. private static UserHandle newUserHandle(int uid) { Parcel userParcel = Parcel.obtain(); 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 a1bb4c1c98c..15d348bacf1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java @@ -88,6 +88,7 @@ public class ShadowUserManager { private Boolean cloneProfile; private boolean userUnlocked = true; private boolean isSystemUser = true; + private volatile boolean isForegroundUser = true; /** * Holds whether or not a managed profile can be unlocked. If a profile is not in this map, it is @@ -1259,4 +1260,14 @@ public void setSomeUserHasAccount(String accountName, String accountType) { public void removeSomeUserHasAccount(String accountName, String accountType) { userAccounts.remove(new Account(accountName, accountType)); } + + /** Sets whether or not the current user is the foreground user. */ + public void setUserForeground(boolean foreground) { + isForegroundUser = foreground; + } + + @Implementation(minSdk = S) + protected boolean isUserForeground() { + return isForegroundUser; + } } From 47eeeb3f4fb29408d5fb906671522f43a22f3fe7 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Tue, 10 Oct 2023 18:59:11 -0700 Subject: [PATCH 09/33] Refactor ShadowVirtualDeviceParams methods into ShadowVirtualDevice. The ShadowVirtualDeviceParams methods were causing infinite recursion issues on github, because they had the same signature as the android framework methods. ShadowVirtualDeviceParams doesn't actually need to be a shadow at all, and its presence was arguably exposing more implementation details than needed. This commit moves the ShadowVirtualDeviceParams into ShadowVirtualDevice, and replaces ReflectionHelper usage with reflector. PiperOrigin-RevId: 572429463 --- .../ShadowVirtualDeviceManagerTest.java | 3 +- .../shadows/ShadowVirtualDeviceManager.java | 34 ++++++++++++++++++- .../shadows/ShadowVirtualDeviceParams.java | 33 ------------------ 3 files changed, 34 insertions(+), 36 deletions(-) delete mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java index f89c3b80cb2..043c3c98b23 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java @@ -133,8 +133,7 @@ public void testGetSensorCallbacks() { .build()); ShadowVirtualDevice shadowDevice = Shadow.extract(virtualDevice); - ShadowVirtualDeviceParams shadowParams = Shadow.extract(shadowDevice.getParams()); - VirtualSensorCallback retrievedCallback = shadowParams.getVirtualSensorCallback(); + VirtualSensorCallback retrievedCallback = shadowDevice.getVirtualSensorCallback(); retrievedCallback.onConfigurationChanged( virtualDevice.getVirtualSensorList().get(0), true, Duration.ZERO, Duration.ZERO); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java index 6c2f1a3a287..e79d1fdcc6a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java @@ -1,6 +1,7 @@ package org.robolectric.shadows; import static android.companion.virtual.VirtualDeviceManager.LAUNCH_SUCCESS; +import static org.robolectric.util.reflector.Reflector.reflector; import android.annotation.NonNull; import android.app.PendingIntent; @@ -9,6 +10,8 @@ import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.sensor.VirtualSensor; +import android.companion.virtual.sensor.VirtualSensorCallback; +import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback; import android.content.Context; import java.util.ArrayList; import java.util.List; @@ -23,6 +26,8 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; import org.robolectric.versioning.AndroidVersions.U; /** Shadow for VirtualDeviceManager. */ @@ -123,7 +128,7 @@ protected int getDeviceId() { @Implementation protected void close() {} - public VirtualDeviceParams getParams() { + VirtualDeviceParams getParams() { return params; } @@ -164,9 +169,36 @@ public PendingIntent getLastLaunchedPendingIntent() { return pendingIntent; } + public VirtualSensorCallback getVirtualSensorCallback() { + return params.getVirtualSensorCallback() == null + ? null + : reflector( + VirtualSensorCallbackDelegateReflector.class, params.getVirtualSensorCallback()) + .getCallback(); + } + + public VirtualSensorDirectChannelCallback getVirtualSensorDirectChannelCallback() { + return params.getVirtualSensorCallback() == null + ? null + : reflector( + VirtualSensorCallbackDelegateReflector.class, params.getVirtualSensorCallback()) + .getDirectChannelCallback(); + } + @Resetter public static void reset() { nextDeviceId.set(1); } } + + @ForType( + className = + "android.companion.virtual.VirtualDeviceParams$Builder$VirtualSensorCallbackDelegate") + private interface VirtualSensorCallbackDelegateReflector { + @Accessor("mCallback") + VirtualSensorCallback getCallback(); + + @Accessor("mDirectChannelCallback") + VirtualSensorDirectChannelCallback getDirectChannelCallback(); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java deleted file mode 100644 index dc372e05427..00000000000 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceParams.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.robolectric.shadows; - -import android.companion.virtual.VirtualDeviceParams; -import android.companion.virtual.sensor.VirtualSensorCallback; -import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback; -import org.robolectric.annotation.Implements; -import org.robolectric.annotation.RealObject; -import org.robolectric.util.ReflectionHelpers; -import org.robolectric.versioning.AndroidVersions.U; - -/** Shadow for VirtualDeviceParams. */ -@Implements( - value = VirtualDeviceParams.class, - minSdk = U.SDK_INT, - // TODO: remove when minimum supported compileSdk is >= 34 - isInAndroidSdk = false) -public class ShadowVirtualDeviceParams { - - @RealObject VirtualDeviceParams realObject; - - public VirtualSensorCallback getVirtualSensorCallback() { - return realObject.getVirtualSensorCallback() == null - ? null - : ReflectionHelpers.getField(realObject.getVirtualSensorCallback(), "mCallback"); - } - - public VirtualSensorDirectChannelCallback getVirtualSensorDirectChannelCallback() { - return realObject.getVirtualSensorCallback() == null - ? null - : ReflectionHelpers.getField( - realObject.getVirtualSensorCallback(), "mDirectChannelCallback"); - } -} From 43dedab54e9153b2757b71bf0f7e24696f7b872a Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Tue, 10 Oct 2023 19:15:45 -0700 Subject: [PATCH 10/33] Attempt to stop superflous `No Compatibility callbacks set` logging. Currently in Robolectric no AppCompatCallbacks are set, which causes the Android framework to spam log warnings like ``` System.logW: No Compatibility callbacks set! Querying change 210923482 ``` This change sets an empty AppCompatCallback to prevent this log. Fixes #8509 PiperOrigin-RevId: 572431911 --- .../android/internal/AndroidTestEnvironment.java | 6 ++++++ .../robolectric/shadows/CompatibilityTest.java | 10 ++++++++++ .../shadows/ShadowChangeReporter.java | 16 ++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowChangeReporter.java diff --git a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java index bca77c22bfe..479c27b05f8 100755 --- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java @@ -7,6 +7,7 @@ import android.annotation.SuppressLint; import android.app.ActivityThread; +import android.app.AppCompatCallbacks; import android.app.Application; import android.app.Instrumentation; import android.app.LoadedApk; @@ -369,6 +370,11 @@ private Application installAndCreateApplication( populateAssetPaths(appResources.getAssets(), appManifest); } + // circument the 'No Compatibility callbacks set!' log. See #8509 + if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.R) { + AppCompatCallbacks.install(new long[0]); + } + PerfStatsCollector.getInstance() .measure( "application onCreate()", diff --git a/robolectric/src/test/java/org/robolectric/shadows/CompatibilityTest.java b/robolectric/src/test/java/org/robolectric/shadows/CompatibilityTest.java index 20c538eea59..85621c7eb0e 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/CompatibilityTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/CompatibilityTest.java @@ -8,6 +8,8 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.annotation.experimental.LazyApplication; +import org.robolectric.annotation.experimental.LazyApplication.LazyLoad; /** Tests to make sure {@link android.compat.Compatibility} is instrumented correctly */ @RunWith(RobolectricTestRunner.class) @@ -23,4 +25,12 @@ public void reportUnconditionalChange() { // Verify this does not cause a crash due to uninstrumented System.logW. Compatibility.reportUnconditionalChange(100); } + + @Test + @LazyApplication(LazyLoad.OFF) + public void isChangeEnabled_logging() { + Compatibility.isChangeEnabled(100); + // verify there are no CompatibilityChangeReporter spam logs + assertThat(ShadowLog.getLogsForTag("CompatibilityChangeReporter")).isEmpty(); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChangeReporter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChangeReporter.java new file mode 100644 index 00000000000..c1e414fd74c --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChangeReporter.java @@ -0,0 +1,16 @@ +package org.robolectric.shadows; + +import com.android.internal.compat.ChangeReporter; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.versioning.AndroidVersions.R; + +@Implements(value = ChangeReporter.class, isInAndroidSdk = false, minSdk = R.SDK_INT) +public class ShadowChangeReporter { + + /** Don't write any compat change to logs, as its spammy in Robolectric. */ + @Implementation + protected boolean shouldWriteToDebug(int uid, long changeId, int state) { + return false; + } +} From 54087220d5373273c830b940e40826c332e5e864 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Wed, 11 Oct 2023 10:28:46 -0700 Subject: [PATCH 11/33] Add android U as a default SDK. This will enable tests to run on android U. Fixes #8404 PiperOrigin-RevId: 572615159 --- .github/workflows/tests.yml | 2 +- README.md | 2 +- .../main/java/org/robolectric/plugins/DefaultSdkProvider.java | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 13528390df9..21b259a81c9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - api-versions: [ '19,21,22', '23,24,25', '26,27,28', '29,30,31', '32,33' ] + api-versions: [ '19,21,22', '23,24,25', '26,27,28', '29,30,31', '32,33,34' ] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 3ec2a8bfa3d..862eaec5c33 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Robolectric is the industry-standard unit testing framework for Android. With Robolectric, your tests run in a simulated Android environment inside a JVM, without the overhead and flakiness of an emulator. Robolectric tests routinely run 10x faster than those on cold-started emulators. -Robolectric supports running unit tests for *17* different versions of Android, ranging from Jelly Bean (API level 16) to TIRAMISU (API level 33). +Robolectric supports running unit tests for *17* different versions of Android, ranging from Jelly Bean (API level 16) to U (API level 34). ## Usage diff --git a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java index 47073fcc480..f4cf49f264f 100644 --- a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java +++ b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java @@ -31,6 +31,7 @@ import org.robolectric.versioning.AndroidVersions.S; import org.robolectric.versioning.AndroidVersions.Sv2; import org.robolectric.versioning.AndroidVersions.T; +import org.robolectric.versioning.AndroidVersions.U; /** * Robolectric's default {@link SdkProvider}. @@ -75,6 +76,7 @@ protected void populateSdks(TreeMap knownSdks) { knownSdks.put(S.SDK_INT, new DefaultSdk(S.SDK_INT, "12", "7732740", "REL", 9)); knownSdks.put(Sv2.SDK_INT, new DefaultSdk(Sv2.SDK_INT, "12.1", "8229987", "REL", 9)); knownSdks.put(T.SDK_INT, new DefaultSdk(T.SDK_INT, "13", "9030017", "Tiramisu", 9)); + knownSdks.put(U.SDK_INT, new DefaultSdk(U.SDK_INT, "14", "10818077", "REL", 17)); } @Override From bb725e60cecb9635da5bded05720eefb46c5d27f Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Wed, 11 Oct 2023 11:37:47 -0700 Subject: [PATCH 12/33] Optimize the ShadowView constructor View constructors are invoked frequently, especially when inflating complex layouts. We can do some optimizations on it: * Avoid shadowing the constructor on SDK >= Q because we don't care about the capturing the 'attributeSet' value. * Use @Direct reflector to invoke the constructor to make it faster. PiperOrigin-RevId: 572638224 --- .../org/robolectric/shadows/ShadowView.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java index f194924965c..3e742a311c2 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java @@ -7,7 +7,6 @@ import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; -import static org.robolectric.shadow.api.Shadow.invokeConstructor; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import static org.robolectric.util.ReflectionHelpers.getField; import static org.robolectric.util.reflector.Reflector.reflector; @@ -57,7 +56,6 @@ import org.robolectric.config.ConfigurationRegistry; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowViewRootImpl.ViewRootImplReflector; -import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.TimeUtils; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; @@ -172,25 +170,16 @@ static int[] getLocationInSurfaceCompat(View view) { @Implementation(maxSdk = KITKAT) protected void __constructor__(Context context, AttributeSet attributeSet, int defStyle) { this.attributeSet = attributeSet; - invokeConstructor( - View.class, - realView, - ClassParameter.from(Context.class, context), - ClassParameter.from(AttributeSet.class, attributeSet), - ClassParameter.from(int.class, defStyle)); + reflector(_View_.class, realView).__constructor__(context, attributeSet, defStyle); } - @Implementation(minSdk = KITKAT_WATCH) + /* Note: maxSdk is R because capturing `attributeSet` is not needed any more after R. */ + @Implementation(minSdk = KITKAT_WATCH, maxSdk = R) protected void __constructor__( Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) { this.attributeSet = attributeSet; - invokeConstructor( - View.class, - realView, - ClassParameter.from(Context.class, context), - ClassParameter.from(AttributeSet.class, attributeSet), - ClassParameter.from(int.class, defStyleAttr), - ClassParameter.from(int.class, defStyleRes)); + reflector(_View_.class, realView) + .__constructor__(context, attributeSet, defStyleAttr, defStyleRes); } @Implementation @@ -948,6 +937,13 @@ void removeOnAttachStateChangeListener( @Direct void setScrollY(int value); + + @Direct + void __constructor__(Context context, AttributeSet attributeSet, int defStyle); + + @Direct + void __constructor__( + Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes); } public void callOnAttachedToWindow() { From 5cd9bc3828961325d8bb89277e45d8fd26fe39e6 Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 11 Oct 2023 12:13:17 -0700 Subject: [PATCH 13/33] ShadowVirtualDeviceManager: Add implementation for isValidVirtualDeviceId. Adding implementation for isValidVirtualDeviceId which will return validity based on the virtualDevices in the shadow. PiperOrigin-RevId: 572649038 --- .../shadows/ShadowVirtualDeviceManagerTest.java | 12 ++++++++++++ .../shadows/ShadowVirtualDeviceManager.java | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java index 043c3c98b23..98db027df54 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVirtualDeviceManagerTest.java @@ -61,6 +61,18 @@ public void testCreateVirtualDevice() { assertThat(virtualDeviceManager.getVirtualDevices().get(0).getName()).isEqualTo("foo"); } + @Test + public void testIsValidVirtualDeviceId() { + VirtualDevice virtualDevice = + virtualDeviceManager.createVirtualDevice( + 0, new VirtualDeviceParams.Builder().setName("foo").build()); + + assertThat(virtualDeviceManager.isValidVirtualDeviceId(virtualDevice.getDeviceId())).isTrue(); + + // Random virtual device id should be false + assertThat(virtualDeviceManager.isValidVirtualDeviceId(999)).isFalse(); + } + @Test public void testGetDevicePolicy() { VirtualDevice virtualDevice = diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java index e79d1fdcc6a..baa55f1ceb3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVirtualDeviceManager.java @@ -87,6 +87,12 @@ protected int getDevicePolicy(int deviceId, int policyType) { .orElse(VirtualDeviceParams.DEVICE_POLICY_DEFAULT); } + @Implementation + protected boolean isValidVirtualDeviceId(int deviceId) { + return mVirtualDevices.stream() + .anyMatch(virtualDevice -> virtualDevice.getDeviceId() == deviceId); + } + /** Shadow for inner class VirtualDeviceManager.VirtualDevice. */ @Implements( value = VirtualDeviceManager.VirtualDevice.class, From 460db5af33579cf7816111b104bb451a20c932b8 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Thu, 12 Oct 2023 00:26:00 -0700 Subject: [PATCH 14/33] Refactor ShadowCamera to use more real Android code Camera.Parameters and Camera.Size are pure Java classes, yet were reimplemented using shadow methods in Robolectric. Update the shadows to use as much real Android code as possible. Also update the shadow APIs to just invoke Android API methods. PiperOrigin-RevId: 572806961 --- .../shadows/ShadowCameraParametersTest.java | 20 +- .../robolectric/shadows/ShadowCameraTest.java | 21 + .../org/robolectric/shadows/ShadowCamera.java | 385 ++++-------------- 3 files changed, 121 insertions(+), 305 deletions(-) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraParametersTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraParametersTest.java index 44d64c753c8..06d9c20670f 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraParametersTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraParametersTest.java @@ -14,7 +14,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Shadows; -import org.robolectric.shadow.api.Shadow; @RunWith(AndroidJUnit4.class) public class ShadowCameraParametersTest { @@ -23,7 +22,7 @@ public class ShadowCameraParametersTest { @Before public void setUp() throws Exception { - parameters = Shadow.newInstanceOf(Camera.Parameters.class); + parameters = Camera.open().getParameters(); } @Test @@ -113,8 +112,7 @@ public void testGetSupportedPreviewSizes() { @Test public void testInitSupportedPreviewSizes() { Shadows.shadowOf(parameters).initSupportedPreviewSizes(); - assertThat(parameters.getSupportedPreviewSizes()).isNotNull(); - assertThat(parameters.getSupportedPreviewSizes()).isEmpty(); + assertThat(parameters.getSupportedPreviewSizes()).isNull(); } @Test @@ -164,7 +162,7 @@ public void testExposureCompensationSetting() { @Test public void testGetSupportedFocusModesDefaultValue() { List supportedFocusModes = parameters.getSupportedFocusModes(); - assertThat(supportedFocusModes).isEmpty(); + assertThat(supportedFocusModes).containsExactly("auto"); } @Test @@ -184,7 +182,7 @@ public void testSetAndGetFocusMode() { @Test public void testGetSupportedFlashModesDefaultValue() { List supportedFlashModes = parameters.getSupportedFlashModes(); - assertThat(supportedFlashModes).isEmpty(); + assertThat(supportedFlashModes).containsExactly("auto", "on", "off"); } @Test @@ -203,7 +201,7 @@ public void testSetAndGetFlashMode() { @Test public void testGetMaxNumFocusAreasDefaultValue() { - assertThat(parameters.getMaxNumFocusAreas()).isEqualTo(0); + assertThat(parameters.getMaxNumFocusAreas()).isEqualTo(1); } @Test @@ -226,7 +224,7 @@ public void testSetAndGetFocusAreas() { @Test public void testGetMaxNumMeteringAreasDefaultValue() { - assertThat(parameters.getMaxNumFocusAreas()).isEqualTo(0); + assertThat(parameters.getMaxNumFocusAreas()).isEqualTo(1); } @Test @@ -258,4 +256,10 @@ public void testSetAndGetCustomParams() { parameters.set(key, value2); assertThat(parameters.get(key)).isEqualTo(value2); } + + @Test + public void testSetAndGetRotation() { + parameters.setRotation(90); + assertThat(Shadows.shadowOf(parameters).getRotation()).isEqualTo(90); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraTest.java index baa064f5b46..0c529708808 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowCameraTest.java @@ -330,6 +330,27 @@ public void testShutterEnabled() { assertThat(shadowCamera.enableShutterSound(true)).isTrue(); } + @Test + public void cameraParameters_areCached() { + assertThat(camera.getParameters()).isSameInstanceAs(Camera.open().getParameters()); + } + + @Test + public void setSupportedFocusModes_empty_clearsCurrentFocusMode() { + Camera.Parameters parameters = camera.getParameters(); + assertThat(parameters.getFocusMode()).isNotNull(); + Shadows.shadowOf(parameters).setSupportedFocusModes(); + assertThat(parameters.getFocusMode()).isNull(); + } + + @Test + public void setSupportedFlashModes_empty_clearsCurrentFocusMode() { + Camera.Parameters parameters = camera.getParameters(); + assertThat(parameters.getFlashMode()).isNotNull(); + Shadows.shadowOf(parameters).setSupportedFlashModes(); + assertThat(parameters.getFlashMode()).isNull(); + } + private void addBackCamera() { Camera.CameraInfo backCamera = new Camera.CameraInfo(); backCamera.facing = Camera.CameraInfo.CAMERA_FACING_BACK; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java index ffe00973fe3..2adcd3c4771 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java @@ -3,12 +3,13 @@ import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static org.robolectric.shadow.api.Shadow.newInstanceOf; -import android.graphics.ImageFormat; import android.hardware.Camera; import android.os.Build; import android.view.SurfaceHolder; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -16,15 +17,50 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; @Implements(Camera.class) public class ShadowCamera { + // These are completely arbitrary and likely outdated default parameters that have been added long + // ago. + private static final ImmutableMap DEFAULT_PARAMS = + ImmutableMap.builder() + .put("picture-size", "1280x960") + .put("preview-size", "640x480") + .put("preview-fps-range", "10,30") + .put("preview-frame-rate", "30") + .put("preview-format", "yuv420sp") + .put("picture-format-values", "yuv420sp,jpeg") + .put("preview-format-values", "yuv420sp,jpeg") + .put("picture-size-values", "320x240,640x480,800x600") + .put("preview-size-values", "320x240,640x480") + .put("preview-fps-range-values", "(15000,15000),(10000,30000)") + .put("preview-frame-rate-values", "10,15,30") + .put("exposure-compensation", "0") + .put("exposure-compensation-step", "0.5") + .put("min-exposure-compensation", "-6") + .put("max-exposure-compensation", "6") + .put("focus-mode-values", Camera.Parameters.FOCUS_MODE_AUTO) + .put("focus-mode", Camera.Parameters.FOCUS_MODE_AUTO) + .put( + "flash-mode-values", + Camera.Parameters.FLASH_MODE_AUTO + + "," + + Camera.Parameters.FLASH_MODE_ON + + "," + + Camera.Parameters.FLASH_MODE_OFF) + .put("flash-mode", Camera.Parameters.FLASH_MODE_AUTO) + .put("max-num-focus-areas", "1") + .put("max-num-metering-areas", "1") + .buildOrThrow(); private static int lastOpenedCameraId; private int id; - private boolean locked; + private boolean locked = true; private boolean previewing; private boolean released; private Camera.Parameters parameters; @@ -34,27 +70,16 @@ public class ShadowCamera { private int displayOrientation; private Camera.AutoFocusCallback autoFocusCallback; private boolean autoFocusing; - private boolean shutterSoundEnabled; + private boolean shutterSoundEnabled = true; - private static Map cameras = new HashMap<>(); + private static final Map cameras = new HashMap<>(); + private static final Map cameraParameters = new HashMap<>(); @RealObject private Camera realCamera; - @Implementation - protected void __constructor__() { - locked = true; - previewing = false; - released = false; - shutterSoundEnabled = true; - } - @Implementation protected static Camera open() { - lastOpenedCameraId = 0; - Camera camera = newInstanceOf(Camera.class); - ShadowCamera shadowCamera = Shadow.extract(camera); - shadowCamera.id = 0; - return camera; + return open(0); } @Implementation @@ -63,6 +88,11 @@ protected static Camera open(int cameraId) { Camera camera = newInstanceOf(Camera.class); ShadowCamera shadowCamera = Shadow.extract(camera); shadowCamera.id = cameraId; + if (cameraParameters.containsKey(cameraId)) { + shadowCamera.parameters = cameraParameters.get(cameraId); + } else { + cameraParameters.put(cameraId, camera.getParameters()); + } return camera; } @@ -82,8 +112,12 @@ protected void reconnect() { @Implementation protected Camera.Parameters getParameters() { - if (null == parameters) { - parameters = newInstanceOf(Camera.Parameters.class); + if (parameters == null) { + parameters = + ReflectionHelpers.callConstructor( + Camera.Parameters.class, ClassParameter.from(Camera.class, realCamera)); + Joiner.MapJoiner mapJoiner = Joiner.on(";").withKeyValueSeparator("="); + parameters.unflatten(mapJoiner.join(DEFAULT_PARAMS)); } return parameters; } @@ -261,149 +295,36 @@ public static void addCameraInfo(int id, Camera.CameraInfo camInfo) { cameras.put(id, camInfo); } + @Resetter public static void clearCameraInfo() { cameras.clear(); + cameraParameters.clear(); } /** Shadows the Android {@code Camera.Parameters} class. */ @Implements(Camera.Parameters.class) public static class ShadowParameters { - private int pictureWidth = 1280; - private int pictureHeight = 960; - private int previewWidth = 640; - private int previewHeight = 480; - private int previewFormat = ImageFormat.NV21; - private int previewFpsMin = 10; - private int previewFpsMax = 30; - private int previewFps = 30; - private int exposureCompensation = 0; - private String flashMode; - private String focusMode; - private List supportedFlashModes = new ArrayList<>(); - private List supportedFocusModes = new ArrayList<>(); - private int maxNumFocusAreas; - private List focusAreas = new ArrayList<>(); - private int maxNumMeteringAreas; - private List meteringAreas = new ArrayList<>(); - private final Map paramsMap = new HashMap<>(); - private static List supportedPreviewSizes; + @SuppressWarnings("nullness:initialization.field.uninitialized") // Managed by Robolectric + @RealObject + private Camera.Parameters realParameters; - /** - * Explicitly initialize custom preview sizes array, to switch from default values to - * individually added. - */ public void initSupportedPreviewSizes() { - supportedPreviewSizes = new ArrayList<>(); - } - - /** Add custom preview sizes to supportedPreviewSizes. */ - public void addSupportedPreviewSize(int width, int height) { - Camera.Size newSize = newInstanceOf(Camera.class).new Size(width, height); - supportedPreviewSizes.add(newSize); - } - - @Implementation - protected Camera.Size getPictureSize() { - Camera.Size pictureSize = newInstanceOf(Camera.class).new Size(0, 0); - pictureSize.width = pictureWidth; - pictureSize.height = pictureHeight; - return pictureSize; - } - - @Implementation - protected int getPreviewFormat() { - return previewFormat; - } - - @Implementation - protected void getPreviewFpsRange(int[] range) { - range[0] = previewFpsMin; - range[1] = previewFpsMax; - } - - @Implementation - protected int getPreviewFrameRate() { - return previewFps; - } - - @Implementation - protected Camera.Size getPreviewSize() { - Camera.Size previewSize = newInstanceOf(Camera.class).new Size(0, 0); - previewSize.width = previewWidth; - previewSize.height = previewHeight; - return previewSize; - } - - @Implementation - protected List getSupportedPictureSizes() { - List supportedSizes = new ArrayList<>(); - addSize(supportedSizes, 320, 240); - addSize(supportedSizes, 640, 480); - addSize(supportedSizes, 800, 600); - return supportedSizes; - } - - @Implementation - protected List getSupportedPictureFormats() { - List formats = new ArrayList<>(); - formats.add(ImageFormat.NV21); - formats.add(ImageFormat.JPEG); - return formats; - } - - @Implementation - protected List getSupportedPreviewFormats() { - List formats = new ArrayList<>(); - formats.add(ImageFormat.NV21); - formats.add(ImageFormat.JPEG); - return formats; - } - - @Implementation - protected List getSupportedPreviewFpsRange() { - List supportedRanges = new ArrayList<>(); - addRange(supportedRanges, 15000, 15000); - addRange(supportedRanges, 10000, 30000); - return supportedRanges; - } - - @Implementation - protected List getSupportedPreviewFrameRates() { - List supportedRates = new ArrayList<>(); - supportedRates.add(10); - supportedRates.add(15); - supportedRates.add(30); - return supportedRates; - } - - @Implementation - protected List getSupportedPreviewSizes() { - if (supportedPreviewSizes == null) { - initSupportedPreviewSizes(); - addSupportedPreviewSize(320, 240); - addSupportedPreviewSize(640, 480); - } - return supportedPreviewSizes; + realParameters.remove("preview-size-values"); } public void setSupportedFocusModes(String... focusModes) { - supportedFocusModes = Arrays.asList(focusModes); - } - - @Implementation - protected List getSupportedFocusModes() { - return supportedFocusModes; - } - - @Implementation - protected String getFocusMode() { - return focusMode; + realParameters.set("focus-mode-values", Joiner.on(",").join(focusModes)); + if (focusModes.length == 0) { + realParameters.remove("focus-mode"); + } } - @Implementation - protected void setFocusMode(String focusMode) { - this.focusMode = focusMode; + public void setSupportedFlashModes(String... flashModes) { + realParameters.set("flash-mode-values", Joiner.on(",").join(flashModes)); + if (flashModes.length == 0) { + realParameters.remove("flash-mode"); + } } /** @@ -411,22 +332,20 @@ protected void setFocusMode(String focusMode) { * Camera.Parameters#getMaxNumFocusAreas}. */ public void setMaxNumFocusAreas(int maxNumFocusAreas) { - this.maxNumFocusAreas = maxNumFocusAreas; + realParameters.set("max-num-focus-areas", maxNumFocusAreas); } - @Implementation - protected int getMaxNumFocusAreas() { - return maxNumFocusAreas; - } - - @Implementation - protected void setFocusAreas(List focusAreas) { - this.focusAreas = focusAreas; - } - - @Implementation - protected List getFocusAreas() { - return focusAreas; + public void addSupportedPreviewSize(int width, int height) { + List sizesStrings = new ArrayList<>(); + List sizes = realParameters.getSupportedPreviewSizes(); + if (sizes == null) { + sizes = ImmutableList.of(); + } + for (Camera.Size size : sizes) { + sizesStrings.add(size.width + "x" + size.height); + } + sizesStrings.add(width + "x" + height); + realParameters.set("preview-size-values", Joiner.on(",").join(sizesStrings)); } /** @@ -434,155 +353,27 @@ protected List getFocusAreas() { * Camera.Parameters#getMaxNumMeteringAreas}. */ public void setMaxNumMeteringAreas(int maxNumMeteringAreas) { - this.maxNumMeteringAreas = maxNumMeteringAreas; - } - - @Implementation - protected int getMaxNumMeteringAreas() { - return maxNumMeteringAreas; - } - - @Implementation - protected void setMeteringAreas(List meteringAreas) { - this.meteringAreas = meteringAreas; - } - - @Implementation - protected List getMeteringAreas() { - return meteringAreas; - } - - @Implementation - protected void setPictureSize(int width, int height) { - pictureWidth = width; - pictureHeight = height; - } - - @Implementation - protected void setPreviewFormat(int pixel_format) { - previewFormat = pixel_format; - } - - @Implementation - protected void setPreviewFpsRange(int min, int max) { - previewFpsMin = min; - previewFpsMax = max; - } - - @Implementation - protected void setPreviewFrameRate(int fps) { - previewFps = fps; - } - - @Implementation - protected void setPreviewSize(int width, int height) { - previewWidth = width; - previewHeight = height; - } - - @Implementation - protected void setRecordingHint(boolean recordingHint) { - // Do nothing - this prevents an NPE in the SDK code - } - - @Implementation - protected void setRotation(int rotation) { - // Do nothing - this prevents an NPE in the SDK code - } - - @Implementation - protected int getMinExposureCompensation() { - return -6; - } - - @Implementation - protected int getMaxExposureCompensation() { - return 6; - } - - @Implementation - protected float getExposureCompensationStep() { - return 0.5f; - } - - @Implementation - protected int getExposureCompensation() { - return exposureCompensation; - } - - @Implementation - protected void setExposureCompensation(int compensation) { - exposureCompensation = compensation; - } - - public void setSupportedFlashModes(String... flashModes) { - supportedFlashModes = Arrays.asList(flashModes); - } - - @Implementation - protected List getSupportedFlashModes() { - return supportedFlashModes; - } - - @Implementation - protected String getFlashMode() { - return flashMode; - } - - @Implementation - protected void setFlashMode(String flashMode) { - this.flashMode = flashMode; - } - - @Implementation - protected void set(String key, String value) { - paramsMap.put(key, value); - } - - @Implementation - protected String get(String key) { - return paramsMap.get(key); + realParameters.set("max-num-metering-areas", maxNumMeteringAreas); } public int getPreviewWidth() { - return previewWidth; + return realParameters.getPreviewSize().width; } public int getPreviewHeight() { - return previewHeight; + return realParameters.getPreviewSize().height; } public int getPictureWidth() { - return pictureWidth; + return realParameters.getPictureSize().width; } public int getPictureHeight() { - return pictureHeight; + return realParameters.getPictureSize().height; } - private void addSize(List sizes, int width, int height) { - Camera.Size newSize = newInstanceOf(Camera.class).new Size(0, 0); - newSize.width = width; - newSize.height = height; - sizes.add(newSize); - } - - private void addRange(List ranges, int min, int max) { - int[] range = new int[2]; - range[0] = min; - range[1] = max; - ranges.add(range); - } - } - - @Implements(Camera.Size.class) - public static class ShadowSize { - @RealObject private Camera.Size realCameraSize; - - @Implementation - protected void __constructor__(Camera camera, int width, int height) { - realCameraSize.width = width; - realCameraSize.height = height; + public int getRotation() { + return realParameters.getInt("rotation"); } } } From aa396fdf06a4430bc0918c7466c967cad74d68e9 Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 12 Oct 2023 14:48:57 -0700 Subject: [PATCH 15/33] Adds the ability to set supported cloud media authorities in ShadowMediaStore. Callers can use `ShadowMediaStore#addSupportedCloudMediaProviderAuthorities` to add supported Cloud Media Provder Authorities to ShadowMediaStore and `ShadowMediaStore#clearSupportedCloudMediaProviderAuthorities` to clear ShadowMediaStore's list of supported Cloud Media Provider Authorities. When `ShadowMediaStore#isSupportedCloudMediaProviderAuthority` is called, it will check to see if the input authority is contained in ShadowMediaStore's list of supported cloud media provider authorities. PiperOrigin-RevId: 573018497 --- .../shadows/ShadowMediaStoreTest.java | 35 ++++++++++++++++--- .../robolectric/shadows/ShadowMediaStore.java | 26 ++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java index d40ce254077..399cdfcdbd6 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java @@ -5,9 +5,13 @@ import static android.provider.MediaStore.Video; import static com.google.common.truth.Truth.assertThat; +import android.content.ContentResolver; import android.provider.MediaStore; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; @@ -17,6 +21,8 @@ public class ShadowMediaStoreTest { private static final String AUTHORITY = "authority"; private static final String INCORRECT_AUTHORITY = "incorrect_authority"; private static final String CURRENT_MEDIA_COLLECTION_ID = "media_collection_id"; + private final ContentResolver resolver = + ApplicationProvider.getApplicationContext().getContentResolver(); @Test public void shouldInitializeFields() { @@ -33,7 +39,7 @@ public void shouldInitializeFields() { @Test @Config(minSdk = TIRAMISU) public void notifyCloudMediaChangedEvent_storesCloudMediaChangedEvent() { - MediaStore.notifyCloudMediaChangedEvent(null, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID); + MediaStore.notifyCloudMediaChangedEvent(resolver, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID); ImmutableList cloudMediaChangedEventList = ShadowMediaStore.getCloudMediaChangedEvents(); @@ -46,7 +52,7 @@ public void notifyCloudMediaChangedEvent_storesCloudMediaChangedEvent() { @Test @Config(minSdk = TIRAMISU) public void clearCloudMediaChangedEventList_clearsCloudMediaChangedEventList() { - MediaStore.notifyCloudMediaChangedEvent(null, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID); + MediaStore.notifyCloudMediaChangedEvent(resolver, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID); assertThat(ShadowMediaStore.getCloudMediaChangedEvents()).isNotEmpty(); ShadowMediaStore.clearCloudMediaChangedEventList(); @@ -54,12 +60,33 @@ public void clearCloudMediaChangedEventList_clearsCloudMediaChangedEventList() { assertThat(ShadowMediaStore.getCloudMediaChangedEvents()).isEmpty(); } + @Test + @Config(minSdk = TIRAMISU) + public void isSupportedCloudMediaProviderAuthority_withCorrectAuthority_returnsTrue() { + List supportedAuthorityList = new ArrayList<>(); + supportedAuthorityList.add(AUTHORITY); + ShadowMediaStore.addSupportedCloudMediaProviderAuthorities(supportedAuthorityList); + + assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(resolver, AUTHORITY)).isTrue(); + } + + @Test + @Config(minSdk = TIRAMISU) + public void isSupportedCloudMediaProviderAuthority_withIncorrectAuthority_returnsFalse() { + List supportedAuthorityList = new ArrayList<>(); + supportedAuthorityList.add(AUTHORITY); + ShadowMediaStore.addSupportedCloudMediaProviderAuthorities(supportedAuthorityList); + + assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(resolver, INCORRECT_AUTHORITY)) + .isFalse(); + } + @Test @Config(minSdk = TIRAMISU) public void isCurrentCloudMediaProviderAuthority_withCorrectAuthority_returnsTrue() { ShadowMediaStore.setCurrentCloudMediaProviderAuthority(AUTHORITY); - assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(null, AUTHORITY)).isTrue(); + assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(resolver, AUTHORITY)).isTrue(); } @Test @@ -67,7 +94,7 @@ public void isCurrentCloudMediaProviderAuthority_withCorrectAuthority_returnsTru public void isCurrentCloudMediaProviderAuthority_withIncorrectAuthority_returnsFalse() { ShadowMediaStore.setCurrentCloudMediaProviderAuthority(AUTHORITY); - assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(null, INCORRECT_AUTHORITY)) + assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(resolver, INCORRECT_AUTHORITY)) .isFalse(); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java index cf349ebdd53..0d1283af802 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java @@ -3,12 +3,13 @@ import static android.os.Build.VERSION_CODES.TIRAMISU; import static org.robolectric.util.reflector.Reflector.reflector; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory.Options; import android.net.Uri; import android.provider.MediaStore; -import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -26,12 +27,14 @@ public class ShadowMediaStore { private static Bitmap stubBitmap = null; private static final List cloudMediaChangedEventList = new ArrayList<>(); + private static final List supportedCloudMediaProviderAuthorities = new ArrayList<>(); @Nullable private static String currentCloudMediaProviderAuthority = null; @Resetter public static void reset() { stubBitmap = null; cloudMediaChangedEventList.clear(); + supportedCloudMediaProviderAuthorities.clear(); currentCloudMediaProviderAuthority = null; } @@ -140,9 +143,28 @@ public static CloudMediaChangedEvent create(String authority, String currentMedi public abstract String currentMediaCollectionId(); } + @Implementation(minSdk = TIRAMISU) + protected static boolean isSupportedCloudMediaProviderAuthority( + @NonNull ContentResolver resolver, @NonNull String authority) { + return supportedCloudMediaProviderAuthorities.contains(authority); + } + + /** + * Mutator method to add the input {@code authorities} to the list of supported cloud media + * provider authorities. + */ + public static void addSupportedCloudMediaProviderAuthorities(@NonNull List authorities) { + supportedCloudMediaProviderAuthorities.addAll(authorities); + } + + /** Mutator method to clear the list of supported cloud media provider authorities. */ + public static void clearSupportedCloudMediaProviderAuthorities() { + supportedCloudMediaProviderAuthorities.clear(); + } + @Implementation(minSdk = TIRAMISU) protected static boolean isCurrentCloudMediaProviderAuthority( - ContentResolver resolver, String authority) { + @NonNull ContentResolver resolver, @NonNull String authority) { return currentCloudMediaProviderAuthority.equals(authority); } From 67509ae5066c1301743089eba68c6ae6a82708ce Mon Sep 17 00:00:00 2001 From: Paul Sowden Date: Thu, 12 Oct 2023 14:54:58 -0700 Subject: [PATCH 16/33] Address some concurrency issues with loopers dying unnaturally There were a couple of issues when interacting with a looper from another thread when that looper died and further interactions with that looper. This is particularly important for the instrumentation test looper mode where the main looper is running on a different thread to the test thread. * In `ShadowPausedMessageQueue.reset` synchronize field access. This wasn't causing any direct problems (that I observed), but these fields should be guarded by the queue as they are not volatile and they need to be thread safe. * In `ShadowPausedLooper.loop` synchronize around setting the uncaught exception on the queue and draining the queue; because if the main thread dies it will first configure the queue with an uncaught exception before draining it, this ensures that we cannot allow another thread to clear the exception (i.e. by calling `reset()`) before the queue has been drained by the looper itself, if that happens any runnables posted to the queue will be drained and not run. * Do not count down the latch in `ControlRunnable.run` if an exception is going to get propagated to the looper; this results in a similar race to the previous issue where the calling thread will immediately unblock with an exception raised, but the looper itself hasn't finished exception handling yet, in this case the calling thread may have further interactions with the looper that should consistently fail (because the looper should have died unnaturally), but that instead get drained from the queue. We want to guarantee that any threads that are interacting with the looper shadow will not have an exception raised until after the looper is in a consistent state and further interactions with the looper will fail consistently. PiperOrigin-RevId: 573020062 --- .../shadows/ShadowPausedLooper.java | 64 +++++++++++++++---- .../shadows/ShadowPausedMessageQueue.java | 8 ++- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java index 6e85f733a07..27e6d9c716d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java @@ -418,18 +418,24 @@ protected static void loop() { if (ignoreUncaughtExceptions) { // ignore } else { - shadowQueue.setUncaughtException(e); - // release any ControlRunnables currently in queue to prevent deadlocks - shadowQueue.drainQueue( - input -> { - if (input instanceof ControlRunnable) { - ((ControlRunnable) input).runLatch.countDown(); - return true; - } - return false; - }); + synchronized (realLooper.getQueue()) { + shadowQueue.setUncaughtException(e); + // release any ControlRunnables currently in queue to prevent deadlocks + shadowQueue.drainQueue( + input -> { + if (input instanceof ControlRunnable) { + ((ControlRunnable) input).runLatch.countDown(); + return true; + } + return false; + }); + } + } + if (e instanceof ControlException) { + ((ControlException) e).rethrowCause(); + } else { + throw e; } - throw e; } } @@ -458,6 +464,28 @@ private void triggerIdleHandlersIfNeeded(Message lastMessageRead) { } } + /** + * An exception raised by a {@link ControlRunnable} if the runnable was interrupted with an + * exception. The looper must call {@link #rethrowCause()} after performing cleanup associated + * with handling the exception. + */ + private static final class ControlException extends RuntimeException { + private final ControlRunnable controlRunnable; + + ControlException(ControlRunnable controlRunnable, RuntimeException cause) { + super(cause); + this.controlRunnable = controlRunnable; + } + + void rethrowCause() { + // Release the control runnable only once the looper has finished draining to avoid any + // races on the thread that posted the control runnable (otherwise the calling thread may + // have subsequent interactions with the looper that result in inconsistent state). + controlRunnable.runLatch.countDown(); + throw (RuntimeException) getCause(); + } + } + /** A runnable that changes looper state, and that must be run from looper's thread */ private abstract static class ControlRunnable implements Runnable { @@ -466,15 +494,19 @@ private abstract static class ControlRunnable implements Runnable { @Override public void run() { + boolean controlExceptionThrown = false; try { doRun(); } catch (RuntimeException e) { if (!ignoreUncaughtExceptions) { exception = e; } - throw e; + controlExceptionThrown = true; + throw new ControlException(this, e); } finally { - runLatch.countDown(); + if (!controlExceptionThrown) { + runLatch.countDown(); + } } } @@ -529,7 +561,11 @@ private void executeOnLooper(ControlRunnable runnable) { // Need to trigger the unpause action in PausedLooperExecutor looperExecutor.execute(runnable); } else { - runnable.run(); + try { + runnable.run(); + } catch (ControlException e) { + e.rethrowCause(); + } } } else { if (looperMode() == LooperMode.Mode.PAUSED && realLooper.equals(Looper.getMainLooper())) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java index 56af8a3bb57..37e5cf94931 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java @@ -386,9 +386,11 @@ Message getNextIgnoringWhen() { @Override public void reset() { MessageQueueReflector msgQueue = reflector(MessageQueueReflector.class, realQueue); - msgQueue.setMessages(null); - msgQueue.setIdleHandlers(new ArrayList<>()); - msgQueue.setNextBarrierToken(0); + synchronized (realQueue) { + msgQueue.setMessages(null); + msgQueue.setIdleHandlers(new ArrayList<>()); + msgQueue.setNextBarrierToken(0); + } setUncaughtException(null); } From d5378b3f04962dbcd37f1c1407d4f4523a961976 Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 12 Oct 2023 15:11:21 -0700 Subject: [PATCH 17/33] Add an implementation of `dup()` to ShadowParcelFileDescriptor. This is to fix the following situation, which currently fails: ``` File testFile = new File(context.getFilesDir(), "test"); ParcelFileDescriptor fd = ParcelFileDescriptor.open(testFile, ParcelFileDescriptor.MODE_READ_WRITE); ParcelFileDescriptor dupFd = fd.dup(); FileDescriptor file = dupFd.getFileDescriptor(); // Fails with NPE. ``` This happens because dup is not implemented in the shadow, so dupFd's shadow does not have the `file` field set. When invoking `dupFd.getFileDescriptor()`, it tries to invoke `getFile().getFd()` which fails with a NullPointerException. By implementing a shadow for `dup`, it can simply call the constructor which sets this `file` field as expected. PiperOrigin-RevId: 573024247 --- .../shadows/ShadowParcelFileDescriptorTest.java | 11 +++++++++++ .../shadows/ShadowParcelFileDescriptor.java | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java index eb2dd485081..4cb7c06c924 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java @@ -417,6 +417,17 @@ public void testCanMarshalUnmarshal_chained() throws Exception { FileDescriptorFromParcelUnavailableException.class, () -> pfd2.getFileDescriptor()); } + @Test + public void testDup_retainsFd() throws Exception { + ParcelFileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE); + ParcelFileDescriptor dupFd = fd.dup(); + FileDescriptor file = fd.getFileDescriptor(); + FileDescriptor dupFile = dupFd.getFileDescriptor(); + assertThat(file).isEqualTo(dupFile); + assertThat(file.valid()).isTrue(); + assertThat(dupFile.valid()).isTrue(); + } + private static String readLine(FileDescriptor fd) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fd), defaultCharset()))) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java index dd66cc764c0..0982971fec9 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java @@ -270,6 +270,11 @@ protected void close() throws IOException { } } + @Implementation + protected ParcelFileDescriptor dup() throws IOException { + return new ParcelFileDescriptor(realParcelFd); + } + static class FileDescriptorFromParcelUnavailableException extends RuntimeException { FileDescriptorFromParcelUnavailableException() { super( From 641923c99af376f8cf2ae0cf3bb630f25a41b75b Mon Sep 17 00:00:00 2001 From: Paul Sowden Date: Thu, 12 Oct 2023 15:24:37 -0700 Subject: [PATCH 18/33] Reuse the main thread after uncaught exceptions. Instead of creating new threads if/when the main thread dies in previous tests reuse the same thread. This provides a little more consistency with the looper itself which is reused and allows code under test to retain static references to the main thread. PiperOrigin-RevId: 573027459 --- .../ShadowInstrumentationTestLooperTest.java | 32 +++++++- .../shadows/ShadowPausedLooper.java | 75 +++++++++++++------ .../shadows/ShadowPausedMessageQueue.java | 6 ++ 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java index a9fb746996e..42ed0251814 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -10,6 +11,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import com.google.common.base.Preconditions; import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -80,7 +82,7 @@ public void idleFor() { } @Test - public void exceptionOnMainThreadPropagated() throws InterruptedException { + public void exceptionOnMainThreadPropagated() { ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper()); Handler mainHandler = new Handler(Looper.getMainLooper()); @@ -91,7 +93,6 @@ public void exceptionOnMainThreadPropagated() throws InterruptedException { assertThrows(RuntimeException.class, () -> shadowMainLooper.idle()); // Restore main looper and main thread to avoid error at tear down - Looper.getMainLooper().getThread().join(); ShadowPausedLooper.resetLoopers(); } @@ -108,4 +109,31 @@ public void backgroundLooperCrash() throws InterruptedException { assertThrows(IllegalStateException.class, () -> handler.post(() -> {})); } + + @Test + public void mainThreadDies_resetRestartsLooper() { + ShadowLooper shadowLooper = shadowOf(Looper.getMainLooper()); + Handler handler = new Handler(Looper.getMainLooper()); + AtomicBoolean didRun = new AtomicBoolean(); + + handler.post( + () -> { + throw new RuntimeException(); + }); + RuntimeException exception = null; + try { + shadowLooper.idle(); + } catch (RuntimeException e) { + exception = e; + } + Preconditions.checkNotNull(exception); + ShadowPausedLooper.resetLoopers(); + handler.post( + () -> { + didRun.set(true); + }); + shadowLooper.idle(); + + assertThat(didRun.get()).isTrue(); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java index 27e6d9c716d..1f0ed435452 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java @@ -25,6 +25,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.GuardedBy; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -282,33 +283,59 @@ public static synchronized void resetLoopers() { } } + private static final Object instrumentationTestMainThreadLock = new Object(); + + @SuppressWarnings("NonFinalStaticField") // State used in static method for main thread. + @GuardedBy("instrumentationTestMainThreadLock") + private static boolean instrumentationTestMainThreadShouldRestart = false; + private static synchronized void createMainThreadAndLooperIfNotAlive() { Looper mainLooper = Looper.getMainLooper(); switch (ConfigurationRegistry.get(LooperMode.Mode.class)) { case INSTRUMENTATION_TEST: - if (mainLooper == null || !mainLooper.getThread().isAlive()) { + if (mainLooper == null) { ConditionVariable mainThreadPrepared = new ConditionVariable(); Thread mainThread = new Thread(String.format("SDK %d Main Thread", RuntimeEnvironment.getApiLevel())) { @Override public void run() { - if (mainLooper == null) { - Looper.prepareMainLooper(); - } else { - ShadowPausedMessageQueue shadowQueue = Shadow.extract(mainLooper.getQueue()); - shadowQueue.reset(); - reflector(LooperReflector.class, mainLooper).setThread(Thread.currentThread()); - reflector(LooperReflector.class).getThreadLocal().set(mainLooper); - } + Looper.prepareMainLooper(); mainThreadPrepared.open(); - Looper.loop(); + while (true) { + try { + Looper.loop(); + } catch (Throwable e) { + // The exception is handled inside of the loop shadow method, so ignore it. + } + // Wait to restart the looper until the looper is reset. + synchronized (instrumentationTestMainThreadLock) { + while (!instrumentationTestMainThreadShouldRestart) { + try { + instrumentationTestMainThreadLock.wait(); + } catch (InterruptedException ie) { + // Shouldn't be interrupted, continue waiting for reset signal. + } + } + instrumentationTestMainThreadShouldRestart = false; + } + } } }; mainThread.start(); mainThreadPrepared.block(); Thread.currentThread() .setName(String.format("SDK %d Test Thread", RuntimeEnvironment.getApiLevel())); + } else { + ShadowPausedMessageQueue shadowQueue = Shadow.extract(mainLooper.getQueue()); + if (shadowQueue.hasUncaughtException()) { + shadowQueue.reset(); + synchronized (instrumentationTestMainThreadLock) { + // If the looper died in a previous test it will be waiting to restart, notify it. + instrumentationTestMainThreadShouldRestart = true; + instrumentationTestMainThreadLock.notify(); + } + } } break; case PAUSED: @@ -528,15 +555,15 @@ private class IdlingRunnable extends ControlRunnable { @Override public void doRun() { - while (true) { - Message msg = getNextExecutableMessage(); - if (msg == null) { - break; - } - msg.getTarget().dispatchMessage(msg); - shadowMsg(msg).recycleUnchecked(); - triggerIdleHandlersIfNeeded(msg); + while (true) { + Message msg = getNextExecutableMessage(); + if (msg == null) { + break; } + msg.getTarget().dispatchMessage(msg); + shadowMsg(msg).recycleUnchecked(); + triggerIdleHandlersIfNeeded(msg); + } } } @@ -545,12 +572,12 @@ private class RunOneRunnable extends ControlRunnable { @Override public void doRun() { - Message msg = shadowQueue().getNextIgnoringWhen(); - if (msg != null) { - SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen()); - msg.getTarget().dispatchMessage(msg); - triggerIdleHandlersIfNeeded(msg); - } + Message msg = shadowQueue().getNextIgnoringWhen(); + if (msg != null) { + SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen()); + msg.getTarget().dispatchMessage(msg); + triggerIdleHandlersIfNeeded(msg); + } } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java index 37e5cf94931..c40abfce9b1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java @@ -444,6 +444,12 @@ void setUncaughtException(Exception e) { } } + boolean hasUncaughtException() { + synchronized (realQueue) { + return uncaughtException != null; + } + } + void checkQueueState() { synchronized (realQueue) { if (uncaughtException != null) { From ea1965556b1e41ee10d269de3e54aac6681ebfa6 Mon Sep 17 00:00:00 2001 From: Paul Sowden Date: Thu, 12 Oct 2023 16:23:38 -0700 Subject: [PATCH 19/33] Add ShadowPausedLooper.postSync This method allows the caller to post a runnable to the looper and then block on returning until after the runnable has completed, and will rethrow any uncaught exception that was thrown on the main thread. Previously in instrumentation test looper mode calling Instrumentation.runOnMainSync would simply block if the main thread had uncaught exceptions. PiperOrigin-RevId: 573041512 --- .../RoboMonitoringInstrumentation.java | 45 +++++++++---------- .../ShadowInstrumentationTestLooperTest.java | 37 +++++++++++++++ .../shadows/ShadowPausedLooper.java | 37 +++++++++++++++ 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java b/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java index 9ff4d1632bd..bfd3104af28 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java @@ -41,9 +41,11 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.LooperMode; +import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowActivity; import org.robolectric.shadows.ShadowInstrumentation; import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowPausedLooper; /** * A Robolectric instrumentation that acts like a slimmed down {@link @@ -163,32 +165,27 @@ public void callApplicationOnCreate(Application app) { */ @Override public void runOnMainSync(Runnable runnable) { - if (ShadowLooper.looperMode() != LooperMode.Mode.INSTRUMENTATION_TEST) { + if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) { + FutureTask wrapped = new FutureTask<>(runnable, null); + Shadow.extract(Looper.getMainLooper()).postSync(wrapped); + try { + wrapped.get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } + throw new RuntimeException(cause); + } + } else { + // TODO: Use ShadowPausedLooper#postSync for PAUSED looper mode which provides more realistic + // behavior (i.e. it only runs to the runnable, it doesn't completely idle). waitForIdleSync(); runnable.run(); - return; - } - - FutureTask wrappedRunnable = new FutureTask<>(runnable, null); - new Handler(Looper.getMainLooper()).post(wrappedRunnable); - if (shadowOf(Looper.getMainLooper()).isPaused()) { - while (!wrappedRunnable.isDone()) { - ShadowLooper.runMainLooperToNextTask(); - } - } - - try { - wrappedRunnable.get(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else if (cause instanceof Error) { - throw (Error) cause; - } - throw new RuntimeException(cause); } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java index 42ed0251814..8dde2972917 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java @@ -21,6 +21,7 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.LooperMode.Mode; +import org.robolectric.shadow.api.Shadow; @LooperMode(Mode.INSTRUMENTATION_TEST) @RunWith(RobolectricTestRunner.class) @@ -136,4 +137,40 @@ public void mainThreadDies_resetRestartsLooper() { assertThat(didRun.get()).isTrue(); } + + @Test + public void postSync_runsOnlyToTheRunnable() { + ShadowPausedLooper shadowLooper = Shadow.extract(Looper.getMainLooper()); + shadowLooper.setPaused(true); + AtomicBoolean firstTaskRan = new AtomicBoolean(); + AtomicBoolean secondTaskRan = new AtomicBoolean(); + AtomicBoolean thirdTaskRan = new AtomicBoolean(); + + new Handler(Looper.getMainLooper()).post(() -> firstTaskRan.set(true)); + shadowLooper.postSync( + () -> { + new Handler(Looper.getMainLooper()).post(() -> thirdTaskRan.set(true)); + secondTaskRan.set(true); + }); + + assertThat(firstTaskRan.get()).isTrue(); + assertThat(secondTaskRan.get()).isTrue(); + assertThat(thirdTaskRan.get()).isFalse(); + } + + @Test + public void postSync_exceptionIsThrown() { + ShadowPausedLooper shadowLooper = Shadow.extract(Looper.getMainLooper()); + + new Handler(Looper.getMainLooper()) + .post( + () -> { + throw new RuntimeException(); + }); + + assertThrows(RuntimeException.class, () -> shadowLooper.postSync(() -> {})); + + // Restore main looper and main thread to avoid error at tear down + ShadowPausedLooper.resetLoopers(); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java index 1f0ed435452..3114b11c5da 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java @@ -5,6 +5,7 @@ import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; import static org.robolectric.util.reflector.Reflector.reflector; +import android.app.Instrumentation; import android.os.Build; import android.os.ConditionVariable; import android.os.Handler; @@ -208,6 +209,15 @@ public boolean postAtFrontOfQueue(Runnable runnable) { return new Handler(realLooper).postAtFrontOfQueue(runnable); } + /** + * Posts the runnable to the looper and idles until the runnable has been run. Generally clients + * should prefer to use {@link Instrumentation#runOnMainSync(Runnable)}, which will reraise + * underlying runtime exceptions to the caller. + */ + public void postSync(Runnable runnable) { + executeOnLooper(new PostAndIdleToRunnable(runnable)); + } + // this API doesn't make sense in LooperMode.PAUSED, but just retain it for backwards // compatibility for now @Override @@ -581,6 +591,33 @@ public void doRun() { } } + /** + * Control runnable that posts the provided runnable to the queue and then idles up to and + * including the posted runnable. Provides essentially similar functionality to {@link + * Instrumentation#runOnMainSync(Runnable)}. + */ + private class PostAndIdleToRunnable extends ControlRunnable { + private final Runnable runnable; + + PostAndIdleToRunnable(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void doRun() { + new Handler(realLooper).post(runnable); + Message msg; + do { + msg = getNextExecutableMessage(); + if (msg == null) { + throw new IllegalStateException("Runnable is not in the queue"); + } + msg.getTarget().dispatchMessage(msg); + triggerIdleHandlersIfNeeded(msg); + } while (msg.getCallback() != runnable); + } + } + /** Executes the given runnable on the loopers thread, and waits for it to complete. */ private void executeOnLooper(ControlRunnable runnable) { if (Thread.currentThread() == realLooper.getThread()) { From c4d1cb2e670c12d3027209800f09b4aaf3dd453d Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 12 Oct 2023 16:38:23 -0700 Subject: [PATCH 20/33] Adapt to new signatures for getting font metrics for Paint and create in bitmap shader. The in development android platform has modified signatures for several methods relating to getting font metrics for Paint and creating in bitmap shader, which were being shadowed. This causes Robolectric's annotation validator to fail when inspecting the new android all jar. This commit is a quick workaround to modify the shadows to add variants for the new signatures, to placate the annotation validator. A future change should consider adding full support for the new `ShadowNativePaint::nGetFontMetrics`, `ShadowNativePaint::nGetFontMetricsInt`, `ShadowNative::nGetFontMetricsInt` and `ShadowNativeBitmapShader::nativeCreate` functionality. PiperOrigin-RevId: 573044873 --- .../shadows/ShadowNativeBitmapShader.java | 18 +++++++++++++++++- .../robolectric/shadows/ShadowNativePaint.java | 16 ++++++++++++++-- .../org/robolectric/shadows/ShadowPaint.java | 10 +++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapShader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapShader.java index 1c4aa80a904..7b04f9190d8 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapShader.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapShader.java @@ -15,6 +15,8 @@ import org.robolectric.nativeruntime.BitmapShaderNatives; import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.shadows.ShadowNativeBitmapShader.Picker; +import org.robolectric.versioning.AndroidVersions.U; +import org.robolectric.versioning.AndroidVersions.V; /** Shadow for {@link BitmapShader} that is backed by native code */ @Implements(value = BitmapShader.class, minSdk = O, shadowPicker = Picker.class) @@ -49,7 +51,7 @@ protected static long nativeCreate( nativeMatrix, bitmapHandle, shaderTileModeX, shaderTileModeY, filter); } - @Implementation(minSdk = TIRAMISU) + @Implementation(minSdk = TIRAMISU, maxSdk = U.SDK_INT) protected static long nativeCreate( long nativeMatrix, long bitmapHandle, @@ -60,6 +62,20 @@ protected static long nativeCreate( return nativeCreate(nativeMatrix, bitmapHandle, shaderTileModeX, shaderTileModeY, filter); } + @Implementation(minSdk = V.SDK_INT) + protected static long nativeCreate( + long nativeMatrix, + long bitmapHandle, + int shaderTileModeX, + int shaderTileModeY, + /* Ignored */ int maxAniso, + boolean filter, + boolean isDirectSampled, + /* Ignored */ long overrideGainmapHandle) { + return nativeCreate( + nativeMatrix, bitmapHandle, shaderTileModeX, shaderTileModeY, filter, isDirectSampled); + } + /** Shadow picker for {@link BitmapShader}. */ public static final class Picker extends GraphicsShadowPicker { public Picker() { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java index 4dbe0b03bce..e7ab81204d8 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java @@ -429,7 +429,13 @@ protected static void nSetFontFeatureSettings(long paintPtr, String settings) { PaintNatives.nSetFontFeatureSettings(paintPtr, settings); } - @Implementation(minSdk = P) + @Implementation(minSdk = V.SDK_INT) + protected static float nGetFontMetrics( + long paintPtr, FontMetrics metrics, /* Ignored */ boolean useLocale) { + return PaintNatives.nGetFontMetrics(paintPtr, metrics); + } + + @Implementation(minSdk = P, maxSdk = U.SDK_INT) protected static float nGetFontMetrics(long paintPtr, FontMetrics metrics) { return PaintNatives.nGetFontMetrics(paintPtr, metrics); } @@ -439,7 +445,13 @@ protected static float nGetFontMetrics(long paintPtr, long typefacePtr, FontMetr return PaintNatives.nGetFontMetrics(paintPtr, typefacePtr, metrics); } - @Implementation(minSdk = P) + @Implementation(minSdk = V.SDK_INT) + protected static int nGetFontMetricsInt( + long paintPtr, FontMetricsInt fmi, /* Ignored */ boolean useLocale) { + return PaintNatives.nGetFontMetricsInt(paintPtr, fmi); + } + + @Implementation(minSdk = P, maxSdk = U.SDK_INT) protected static int nGetFontMetricsInt(long paintPtr, FontMetricsInt fmi) { return PaintNatives.nGetFontMetricsInt(paintPtr, fmi); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java index b103034a266..6a6a4a2d065 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java @@ -29,6 +29,8 @@ import org.robolectric.config.ConfigurationRegistry; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.versioning.AndroidVersions.U; +import org.robolectric.versioning.AndroidVersions.V; @SuppressWarnings({"UnusedDeclaration"}) @Implements(value = Paint.class, looseSignatures = true) @@ -498,7 +500,13 @@ private static int breakText(String text, float maxWidth, float[] measuredWidth) return text.length(); } - @Implementation(minSdk = P) + @Implementation(minSdk = V.SDK_INT) + protected static int nGetFontMetricsInt( + long paintPtr, FontMetricsInt fmi, /* Ignored */ boolean useLocale) { + return nGetFontMetricsInt(paintPtr, fmi); + } + + @Implementation(minSdk = P, maxSdk = U.SDK_INT) protected static int nGetFontMetricsInt(long paintPtr, FontMetricsInt fmi) { if (ConfigurationRegistry.get(TextLayoutMode.Mode.class) == REALISTIC) { // TODO: hack, just set values to those we see on emulator From 638ce69a2245340217099b5e2dbfdbacaa6afa4b Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Thu, 12 Oct 2023 22:15:45 -0700 Subject: [PATCH 21/33] Fix issue using AttributeSetBuilder with compact resource table entries AttributeSetBuilder uses CppAssetManager, which did not support compact resource table entries. PiperOrigin-RevId: 573103527 --- .../org/robolectric/res/android/ResTable.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/resources/src/main/java/org/robolectric/res/android/ResTable.java b/resources/src/main/java/org/robolectric/res/android/ResTable.java index 9a696b17eb8..dd3ed1b0993 100644 --- a/resources/src/main/java/org/robolectric/res/android/ResTable.java +++ b/resources/src/main/java/org/robolectric/res/android/ResTable.java @@ -712,11 +712,12 @@ private int getEntry( // reinterpret_cast(bestType) + bestOffset); final ResTable_entry entry = new ResTable_entry(bestType.myBuf(), bestType.myOffset() + bestOffset); - if (dtohs(entry.size) < ResTable_entry.SIZEOF) { + int entrySize = entry.isCompact() ? ResTable_entry.SIZEOF : dtohs(entry.size); + if (entrySize < ResTable_entry.SIZEOF) { ALOGW("ResTable_entry size 0x%x is too small", dtohs(entry.size)); return BAD_TYPE; } - + if (outEntry != null) { outEntry.entry = entry; outEntry.config = bestConfig; @@ -724,7 +725,7 @@ private int getEntry( outEntry.specFlags = specFlags; outEntry._package_ = bestPackage; outEntry.typeStr = new StringPoolRef(bestPackage.typeStrings, actualTypeIndex - bestPackage.typeIdOffset); - outEntry.keyStr = new StringPoolRef(bestPackage.keyStrings, dtohl(entry.key.index)); + outEntry.keyStr = new StringPoolRef(bestPackage.keyStrings, dtohl(entry.getKeyIndex())); } return NO_ERROR; } @@ -960,10 +961,12 @@ int parsePackage(ResTable_package pkg, dtohs(type.header.headerSize), typeSize)); } - if (dtohs(type.header.headerSize)+(4/*sizeof(int)*/*newEntryCount) > typeSize) { - ALOGW("ResTable_type entry index to %s extends beyond chunk end 0x%x.", - (dtohs(type.header.headerSize) + (4/*sizeof(int)*/*newEntryCount)), - typeSize); + // Check if the table uses compact encoding. + int bytesPerEntry = isTruthy(type.flags & ResTable_type.FLAG_OFFSET16) ? 2 : 4; + if (dtohs(type.header.headerSize) + (bytesPerEntry * newEntryCount) > typeSize) { + ALOGW( + "ResTable_type entry index to %s extends beyond chunk end 0x%x.", + (dtohs(type.header.headerSize) + (bytesPerEntry * newEntryCount)), typeSize); return (mError=BAD_TYPE); } From ef2b80df361f45db6356ee19ad4c22d239351dd7 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Fri, 13 Oct 2023 09:38:22 -0700 Subject: [PATCH 22/33] Default to binary resources mode. Currently Robolectric will select binary resources mode if and only if there is a binary resources file present. Legacy resources mode is not supported on Android SDKs greater than P. Thus if a test setup doesn't have a binary resources file, and is executed on a SDK > P, Robolectric will skip executing the test. This commit changes Robolectric behavior so that: - binary resources mode is preferred by default. - Support no resources in binary resources mode by defaulting manifest values, similar to the legacy resource logic. - Fail a test if legacy resource mode is selected when running on SDKs > P. A future change, at a TBD date, will remove legacy resources mode support entirely. Fixes #7249 PiperOrigin-RevId: 573243275 --- .../java/org/robolectric/manifest/AndroidManifest.java | 7 +------ .../main/java/org/robolectric/RobolectricTestRunner.java | 7 ++----- .../android/internal/AndroidTestEnvironment.java | 7 ++++++- .../org/robolectric/RobolectricTestRunnerMultiApiTest.java | 2 -- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java index 073b0fe1717..4dd4be2df91 100644 --- a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java +++ b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java @@ -68,8 +68,6 @@ public class AndroidManifest implements UsesSdk { private final Map applicationAttributes = new HashMap<>(); private MetaData applicationMetaData; - private Boolean supportsBinaryResourcesMode; - /** * Creates a Robolectric configuration using specified locations. * @@ -854,9 +852,6 @@ public final boolean supportsLegacyResourcesMode() { /** @deprecated Do not use. */ @Deprecated synchronized public boolean supportsBinaryResourcesMode() { - if (supportsBinaryResourcesMode == null) { - supportsBinaryResourcesMode = apkFile != null && Files.exists(apkFile); - } - return supportsBinaryResourcesMode; + return true; } } diff --git a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java index e2a38bb690a..4cac1665f51 100644 --- a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java +++ b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java @@ -17,7 +17,6 @@ import java.util.Properties; import javax.annotation.Nonnull; import javax.annotation.Priority; -import org.junit.AssumptionViolatedException; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; @@ -202,7 +201,6 @@ private static ResModeStrategy getFromProperties() { boolean includeLegacy(AndroidManifest appManifest) { return appManifest.supportsLegacyResourcesMode() && (this == legacy - || (this == best && !appManifest.supportsBinaryResourcesMode()) || this == both); } @@ -277,11 +275,10 @@ protected AndroidSandbox getSandbox(FrameworkMethod method) { if (resourcesMode == ResourcesMode.LEGACY && sdk.getApiLevel() > Build.VERSION_CODES.P) { System.err.println( - "Skip " + "Failure for " + method.getName() + " because Robolectric doesn't support legacy resources mode after P"); - throw new AssumptionViolatedException( - "Robolectric doesn't support legacy resources mode after P"); + throw new AssertionError("Robolectric doesn't support legacy resources mode after P"); } LooperMode.Mode looperMode = roboMethod.configuration == null diff --git a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java index 479c27b05f8..7f595c22a8d 100755 --- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java @@ -414,7 +414,12 @@ private Package loadAppPackage_measured(Config config, AndroidManifest appManife RuntimeEnvironment.compileTimeSystemResourcesFile = compileSdk.getJarPath(); Path packageFile = appManifest.getApkFile(); - parsedPackage = ShadowPackageParser.callParsePackage(packageFile); + if (packageFile != null) { + parsedPackage = ShadowPackageParser.callParsePackage(packageFile); + } else { + parsedPackage = new Package("org.robolectric.default"); + parsedPackage.applicationInfo.targetSdkVersion = appManifest.getTargetSdkVersion(); + } } if (parsedPackage != null && parsedPackage.applicationInfo != null diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerMultiApiTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerMultiApiTest.java index b978e446721..d50ca474b1c 100644 --- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerMultiApiTest.java +++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerMultiApiTest.java @@ -67,7 +67,6 @@ public void setUp() { delegateSdkPicker = new DefaultSdkPicker(sdkCollection, null); priorResourcesMode = System.getProperty("robolectric.resourcesMode"); - System.setProperty("robolectric.resourcesMode", "legacy"); priorAlwaysInclude = System.getProperty("robolectric.alwaysIncludeVariantMarkersInTestName"); System.clearProperty("robolectric.alwaysIncludeVariantMarkersInTestName"); @@ -77,7 +76,6 @@ public void setUp() { public void tearDown() throws Exception { TestUtil.resetSystemProperty( "robolectric.alwaysIncludeVariantMarkersInTestName", priorAlwaysInclude); - TestUtil.resetSystemProperty("robolectric.resourcesMode", priorResourcesMode); } @Test From e9f1264d8d4c5683c9ab911fe785f830bcf19395 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Fri, 13 Oct 2023 12:11:31 -0700 Subject: [PATCH 23/33] Update GitHub CI workflows for Copybara PRs on the 'google' branch When a Robolectric change is made by a Googler, all Robolectric tests are already run on Google's infrastructure. It is redundant to re-run tests on GitHub actions. With this change, when a PR is made by the Copybara service: 1) The PR will be built to ensure that there are no compile errors. 2) All integration tests will be run on the latest SDK version. 3) GitHub CI tests will be run when the commit is pushed to the 'google' branch. This will reduce the amount of contention for GitHub CI and will significantly decrease the amount of time required for Copybara PRs to be merged. Also, update AndroidProjectConfigPlugin.groovy to avoid running tests on the 'release' variant. PiperOrigin-RevId: 573287429 --- .github/workflows/copybara_build_and_test.yml | 42 +++++++++++++++++++ .github/workflows/tests.yml | 6 +-- .../gradle/AndroidProjectConfigPlugin.groovy | 7 ++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/copybara_build_and_test.yml diff --git a/.github/workflows/copybara_build_and_test.yml b/.github/workflows/copybara_build_and_test.yml new file mode 100644 index 00000000000..b2b405f11b2 --- /dev/null +++ b/.github/workflows/copybara_build_and_test.yml @@ -0,0 +1,42 @@ +name: Copybara tests + +on: + pull_request: + branches: [ google ] + paths-ignore: + - '**.md' + +permissions: + contents: read + +jobs: + copybara-tests: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + + - uses: gradle/gradle-build-action@v2 + + - name: Build + run: | + SKIP_ERRORPRONE=true SKIP_JAVADOC=true \ + ./gradlew assemble testClasses --parallel --stacktrace --no-watch-fs + + - name: Integration tests + run: | + # Only run integration tests on Copybara PRs + (cd integration_tests && \ + SKIP_ERRORPRONE=true SKIP_JAVADOC=true \ + ../gradlew test --info --stacktrace --continue --parallel --no-watch-fs \ + -Drobolectric.alwaysIncludeVariantMarkersInTestName=true \ + -Drobolectric.enabledSdks=34 \ + -Dorg.gradle.workers.max=2 \ + -x :integration_tests:nativegraphics:test \ + ) \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 21b259a81c9..b03916fb41b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,12 +2,12 @@ name: Tests on: push: - branches: [ master, 'robolectric-*.x' ] + branches: [ master, 'robolectric-*.x', 'google' ] paths-ignore: - '**.md' pull_request: - branches: [ master, google ] + branches: [ master ] paths-ignore: - '**.md' @@ -35,7 +35,7 @@ jobs: - name: Build run: | SKIP_ERRORPRONE=true SKIP_JAVADOC=true \ - ./gradlew clean assemble testClasses --parallel --stacktrace --no-watch-fs + ./gradlew assemble testClasses --parallel --stacktrace --no-watch-fs unit-tests: runs-on: ubuntu-22.04 diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy index d65f71cba07..fc2ac09a1aa 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy @@ -2,6 +2,7 @@ package org.robolectric.gradle import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test public class AndroidProjectConfigPlugin implements Plugin { @Override @@ -63,5 +64,11 @@ public class AndroidProjectConfigPlugin implements Plugin { } } } + + // Only run tests in the debug variant. This is to avoid running tests twice when `./gradlew test` is run at the top-level. + project.tasks.withType(Test) { + onlyIf { variantName.toLowerCase().contains('debug') } + } } } + From 130d0a4fb5fc52a263e3294dbdbbc77fbf7cb841 Mon Sep 17 00:00:00 2001 From: Hunter Knepshield Date: Fri, 13 Oct 2023 14:27:56 -0700 Subject: [PATCH 24/33] Add OPERATOR_* values as valid barring types for BarringInfoBuilder. These values are not in the public SDK, but they are defined in the HAL and devices do return these values, so apps should be able to generate fake data with these constants for completeness. PiperOrigin-RevId: 573319895 --- .../shadows/BarringInfoBuilderTest.java | 9 +++ .../shadows/BarringInfoBuilder.java | 68 ++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/robolectric/src/test/java/org/robolectric/shadows/BarringInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/BarringInfoBuilderTest.java index d1649c37a0d..3831c12b989 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/BarringInfoBuilderTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/BarringInfoBuilderTest.java @@ -1,5 +1,7 @@ package org.robolectric.shadows; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_1; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_32; import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_CS_VOICE; import static android.telephony.BarringInfo.BarringServiceInfo.BARRING_TYPE_CONDITIONAL; import static android.telephony.BarringInfo.BarringServiceInfo.BARRING_TYPE_NONE; @@ -82,6 +84,8 @@ public void buildBarringInfo_fromSdkR() throws Exception { BarringInfoBuilder.newBuilder() .setCellIdentity(cellIdentityLte) .addBarringServiceInfo(BARRING_SERVICE_TYPE_CS_VOICE, barringServiceInfo) + .addBarringServiceInfo(SERVICE_TYPE_OPERATOR_1, barringServiceInfo) + .addBarringServiceInfo(SERVICE_TYPE_OPERATOR_32, barringServiceInfo) .build(); BarringServiceInfo outBarringServiceInfo = @@ -91,5 +95,10 @@ public void buildBarringInfo_fromSdkR() throws Exception { assertThat(outBarringServiceInfo.getConditionalBarringFactor()).isEqualTo(20); assertThat(outBarringServiceInfo.getConditionalBarringTimeSeconds()).isEqualTo(30); assertThat(outBarringServiceInfo.isBarred()).isTrue(); + // Repeated data, just different service types + assertThat(barringInfo.getBarringServiceInfo(SERVICE_TYPE_OPERATOR_1)) + .isEqualTo(barringServiceInfo); + assertThat(barringInfo.getBarringServiceInfo(SERVICE_TYPE_OPERATOR_32)) + .isEqualTo(barringServiceInfo); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BarringInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/BarringInfoBuilder.java index 74ee1402fea..d302198248d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/BarringInfoBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/BarringInfoBuilder.java @@ -1,5 +1,37 @@ package org.robolectric.shadows; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_1; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_10; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_11; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_12; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_13; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_14; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_15; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_16; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_17; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_18; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_19; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_2; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_20; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_21; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_22; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_23; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_24; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_25; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_26; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_27; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_28; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_29; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_3; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_30; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_31; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_32; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_4; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_5; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_6; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_7; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_8; +import static android.hardware.radio.network.BarringInfo.SERVICE_TYPE_OPERATOR_9; import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_CS_FALLBACK; import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_CS_SERVICE; import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_CS_VOICE; @@ -127,7 +159,41 @@ private void validateBarringServiceType(int barringServiceType) { && barringServiceType != BARRING_SERVICE_TYPE_MMTEL_VOICE && barringServiceType != BARRING_SERVICE_TYPE_MMTEL_VIDEO && barringServiceType != BARRING_SERVICE_TYPE_EMERGENCY - && barringServiceType != BARRING_SERVICE_TYPE_SMS) { + && barringServiceType != BARRING_SERVICE_TYPE_SMS + // OPERATOR_* values are not in the public SDK, but they are defined in the HAL and will be + // returned on real devices, so we still let them through. + && barringServiceType != SERVICE_TYPE_OPERATOR_1 + && barringServiceType != SERVICE_TYPE_OPERATOR_2 + && barringServiceType != SERVICE_TYPE_OPERATOR_3 + && barringServiceType != SERVICE_TYPE_OPERATOR_4 + && barringServiceType != SERVICE_TYPE_OPERATOR_5 + && barringServiceType != SERVICE_TYPE_OPERATOR_6 + && barringServiceType != SERVICE_TYPE_OPERATOR_7 + && barringServiceType != SERVICE_TYPE_OPERATOR_8 + && barringServiceType != SERVICE_TYPE_OPERATOR_9 + && barringServiceType != SERVICE_TYPE_OPERATOR_10 + && barringServiceType != SERVICE_TYPE_OPERATOR_11 + && barringServiceType != SERVICE_TYPE_OPERATOR_12 + && barringServiceType != SERVICE_TYPE_OPERATOR_13 + && barringServiceType != SERVICE_TYPE_OPERATOR_14 + && barringServiceType != SERVICE_TYPE_OPERATOR_15 + && barringServiceType != SERVICE_TYPE_OPERATOR_16 + && barringServiceType != SERVICE_TYPE_OPERATOR_17 + && barringServiceType != SERVICE_TYPE_OPERATOR_18 + && barringServiceType != SERVICE_TYPE_OPERATOR_19 + && barringServiceType != SERVICE_TYPE_OPERATOR_20 + && barringServiceType != SERVICE_TYPE_OPERATOR_21 + && barringServiceType != SERVICE_TYPE_OPERATOR_22 + && barringServiceType != SERVICE_TYPE_OPERATOR_23 + && barringServiceType != SERVICE_TYPE_OPERATOR_24 + && barringServiceType != SERVICE_TYPE_OPERATOR_25 + && barringServiceType != SERVICE_TYPE_OPERATOR_26 + && barringServiceType != SERVICE_TYPE_OPERATOR_27 + && barringServiceType != SERVICE_TYPE_OPERATOR_28 + && barringServiceType != SERVICE_TYPE_OPERATOR_29 + && barringServiceType != SERVICE_TYPE_OPERATOR_30 + && barringServiceType != SERVICE_TYPE_OPERATOR_31 + && barringServiceType != SERVICE_TYPE_OPERATOR_32) { throw new IllegalArgumentException("Unknown barringServiceType: " + barringServiceType); } } From e30efa6f7197aeac54d12287b758ec1788ebc05d Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Mon, 16 Oct 2023 10:57:15 -0700 Subject: [PATCH 25/33] Run copybara tests workflow even on .md file only changes. Google copybara infra will always wait for the result of the copybara workflow. So currently CLs that contain only '.md' file changes will timeout. PiperOrigin-RevId: 573864772 --- .github/workflows/copybara_build_and_test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/copybara_build_and_test.yml b/.github/workflows/copybara_build_and_test.yml index b2b405f11b2..5b90d3cbe42 100644 --- a/.github/workflows/copybara_build_and_test.yml +++ b/.github/workflows/copybara_build_and_test.yml @@ -3,8 +3,6 @@ name: Copybara tests on: pull_request: branches: [ google ] - paths-ignore: - - '**.md' permissions: contents: read @@ -39,4 +37,4 @@ jobs: -Drobolectric.enabledSdks=34 \ -Dorg.gradle.workers.max=2 \ -x :integration_tests:nativegraphics:test \ - ) \ No newline at end of file + ) From 4799ca152c40734b20ce7bc26ef1cd40f1843f69 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Mon, 16 Oct 2023 11:15:12 -0700 Subject: [PATCH 26/33] Fix SDK support statement in readme. Clarify that KitKat through U is supported now. PiperOrigin-RevId: 573870870 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 862eaec5c33..27ee5f3a3e6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Robolectric is the industry-standard unit testing framework for Android. With Robolectric, your tests run in a simulated Android environment inside a JVM, without the overhead and flakiness of an emulator. Robolectric tests routinely run 10x faster than those on cold-started emulators. -Robolectric supports running unit tests for *17* different versions of Android, ranging from Jelly Bean (API level 16) to U (API level 34). +Robolectric supports running unit tests for *15* different versions of Android, ranging from KitKat (API level 19) to U (API level 34). ## Usage From d7dc106b2a6c62270b8e60ec8ef676228d16008e Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Mon, 16 Oct 2023 11:25:36 -0700 Subject: [PATCH 27/33] Rebalance github test jobs. Higher SDK levels take longer to run than lower ones. The 32,33,34 SDK group takes almost 2X as long to run as 19,21,22. This commit regroup sets of SDK levels tested to group higer SDK levels with older ones, to attempt to reduce overall presubmit time. PiperOrigin-RevId: 573874274 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b03916fb41b..1df25bd2b39 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - api-versions: [ '19,21,22', '23,24,25', '26,27,28', '29,30,31', '32,33,34' ] + api-versions: [ '19,21,34', '22,23,33', '24,25,32', '26,27,28', '29,30,31', ] steps: - uses: actions/checkout@v4 From cf9c867823f9674f10817068ceb287d66258bb49 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Mon, 16 Oct 2023 15:49:03 -0700 Subject: [PATCH 28/33] Use ImmutableMap.build instead of ImmutableMap.buildOrThrow in ShadowCamera Some Runtime classpaths have an older version of Guava that does not contain ImmutableMap.buildOrThrow. PiperOrigin-RevId: 573952593 --- .../src/main/java/org/robolectric/shadows/ShadowCamera.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java index 2adcd3c4771..0c2c3a89566 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java @@ -55,7 +55,7 @@ public class ShadowCamera { .put("flash-mode", Camera.Parameters.FLASH_MODE_AUTO) .put("max-num-focus-areas", "1") .put("max-num-metering-areas", "1") - .buildOrThrow(); + .build(); private static int lastOpenedCameraId; From 6efa4e919e5b89049a16f7e24e984ce465d3232d Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Mon, 16 Oct 2023 16:51:45 -0700 Subject: [PATCH 29/33] Add integration tests to verify androidx.test main thread checking. In INSTRUMENTATION_TEST LooperMode, we expect onView to not be allowed to be called from main thread. PiperOrigin-RevId: 573967897 --- .../integrationtests/axt/EspressoTest.java | 17 +++++++++++++++++ .../axt/ActivityScenarioTest.java | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java b/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java index 0c0df8b19f8..e703a282c6c 100644 --- a/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java +++ b/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java @@ -13,7 +13,10 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import android.os.Handler; +import android.os.Looper; import android.view.KeyEvent; import android.widget.Button; import android.widget.EditText; @@ -30,6 +33,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; import org.robolectric.integration.axt.R; /** Simple tests to verify espresso APIs can be used on both Robolectric and device. */ @@ -223,4 +228,16 @@ public void clickButton_after_swipeUp() { activityScenario.onActivity(action -> assertThat(action.buttonClicked).isTrue()); } } + + @Test + @LooperMode(Mode.INSTRUMENTATION_TEST) // only instrumentation test mode has the correct behavior + public void onView_mainThread() { + new Handler(Looper.getMainLooper()) + .post( + () -> + assertThrows( + IllegalStateException.class, + () -> onView(withId(R.id.edit_text)).perform(typeText("Some text.")))); + Espresso.onIdle(); + } } diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java index 72031be7452..5e44b5fe497 100644 --- a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java +++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java @@ -10,6 +10,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; +import android.os.Looper; import androidx.fragment.app.Fragment; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.R; @@ -279,4 +280,15 @@ public void onActivityExceptionPropagated() { })); } } + + @Test + public void onActivity_runsOnMainLooperThread() { + try (ActivityScenario activityScenario = + ActivityScenario.launch(TranscriptActivity.class)) { + activityScenario.onActivity( + activity -> { + assertThat(Looper.getMainLooper().getThread()).isEqualTo(Thread.currentThread()); + }); + } + } } From 01e444147d3732ae0e624d14a3b6db80b3a10e6e Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 17 Oct 2023 07:21:35 -0700 Subject: [PATCH 30/33] Add shadow for AlwaysOnHotwordDetector The goal of this change is to allow testing of a VoiceInteractionService that calls createAlwaysOnHotwordDetector and have the detector return true when startRecognition is called. I've removed some unneeded shadowing in ShadowVoiceInteractionService in favor of real code. For the detector, there are two critical fields that I'm now setting when the object is created. Note that a test hoping to achieve this result should also provide some fake metadata in its manifest to convince KeyphraseEnrollmentInfo that an enrollment apk is present. PiperOrigin-RevId: 574140904 --- .../ShadowAlwaysOnHotwordDetectorTest.java | 96 +++++++ .../ShadowVoiceInteractionServiceTest.java | 2 +- .../ShadowAlwaysOnHotwordDetector.java | 268 ++++++++++++++++++ .../shadows/ShadowServiceManager.java | 3 + .../ShadowVoiceInteractionService.java | 49 ++-- 5 files changed, 390 insertions(+), 28 deletions(-) create mode 100644 robolectric/src/test/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetectorTest.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetector.java diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetectorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetectorTest.java new file mode 100644 index 00000000000..0bebfba912c --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetectorTest.java @@ -0,0 +1,96 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; +import android.hardware.soundtrigger.KeyphraseMetadata; +import android.media.AudioFormat; +import android.service.voice.AlwaysOnHotwordDetector; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.HashSet; +import java.util.Locale; +import java.util.concurrent.Executors; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +/** Test for ShadowAlwaysOnHotwordDetector. */ +@RunWith(AndroidJUnit4.class) +@Config(sdk = UPSIDE_DOWN_CAKE) +public class ShadowAlwaysOnHotwordDetectorTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private Object mockCallback; + @Captor private ArgumentCaptor payloadCaptor; + + @Before + public void setUp() { + mockCallback = mock(AlwaysOnHotwordDetector.Callback.class); + } + + @Test + public void testGetSupportedRecognitionModes() { + AlwaysOnHotwordDetector detector = (AlwaysOnHotwordDetector) createDetector(); + assertThat(detector.getSupportedRecognitionModes()) + .isEqualTo(AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER); + } + + @Test + public void testCallback_onError() { + AlwaysOnHotwordDetector detector = (AlwaysOnHotwordDetector) createDetector(); + ShadowAlwaysOnHotwordDetector shadowDetector = Shadow.extract(detector); + + shadowDetector.triggerOnErrorCallback(); + verify((AlwaysOnHotwordDetector.Callback) mockCallback).onError(); + } + + @Test + public void testCallback_onDetected() { + AlwaysOnHotwordDetector detector = (AlwaysOnHotwordDetector) createDetector(); + ShadowAlwaysOnHotwordDetector shadowDetector = Shadow.extract(detector); + + AudioFormat audioFormat = new AudioFormat.Builder().setSampleRate(8000).build(); + byte[] data = new byte[0]; + int captureSession = 99; + shadowDetector.triggerOnDetectedCallback( + ShadowAlwaysOnHotwordDetector.createEventPayload( + true, true, audioFormat, captureSession, data)); + verify((AlwaysOnHotwordDetector.Callback) mockCallback).onDetected(payloadCaptor.capture()); + + assertThat(payloadCaptor.getValue().getCaptureAudioFormat()).isEqualTo(audioFormat); + assertThat(payloadCaptor.getValue().getCaptureSession()).isEqualTo(captureSession); + assertThat(payloadCaptor.getValue().getData()).isEqualTo(data); + } + + private Object createDetector() { + KeyphraseMetadata keyphraseMetadata = + new KeyphraseMetadata( + 1, + "keyphrase", + new HashSet<>(), + AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER); + KeyphraseEnrollmentInfo keyphraseEnrollmentInfo = mock(KeyphraseEnrollmentInfo.class); + when(keyphraseEnrollmentInfo.getKeyphraseMetadata(any(), any())).thenReturn(keyphraseMetadata); + return new AlwaysOnHotwordDetector( + "keyphrase", + Locale.US, + Executors.newSingleThreadExecutor(), + (AlwaysOnHotwordDetector.Callback) mockCallback, + keyphraseEnrollmentInfo, + null, + 0, + false); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionServiceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionServiceTest.java index 46c7e5bee47..7a935f48922 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionServiceTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionServiceTest.java @@ -75,7 +75,7 @@ public void setActiveService_returnsDefaultFalse() { @Config(minSdk = M) public void showSessionInvokedBeforeServiceReady_throwsException() { assertThrows( - NullPointerException.class, + IllegalStateException.class, () -> { service.showSession(new Bundle(), 0); }); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetector.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetector.java new file mode 100644 index 00000000000..7190b1e12ca --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlwaysOnHotwordDetector.java @@ -0,0 +1,268 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static android.service.voice.AlwaysOnHotwordDetector.EventPayload.DATA_FORMAT_TRIGGER_AUDIO; +import static android.service.voice.AlwaysOnHotwordDetector.STATE_KEYPHRASE_ENROLLED; +import static org.robolectric.shadow.api.Shadow.invokeConstructor; +import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; +import android.hardware.soundtrigger.KeyphraseMetadata; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra; +import android.media.AudioFormat; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.SharedMemory; +import android.service.voice.AlwaysOnHotwordDetector; +import android.service.voice.AlwaysOnHotwordDetector.Callback; +import android.service.voice.AlwaysOnHotwordDetector.EventPayload; +import android.service.voice.HotwordDetectedResult; +import android.service.voice.IVoiceInteractionService; +import com.android.internal.app.IVoiceInteractionManagerService; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Executor; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Shadow implementation of {@link android.service.voice.AlwaysOnHotwordDetector}. */ +@Implements(value = AlwaysOnHotwordDetector.class, minSdk = LOLLIPOP, isInAndroidSdk = false) +public class ShadowAlwaysOnHotwordDetector { + + @RealObject private AlwaysOnHotwordDetector realObject; + + @Implementation(maxSdk = Q) + protected void __constructor__( + String text, + Locale locale, + AlwaysOnHotwordDetector.Callback callback, + KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, + IVoiceInteractionService voiceInteractionService, + IVoiceInteractionManagerService modelManagementService) { + invokeConstructor( + AlwaysOnHotwordDetector.class, + realObject, + from(String.class, text), + from(Locale.class, locale), + from(AlwaysOnHotwordDetector.Callback.class, callback), + from(KeyphraseEnrollmentInfo.class, keyphraseEnrollmentInfo), + from(IVoiceInteractionService.class, voiceInteractionService), + from(IVoiceInteractionManagerService.class, modelManagementService)); + setEnrollmentFields(text, locale, keyphraseEnrollmentInfo); + } + + @Implementation(minSdk = R, maxSdk = R) + protected void __constructor__( + String text, + Locale locale, + AlwaysOnHotwordDetector.Callback callback, + KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, + IVoiceInteractionManagerService modelManagementService) { + invokeConstructor( + AlwaysOnHotwordDetector.class, + realObject, + from(String.class, text), + from(Locale.class, locale), + from(AlwaysOnHotwordDetector.Callback.class, callback), + from(KeyphraseEnrollmentInfo.class, keyphraseEnrollmentInfo), + from(IVoiceInteractionManagerService.class, modelManagementService)); + setEnrollmentFields(text, locale, keyphraseEnrollmentInfo); + } + + @Implementation(minSdk = S, maxSdk = TIRAMISU) + protected void __constructor__( + String text, + Locale locale, + AlwaysOnHotwordDetector.Callback callback, + KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, + IVoiceInteractionManagerService modelManagementService, + int targetSdkVersion, + boolean supportHotwordDetectionService, + PersistableBundle options, + SharedMemory sharedMemory) { + invokeConstructor( + AlwaysOnHotwordDetector.class, + realObject, + from(String.class, text), + from(Locale.class, locale), + from(AlwaysOnHotwordDetector.Callback.class, callback), + from(KeyphraseEnrollmentInfo.class, keyphraseEnrollmentInfo), + from(IVoiceInteractionManagerService.class, modelManagementService), + from(int.class, targetSdkVersion), + from(boolean.class, supportHotwordDetectionService), + from(PersistableBundle.class, options), + from(SharedMemory.class, sharedMemory)); + setEnrollmentFields(text, locale, keyphraseEnrollmentInfo); + } + + @Implementation(minSdk = UPSIDE_DOWN_CAKE, maxSdk = UPSIDE_DOWN_CAKE) + protected void __constructor__( + String text, + Locale locale, + Executor executor, + AlwaysOnHotwordDetector.Callback callback, + KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, + IVoiceInteractionManagerService modelManagementService, + int targetSdkVersion, + boolean supportSandboxedDetectionService) { + invokeConstructor( + AlwaysOnHotwordDetector.class, + realObject, + from(String.class, text), + from(Locale.class, locale), + from(Executor.class, executor), + from(AlwaysOnHotwordDetector.Callback.class, callback), + from(KeyphraseEnrollmentInfo.class, keyphraseEnrollmentInfo), + from(IVoiceInteractionManagerService.class, modelManagementService), + from(int.class, targetSdkVersion), + from(boolean.class, supportSandboxedDetectionService)); + setEnrollmentFields(text, locale, keyphraseEnrollmentInfo); + } + + /** Invokes Callback#onError. */ + public void triggerOnErrorCallback() { + reflector(AlwaysOnHotwordDetectorReflector.class, realObject).getCallback().onError(); + } + + /** Invokes Callback#onDetected. */ + public void triggerOnDetectedCallback(EventPayload eventPayload) { + reflector(AlwaysOnHotwordDetectorReflector.class, realObject) + .getCallback() + .onDetected(eventPayload); + } + + private void setEnrollmentFields( + String text, Locale locale, KeyphraseEnrollmentInfo keyphraseEnrollmentInfo) { + reflector(AlwaysOnHotwordDetectorReflector.class, realObject) + .setAvailability(STATE_KEYPHRASE_ENROLLED); + if (RuntimeEnvironment.getApiLevel() > Q && keyphraseEnrollmentInfo != null) { + reflector(AlwaysOnHotwordDetectorReflector.class, realObject) + .setKeyphraseMetadata(keyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale)); + } + } + + /** Shadow for AsyncTask kicked off in the constructor of AlwaysOnHotwordDetector. */ + @Implements( + className = "android.service.voice.AlwaysOnHotwordDetector$RefreshAvailabiltyTask", + minSdk = LOLLIPOP, + maxSdk = TIRAMISU, + isInAndroidSdk = false) + public static class ShadowRefreshAvailabilityTask + extends ShadowPausedAsyncTask { + + @Implementation + protected int internalGetInitialAvailability() { + return STATE_KEYPHRASE_ENROLLED; + } + + @Implementation + protected boolean internalGetIsEnrolled(int keyphraseId, Locale locale) { + return true; + } + + @Implementation(minSdk = R) + protected void internalUpdateEnrolledKeyphraseMetadata() { + // No-op, we already set this field in #setEnrollmentFields() + } + } + + /** Invokes the normally hidden EventPayload constructor for passing to Callback#onDetected(). */ + public static EventPayload createEventPayload( + boolean triggerAvailable, + boolean captureAvailable, + AudioFormat audioFormat, + int captureSession, + byte[] data) { + if (RuntimeEnvironment.getApiLevel() <= Q) { + return reflector(EventPayloadReflector.class) + .newEventPayload(triggerAvailable, captureAvailable, audioFormat, captureSession, data); + } else if (RuntimeEnvironment.getApiLevel() == TIRAMISU) { + return reflector(EventPayloadReflector.class) + .newEventPayload( + captureAvailable, + audioFormat, + captureSession, + DATA_FORMAT_TRIGGER_AUDIO, + data, + null, + null, + new ArrayList<>()); + } else { + return reflector(EventPayloadReflector.class) + .newEventPayload( + captureAvailable, + audioFormat, + captureSession, + DATA_FORMAT_TRIGGER_AUDIO, + data, + null, + null, + new ArrayList<>(), + 0); + } + } + + /** Accessor interface for AlwaysOnHotwordDetector's internals. */ + @ForType(AlwaysOnHotwordDetector.class) + interface AlwaysOnHotwordDetectorReflector { + + @Accessor("mAvailability") + void setAvailability(int availability); + + @Accessor("mKeyphraseMetadata") + void setKeyphraseMetadata(KeyphraseMetadata keyphraseMetadata); + + @Accessor("mExternalCallback") + Callback getCallback(); + } + + /** Accessor interface for inner class EventPayload which has a private constructor. */ + @ForType(AlwaysOnHotwordDetector.EventPayload.class) + interface EventPayloadReflector { + + @Constructor + EventPayload newEventPayload( + boolean triggerAvailable, + boolean captureAvailable, + AudioFormat audioFormat, + int captureSession, + byte[] data); + + @Constructor + EventPayload newEventPayload( + boolean captureAvailable, + AudioFormat audioFormat, + int captureSession, + int dataFormat, + byte[] data, + @Nullable HotwordDetectedResult hotwordDetectedResult, + @Nullable ParcelFileDescriptor audioStream, + @Nonnull List keyphraseExtras); + + @Constructor + EventPayload newEventPayload( + boolean captureAvailable, + AudioFormat audioFormat, + int captureSession, + int dataFormat, + byte[] data, + @Nullable HotwordDetectedResult hotwordDetectedResult, + @Nullable ParcelFileDescriptor audioStream, + @Nonnull List keyphraseExtras, + long halEventReceivedMillis); + } +} 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 b2ad1e3f87e..0212d2a39d0 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java @@ -93,6 +93,7 @@ import com.android.internal.app.IAppOpsService; import com.android.internal.app.IBatteryStats; import com.android.internal.app.ISoundTriggerService; +import com.android.internal.app.IVoiceInteractionManagerService; import com.android.internal.appwidget.IAppWidgetService; import com.android.internal.os.IDropBoxManagerService; import com.android.internal.statusbar.IStatusBar; @@ -164,6 +165,8 @@ public class ShadowServiceManager { addBinderService(Context.USAGE_STATS_SERVICE, IUsageStatsManager.class); addBinderService(Context.MEDIA_ROUTER_SERVICE, IMediaRouterService.class); addBinderService(Context.MEDIA_SESSION_SERVICE, ISessionManager.class, true); + addBinderService( + Context.VOICE_INTERACTION_MANAGER_SERVICE, IVoiceInteractionManagerService.class, true); } if (RuntimeEnvironment.getApiLevel() >= M) { addBinderService(Context.FINGERPRINT_SERVICE, IFingerprintService.class); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionService.java index 18c3c56f866..6a453d56167 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionService.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionService.java @@ -3,6 +3,7 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.Q; +import static org.robolectric.util.reflector.Reflector.reflector; import android.content.ComponentName; import android.content.Context; @@ -17,6 +18,9 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.util.reflector.Direct; +import org.robolectric.util.reflector.ForType; /** Shadow implementation of {@link android.service.voice.VoiceInteractionService}. */ @Implements(value = VoiceInteractionService.class, minSdk = LOLLIPOP) @@ -24,11 +28,11 @@ public class ShadowVoiceInteractionService extends ShadowService { private final List hintBundles = Collections.synchronizedList(new ArrayList<>()); private final List sessionBundles = Collections.synchronizedList(new ArrayList<>()); - private boolean isReady = false; + @RealObject private VoiceInteractionService realVic; /** - * Sets return value for {@link #isActiveService(Context context, ComponentName componentName)} - * method. + * Sets return value for {@link VoiceInteractionService#isActiveService(Context context, + * ComponentName componentName)} method. */ public static void setActiveService(@Nullable ComponentName activeService) { Settings.Secure.putString( @@ -37,36 +41,16 @@ public static void setActiveService(@Nullable ComponentName activeService) { activeService == null ? "" : activeService.flattenToString()); } - @Implementation - protected void onReady() { - isReady = true; - } - @Implementation(minSdk = Q) protected void setUiHints(Bundle hints) { - // The actual implementation of this code on Android will also throw the exception if the - // service isn't ready. - // Throwing here will hopefully make sure these issues are caught before production. - if (!isReady) { - throw new NullPointerException( - "setUiHints() called before onReady() callback for VoiceInteractionService!"); - } - - if (hints != null) { - hintBundles.add(hints); - } + reflector(VoiceInteractionServiceReflector.class, realVic).setUiHints(hints); + hintBundles.add(hints); } @Implementation(minSdk = M) protected void showSession(Bundle args, int flags) { - if (!isReady) { - throw new NullPointerException( - "showSession() called before onReady() callback for VoiceInteractionService!"); - } - - if (args != null) { - sessionBundles.add(args); - } + reflector(VoiceInteractionServiceReflector.class, realVic).showSession(args, flags); + sessionBundles.add(args); } /** @@ -98,4 +82,15 @@ public Bundle getLastUiHintBundle() { public Bundle getLastSessionBundle() { return Iterables.getLast(sessionBundles, null); } + + /** Accessor interface for VoiceInteractionService's internals. */ + @ForType(VoiceInteractionService.class) + interface VoiceInteractionServiceReflector { + + @Direct + void showSession(Bundle args, int flags); + + @Direct + void setUiHints(Bundle hints); + } } From 666589f8b4698467ec7ea683d973a9e0ee8e2475 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Tue, 17 Oct 2023 11:31:05 -0700 Subject: [PATCH 31/33] Add support for TextRunShaper in RNG This involves adding support for native methods in TextRunShaper and PositionedGlyphs. Bump the native runtime version to 1.0.2 which contains the native code changes (aosp/2791429). PiperOrigin-RevId: 574214410 --- gradle/libs.versions.toml | 2 +- .../fonts/WeightEqualsEmVariableFont.ttf | Bin 0 -> 2056 bytes .../ShadowNativeTextRunShaperTest.java | 354 ++++++++++++++++++ .../PositionedGlyphsNatives.java | 46 +++ .../nativeruntime/TextRunShaperNatives.java | 46 +++ .../shadows/ShadowNativePositionedGlyphs.java | 65 ++++ .../shadows/ShadowNativeTextRunShaper.java | 47 +++ 7 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 integration_tests/nativegraphics/src/main/assets/fonts/WeightEqualsEmVariableFont.ttf create mode 100644 integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTextRunShaperTest.java create mode 100644 nativeruntime/src/main/java/org/robolectric/nativeruntime/PositionedGlyphsNatives.java create mode 100644 nativeruntime/src/main/java/org/robolectric/nativeruntime/TextRunShaperNatives.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTextRunShaper.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20982fadd59..e8f01a91d5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] robolectric-compat = "4.10.2" -robolectric-nativeruntime-dist-compat = "1.0.1" +robolectric-nativeruntime-dist-compat = "1.0.2" # https://developer.android.com/studio/releases android-gradle = "8.1.2" diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/WeightEqualsEmVariableFont.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/WeightEqualsEmVariableFont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..06df4cb1dcb59ec41f424d4e52eaffb4cca32009 GIT binary patch literal 2056 zcmb7_zi(Sr6vxkdb{bQqEko6z>TqQRsYtC$T?L}jAt7zlC{8bi3>_G|up$Qj0N5B{HlK6edueR7LcHsD@A-Yt`JVf(#S@WJ zav&MGdwXMkb?(gffym@8((9%2m10;7e-iQDhyS?J3_7wef5HE(eEmVteHcVvWB(L> zI^5o?&wNvPN5sDfe>Oz_=-W5)=zoJRG-^S0>QQq6`gh*98wjK(GsHU!Kiz2dccm!z z;1A(*+wDpqC!xf5OedPbZb!0YD&8!-YX!|(@9^<==x>NjTGkvR!uKjVh8@;vj0JV$B!cz^rPpdGK_#2RbB zM5e%IjWx?q>;2)7Ik4G7rk4MI;ATBY>#QnDS7&C522D=tib3`@tQ5nT_T(H~O#5=0 zGZWLWoTL>8ni9odtvL=s9v`qY0B>6@%Yc}uR! zEYupB8tc5Y*t?5Q#OkDkZw;A~GO`^s6|=i*HlILOh*$>iCYIZ#Tg0-3bQLbdXB%yI z#3r@4JZ5u;T{V77dd#~2j^$Is-w`p*u|vd4!j1Y|!fpfKE)gQjL#r%Mr&gUt$jhi# z9yH#iPwv>*vw9tM1)%51qm932q`Ae}M9_{NMoHy0*|q#TW*On*;M)l{5s_M^cQU|7 zvw2a~XhwyxY}WBqbbGdA7wSHlMARAMv}HL4^gz3yCk)hgQ9|$flpfd|A&%tJ577xmQKd^WzLHCwb0f z8E%O=&-$M6%8h2TiN%`S=ESbEI@!c-l`$_X@Jn*bqLh$0PJapMP4ieXT6ZYPBD30m zz`Bf9Z4`$Z`Nn5Q&!Kj?V|c{fh;faMx~#~K_pgjsdTMkX$D^KfCq1D(cF=DtveQ+; zL%XPb*QCHjl?q7kE+h=s- zx?EulY!ry|;>#>pMIqYwJf>fD56K;{U7#Z?^yHRYPQK7hYDw=Xy^(o_KcaitF6*-^$ubnsU T= metrics.descent).isTrue(); + } + } + + @Test + public void shapeText_context() { + // Setup + Paint paint = new Paint(); + paint.setTextSize(100f); + + // Arabic script change form (glyph) based on position. + String text = "\u0645\u0631\u062D\u0628\u0627"; + + // Act + PositionedGlyphs resultWithContext = + TextRunShaper.shapeTextRun(text, 0, 1, 0, text.length(), 0f, 0f, true, paint); + PositionedGlyphs resultWithoutContext = + TextRunShaper.shapeTextRun(text, 0, 1, 0, 1, 0f, 0f, true, paint); + + // Assert + assertThat(resultWithContext.getGlyphId(0)).isNotEqualTo(resultWithoutContext.getGlyphId(0)); + } + + @Test + public void shapeText_twoAPISameResult() { + // Setup + Paint paint = new Paint(); + String text = "Hello, World."; + paint.setTextSize(100f); // Shape text with 100px + + // Act + PositionedGlyphs resultString = + TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); + + char[] charArray = text.toCharArray(); + PositionedGlyphs resultChars = + TextRunShaper.shapeTextRun( + charArray, 0, charArray.length, 0, charArray.length, 0f, 0f, false, paint); + + // Asserts + assertThat(resultString.glyphCount()).isEqualTo(resultChars.glyphCount()); + assertThat(resultString.getAdvance()).isEqualTo(resultChars.getAdvance()); + assertThat(resultString.getAscent()).isEqualTo(resultChars.getAscent()); + assertThat(resultString.getDescent()).isEqualTo(resultChars.getDescent()); + for (int i = 0; i < resultString.glyphCount(); ++i) { + assertThat(resultString.getGlyphId(i)).isEqualTo(resultChars.getGlyphId(i)); + assertThat(resultString.getFont(i)).isEqualTo(resultChars.getFont(i)); + assertThat(resultString.getGlyphX(i)).isEqualTo(resultChars.getGlyphX(i)); + assertThat(resultString.getGlyphY(i)).isEqualTo(resultChars.getGlyphY(i)); + } + } + + @Test + public void shapeText_multiLanguage() { + // Setup + Paint paint = new Paint(); + paint.setTextSize(100f); + String text = "Hello, Emoji: \uD83E\uDE90"; // Usually emoji is came from ColorEmoji font. + + // Act + PositionedGlyphs result = + TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); + + // Assert + HashSet set = new HashSet<>(); + for (int i = 0; i < result.glyphCount(); ++i) { + set.add(result.getFont(i)); + } + assertThat(set.size()).isEqualTo(2); // Roboto + Emoji is expected + } + + @Test + public void shapeText_fontCreateFromNative() throws IOException { + // Setup + Context ctx = ApplicationProvider.getApplicationContext(); + Paint paint = new Paint(); + Font originalFont = + new Font.Builder(ctx.getAssets(), "fonts/WeightEqualsEmVariableFont.ttf").build(); + Typeface typeface = + new Typeface.CustomFallbackBuilder(new FontFamily.Builder(originalFont).build()).build(); + paint.setTypeface(typeface); + // setFontVariationSettings creates Typeface internally and it is not from Java Font object. + paint.setFontVariationSettings("'wght' 250"); + + // Act + PositionedGlyphs res = TextRunShaper.shapeTextRun("a", 0, 1, 0, 1, 0f, 0f, false, paint); + + // Assert + Font font = res.getFont(0); + assertThat(font.getBuffer()).isEqualTo(originalFont.getBuffer()); + assertThat(font.getTtcIndex()).isEqualTo(originalFont.getTtcIndex()); + FontVariationAxis[] axes = font.getAxes(); + assertThat(axes.length).isEqualTo(1); + assertThat(axes[0].getTag()).isEqualTo("wght"); + assertThat(axes[0].getStyleValue()).isEqualTo(250f); + } + + @Test + public void positionedGlyphs_equality() { + // Setup + Paint paint = new Paint(); + paint.setTextSize(100f); + + // Act + PositionedGlyphs glyphs = TextRunShaper.shapeTextRun("abcde", 0, 5, 0, 5, 0f, 0f, true, paint); + PositionedGlyphs eqGlyphs = + TextRunShaper.shapeTextRun("abcde", 0, 5, 0, 5, 0f, 0f, true, paint); + PositionedGlyphs reversedGlyphs = + TextRunShaper.shapeTextRun("edcba", 0, 5, 0, 5, 0f, 0f, true, paint); + PositionedGlyphs substrGlyphs = + TextRunShaper.shapeTextRun("edcba", 0, 3, 0, 3, 0f, 0f, true, paint); + paint.setTextSize(50f); + PositionedGlyphs differentStyleGlyphs = + TextRunShaper.shapeTextRun("edcba", 0, 3, 0, 3, 0f, 0f, true, paint); + + // Assert + assertThat(glyphs).isEqualTo(eqGlyphs); + + assertThat(glyphs).isNotEqualTo(reversedGlyphs); + assertThat(glyphs).isNotEqualTo(substrGlyphs); + assertThat(glyphs).isNotEqualTo(differentStyleGlyphs); + } + + @Test + public void positionedGlyphs_illegalArgument_glyphID() { + // Setup + Paint paint = new Paint(); + String text = "Hello, World."; + paint.setTextSize(100f); // Shape text with 100px + PositionedGlyphs res = + TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); + + // Act + assertThrows( + IllegalArgumentException.class, + () -> res.getGlyphId(res.glyphCount())); // throws IllegalArgumentException + } + + @Test + public void resultTest_illegalArgument_font() { + // Setup + Paint paint = new Paint(); + String text = "Hello, World."; + paint.setTextSize(100f); // Shape text with 100px + PositionedGlyphs res = + TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); + + // Act + assertThrows( + IllegalArgumentException.class, + () -> res.getFont(res.glyphCount())); // throws IllegalArgumentException + } + + @Test + public void resultTest_illegalArgument_x() { + // Setup + Paint paint = new Paint(); + String text = "Hello, World."; + paint.setTextSize(100f); // Shape text with 100px + PositionedGlyphs res = + TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); + + // Act + assertThrows( + IllegalArgumentException.class, + () -> res.getGlyphX(res.glyphCount())); // throws IllegalArgumentException + } + + @Test + public void resultTest_illegalArgument_y() { + // Setup + Paint paint = new Paint(); + String text = "Hello, World."; + paint.setTextSize(100f); // Shape text with 100px + PositionedGlyphs res = + TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); + + // Act + assertThrows( + IllegalArgumentException.class, + () -> res.getGlyphY(res.glyphCount())); // throws IllegalArgumentException + } + + public void assertSameDrawResult( + CharSequence text, TextPaint paint, TextDirectionHeuristic textDir) { + int width = (int) Math.ceil(Layout.getDesiredWidth(text, paint)); + Paint.FontMetricsInt fmi = paint.getFontMetricsInt(); + int height = fmi.descent - fmi.ascent; + boolean isRtl = textDir.isRtl(text, 0, text.length()); + + // Expected bitmap output + Bitmap layoutResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas layoutCanvas = new Canvas(layoutResult); + layoutCanvas.translate(0f, -fmi.ascent); + layoutCanvas.drawTextRun( + text, + 0, + text.length(), // range + 0, + text.length(), // context range + 0f, + 0f, // position + isRtl, + paint); + + // Actual bitmap output + Bitmap glyphsResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas glyphsCanvas = new Canvas(glyphsResult); + glyphsCanvas.translate(0f, -fmi.ascent); + PositionedGlyphs glyphs = + TextRunShaper.shapeTextRun( + text, + 0, + text.length(), // range + 0, + text.length(), // context range + 0f, + 0f, // position + isRtl, + paint); + for (int i = 0; i < glyphs.glyphCount(); ++i) { + glyphsCanvas.drawGlyphs( + new int[] {glyphs.getGlyphId(i)}, + 0, + new float[] {glyphs.getGlyphX(i), glyphs.getGlyphY(i)}, + 0, + 1, + glyphs.getFont(i), + paint); + } + + assertThat(glyphsResult.sameAs(layoutResult)).isTrue(); + } + + @Test + public void testDrawConsistency() { + TextPaint paint = new TextPaint(); + paint.setTextSize(32f); + paint.setColor(Color.BLUE); + assertSameDrawResult("Hello, Android.", paint, TextDirectionHeuristics.LTR); + } + + @Test + public void testDrawConsistencyMultiFont() { + TextPaint paint = new TextPaint(); + paint.setTextSize(32f); + paint.setColor(Color.BLUE); + assertSameDrawResult("こんにちは、Android.", paint, TextDirectionHeuristics.LTR); + } + + @Test + public void testDrawConsistencyBidi() { + TextPaint paint = new TextPaint(); + paint.setTextSize(32f); + paint.setColor(Color.BLUE); + assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR); + assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.LTR); + assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.RTL); + } + + @Test + public void testDrawConsistencyBidi2() { + TextPaint paint = new TextPaint(); + paint.setTextSize(32f); + paint.setColor(Color.BLUE); + assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR); + assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.LTR); + assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.RTL); + } +} diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PositionedGlyphsNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PositionedGlyphsNatives.java new file mode 100644 index 00000000000..2b1add0e871 --- /dev/null +++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PositionedGlyphsNatives.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.robolectric.nativeruntime; + +/** + * Native methods for PositionedGlyphs JNI registration. + * + *

Native method signatures are derived from + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/text/PositionedGlyphs.java + */ +public final class PositionedGlyphsNatives { + + public static native int nGetGlyphCount(long minikinLayout); + + public static native float nGetTotalAdvance(long minikinLayout); + + public static native float nGetAscent(long minikinLayout); + + public static native float nGetDescent(long minikinLayout); + + public static native int nGetGlyphId(long minikinLayout, int i); + + public static native float nGetX(long minikinLayout, int i); + + public static native float nGetY(long minikinLayout, int i); + + public static native long nGetFont(long minikinLayout, int i); + + public static native long nReleaseFunc(); + + private PositionedGlyphsNatives() {} +} diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/TextRunShaperNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/TextRunShaperNatives.java new file mode 100644 index 00000000000..a9d2f5a4f83 --- /dev/null +++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/TextRunShaperNatives.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.robolectric.nativeruntime; + +/** + * Native methods for TextRunShaper JNI registration. + * + *

Native method signatures are derived from + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/text/TextRunShaper.java + */ +public final class TextRunShaperNatives { + + public static native long nativeShapeTextRun( + char[] text, + int start, + int count, + int contextStart, + int contextCount, + boolean isRtl, + long nativePaint); + + public static native long nativeShapeTextRun( + String text, + int start, + int count, + int contextStart, + int contextCount, + boolean isRtl, + long nativePaint); + + private TextRunShaperNatives() {} +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java new file mode 100644 index 00000000000..c024a7a715f --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java @@ -0,0 +1,65 @@ +package org.robolectric.shadows; + +import android.graphics.text.MeasuredText; +import android.graphics.text.PositionedGlyphs; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.nativeruntime.PositionedGlyphsNatives; +import org.robolectric.shadows.ShadowNativePositionedGlyphs.Picker; +import org.robolectric.versioning.AndroidVersions.S; + +/** Shadow for {@link PositionedGlyphs} that is backed by native code */ +@Implements(value = PositionedGlyphs.class, minSdk = S.SDK_INT, shadowPicker = Picker.class) +public class ShadowNativePositionedGlyphs { + @Implementation + protected static int nGetGlyphCount(long minikinLayout) { + return PositionedGlyphsNatives.nGetGlyphCount(minikinLayout); + } + + @Implementation + protected static float nGetTotalAdvance(long minikinLayout) { + return PositionedGlyphsNatives.nGetTotalAdvance(minikinLayout); + } + + @Implementation + protected static float nGetAscent(long minikinLayout) { + return PositionedGlyphsNatives.nGetAscent(minikinLayout); + } + + @Implementation + protected static float nGetDescent(long minikinLayout) { + return PositionedGlyphsNatives.nGetDescent(minikinLayout); + } + + @Implementation + protected static int nGetGlyphId(long minikinLayout, int i) { + return PositionedGlyphsNatives.nGetGlyphId(minikinLayout, i); + } + + @Implementation + protected static float nGetX(long minikinLayout, int i) { + return PositionedGlyphsNatives.nGetX(minikinLayout, i); + } + + @Implementation + protected static float nGetY(long minikinLayout, int i) { + return PositionedGlyphsNatives.nGetY(minikinLayout, i); + } + + @Implementation + protected static long nGetFont(long minikinLayout, int i) { + return PositionedGlyphsNatives.nGetFont(minikinLayout, i); + } + + @Implementation + protected static long nReleaseFunc() { + return PositionedGlyphsNatives.nReleaseFunc(); + } + + /** Shadow picker for {@link MeasuredText}. */ + public static final class Picker extends GraphicsShadowPicker { + public Picker() { + super(null, ShadowNativePositionedGlyphs.class); + } + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTextRunShaper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTextRunShaper.java new file mode 100644 index 00000000000..aaa8cb0590d --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTextRunShaper.java @@ -0,0 +1,47 @@ +package org.robolectric.shadows; + +import android.graphics.text.MeasuredText; +import android.graphics.text.TextRunShaper; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.nativeruntime.TextRunShaperNatives; +import org.robolectric.shadows.ShadowNativeTextRunShaper.Picker; +import org.robolectric.versioning.AndroidVersions.S; + +/** Shadow for {@link TextRunShaper} that is backed by native code */ +@Implements(value = TextRunShaper.class, minSdk = S.SDK_INT, shadowPicker = Picker.class) +public class ShadowNativeTextRunShaper { + + @Implementation + protected static long nativeShapeTextRun( + char[] text, + int start, + int count, + int contextStart, + int contextCount, + boolean isRtl, + long nativePaint) { + return TextRunShaperNatives.nativeShapeTextRun( + text, start, count, contextStart, contextCount, isRtl, nativePaint); + } + + @Implementation + protected static long nativeShapeTextRun( + String text, + int start, + int count, + int contextStart, + int contextCount, + boolean isRtl, + long nativePaint) { + return TextRunShaperNatives.nativeShapeTextRun( + text, start, count, contextStart, contextCount, isRtl, nativePaint); + } + + /** Shadow picker for {@link MeasuredText}. */ + public static final class Picker extends GraphicsShadowPicker { + public Picker() { + super(null, ShadowNativeTextRunShaper.class); + } + } +} From 2e3bd5dc7c3988fdf845b15256c4eb86f9e4fabc Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Tue, 17 Oct 2023 13:10:00 -0700 Subject: [PATCH 32/33] Ensure that RNG is loaded if the PositionedGlyphs static initializer is called It is possible that PositionedGlyphs.nReleaseFunc is the first native method called in a test. Ensure that it can lazy load RNG if that happens. PiperOrigin-RevId: 574243158 --- .../nativegraphics/ShadowNativeTextRunShaperTest.java | 10 ++++++++++ .../shadows/ShadowNativePositionedGlyphs.java | 2 ++ 2 files changed, 12 insertions(+) diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTextRunShaperTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTextRunShaperTest.java index b1e7dc28ece..f2c2b897e7e 100644 --- a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTextRunShaperTest.java +++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTextRunShaperTest.java @@ -40,6 +40,7 @@ import androidx.test.core.app.ApplicationProvider; import java.io.IOException; import java.util.HashSet; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -49,6 +50,15 @@ @Config(minSdk = S.SDK_INT) @RunWith(RobolectricTestRunner.class) public class ShadowNativeTextRunShaperTest { + + /** + * Perform static initialization on {@link PositionedGlyphs} to ensure that it can lazy-load RNG. + */ + @Before + public void clinitPositionedGlyphs() throws Exception { + Class.forName("android.graphics.text.PositionedGlyphs"); + } + @Test public void shapeText() { // Setup diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java index c024a7a715f..90a6f977ae2 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePositionedGlyphs.java @@ -4,6 +4,7 @@ import android.graphics.text.PositionedGlyphs; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.nativeruntime.PositionedGlyphsNatives; import org.robolectric.shadows.ShadowNativePositionedGlyphs.Picker; import org.robolectric.versioning.AndroidVersions.S; @@ -53,6 +54,7 @@ protected static long nGetFont(long minikinLayout, int i) { @Implementation protected static long nReleaseFunc() { + DefaultNativeRuntimeLoader.injectAndLoad(); return PositionedGlyphsNatives.nReleaseFunc(); } From 1012128051ff768cdd650d926e2ef78882f06cd2 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Tue, 17 Oct 2023 15:45:52 -0700 Subject: [PATCH 33/33] Increment targetSdk and compileSdk from 33 to 34 for integration tests. Now that Robolectric supports SDK 34, increment existing integration tests to target and compile against it. Also update targetSdkVersion in manifests from 16 to 19. This also required a couple additional changes: - use native graphics mode for androidx_test. With targetSdk 34, legacy graphics mode resulted in 0 width views - Specify an intent filter for androidx_test Activity. Otherwise IntentsTest will fail with 'No activities found', because the AppsFilter will block access to other apps. PiperOrigin-RevId: 574287184 --- integration_tests/agp/build.gradle | 4 ++-- integration_tests/agp/testsupport/build.gradle | 4 ++-- integration_tests/androidx/AndroidManifest.xml | 2 +- integration_tests/androidx/build.gradle | 4 ++-- integration_tests/androidx_test/build.gradle | 7 +++++-- .../src/main/AndroidManifest-NoTestPackageActivities.xml | 2 +- .../androidx_test/src/main/AndroidManifest.xml | 7 ++++++- .../sharedTest/AndroidManifest-NoTestPackageActivities.xml | 4 ++-- .../src/test/AndroidManifest-ActivityScenario.xml | 4 ++-- .../src/test/AndroidManifest-ActivityTestRule.xml | 4 ++-- .../androidx_test/src/test/AndroidManifest-Intents.xml | 7 ++++--- .../src/test/AndroidManifest-NoTestPackageActivities.xml | 4 ++-- integration_tests/ctesque/AndroidManifest.xml | 2 +- integration_tests/ctesque/build.gradle | 4 ++-- integration_tests/memoryleaks/build.gradle | 4 ++-- integration_tests/multidex/src/test/AndroidManifest.xml | 2 +- integration_tests/nativegraphics/AndroidManifest.xml | 2 +- integration_tests/nativegraphics/build.gradle | 4 ++-- .../nativegraphics/src/main/AndroidManifest.xml | 2 +- integration_tests/sparsearray/build.gradle | 4 ++-- .../src/test/resources/rawresources/AndroidManifest.xml | 2 +- .../java/org/robolectric/manifest/AndroidManifestTest.java | 2 +- .../test/resources/TestAndroidManifestForActivities.xml | 2 +- .../TestAndroidManifestForActivitiesWithIntentFilter.xml | 2 +- ...ndroidManifestForActivitiesWithIntentFilterWithData.xml | 2 +- ...droidManifestForActivitiesWithMultipleIntentFilters.xml | 2 +- .../TestAndroidManifestForActivitiesWithTaskAffinity.xml | 2 +- .../resources/TestAndroidManifestForActivityAliases.xml | 2 +- .../resources/TestAndroidManifestNoApplicationElement.xml | 2 +- .../TestAndroidManifestWithAppComponentFactory.xml | 2 +- .../test/resources/TestAndroidManifestWithAppMetaData.xml | 2 +- .../resources/TestAndroidManifestWithContentProviders.xml | 2 +- .../TestAndroidManifestWithNoContentProviders.xml | 2 +- .../test/resources/TestAndroidManifestWithPermissions.xml | 2 +- .../resources/TestAndroidManifestWithProtectionLevels.xml | 2 +- .../test/resources/TestAndroidManifestWithReceivers.xml | 2 +- .../src/test/resources/TestAndroidManifestWithServices.xml | 2 +- .../resources/TestAndroidManifestWithoutPermissions.xml | 2 +- shadows/httpclient/src/test/resources/AndroidManifest.xml | 2 +- testapp/build.gradle | 4 ++-- 40 files changed, 64 insertions(+), 55 deletions(-) diff --git a/integration_tests/agp/build.gradle b/integration_tests/agp/build.gradle index 43223e30663..14a8b862be2 100644 --- a/integration_tests/agp/build.gradle +++ b/integration_tests/agp/build.gradle @@ -4,12 +4,12 @@ apply plugin: 'com.android.library' apply plugin: AndroidProjectConfigPlugin android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.integrationtests.agp' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 } compileOptions { diff --git a/integration_tests/agp/testsupport/build.gradle b/integration_tests/agp/testsupport/build.gradle index a257b80ddea..4da0f495b19 100644 --- a/integration_tests/agp/testsupport/build.gradle +++ b/integration_tests/agp/testsupport/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.integrationtests.agp.testsupport' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 } compileOptions { diff --git a/integration_tests/androidx/AndroidManifest.xml b/integration_tests/androidx/AndroidManifest.xml index b5ffbde8eb7..2ea38fc5cad 100644 --- a/integration_tests/androidx/AndroidManifest.xml +++ b/integration_tests/androidx/AndroidManifest.xml @@ -5,7 +5,7 @@ + android:targetSdkVersion="34" /> diff --git a/integration_tests/androidx/build.gradle b/integration_tests/androidx/build.gradle index b09b8eafc1e..8f722119a4e 100644 --- a/integration_tests/androidx/build.gradle +++ b/integration_tests/androidx/build.gradle @@ -4,12 +4,12 @@ apply plugin: 'com.android.library' apply plugin: AndroidProjectConfigPlugin android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.integrationtests.androidx' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 } compileOptions { diff --git a/integration_tests/androidx_test/build.gradle b/integration_tests/androidx_test/build.gradle index 854a431cf39..3c9b2a8b208 100644 --- a/integration_tests/androidx_test/build.gradle +++ b/integration_tests/androidx_test/build.gradle @@ -6,12 +6,12 @@ apply plugin: AndroidProjectConfigPlugin apply plugin: GradleManagedDevicePlugin android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.integration.axt' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments useTestStorageService: 'true' @@ -25,6 +25,9 @@ android { testOptions { unitTests { includeAndroidResources = true + all { + systemProperty 'robolectric.graphicsMode', 'NATIVE' + } } } sourceSets { diff --git a/integration_tests/androidx_test/src/main/AndroidManifest-NoTestPackageActivities.xml b/integration_tests/androidx_test/src/main/AndroidManifest-NoTestPackageActivities.xml index fc3a59520d2..0eb17b2cf2b 100644 --- a/integration_tests/androidx_test/src/main/AndroidManifest-NoTestPackageActivities.xml +++ b/integration_tests/androidx_test/src/main/AndroidManifest-NoTestPackageActivities.xml @@ -4,7 +4,7 @@ --> - + diff --git a/integration_tests/androidx_test/src/main/AndroidManifest.xml b/integration_tests/androidx_test/src/main/AndroidManifest.xml index b6a6ab9d39c..5d77da86277 100644 --- a/integration_tests/androidx_test/src/main/AndroidManifest.xml +++ b/integration_tests/androidx_test/src/main/AndroidManifest.xml @@ -9,7 +9,12 @@ + android:exported="true" > + + + + + + android:minSdkVersion="19" + android:targetSdkVersion="34"/> diff --git a/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml index 7f2d386667a..9bcc80ee95f 100644 --- a/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml +++ b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml @@ -4,8 +4,8 @@ package="org.robolectric.integrationtests.axt"> + android:minSdkVersion="19" + android:targetSdkVersion="34"/> + android:minSdkVersion="19" + android:targetSdkVersion="34"/> + android:minSdkVersion="19" + android:targetSdkVersion="34"/> + android:exported = "true"> + + android:minSdkVersion="19" + android:targetSdkVersion="34"/> diff --git a/integration_tests/ctesque/AndroidManifest.xml b/integration_tests/ctesque/AndroidManifest.xml index 7c686df8cfd..2600a4e7c2e 100644 --- a/integration_tests/ctesque/AndroidManifest.xml +++ b/integration_tests/ctesque/AndroidManifest.xml @@ -6,7 +6,7 @@ package="org.robolectric.integrationtests.ctesque"> + android:targetSdkVersion="34" /> diff --git a/integration_tests/ctesque/build.gradle b/integration_tests/ctesque/build.gradle index 8e3bb5abe59..e192e0d8142 100644 --- a/integration_tests/ctesque/build.gradle +++ b/integration_tests/ctesque/build.gradle @@ -6,12 +6,12 @@ apply plugin: AndroidProjectConfigPlugin apply plugin: GradleManagedDevicePlugin android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.integrationtests.ctesque' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/integration_tests/memoryleaks/build.gradle b/integration_tests/memoryleaks/build.gradle index 070f70bdb8c..1831381010e 100644 --- a/integration_tests/memoryleaks/build.gradle +++ b/integration_tests/memoryleaks/build.gradle @@ -4,12 +4,12 @@ apply plugin: 'com.android.library' apply plugin: AndroidProjectConfigPlugin android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.integrationtests.memoryleaks' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 } compileOptions { diff --git a/integration_tests/multidex/src/test/AndroidManifest.xml b/integration_tests/multidex/src/test/AndroidManifest.xml index 4c3f649b8e0..df7a6251cf4 100644 --- a/integration_tests/multidex/src/test/AndroidManifest.xml +++ b/integration_tests/multidex/src/test/AndroidManifest.xml @@ -5,7 +5,7 @@ + android:targetSdkVersion="34"/> diff --git a/integration_tests/nativegraphics/AndroidManifest.xml b/integration_tests/nativegraphics/AndroidManifest.xml index cdffdf1876e..27925244910 100644 --- a/integration_tests/nativegraphics/AndroidManifest.xml +++ b/integration_tests/nativegraphics/AndroidManifest.xml @@ -4,6 +4,6 @@ package="org.robolectric.integrationtests.nativegraphics"> + android:targetSdkVersion="34" /> diff --git a/integration_tests/nativegraphics/build.gradle b/integration_tests/nativegraphics/build.gradle index f88f53a3f02..a243ea85c81 100644 --- a/integration_tests/nativegraphics/build.gradle +++ b/integration_tests/nativegraphics/build.gradle @@ -6,12 +6,12 @@ apply plugin: AndroidProjectConfigPlugin apply plugin: GradleManagedDevicePlugin android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.integrationtests.nativegraphics' defaultConfig { minSdk 26 - targetSdk 33 + targetSdk 34 } testOptions { diff --git a/integration_tests/nativegraphics/src/main/AndroidManifest.xml b/integration_tests/nativegraphics/src/main/AndroidManifest.xml index cdffdf1876e..27925244910 100644 --- a/integration_tests/nativegraphics/src/main/AndroidManifest.xml +++ b/integration_tests/nativegraphics/src/main/AndroidManifest.xml @@ -4,6 +4,6 @@ package="org.robolectric.integrationtests.nativegraphics"> + android:targetSdkVersion="34" /> diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle index 211214559d0..3d9c33a84c2 100644 --- a/integration_tests/sparsearray/build.gradle +++ b/integration_tests/sparsearray/build.gradle @@ -13,12 +13,12 @@ spotless { } android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.sparsearray' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 } compileOptions { diff --git a/resources/src/test/resources/rawresources/AndroidManifest.xml b/resources/src/test/resources/rawresources/AndroidManifest.xml index 07842c79130..bddaae64c1e 100644 --- a/resources/src/test/resources/rawresources/AndroidManifest.xml +++ b/resources/src/test/resources/rawresources/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/robolectric/src/test/java/org/robolectric/manifest/AndroidManifestTest.java b/robolectric/src/test/java/org/robolectric/manifest/AndroidManifestTest.java index eeb1c5c6639..3847ed03151 100644 --- a/robolectric/src/test/java/org/robolectric/manifest/AndroidManifestTest.java +++ b/robolectric/src/test/java/org/robolectric/manifest/AndroidManifestTest.java @@ -401,7 +401,7 @@ public void shouldReadMultipleIntentFilters() { @Test public void shouldReadTaskAffinity() { AndroidManifest appManifest = newConfig("TestAndroidManifestForActivitiesWithTaskAffinity.xml"); - assertThat(appManifest.getTargetSdkVersion()).isEqualTo(16); + assertThat(appManifest.getTargetSdkVersion()).isEqualTo(19); ActivityData activityData = appManifest.getActivityData("org.robolectric.shadows.TestTaskAffinityActivity"); diff --git a/robolectric/src/test/resources/TestAndroidManifestForActivities.xml b/robolectric/src/test/resources/TestAndroidManifestForActivities.xml index b5e522778e3..a8114412a99 100644 --- a/robolectric/src/test/resources/TestAndroidManifestForActivities.xml +++ b/robolectric/src/test/resources/TestAndroidManifestForActivities.xml @@ -1,7 +1,7 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilter.xml b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilter.xml index a44e70a02fa..8325b97767e 100644 --- a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilter.xml +++ b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilter.xml @@ -1,7 +1,7 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilterWithData.xml b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilterWithData.xml index b20f7fe9bf4..02b426bb8ed 100644 --- a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilterWithData.xml +++ b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithIntentFilterWithData.xml @@ -2,7 +2,7 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithMultipleIntentFilters.xml b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithMultipleIntentFilters.xml index 5d4025d3bae..e3938a8078f 100644 --- a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithMultipleIntentFilters.xml +++ b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithMultipleIntentFilters.xml @@ -1,7 +1,7 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithTaskAffinity.xml b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithTaskAffinity.xml index 7329d1a5371..9606d71aa55 100644 --- a/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithTaskAffinity.xml +++ b/robolectric/src/test/resources/TestAndroidManifestForActivitiesWithTaskAffinity.xml @@ -1,7 +1,7 @@ - + - + diff --git a/robolectric/src/test/resources/TestAndroidManifestNoApplicationElement.xml b/robolectric/src/test/resources/TestAndroidManifestNoApplicationElement.xml index cc6436b26fb..3ea07b105f9 100644 --- a/robolectric/src/test/resources/TestAndroidManifestNoApplicationElement.xml +++ b/robolectric/src/test/resources/TestAndroidManifestNoApplicationElement.xml @@ -1,6 +1,6 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml index cbda17e65b6..8741436a8fe 100644 --- a/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml +++ b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml @@ -1,6 +1,6 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestWithAppMetaData.xml b/robolectric/src/test/resources/TestAndroidManifestWithAppMetaData.xml index 8b463b0df6e..af902546bbc 100644 --- a/robolectric/src/test/resources/TestAndroidManifestWithAppMetaData.xml +++ b/robolectric/src/test/resources/TestAndroidManifestWithAppMetaData.xml @@ -2,7 +2,7 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestWithContentProviders.xml b/robolectric/src/test/resources/TestAndroidManifestWithContentProviders.xml index 78c51ebcf89..72cc267dfb0 100644 --- a/robolectric/src/test/resources/TestAndroidManifestWithContentProviders.xml +++ b/robolectric/src/test/resources/TestAndroidManifestWithContentProviders.xml @@ -1,6 +1,6 @@ - + - + diff --git a/robolectric/src/test/resources/TestAndroidManifestWithPermissions.xml b/robolectric/src/test/resources/TestAndroidManifestWithPermissions.xml index cf3eb788906..355ac2f6f21 100644 --- a/robolectric/src/test/resources/TestAndroidManifestWithPermissions.xml +++ b/robolectric/src/test/resources/TestAndroidManifestWithPermissions.xml @@ -1,7 +1,7 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestWithProtectionLevels.xml b/robolectric/src/test/resources/TestAndroidManifestWithProtectionLevels.xml index c853365c06f..340b7b109ff 100644 --- a/robolectric/src/test/resources/TestAndroidManifestWithProtectionLevels.xml +++ b/robolectric/src/test/resources/TestAndroidManifestWithProtectionLevels.xml @@ -1,7 +1,7 @@ - + diff --git a/robolectric/src/test/resources/TestAndroidManifestWithReceivers.xml b/robolectric/src/test/resources/TestAndroidManifestWithReceivers.xml index 95120e27690..d5b5103c6ed 100644 --- a/robolectric/src/test/resources/TestAndroidManifestWithReceivers.xml +++ b/robolectric/src/test/resources/TestAndroidManifestWithReceivers.xml @@ -1,6 +1,6 @@ - + - + diff --git a/robolectric/src/test/resources/TestAndroidManifestWithoutPermissions.xml b/robolectric/src/test/resources/TestAndroidManifestWithoutPermissions.xml index 2a1a4f9fb70..df6ddd01974 100644 --- a/robolectric/src/test/resources/TestAndroidManifestWithoutPermissions.xml +++ b/robolectric/src/test/resources/TestAndroidManifestWithoutPermissions.xml @@ -1,7 +1,7 @@ - + diff --git a/shadows/httpclient/src/test/resources/AndroidManifest.xml b/shadows/httpclient/src/test/resources/AndroidManifest.xml index b09fe8b9f77..1180b52f241 100644 --- a/shadows/httpclient/src/test/resources/AndroidManifest.xml +++ b/shadows/httpclient/src/test/resources/AndroidManifest.xml @@ -2,7 +2,7 @@ - + diff --git a/testapp/build.gradle b/testapp/build.gradle index 3e7e5b3432a..0800a09f8f7 100644 --- a/testapp/build.gradle +++ b/testapp/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdk 33 + compileSdk 34 namespace 'org.robolectric.testapp' defaultConfig { minSdk 19 - targetSdk 33 + targetSdk 34 versionCode 1 versionName "1.0"