From adb15888a30a86279540023047e53d24070e5bfb Mon Sep 17 00:00:00 2001 From: hoisie Date: Mon, 15 Nov 2021 23:08:33 -0800 Subject: [PATCH] Fix UnsupportedOperationException if a leaked Activity.recreate is called Previously, ActivityController.recreate determined the current state of the underlying Activity by calling the global ActivityLifecycleMonitorRegistry.getLifecycleStageOf This method is unreliable for leaked Activities as a new ActivityLifecycleMonitor is created before each test. This means that, for a leaked Activity, the ActivityLifecycleMonitor from the previous test has knowledge if its lifecycle state, not the new one. When the Activity state was queried using the current ActivityLifecycleMonitor, an UnsupportedOperationException occurred. To fix this, avoid using ActivityLifecycleMonitor as the source of truth for Activity State in ActivityController, and instead add a new member variable to ActivityController that maintains the state. Having ActivityController rely on ActivityLifecycleMonitor for Activity state is circuitous -- AndroidX Test is already using ActivityController to drive Activity lifecycles, so the source-of-truth state should exist inside of ActivityController itself. PiperOrigin-RevId: 410168815 --- .../org/robolectric/res/android/Asset.java | 8 +- .../org/robolectric/res/android/Chunk.java | 1 - .../res/android/CppAssetManager.java | 4 +- .../org/robolectric/res/android/FileMap.java | 2 +- .../android/internal/LocalUiController.java | 6 +- .../ActivityControllerRecreateTest.java | 34 ++++++ .../shadows/ShadowBugreportManagerTest.java | 23 ++-- .../robolectric/shadows/ShadowBuildTest.java | 9 ++ .../robolectric/shadows/ShadowIconTest.java | 55 +++++++++- .../shadows/ShadowLegacyMessageQueueTest.java | 2 +- .../internal/bytecode/SandboxClassLoader.java | 2 +- .../controller/ActivityController.java | 103 ++++++++++++------ .../org/robolectric/shadows/ShadowBuild.java | 13 +++ .../org/robolectric/shadows/ShadowIcon.java | 48 ++++++++ .../shadows/ShadowLegacyMessageQueue.java | 3 +- .../org/robolectric/shadows/ShadowParcel.java | 2 +- .../shadows/ShadowSQLiteOpenHelper.java | 2 +- .../shadows/ShadowTelephonyManager.java | 2 +- .../shadows/ShadowTimePickerDialog.java | 22 ---- .../org/robolectric/shadows/_Activity_.java | 3 + 20 files changed, 255 insertions(+), 89 deletions(-) create mode 100644 robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerRecreateTest.java diff --git a/resources/src/main/java/org/robolectric/res/android/Asset.java b/resources/src/main/java/org/robolectric/res/android/Asset.java index 06fd5b83b77..bc88e3f5021 100644 --- a/resources/src/main/java/org/robolectric/res/android/Asset.java +++ b/resources/src/main/java/org/robolectric/res/android/Asset.java @@ -656,7 +656,7 @@ public boolean isNinePatch() { // : mStart(0), mLength(0), mOffset(0), mFp(null), mFileName(null), mMap(null), mBuf(null) { // Register the Asset with the global list here after it is fully constructed and its - // vtable pointer points to this concrete type. b/31113965 + // vtable pointer points to this concrete type. registerAsset(this); } @@ -668,7 +668,7 @@ protected void finalize() { close(); // Unregister the Asset from the global list here before it is destructed and while its vtable - // pointer still points to this concrete type. b/31113965 + // pointer still points to this concrete type. unregisterAsset(this); } @@ -1136,7 +1136,7 @@ public boolean isNinePatch() { mFd = -1; // Register the Asset with the global list here after it is fully constructed and its - // vtable pointer points to this concrete type. b/31113965 + // vtable pointer points to this concrete type. registerAsset(this); } @@ -1167,7 +1167,7 @@ protected void finalize() { close(); // Unregister the Asset from the global list here before it is destructed and while its vtable - // pointer still points to this concrete type. b/31113965 + // pointer still points to this concrete type. unregisterAsset(this); } diff --git a/resources/src/main/java/org/robolectric/res/android/Chunk.java b/resources/src/main/java/org/robolectric/res/android/Chunk.java index 823312ceaad..9318af9e74b 100644 --- a/resources/src/main/java/org/robolectric/res/android/Chunk.java +++ b/resources/src/main/java/org/robolectric/res/android/Chunk.java @@ -166,7 +166,6 @@ Chunk Next() { return new Chunk(this_chunk); } - // TODO(b/111401637) remove this and have full resource file verification // Returns false if there was an error. For legacy purposes. boolean VerifyNextChunkNonFatal() { if (len_ < ResChunk_header.SIZEOF) { diff --git a/resources/src/main/java/org/robolectric/res/android/CppAssetManager.java b/resources/src/main/java/org/robolectric/res/android/CppAssetManager.java index 30229121257..21e5fb15394 100644 --- a/resources/src/main/java/org/robolectric/res/android/CppAssetManager.java +++ b/resources/src/main/java/org/robolectric/res/android/CppAssetManager.java @@ -51,8 +51,8 @@ enum FileType { kFileTypeSocket, } - - // transliterated from https://cs.corp.google.com/android/frameworks/base/libs/androidfw/include/androidfw/AssetManager.h + // transliterated from + // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/androidfw/include/androidfw/AssetManager.h private static class asset_path { // asset_path() : path(""), type(kFileTypeRegular), idmap(""), // isSystemOverlay(false), isSystemAsset(false) {} diff --git a/resources/src/main/java/org/robolectric/res/android/FileMap.java b/resources/src/main/java/org/robolectric/res/android/FileMap.java index 09fb4e4132a..fa4fecfd523 100644 --- a/resources/src/main/java/org/robolectric/res/android/FileMap.java +++ b/resources/src/main/java/org/robolectric/res/android/FileMap.java @@ -239,7 +239,7 @@ static ImmutableMap guessDataOffsets(File zipFile, int length) { while (true) { // Instead of trusting numRecords, read until we find the // end-of-central-directory signature. numRecords may wrap - // around with >64K entries (b/5455504). + // around with >64K entries. int sig = readInt(buffer, offset); if (sig == ENDSIG || sig == ENDSIG64) { break; diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java index 6a12b793048..02d570a3b9a 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java @@ -57,7 +57,8 @@ public boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityExceptio return true; } - // TODO(b/80130000): implementation copied from espresso's UIControllerImpl. Refactor code into common location + // TODO: implementation copied from espresso's UIControllerImpl. Refactor code into common + // location @Override public boolean injectString(String str) throws InjectEventSecurityException { checkNotNull(str); @@ -72,8 +73,7 @@ public boolean injectString(String str) throws InjectEventSecurityException { boolean eventInjected = false; KeyCharacterMap keyCharacterMap = getKeyCharacterMap(); - // TODO(b/80130875): Investigate why not use (as suggested in javadoc of - // keyCharacterMap.getEvents): + // TODO: Investigate why not use (as suggested in javadoc of keyCharacterMap.getEvents): // http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long, // java.lang.String, int, int) KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray()); diff --git a/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerRecreateTest.java b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerRecreateTest.java new file mode 100644 index 00000000000..0be5db7797a --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerRecreateTest.java @@ -0,0 +1,34 @@ +package org.robolectric.android.controller; + +import android.app.Activity; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; + +/** + * This test captures an issue where {@link ActivityController#recreate()} would throw an {@link + * UnsupportedOperationException} if an Activity from a previous test was recreated. + */ +@RunWith(AndroidJUnit4.class) +public class ActivityControllerRecreateTest { + private static final AtomicReference> createdActivity = + new AtomicReference<>(); + + @Before + public void setUp() { + createdActivity.compareAndSet(null, Robolectric.buildActivity(Activity.class).create()); + } + + @Test + public void failsTryingToRecreateActivityFromOtherTest1() { + createdActivity.get().recreate(); + } + + @Test + public void failsTryingToRecreateActivityFromOtherTest2() { + createdActivity.get().recreate(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java index 20931cc2be6..e8235b5f7ed 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java @@ -4,7 +4,6 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.junit.Assert.assertThrows; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; @@ -69,18 +68,16 @@ public void startBugreport_noPermission() throws Exception { BugreportCallback callback = mock(BugreportCallback.class); shadowBugreportManager.setHasPermission(false); - // TODO(b/179958637) switch to assertThrows once ThrowingRunnable no longer causes a test - // instantiation failure. - try { - shadowBugreportManager.startBugreport( - createWriteFile("bugreport"), - createWriteFile("screenshot"), - new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL), - directExecutor(), - callback); - fail("Expected SecurityException"); - } catch (SecurityException expected) { - } + assertThrows( + SecurityException.class, + () -> { + shadowBugreportManager.startBugreport( + createWriteFile("bugreport"), + createWriteFile("screenshot"), + new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL), + directExecutor(), + callback); + }); shadowMainLooper().idle(); assertThat(shadowBugreportManager.isBugreportInProgress()).isFalse(); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java index db6aaa572ae..43f0f0363dc 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java @@ -2,6 +2,8 @@ import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.O; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; import static com.google.common.truth.Truth.assertThat; import android.os.Build; @@ -56,6 +58,13 @@ public void setVersionIncremental() { assertThat(VERSION.INCREMENTAL).isEqualTo("robo_incremental"); } + @Test + @Config(minSdk = S) + public void setVersionMediaPerformanceClass() { + ShadowBuild.setVersionMediaPerformanceClass(R); + assertThat(VERSION.MEDIA_PERFORMANCE_CLASS).isEqualTo(R); + } + @Test @Config(minSdk = M) public void setVersionSecurityPatch() { diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowIconTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowIconTest.java index d317ba92210..b1f6676eb1a 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowIconTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowIconTest.java @@ -2,11 +2,18 @@ import static android.os.Build.VERSION_CODES.M; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.robolectric.Shadows.shadowOf; +import android.content.Context; import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -16,16 +23,30 @@ @RunWith(AndroidJUnit4.class) @Config(minSdk = M) public class ShadowIconTest { + + private static final int MSG_ICON_LOADED = 312; + public static final int TYPE_BITMAP = 1; public static final int TYPE_RESOURCE = 2; public static final int TYPE_DATA = 3; public static final int TYPE_URI = 4; + @Nullable private Drawable loadedDrawable; + + private final Context appContext = ApplicationProvider.getApplicationContext(); + private final Handler mainHandler = + new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_ICON_LOADED) { + loadedDrawable = (Drawable) msg.obj; + } + } + }; + @Test public void testGetRes() { - Icon icon = - Icon.createWithResource( - ApplicationProvider.getApplicationContext(), android.R.drawable.ic_delete); + Icon icon = Icon.createWithResource(appContext, android.R.drawable.ic_delete); assertThat(shadowOf(icon).getType()).isEqualTo(TYPE_RESOURCE); assertThat(shadowOf(icon).getResId()).isEqualTo(android.R.drawable.ic_delete); } @@ -55,4 +76,32 @@ public void testGetUri() { assertThat(shadowOf(icon).getType()).isEqualTo(TYPE_URI); assertThat(shadowOf(icon).getUri()).isEqualTo(uri); } + + @Test + public void testLoadDrawableAsyncWithMessage() { + ShadowIcon.overrideExecutor(directExecutor()); + + Icon icon = Icon.createWithResource(appContext, android.R.drawable.ic_delete); + + Message andThen = Message.obtain(mainHandler, MSG_ICON_LOADED); + + icon.loadDrawableAsync(appContext, andThen); + ShadowLooper.idleMainLooper(); + + assertThat(shadowOf(loadedDrawable).getCreatedFromResId()) + .isEqualTo(android.R.drawable.ic_delete); + } + + @Test + public void testLoadDrawableAsyncWithListener() { + ShadowIcon.overrideExecutor(directExecutor()); + + Icon icon = Icon.createWithResource(appContext, android.R.drawable.ic_delete); + + icon.loadDrawableAsync(appContext, drawable -> this.loadedDrawable = drawable, mainHandler); + ShadowLooper.idleMainLooper(); + + assertThat(shadowOf(loadedDrawable).getCreatedFromResId()) + .isEqualTo(android.R.drawable.ic_delete); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyMessageQueueTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyMessageQueueTest.java index 399827597da..89892fa863f 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyMessageQueueTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyMessageQueueTest.java @@ -204,7 +204,7 @@ public void postAndRemoveSyncBarrierToken() { } @Test - // TODO(b/74402484): enable once workaround is removed + // TODO(https://github.com/robolectric/robolectric/issues/6852): enable once workaround is removed @Ignore public void removeInvalidSyncBarrierToken() { try { diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java index fa0f96c0b1e..c71a735c6db 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java @@ -82,7 +82,7 @@ private static URL[] getClassPathUrls(ClassLoader classloader) { return parseJavaClassPath(); } - // TODO(b/65488446): Use a public API once one is available. + // TODO(https://github.com/google/guava/issues/2956): Use a public API once one is available. private static URL[] parseJavaClassPath() { ImmutableList.Builder urls = ImmutableList.builder(); for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) { diff --git a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java index 1105f5d39f3..b58df28e72c 100644 --- a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java +++ b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java @@ -7,7 +7,6 @@ import static org.robolectric.util.reflector.Reflector.reflector; import android.app.Activity; -import android.app.ActivityThread; import android.app.Application; import android.app.Instrumentation; import android.content.ComponentName; @@ -19,8 +18,6 @@ import android.os.Bundle; import android.view.ViewRootImpl; import android.view.WindowManager; -import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; -import androidx.test.runner.lifecycle.Stage; import javax.annotation.Nullable; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadow.api.Shadow; @@ -49,7 +46,19 @@ public class ActivityController extends ComponentController, T> { + enum LifecycleState { + INITIAL, + CREATED, + RESTARTED, + STARTED, + RESUMED, + PAUSED, + STOPPED, + DESTROYED + } + private _Activity_ _component_; + private LifecycleState currentState = LifecycleState.INITIAL; public static ActivityController of( T activity, Intent intent, @Nullable Bundle activityOptions) { @@ -110,7 +119,11 @@ private ActivityInfo getActivityInfo(Application application) { } public ActivityController create(@Nullable final Bundle bundle) { - shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle)); + shadowMainLooper.runPaused( + () -> { + getInstrumentation().callActivityOnCreate(component, bundle); + currentState = LifecycleState.CREATED; + }); return this; } @@ -119,11 +132,15 @@ public ActivityController create(@Nullable final Bundle bundle) { } public ActivityController restart() { - if (RuntimeEnvironment.getApiLevel() <= O_MR1) { - invokeWhilePaused(() -> _component_.performRestart()); - } else { - invokeWhilePaused(() -> _component_.performRestart(true, "restart()")); - } + invokeWhilePaused( + () -> { + if (RuntimeEnvironment.getApiLevel() <= O_MR1) { + _component_.performRestart(); + } else { + _component_.performRestart(true, "restart()"); + } + currentState = LifecycleState.RESTARTED; + }); return this; } @@ -131,11 +148,15 @@ public ActivityController start() { // Start and stop are tricky cases. Unlike other lifecycle methods such as // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite. - if (RuntimeEnvironment.getApiLevel() <= O_MR1) { - invokeWhilePaused(() -> _component_.performStart()); - } else { - invokeWhilePaused(() -> _component_.performStart("start()")); - } + invokeWhilePaused( + () -> { + if (RuntimeEnvironment.getApiLevel() <= O_MR1) { + _component_.performStart(); + } else { + _component_.performStart("start()"); + } + currentState = LifecycleState.STARTED; + }); return this; } @@ -151,11 +172,15 @@ public ActivityController postCreate(@Nullable Bundle bundle) { } public ActivityController resume() { - if (RuntimeEnvironment.getApiLevel() <= O_MR1) { - invokeWhilePaused(() -> _component_.performResume()); - } else { - invokeWhilePaused(() -> _component_.performResume(true, "resume()")); - } + invokeWhilePaused( + () -> { + if (RuntimeEnvironment.getApiLevel() <= O_MR1) { + _component_.performResume(); + } else { + _component_.performResume(true, "resume()"); + } + currentState = LifecycleState.RESUMED; + }); return this; } @@ -215,7 +240,11 @@ public ActivityController userLeaving() { } public ActivityController pause() { - shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component)); + shadowMainLooper.runPaused( + () -> { + getInstrumentation().callActivityOnPause(component); + currentState = LifecycleState.PAUSED; + }); return this; } @@ -229,13 +258,17 @@ public ActivityController stop() { // Stop and start are tricky cases. Unlike other lifecycle methods such as // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite. - if (RuntimeEnvironment.getApiLevel() <= M) { - invokeWhilePaused(() -> _component_.performStop()); - } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) { - invokeWhilePaused(() -> _component_.performStop(true)); - } else { - invokeWhilePaused(() -> _component_.performStop(true, "stop()")); - } + invokeWhilePaused( + () -> { + if (RuntimeEnvironment.getApiLevel() <= M) { + _component_.performStop(); + } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) { + _component_.performStop(true); + } else { + _component_.performStop(true, "stop()"); + } + currentState = LifecycleState.STOPPED; + }); return this; } @@ -245,6 +278,7 @@ public ActivityController destroy() { () -> { getInstrumentation().callActivityOnDestroy(component); makeActivityEligibleForGc(); + currentState = LifecycleState.DESTROYED; }); return this; } @@ -434,11 +468,11 @@ public ActivityController configurationChange(final Configuration newConfigur */ @SuppressWarnings("unchecked") public ActivityController recreate() { - Stage originalStage = - ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(component); - switch (originalStage) { - case PRE_ON_CREATE: + LifecycleState originalState = currentState; + + switch (originalState) { + case INITIAL: create(); // fall through case CREATED: @@ -489,7 +523,7 @@ public ActivityController recreate() { // Move back to the original stage. If the original stage was transient stage, it will bring it // to resumed state to match the on device behavior. - switch (originalStage) { + switch (originalState) { case PAUSED: pause(); return this; @@ -502,8 +536,9 @@ public ActivityController recreate() { } } - private static Instrumentation getInstrumentation() { - return ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation(); + // Get the Instrumentation object scoped to the Activity. + private Instrumentation getInstrumentation() { + return _component_.getInstrumentation(); } /** Accessor interface for android.app.Activity.NonConfigurationInstances's internals. */ diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java index a859b18ec75..ea00874b1e5 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.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.O; +import static android.os.Build.VERSION_CODES.S; import static org.robolectric.util.reflector.Reflector.reflector; import android.annotation.TargetApi; @@ -110,6 +111,18 @@ public static void setVersionIncremental(String versionIncremental) { ReflectionHelpers.setStaticField(Build.VERSION.class, "INCREMENTAL", versionIncremental); } + /** + * Sets the value of the {@link Build.VERSION#MEDIA_PERFORMANCE_CLASS} field. Available in Android + * S+. + * + *

It will be reset for the next test. + */ + @TargetApi(S) + public static void setVersionMediaPerformanceClass(int performanceClass) { + ReflectionHelpers.setStaticField( + Build.VERSION.class, "MEDIA_PERFORMANCE_CLASS", performanceClass); + } + /** * Sets the value of the {@link Build.VERSION#RELEASE} field. * diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowIcon.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowIcon.java index df8ced15c9f..72aa2fe321f 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowIcon.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowIcon.java @@ -3,9 +3,16 @@ import static android.os.Build.VERSION_CODES.M; import static org.robolectric.util.reflector.Reflector.reflector; +import android.content.Context; import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.graphics.drawable.Icon.OnDrawableLoadedListener; import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import androidx.annotation.Nullable; +import java.util.concurrent.Executor; import org.robolectric.annotation.HiddenApi; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -17,6 +24,13 @@ @Implements(value = Icon.class, minSdk = M) public class ShadowIcon { + @Nullable private static Executor executorOverride; + + /** Set the executor where async drawable loading will run. */ + public static void overrideExecutor(Executor executor) { + executorOverride = executor; + } + @RealObject private Icon realIcon; @HiddenApi @@ -61,6 +75,33 @@ public byte[] getDataBytes() { return reflector(IconReflector.class, realIcon).getDataBytes(); } + @Implementation + protected void loadDrawableAsync(Context context, Message andThen) { + if (executorOverride != null) { + executorOverride.execute( + () -> { + andThen.obj = realIcon.loadDrawable(context); + andThen.sendToTarget(); + }); + } else { + reflector(IconReflector.class, realIcon).loadDrawableAsync(context, andThen); + } + } + + @Implementation + protected void loadDrawableAsync( + Context context, final OnDrawableLoadedListener listener, Handler handler) { + if (executorOverride != null) { + executorOverride.execute( + () -> { + Drawable result = realIcon.loadDrawable(context); + handler.post(() -> listener.onDrawableLoaded(result)); + }); + } else { + reflector(IconReflector.class, realIcon).loadDrawableAsync(context, listener, handler); + } + } + @ForType(Icon.class) interface IconReflector { @@ -84,5 +125,12 @@ interface IconReflector { @Direct byte[] getDataBytes(); + + @Direct + void loadDrawableAsync(Context context, Message andThen); + + @Direct + void loadDrawableAsync( + Context context, final OnDrawableLoadedListener listener, Handler handler); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessageQueue.java index f60f1b4c9e2..9934fb4ef9b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessageQueue.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessageQueue.java @@ -166,7 +166,8 @@ private static void dispatchMessage(Message msg) { @Implementation @HiddenApi protected void removeSyncBarrier(int token) { - // TODO(b/74402484): workaround scheduler corruption of message queue + // TODO(https://github.com/robolectric/robolectric/issues/6852): workaround scheduler corruption + // of message queue try { reflector(MessageQueueReflector.class, realQueue).removeSyncBarrier(token); } catch (IllegalStateException e) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java index c0e3e930403..5b11bb9d8a3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java @@ -89,7 +89,7 @@ public Parcelable.Creator readParcelableCreator(ClassLoader loader) { // classloader" behavior. ClassLoader parcelableClassLoader = (loader == null ? getClass().getClassLoader() : loader); // Avoid initializing the Parcelable class until we know it implements - // Parcelable and has the necessary CREATOR field. http://b/1171613. + // Parcelable and has the necessary CREATOR field. Class parcelableClass = Class.forName(name, false /* initialize */, parcelableClassLoader); if (!Parcelable.class.isAssignableFrom(parcelableClass)) { throw new BadParcelableException( diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteOpenHelper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteOpenHelper.java index 8621359d112..a3208e034ae 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteOpenHelper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteOpenHelper.java @@ -15,6 +15,6 @@ public class ShadowSQLiteOpenHelper { @Implementation(minSdk = O_MR1) protected void setIdleConnectionTimeout(long idleConnectionTimeoutMs) { // Calling the real one currently results in a Robolectric deadlock. Just ignore it. - // See b/78464547 . + // See https://github.com/robolectric/robolectric/issues/6853. } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java index e628ecb25fe..d1b965256b0 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java @@ -918,7 +918,7 @@ public void setSignalStrength(SignalStrength signalStrength) { /** * Cribbed from {@link android.telephony.PhoneNumberUtils#isEmergencyNumberInternal}. * - *

TODO(b/122324733) need better implementation + *

TODO: need better implementation */ @Implementation(minSdk = Build.VERSION_CODES.Q) protected boolean isEmergencyNumber(String number) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimePickerDialog.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimePickerDialog.java index 4a4c874d255..0c06e8927c6 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimePickerDialog.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimePickerDialog.java @@ -3,13 +3,9 @@ import static org.robolectric.util.reflector.Reflector.reflector; import android.app.TimePickerDialog; -import android.content.Context; import android.widget.TimePicker; -import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; @@ -18,24 +14,6 @@ public class ShadowTimePickerDialog extends ShadowAlertDialog { @RealObject protected TimePickerDialog realTimePickerDialog; - @Implementation - protected void __constructor__( - Context context, - int theme, - TimePickerDialog.OnTimeSetListener callBack, - int hourOfDay, - int minute, - boolean is24HourView) { - - Shadow.invokeConstructor(TimePickerDialog.class, realTimePickerDialog, - ClassParameter.from(Context.class, context), - ClassParameter.from(int.class, theme), - ClassParameter.from(TimePickerDialog.OnTimeSetListener.class, callBack), - ClassParameter.from(int.class, hourOfDay), - ClassParameter.from(int.class, minute), - ClassParameter.from(boolean.class, is24HourView)); - } - public int getHourOfDay() { return reflector(TimePickerDialogProvider.class, realTimePickerDialog) .getTimePicker() diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java b/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java index 450782f8c35..e327bcec8a5 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java @@ -372,4 +372,7 @@ void setVoiceInteractor( @Accessor("mConfigChangeFlags") void setConfigChangeFlags(int value); + + @Accessor("mInstrumentation") + Instrumentation getInstrumentation(); }