aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-09-07 01:15:44 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-09-07 01:15:44 +0000
commit4f883d02ffa29881b6a439d17540374838469702 (patch)
tree9e8035c4d2478e788183dff96c4861e8594a6097
parent724a4990bf96018acc8ea8be7ce03f9bf2230183 (diff)
parentb07ae071ca91900fc506ed287803a7df8b07a5c6 (diff)
downloadrobolectric-android14-d2-release.tar.gz
Change-Id: Ida5ef5d7ab7f84a235b8e2ea185de24f5072b4fb
-rw-r--r--.github/workflows/check_code_formatting.yml2
-rw-r--r--.github/workflows/gradle_tasks_validation.yml23
-rw-r--r--.github/workflows/gradle_wrapper_validation.yml2
-rw-r--r--.github/workflows/graphics_tests.yml5
-rw-r--r--.github/workflows/tests.yml6
-rw-r--r--.github/workflows/validate_commit_message.yml42
-rw-r--r--Android.bp40
-rw-r--r--METADATA15
-rw-r--r--README.md14
-rw-r--r--annotations/src/main/java/org/robolectric/annotation/LooperMode.java14
-rw-r--r--buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy13
-rw-r--r--buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy13
-rw-r--r--gradle/libs.versions.toml32
-rw-r--r--integration_tests/androidx/build.gradle1
-rw-r--r--integration_tests/androidx_test/build.gradle1
-rw-r--r--integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml (renamed from integration_tests/androidx_test/src/test/AndroidManifest.xml)13
-rw-r--r--integration_tests/androidx_test/src/test/AndroidManifest-ActivityTestRule.xml20
-rw-r--r--integration_tests/androidx_test/src/test/AndroidManifest-Intents.xml20
-rw-r--r--integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java57
-rw-r--r--integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/CryptoObjectTest.java65
-rw-r--r--integration_tests/compat-target28/build.gradle4
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java13
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/graphics/ColorTest.java (renamed from robolectric/src/test/java/org/robolectric/shadows/ShadowColorTest.java)27
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/os/LooperTest.java26
-rw-r--r--integration_tests/kotlin/build.gradle7
-rw-r--r--integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java14
-rw-r--r--integration_tests/mockito-kotlin/build.gradle9
-rw-r--r--integration_tests/nativegraphics/config/robolectric.properties15
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java48
-rw-r--r--integration_tests/sparsearray/build.gradle12
-rw-r--r--nativeruntime/src/test/resources/resources.ap_bin555 -> 555 bytes
-rw-r--r--plugins/maven-dependency-resolver/build.gradle22
-rw-r--r--plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java2
-rw-r--r--preinstrumented/build.gradle3
-rw-r--r--preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java89
-rw-r--r--preinstrumented/src/test/java/org/robolectric/preinstrumented/JarInstrumentorTest.java71
-rw-r--r--processor/Android.bp1
-rw-r--r--processor/build.gradle1
-rw-r--r--processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java20
-rw-r--r--resources/src/main/java/org/robolectric/res/android/ResTable.java17
-rw-r--r--resources/src/main/java/org/robolectric/res/android/ResourceTypes.java28
-rw-r--r--robolectric/Android.bp1
-rw-r--r--robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java4
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java89
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java3
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java135
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/NoOpThreadChecker.java16
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java141
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/RobolectricThreadChecker.java38
-rw-r--r--robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java3
-rw-r--r--robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker2
-rw-r--r--robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java25
-rw-r--r--robolectric/src/test/java/org/robolectric/R.java26
-rw-r--r--robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java6
-rw-r--r--robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java119
-rw-r--r--robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentApplicationInfoTest.java24
-rw-r--r--robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java47
-rw-r--r--robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java8
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java21
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java29
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilderTest.java122
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java7
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ServiceStateBuilderTest.java126
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataInputTest.java188
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataOutputTest.java103
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java82
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java10
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java6
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java26
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java215
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java4
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java34
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java25
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowImageDecoderTest.java76
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java81
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java4
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java111
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java45
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java5
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java29
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java49
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java22
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java10
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java32
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java90
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java15
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java155
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java8
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowStatsManagerTest.java80
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java44
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java90
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java94
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java2
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java13
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java78
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java32
-rw-r--r--robolectric/src/test/resources/res/values/attrs.xml1
-rw-r--r--robolectric/src/test/resources/resources.ap_bin304795 -> 304864 bytes
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java18
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java137
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java17
-rw-r--r--sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java18
-rw-r--r--sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java158
-rw-r--r--sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java218
-rw-r--r--shadows/framework/Android.bp1
-rw-r--r--shadows/framework/build.gradle6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java1
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java61
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/BackupDataEntity.java58
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/BackupDataInputBuilder.java46
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/BackupDataOutputFactory.java48
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java31
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java63
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilder.java138
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/PhoneCapabilityFactory.java26
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ServiceStateBuilder.java141
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java21
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataInput.java107
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataOutput.java109
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java40
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java46
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java9
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java15
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java16
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java12
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java198
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCryptoObject.java31
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java5
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java19
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java44
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java20
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java67
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java40
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java11
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java19
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java14
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java53
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java8
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java38
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java18
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java15
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java14
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java18
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java31
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java5
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java32
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java11
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java120
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java107
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java20
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java4
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java123
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java17
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java34
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowStatsManager.java44
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java27
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java53
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java106
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java4
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java98
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java129
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java24
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java18
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java11
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java121
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java50
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java4
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java77
-rw-r--r--shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java5
-rw-r--r--utils/build.gradle24
-rw-r--r--utils/src/main/java/org/robolectric/util/Util.java7
198 files changed, 6412 insertions, 906 deletions
diff --git a/.github/workflows/check_code_formatting.yml b/.github/workflows/check_code_formatting.yml
index e99374582..205901db6 100644
--- a/.github/workflows/check_code_formatting.yml
+++ b/.github/workflows/check_code_formatting.yml
@@ -16,7 +16,7 @@ permissions:
jobs:
check_code_formatting:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml
index 9762b9e51..2494a8355 100644
--- a/.github/workflows/gradle_tasks_validation.yml
+++ b/.github/workflows/gradle_tasks_validation.yml
@@ -16,7 +16,7 @@ permissions:
jobs:
run_checkForApiChanges:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
@@ -33,7 +33,7 @@ jobs:
run: ./gradlew checkForApiChanges
run_aggregateDocs:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
@@ -49,8 +49,25 @@ jobs:
- name: Run aggregateDocs
run: ./gradlew clean aggregateDocs
+ run_javadocJar:
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: 11
+
+ - uses: gradle/gradle-build-action@v2
+
+ - name: Run javadocJar
+ run: ./gradlew clean javadocJar
+
run_instrumentAll:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/gradle_wrapper_validation.yml b/.github/workflows/gradle_wrapper_validation.yml
index b7e100892..04388fcec 100644
--- a/.github/workflows/gradle_wrapper_validation.yml
+++ b/.github/workflows/gradle_wrapper_validation.yml
@@ -17,7 +17,7 @@ permissions:
jobs:
validation:
name: Validation
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
diff --git a/.github/workflows/graphics_tests.yml b/.github/workflows/graphics_tests.yml
index e0d385a16..f2d27c84a 100644
--- a/.github/workflows/graphics_tests.yml
+++ b/.github/workflows/graphics_tests.yml
@@ -16,7 +16,10 @@ permissions:
jobs:
graphics_tests:
- runs-on: self-hosted
+ strategy:
+ matrix:
+ device: [ macos-12, ubuntu-22.04, self-hosted ]
+ runs-on: ${{ matrix.device }}
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b47f37522..7d6c33f88 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -19,7 +19,7 @@ env:
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
@@ -38,7 +38,7 @@ jobs:
./gradlew clean assemble testClasses --parallel --stacktrace --no-watch-fs
unit-tests:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
needs: build
strategy:
fail-fast: false
@@ -149,7 +149,7 @@ jobs:
**/build/outputs/*/connected/*
publish-to-snapshots:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
env:
SONATYPE_LOGIN: ${{ secrets.SONATYPE_LOGIN }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
diff --git a/.github/workflows/validate_commit_message.yml b/.github/workflows/validate_commit_message.yml
new file mode 100644
index 000000000..1851822bd
--- /dev/null
+++ b/.github/workflows/validate_commit_message.yml
@@ -0,0 +1,42 @@
+name: Validate commit message
+
+on:
+ pull_request:
+ branches: [ master, google ]
+
+permissions:
+ contents: read
+
+jobs:
+ validate_commit_message:
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+
+ - name: Validate commit title
+ run: |
+ # Check that the commit title isn't excessively long.
+ commit_title="$(git log -1 --pretty=format:'%s')"
+ if [ "${#commit_title}" -gt 120 ]; then
+ echo "Error: The title of commit is too long"
+ exit 1
+ fi
+
+ lowercase_title="$(echo $commit_title | awk '{print tolower($0)}')"
+ # Check that the commit title isn't 'internal' (ignore case)
+ if [ "$lowercase_title" = "internal" ]; then
+ echo "Error: '$commit_title' is not a valid commit title"
+ exit 1
+ fi
+
+ - name: Validate commit body
+ run: |
+ # Check that the commit has a body
+ commit_body="$(git log -1 --pretty=format:'%b' | grep -v 'PiperOrigin-RevId')"
+ if [ -z "$commit_body" ]; then
+ echo "Error: The commit message should have a descriptive body"
+ exit 1
+ fi
diff --git a/Android.bp b/Android.bp
index 135e7c151..a3e54dced 100644
--- a/Android.bp
+++ b/Android.bp
@@ -16,6 +16,8 @@ package {
default_visibility: [
"//external/robolectric:__subpackages__",
"//test/robolectric-extensions:__subpackages__",
+ "//frameworks/base/packages/SettingsLib/tests/robotests:__subpackages__",
+ "//packages/apps/Settings/tests/robotests:__subpackages__"
],
default_applicable_licenses: ["external_robolectric_license"],
}
@@ -60,8 +62,9 @@ robolectric_build_props {
name: "robolectric_build_props_upstream",
}
-java_genrule_host {
+java_genrule {
name: "robolectric_framework_res_upstream",
+ host_supported: true,
tools: ["zip2zip"],
srcs: [":framework-res"],
out: ["robolectric_framework_res_upstream.jar"],
@@ -118,46 +121,16 @@ java_library_host {
":__subpackages__",
"//prebuilts/misc/common/robolectric",
"//test/robolectric-extensions:__subpackages__",
+ "//frameworks/base/packages/SettingsLib/tests/robotests:__subpackages__",
],
}
-//#############################################
-// Assemble Robolectric_all
-//#############################################
-
-// This is a hack and should be removed with proper resource merging a la maven-shaded-plugin
-//
-// In order to use AndroidXTest APIs in Robolectric (e.g. ActivityScenario), it
-// is necessary to define the service metadata at the top-level. The classes
-// themselves (e.g. LocalUiController) do not use `@AutoService` at the moment.
-// When they are migrated to `@AutoService` the AndroidXTest service metadata
-// can be removed.
-java_genrule_host {
- name: "robolectric_meta_service_file",
- out: ["robolectric_meta_service_file.jar"],
- tools: ["soong_zip"],
- cmd: "mkdir -p $(genDir)/META-INF/services/ && " +
- "echo -e 'org.robolectric.Shadows\norg.robolectric.shadows.httpclient.Shadows\norg.robolectric.shadows.multidex.Shadows' > " +
- "$(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider &&" +
- "echo org.robolectric.android.internal.LocalUiController > " +
- "$(genDir)/META-INF/services/androidx.test.platform.ui.UiController &&" +
- "echo org.robolectric.android.internal.LocalActivityInvoker > " +
- "$(genDir)/META-INF/services/androidx.test.internal.platform.app.ActivityInvoker &&" +
- "echo org.robolectric.android.internal.LocalPermissionGranter > " +
- "$(genDir)/META-INF/services/androidx.test.internal.platform.content.PermissionGranter &&" +
- "echo org.robolectric.android.internal.NoOpThreadChecker > " +
- "$(genDir)/META-INF/services/androidx.test.internal.platform.ThreadChecker &&" +
- "echo org.robolectric.android.internal.LocalControlledLooper > " +
- "$(genDir)/META-INF/services/androidx.test.internal.platform.os.ControlledLooper &&" +
- "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)/META-INF/services/",
-}
java_library_host {
name: "Robolectric_all_upstream",
static_libs: [
"Robolectric-aosp-plugins",
- "robolectric_meta_service_file",
"Robolectric_shadows_httpclient_upstream",
"Robolectric_shadows_framework_upstream",
"Robolectric_shadows_multidex_upstream",
@@ -209,12 +182,13 @@ java_host_for_device {
"//external/mobile-data-download/javatests:__pkg__",
"//frameworks/base/services/robotests:__pkg__",
"//frameworks/base/services/robotests/backup:__pkg__",
- "//frameworks/base/packages/SettingsLib/tests/robotests:__pkg__",
+ "//frameworks/base/packages/SettingsLib/tests/robotests:__subpackages__",
"//frameworks/base/packages/SystemUI:__pkg__",
"//frameworks/opt/car/setupwizard/library/main/tests/robotests:__pkg__",
"//frameworks/opt/localepicker/tests:__pkg__",
"//frameworks/opt/wear/signaldetector/robotests:__pkg__",
"//frameworks/opt/wear/robotests:__pkg__",
+ "//packages/modules/Bluetooth/service:__pkg__",
"//packages/modules/Connectivity/nearby/tests/multidevices/clients/test_support/snippet_helper/tests:__pkg__",
"//packages/modules/Connectivity/nearby/tests/robotests:__pkg__",
"//packages/modules/DeviceLock/DeviceLockController/tests/robolectric:__pkg__",
diff --git a/METADATA b/METADATA
new file mode 100644
index 000000000..43c94c633
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,15 @@
+name: "robolectric"
+description: "Android Unit Testing Framework"
+third_party {
+ url {
+ type: GIT
+ value: "https://github.com/robolectric/robolectric"
+ }
+ version: "68ec9953ea0ac7c47588db540145a728a1f00ba8"
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2023
+ month: 5
+ day: 24
+ }
+}
diff --git a/README.md b/README.md
index b183647e0..2efdf4e88 100644
--- a/README.md
+++ b/README.md
@@ -79,3 +79,17 @@ Run compatibility test suites on opening Emulator:
./gradlew connectedCheck
+### Using Snapshots
+
+If you would like to live on the bleeding edge, you can try running against a snapshot build. Keep in mind that snapshots represent the most recent changes on master and may contain bugs.
+
+#### build.gradle:
+
+```groovy
+repositories {
+ maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
+}
+dependencies {
+ testImplementation "org.robolectric:robolectric:4.11-SNAPSHOT"
+}
+```
diff --git a/annotations/src/main/java/org/robolectric/annotation/LooperMode.java b/annotations/src/main/java/org/robolectric/annotation/LooperMode.java
index 264d6cc9d..5849daab5 100644
--- a/annotations/src/main/java/org/robolectric/annotation/LooperMode.java
+++ b/annotations/src/main/java/org/robolectric/annotation/LooperMode.java
@@ -118,6 +118,20 @@ public @interface LooperMode {
PAUSED,
/**
+ * A mode that simulates an android instrumentation test threading model, which has a separate
+ * test thread distinct from the main looper thread.
+ *
+ * <p>Otherwise it is quite similar to PAUSED mode. The clock time is still fixed, and you can
+ * use shadowLooper methods to pause, unpause, and wait for any looper to be idle.
+ *
+ * <p>It is recommended to use this mode in tests that mostly use androidx.test APIs, which will
+ * support being called directly on the main thread or on the test thread. Most org.robolectric
+ * APIs that interact with the android UI (e.g. ActivityController) will raise an exception if
+ * called off the main thread.
+ */
+ INSTRUMENTATION_TEST,
+
+ /**
* Currently not supported.
*
* <p>In future, will have free running threads with an automatically increasing clock.
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy
index bcc79e34a..d65f71cba 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy
@@ -28,6 +28,19 @@ public class AndroidProjectConfigPlugin implements Plugin<Project> {
.findAll { k,v -> k.startsWith("robolectric.") }
.collect { k,v -> "-D$k=$v" }
jvmArgs = forwardedSystemProperties
+ jvmArgs += [
+ '--add-opens=java.base/java.lang=ALL-UNNAMED',
+ '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED',
+ '--add-opens=java.base/java.io=ALL-UNNAMED',
+ '--add-opens=java.base/java.net=ALL-UNNAMED',
+ '--add-opens=java.base/java.security=ALL-UNNAMED',
+ '--add-opens=java.base/java.text=ALL-UNNAMED',
+ '--add-opens=java.base/java.util=ALL-UNNAMED',
+ '--add-opens=java.desktop/java.awt.font=ALL-UNNAMED',
+ '--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
+ '--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED',
+ '--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
+ ]
doFirst {
if (!forwardedSystemProperties.isEmpty()) {
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
index deb97c994..adffa3810 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
@@ -71,6 +71,19 @@ class RoboJavaModulePlugin implements Plugin<Project> {
.findAll { k,v -> k.startsWith("robolectric.") }
.collect { k,v -> "-D$k=$v" }
jvmArgs = forwardedSystemProperties
+ jvmArgs += [
+ '--add-opens=java.base/java.lang=ALL-UNNAMED',
+ '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED',
+ '--add-opens=java.base/java.io=ALL-UNNAMED',
+ '--add-opens=java.base/java.net=ALL-UNNAMED',
+ '--add-opens=java.base/java.security=ALL-UNNAMED',
+ '--add-opens=java.base/java.text=ALL-UNNAMED',
+ '--add-opens=java.base/java.util=ALL-UNNAMED',
+ '--add-opens=java.desktop/java.awt.font=ALL-UNNAMED',
+ '--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
+ '--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED',
+ '--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
+ ]
doFirst {
if (!forwardedSystemProperties.isEmpty()) {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7b43dfa1f..46bd35ef9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,7 +9,7 @@ android-gradle = "7.4.2"
conscrypt = "2.5.2"
# https://github.com/bcgit/bc-java/tags
-bouncycastle = "1.73"
+bouncycastle = "1.76"
# https://github.com/findbugsproject/findbugs/tags
findbugs-jsr305 = "3.0.2"
@@ -21,17 +21,20 @@ hamcrest = "2.0.0.0"
aggregate-javadocs-gradle = "3.0.1"
# https://github.com/google/error-prone/releases
-error-prone = "2.19.1"
+error-prone = "2.20.0"
error-prone-javac = "9+181-r4173-1"
# https://github.com/tbroyer/gradle-errorprone-plugin/releases
error-prone-gradle = "3.1.0"
# https://kotlinlang.org/docs/releases.html#release-details
-kotlin = "1.8.10"
+kotlin = "1.9.0"
+
+# https://github.com/Kotlin/kotlinx.coroutines/releases/
+kotlinx-coroutines = '1.7.3'
# https://github.com/diffplug/spotless/blob/main/CHANGES.md
-spotless-gradle = "6.18.0"
+spotless-gradle = "6.20.0"
# https://hc.apache.org/news.html
apache-http-core = "4.0.1"
@@ -41,23 +44,23 @@ apache-http-client = "4.0.3"
asm = "9.5"
# https://github.com/google/auto/releases
-auto-common = "1.2.1"
-auto-service = "1.0.1"
-auto-value = "1.10.1"
+auto-common = "1.2.2"
+auto-service = "1.1.1"
+auto-value = "1.10.2"
compile-testing = "0.21.0"
# https://github.com/google/guava/releases
-guava-jre = "31.1-jre"
+guava-jre = "32.0.1-jre"
# https://github.com/google/gson/releases
gson = "2.10.1"
# https://github.com/google/truth/releases
-truth = "1.1.3"
+truth = "1.1.5"
# https://github.com/unicode-org/icu/releases
-icu4j = "73.1"
+icu4j = "73.2"
jacoco = "0.8.10"
@@ -73,7 +76,7 @@ jetbrains-annotations = "24.0.1"
junit4 = "4.13.2"
# https://github.com/google/libphonenumber/releases
-libphonenumber = "8.13.11"
+libphonenumber = "8.13.17"
# https://github.com/mockito/mockito/releases
mockito = "4.11.0"
@@ -92,11 +95,12 @@ sqlite4java = "1.0.392"
# https://developer.android.com/jetpack/androidx/versions
androidx-annotation = "1.3.0"
androidx-appcompat = "1.6.1"
+androidx-biometric = "1.1.0"
androidx-constraintlayout = "2.1.4"
androidx-core = "1.10.1"
-androidx-fragment = "1.5.7"
+androidx-fragment = "1.6.1"
androidx-multidex = "2.0.1"
-androidx-window = "1.0.0"
+androidx-window = "1.1.0"
# https://github.com/android/android-test/tags
androidx-test-annotation = "1.0.1"
@@ -119,6 +123,7 @@ kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless-gradle" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines"}
auto-common = { module = "com.google.auto:auto-common", version.ref = "auto-common" }
auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "auto-service" }
@@ -195,6 +200,7 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
+androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "androidx-biometric" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }
androidx-fragment = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment" }
diff --git a/integration_tests/androidx/build.gradle b/integration_tests/androidx/build.gradle
index 10cc8c650..2eac30a44 100644
--- a/integration_tests/androidx/build.gradle
+++ b/integration_tests/androidx/build.gradle
@@ -26,6 +26,7 @@ android {
}
dependencies {
+ implementation libs.kotlinx.coroutines.android
implementation libs.androidx.appcompat
implementation libs.androidx.window
diff --git a/integration_tests/androidx_test/build.gradle b/integration_tests/androidx_test/build.gradle
index d07ef2ed6..e7dfe9304 100644
--- a/integration_tests/androidx_test/build.gradle
+++ b/integration_tests/androidx_test/build.gradle
@@ -55,6 +55,7 @@ dependencies {
testImplementation libs.androidx.test.espresso.core
testImplementation libs.androidx.test.ext.truth
testImplementation libs.androidx.test.core
+ testImplementation libs.androidx.biometric
testImplementation libs.androidx.fragment
testImplementation libs.androidx.fragment.testing
testImplementation libs.androidx.test.ext.junit
diff --git a/integration_tests/androidx_test/src/test/AndroidManifest.xml b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml
index e79dbdb80..7f2d38666 100644
--- a/integration_tests/androidx_test/src/test/AndroidManifest.xml
+++ b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml
@@ -9,16 +9,6 @@
<application>
<activity
- android:name="org.robolectric.integrationtests.axt.ActivityTestRuleTest$TranscriptActivity"
- android:exported="true"/>
- <activity
- android:name="org.robolectric.integrationtests.axt.EspressoActivity"
- android:exported="true"/>
- <activity
- android:name="org.robolectric.integrationtests.axt.EspressoScrollingActivity"
- android:exported="true"/>
-
- <activity
android:name="org.robolectric.integrationtests.axt.ActivityScenarioTest$LifecycleOwnerActivity"
android:exported="true"/>
<activity
@@ -27,9 +17,6 @@
<activity-alias
android:name="org.robolectric.integrationtests.axt.ActivityScenarioTestAlias"
android:targetActivity="org.robolectric.integrationtests.axt.ActivityScenarioTest$TranscriptActivity" />
- <activity
- android:name="org.robolectric.integrationtests.axt.IntentsTest$ResultCapturingActivity"
- android:exported = "true"/>
</application>
<instrumentation
diff --git a/integration_tests/androidx_test/src/test/AndroidManifest-ActivityTestRule.xml b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityTestRule.xml
new file mode 100644
index 000000000..a71754e6b
--- /dev/null
+++ b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityTestRule.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.robolectric.integrationtests.axt">
+
+ <uses-sdk
+ android:minSdkVersion="14"
+ android:targetSdkVersion="27"/>
+
+ <application>
+ <activity
+ android:name="org.robolectric.integrationtests.axt.ActivityTestRuleTest$TranscriptActivity"
+ android:exported="true"/>
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="org.robolectric.integration.axt"/>
+
+</manifest>
diff --git a/integration_tests/androidx_test/src/test/AndroidManifest-Intents.xml b/integration_tests/androidx_test/src/test/AndroidManifest-Intents.xml
new file mode 100644
index 000000000..86f9e36cf
--- /dev/null
+++ b/integration_tests/androidx_test/src/test/AndroidManifest-Intents.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.robolectric.integrationtests.axt">
+
+ <uses-sdk
+ android:minSdkVersion="14"
+ android:targetSdkVersion="27"/>
+
+ <application>
+ <activity
+ android:name="org.robolectric.integrationtests.axt.IntentsTest$ResultCapturingActivity"
+ android:exported = "true"/>
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="org.robolectric.integration.axt"/>
+
+</manifest>
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 acbd67a5f..72031be74 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
@@ -2,6 +2,7 @@ package org.robolectric.integrationtests.axt;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import android.app.Activity;
import android.app.UiAutomation;
@@ -106,18 +107,19 @@ public class ActivityScenarioTest {
@Test
public void launch_callbackSequence() {
- ActivityScenario<TranscriptActivity> activityScenario =
- ActivityScenario.launch(TranscriptActivity.class);
+ try (ActivityScenario<TranscriptActivity> activityScenario =
+ ActivityScenario.launch(TranscriptActivity.class)) {
assertThat(activityScenario).isNotNull();
assertThat(callbacks)
.containsExactly(
"onCreate", "onStart", "onPostCreate", "onResume", "onWindowFocusChanged true");
+ }
}
@Test
public void launch_pauseAndResume_callbackSequence() {
- ActivityScenario<TranscriptActivity> activityScenario =
- ActivityScenario.launch(TranscriptActivity.class);
+ try (ActivityScenario<TranscriptActivity> activityScenario =
+ ActivityScenario.launch(TranscriptActivity.class)) {
assertThat(activityScenario).isNotNull();
activityScenario.moveToState(State.STARTED);
activityScenario.moveToState(State.RESUMED);
@@ -125,12 +127,13 @@ public class ActivityScenarioTest {
.containsExactly(
"onCreate", "onStart", "onPostCreate", "onResume", "onWindowFocusChanged true",
"onPause", "onResume");
+ }
}
@Test
public void launch_stopAndResume_callbackSequence() {
- ActivityScenario<TranscriptActivity> activityScenario =
- ActivityScenario.launch(TranscriptActivity.class);
+ try (ActivityScenario<TranscriptActivity> activityScenario =
+ ActivityScenario.launch(TranscriptActivity.class)) {
assertThat(activityScenario).isNotNull();
activityScenario.moveToState(State.CREATED);
activityScenario.moveToState(State.RESUMED);
@@ -146,16 +149,17 @@ public class ActivityScenarioTest {
"onRestart",
"onStart",
"onResume");
+ }
}
@Test
public void launchAlias_createTargetAndCallbackSequence() {
Context context = ApplicationProvider.getApplicationContext();
- ActivityScenario<Activity> activityScenario =
+ try (ActivityScenario<Activity> activityScenario =
ActivityScenario.launch(
new Intent()
.setClassName(
- context, "org.robolectric.integrationtests.axt.ActivityScenarioTestAlias"));
+ context, "org.robolectric.integrationtests.axt.ActivityScenarioTestAlias"))) {
assertThat(activityScenario).isNotNull();
activityScenario.onActivity(
@@ -163,12 +167,13 @@ public class ActivityScenarioTest {
assertThat(callbacks)
.containsExactly(
"onCreate", "onStart", "onPostCreate", "onResume", "onWindowFocusChanged true");
+ }
}
@Test
public void launch_lifecycleOwnerActivity() {
- ActivityScenario<LifecycleOwnerActivity> activityScenario =
- ActivityScenario.launch(LifecycleOwnerActivity.class);
+ try (ActivityScenario<LifecycleOwnerActivity> activityScenario =
+ ActivityScenario.launch(LifecycleOwnerActivity.class)) {
assertThat(activityScenario).isNotNull();
activityScenario.onActivity(
activity -> assertThat(activity.getLifecycle().getCurrentState()).isEqualTo(State.RESUMED));
@@ -178,14 +183,15 @@ public class ActivityScenarioTest {
activityScenario.moveToState(State.CREATED);
activityScenario.onActivity(
activity -> assertThat(activity.getLifecycle().getCurrentState()).isEqualTo(State.CREATED));
+ }
}
@Test
public void recreate_retainFragmentHostingActivity() {
Fragment fragment = new Fragment();
fragment.setRetainInstance(true);
- ActivityScenario<LifecycleOwnerActivity> activityScenario =
- ActivityScenario.launch(LifecycleOwnerActivity.class);
+ try (ActivityScenario<LifecycleOwnerActivity> activityScenario =
+ ActivityScenario.launch(LifecycleOwnerActivity.class)) {
assertThat(activityScenario).isNotNull();
activityScenario.onActivity(
activity -> {
@@ -202,14 +208,15 @@ public class ActivityScenarioTest {
activity ->
assertThat(activity.getSupportFragmentManager().findFragmentById(android.R.id.content))
.isSameInstanceAs(fragment));
+ }
}
@Test
public void recreate_nonRetainFragmentHostingActivity() {
Fragment fragment = new Fragment();
fragment.setRetainInstance(false);
- ActivityScenario<LifecycleOwnerActivity> activityScenario =
- ActivityScenario.launch(LifecycleOwnerActivity.class);
+ try (ActivityScenario<LifecycleOwnerActivity> activityScenario =
+ ActivityScenario.launch(LifecycleOwnerActivity.class)) {
assertThat(activityScenario).isNotNull();
activityScenario.onActivity(
activity -> {
@@ -226,6 +233,7 @@ public class ActivityScenarioTest {
activity ->
assertThat(activity.getSupportFragmentManager().findFragmentById(android.R.id.content))
.isNotSameInstanceAs(fragment));
+ }
}
@Test
@@ -242,13 +250,14 @@ public class ActivityScenarioTest {
@Test
public void setRotation_recreatesActivity() {
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
- try (ActivityScenario<?> scenario = ActivityScenario.launch(TranscriptActivity.class)) {
+ try (ActivityScenario<TranscriptActivity> activityScenario =
+ ActivityScenario.launch(TranscriptActivity.class)) {
AtomicReference<Activity> originalActivity = new AtomicReference<>();
- scenario.onActivity(originalActivity::set);
+ activityScenario.onActivity(originalActivity::set);
uiAutomation.setRotation(UiAutomation.ROTATION_FREEZE_90);
- scenario.onActivity(
+ activityScenario.onActivity(
activity -> {
assertThat(activity.getResources().getConfiguration().orientation)
.isEqualTo(Configuration.ORIENTATION_LANDSCAPE);
@@ -256,4 +265,18 @@ public class ActivityScenarioTest {
});
}
}
+
+ @Test
+ public void onActivityExceptionPropagated() {
+ try (ActivityScenario<TranscriptActivity> activityScenario =
+ ActivityScenario.launch(TranscriptActivity.class)) {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ activityScenario.onActivity(
+ activity -> {
+ throw new IllegalStateException("test");
+ }));
+ }
+ }
}
diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/CryptoObjectTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/CryptoObjectTest.java
new file mode 100644
index 000000000..2ece12427
--- /dev/null
+++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/CryptoObjectTest.java
@@ -0,0 +1,65 @@
+package org.robolectric.integrationtests.axt;
+
+import static org.junit.Assert.fail;
+
+import androidx.annotation.NonNull;
+import androidx.biometric.BiometricPrompt;
+import androidx.biometric.BiometricPrompt.PromptInfo;
+import androidx.fragment.app.FragmentActivity;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.Executor;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.android.controller.ActivityController;
+
+/** Test intercepting classes not present in OpenJDK. */
+@RunWith(AndroidJUnit4.class)
+public class CryptoObjectTest {
+
+ private FragmentActivity fragmentActivity;
+
+ @Before
+ public void setUp() {
+ fragmentActivity =
+ ActivityController.of(new FragmentActivity()).create().resume().start().get();
+ }
+
+ @Test
+ public void biometricPromptAuthenticateShouldNotCrashWithNoSuchMethodError()
+ throws NoSuchPaddingException, NoSuchAlgorithmException {
+ BiometricPrompt biometricPrompt =
+ new BiometricPrompt(
+ fragmentActivity,
+ new Executor() {
+ @Override
+ public void execute(Runnable command) {}
+ },
+ new BiometricPrompt.AuthenticationCallback() {
+ @Override
+ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {}
+
+ @Override
+ public void onAuthenticationSucceeded(
+ @NonNull BiometricPrompt.AuthenticationResult result) {}
+
+ @Override
+ public void onAuthenticationFailed() {}
+ });
+
+ PromptInfo promptInfo =
+ new PromptInfo.Builder()
+ .setTitle("Set and not empty")
+ .setNegativeButtonText("Set and not empty")
+ .build();
+ try {
+ biometricPrompt.authenticate(
+ promptInfo, new BiometricPrompt.CryptoObject(Cipher.getInstance("RSA")));
+ } catch (NoSuchMethodError e) {
+ fail();
+ }
+ }
+}
diff --git a/integration_tests/compat-target28/build.gradle b/integration_tests/compat-target28/build.gradle
index 1fc7485ae..bbcad7220 100644
--- a/integration_tests/compat-target28/build.gradle
+++ b/integration_tests/compat-target28/build.gradle
@@ -27,6 +27,10 @@ android {
targetCompatibility = '1.8'
}
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
testOptions.unitTests.includeAndroidResources true
}
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java b/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java
index 12c992f6c..0c2823fe3 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java
@@ -2,6 +2,7 @@ package android.app;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import android.os.Handler;
@@ -48,4 +49,16 @@ public final class InstrumentationTest {
assertThat(events).containsExactly("before runOnMainSync", "in runOnMainSync").inOrder();
}
+
+ @Test
+ public void runOnMainSync_propagatesException() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ throw new IllegalStateException("test");
+ }));
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowColorTest.java b/integration_tests/ctesque/src/sharedTest/java/android/graphics/ColorTest.java
index 46387d653..308410b44 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowColorTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/graphics/ColorTest.java
@@ -1,15 +1,16 @@
-package org.robolectric.shadows;
+package android.graphics;
import static com.google.common.truth.Truth.assertThat;
-import android.graphics.Color;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+/** Compatibility tests for {@link Color} */
+@DoNotInstrument
@RunWith(AndroidJUnit4.class)
-public class ShadowColorTest {
-
+public class ColorTest {
@Test
public void testRgb() {
int color = Color.rgb(160, 160, 160);
@@ -69,9 +70,21 @@ public class ShadowColorTest {
@Test
public void HSVToColorShouldReverseColorToHSV() {
- float[] hsv = new float[3];
- Color.colorToHSV(Color.RED, hsv);
+ float[] hsv = new float[3];
+ Color.colorToHSV(Color.RED, hsv);
+
+ assertThat(Color.HSVToColor(hsv)).isEqualTo(Color.RED);
+ }
- assertThat(Color.HSVToColor(hsv)).isEqualTo(Color.RED);
+ @Test
+ public void HSVToColorValueShouldBePinned() {
+ assertThat(Color.HSVToColor(new float[] {0f, 0f, -1.0f})).isEqualTo(Color.BLACK);
+ assertThat(Color.HSVToColor(new float[] {0f, 0f, 2.0f})).isEqualTo(Color.WHITE);
+ }
+
+ @Test
+ public void HSVToColorSaturationShouldBePinned() {
+ assertThat(Color.HSVToColor(new float[] {0f, -1.0f, 0.5f})).isEqualTo(0xff808080);
+ assertThat(Color.HSVToColor(new float[] {0f, 2.0f, 0.5f})).isEqualTo(0xff800000);
}
}
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/os/LooperTest.java b/integration_tests/ctesque/src/sharedTest/java/android/os/LooperTest.java
new file mode 100644
index 000000000..14a4a7d79
--- /dev/null
+++ b/integration_tests/ctesque/src/sharedTest/java/android/os/LooperTest.java
@@ -0,0 +1,26 @@
+package android.os;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.concurrent.CountDownLatch;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/** Tests to verify INSTRUMENTATION_TEST mode Looper behaves like a looping Looper. */
+@DoNotInstrument
+@RunWith(AndroidJUnit4.class)
+public class LooperTest {
+
+ @Test
+ @LooperMode(Mode.INSTRUMENTATION_TEST)
+ public void postAndWait() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ new Handler(Looper.getMainLooper()).post(latch::countDown);
+ assertTrue(latch.await(1, SECONDS));
+ }
+}
diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle
index fd52d973d..a8bf910c8 100644
--- a/integration_tests/kotlin/build.gradle
+++ b/integration_tests/kotlin/build.gradle
@@ -1,3 +1,4 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.robolectric.gradle.RoboJavaModulePlugin
apply plugin: RoboJavaModulePlugin
@@ -12,7 +13,11 @@ spotless {
}
compileKotlin {
- compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
+}
+
+compileTestKotlin {
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
dependencies {
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 ec7ca8bf9..3e990d133 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
@@ -3,7 +3,9 @@ package org.robolectric.integrationtests.memoryleaks;
import static org.robolectric.Shadows.shadowOf;
import android.app.Activity;
+import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.TypedArray;
import android.os.Looper;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -14,8 +16,10 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
/**
* A test that verifies that activities and fragments become GC candidates after being destroyed, or
@@ -126,5 +130,15 @@ public abstract class BaseMemoryLeaksTest {
}
}
+ @Test
+ public void typedArrayData() {
+ assertNotLeaking(
+ () -> {
+ Context context = RuntimeEnvironment.getApplication();
+ TypedArray typedArray = context.obtainStyledAttributes(new int[] {});
+ return ReflectionHelpers.getField(typedArray, "mData");
+ });
+ }
+
public abstract <T> void assertNotLeaking(Callable<T> potentiallyLeakingCallable);
}
diff --git a/integration_tests/mockito-kotlin/build.gradle b/integration_tests/mockito-kotlin/build.gradle
index 776f33bd2..22df04b0f 100644
--- a/integration_tests/mockito-kotlin/build.gradle
+++ b/integration_tests/mockito-kotlin/build.gradle
@@ -1,3 +1,4 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.robolectric.gradle.RoboJavaModulePlugin
apply plugin: RoboJavaModulePlugin
@@ -11,6 +12,14 @@ spotless {
}
}
+compileKotlin {
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
+}
+
+compileTestKotlin {
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
+}
+
dependencies {
api project(":robolectric")
compileOnly AndroidSdk.MAX_SDK.coordinates
diff --git a/integration_tests/nativegraphics/config/robolectric.properties b/integration_tests/nativegraphics/config/robolectric.properties
new file mode 100644
index 000000000..0d16e6b27
--- /dev/null
+++ b/integration_tests/nativegraphics/config/robolectric.properties
@@ -0,0 +1,15 @@
+# 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.
+#
+sdk=NEWEST_SDK
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java
index 77f548373..b029f01f5 100644
--- a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java
@@ -15,6 +15,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowDrawable;
@@ -71,4 +72,51 @@ public class ShadowNativeAnimatedVectorDrawableTest {
assertEquals(
R.drawable.animation_vector_drawable_grouping_1, shadowDrawable.getCreatedFromResId());
}
+
+ @Test
+ public void start_isRunning_returnsTrue() throws Exception {
+ // Setup AnimatedVectorDrawable from xml file
+ XmlPullParser parser = resources.getXml(RES_ID);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+ AnimatedVectorDrawable drawable = new AnimatedVectorDrawable();
+ drawable.inflate(resources, parser, attrs);
+
+ drawable.start();
+
+ assertEquals(true, Shadows.shadowOf(drawable).isStartInitiated());
+ }
+
+ @Test
+ public void stop_returnsFalse() throws Exception {
+ // Setup AnimatedVectorDrawable from xml file
+ XmlPullParser parser = resources.getXml(RES_ID);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+ AnimatedVectorDrawable drawable = new AnimatedVectorDrawable();
+ drawable.inflate(resources, parser, attrs);
+
+ drawable.start();
+ drawable.stop();
+
+ assertEquals(false, Shadows.shadowOf(drawable).isStartInitiated());
+ }
}
diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle
index 1e4ba1ddf..c8b4c5a0e 100644
--- a/integration_tests/sparsearray/build.gradle
+++ b/integration_tests/sparsearray/build.gradle
@@ -26,11 +26,13 @@ android {
targetCompatibility = '1.8'
}
- android {
- testOptions {
- unitTests {
- includeAndroidResources = true
- }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
}
}
}
diff --git a/nativeruntime/src/test/resources/resources.ap_ b/nativeruntime/src/test/resources/resources.ap_
index bc05da2ad..13cc837ec 100644
--- a/nativeruntime/src/test/resources/resources.ap_
+++ b/nativeruntime/src/test/resources/resources.ap_
Binary files differ
diff --git a/plugins/maven-dependency-resolver/build.gradle b/plugins/maven-dependency-resolver/build.gradle
index 2aa33d9f5..acbff55b6 100644
--- a/plugins/maven-dependency-resolver/build.gradle
+++ b/plugins/maven-dependency-resolver/build.gradle
@@ -21,29 +21,11 @@ tasks.withType(GenerateModuleMetadata).configureEach {
}
compileKotlin {
- // Use java/main classes directory to replace default kotlin/main to
- // avoid d8 error when dexing & desugaring kotlin classes with non-exist
- // kotlin/main directory because this module doesn't have kotlin code
- // in production. If utils module starts to add Kotlin code in main source
- // set, we can remove this destinationDirectory modification.
- destinationDirectory = file("${projectDir}/build/classes/java/main")
compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
-afterEvaluate {
- configurations {
- runtimeElements {
- attributes {
- // We should add artifactType with jar to ensure standard runtimeElements variant
- // has a max priority selection sequence than other variants that brought by
- // kotlin plugin.
- attribute(
- Attribute.of("artifactType", String.class),
- ArtifactTypeDefinition.JAR_TYPE
- )
- }
- }
- }
+compileTestKotlin {
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
dependencies {
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
index bb5604d80..91d2af75e 100644
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
@@ -35,7 +35,7 @@ import org.xml.sax.SAXException;
* client library here could create conflicts with the ones in the Android system.
*
* @see <a href="https://maven.apache.org/ant-tasks/">maven-ant-tasks</a>
- * @see <a href="https://maven.apache.org/resolver/index.html">Maven Resolver</a></a>
+ * @see <a href="https://maven.apache.org/resolver/index.html">Maven Resolver</a>
*/
public class MavenDependencyResolver implements DependencyResolver {
diff --git a/preinstrumented/build.gradle b/preinstrumented/build.gradle
index 8ffb5bbf2..dc1e5d0d5 100644
--- a/preinstrumented/build.gradle
+++ b/preinstrumented/build.gradle
@@ -20,6 +20,9 @@ dependencies {
implementation libs.guava
implementation project(":sandbox")
implementation project(":shadows:versioning")
+
+ testImplementation libs.junit4
+ testImplementation libs.mockito
}
tasks.register('instrumentAll') {
diff --git a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
index 753c1670d..1410c33d5 100644
--- a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
+++ b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
@@ -1,5 +1,6 @@
package org.robolectric.preinstrumented;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import java.io.BufferedOutputStream;
import java.io.File;
@@ -19,6 +20,7 @@ import org.robolectric.internal.bytecode.ClassInstrumentor;
import org.robolectric.internal.bytecode.ClassNodeProvider;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
import org.robolectric.internal.bytecode.Interceptors;
+import org.robolectric.internal.bytecode.NativeCallHandler;
import org.robolectric.util.inject.Injector;
import org.robolectric.versioning.AndroidVersionInitTools;
import org.robolectric.versioning.AndroidVersions.AndroidRelease;
@@ -33,6 +35,10 @@ public class JarInstrumentor {
private final ClassInstrumentor classInstrumentor;
private final InstrumentationConfiguration instrumentationConfiguration;
+ public static void main(String[] args) throws IOException, ClassNotFoundException {
+ new JarInstrumentor().processCommandLine(args);
+ }
+
public JarInstrumentor() {
AndroidConfigurer androidConfigurer = INJECTOR.getInstance(AndroidConfigurer.class);
classInstrumentor = INJECTOR.getInstance(ClassInstrumentor.class);
@@ -43,18 +49,61 @@ public class JarInstrumentor {
instrumentationConfiguration = builder.build();
}
- public static void main(String[] args) throws IOException, ClassNotFoundException {
- if (args.length != 2) {
- System.err.println("Usage: JarInstrumentor <source jar> <dest jar>");
- System.exit(1);
+ @VisibleForTesting
+ void processCommandLine(String[] args) throws IOException, ClassNotFoundException {
+ if (args.length >= 2) {
+ File sourceFile = new File(args[0]);
+ File destJarFile = new File(args[1]);
+
+ File destNativesFile = null;
+ boolean throwOnNatives = false;
+ boolean parseError = false;
+ for (int i = 2; i < args.length; i++) {
+ if (args[i].startsWith("--write-natives=")) {
+ destNativesFile = new File(args[i].substring("--write-natives=".length()));
+ } else if (args[i].equals("--throw-on-natives")) {
+ throwOnNatives = true;
+ } else {
+ System.err.println("Unknown argument: " + args[i]);
+ parseError = true;
+ break;
+ }
+ }
+
+ if (!parseError) {
+ instrumentJar(sourceFile, destJarFile, destNativesFile, throwOnNatives);
+ return;
+ }
}
- new JarInstrumentor().instrumentJar(new File(args[0]), new File(args[1]));
+
+ System.err.println(
+ "Usage: JarInstrumentor <source jar> <dest jar> "
+ + "[--write-natives=<file>] "
+ + "[--throw-on-natives]");
+ exit(1);
+ }
+
+ /** Calls {@link System#exit(int)}. Overridden during tests to avoid exiting during tests. */
+ @VisibleForTesting
+ protected void exit(int status) {
+ System.exit(status);
}
- private void instrumentJar(File sourceFile, File destFile)
+ /**
+ * Performs the JAR instrumentation.
+ *
+ * @param sourceJarFile The source JAR to process.
+ * @param destJarFile The destination JAR with the instrumented method calls.
+ * @param destNativesFile Optional file to write native calls signature. Null to disable.
+ * @param throwOnNatives Whether native calls should be instrumented as throwing a dedicated
+ * exception (true) or no-op (false).
+ */
+ @VisibleForTesting
+ protected void instrumentJar(
+ File sourceJarFile, File destJarFile, File destNativesFile, boolean throwOnNatives)
throws IOException, ClassNotFoundException {
long startNs = System.nanoTime();
- JarFile jarFile = new JarFile(sourceFile);
+ JarFile jarFile = new JarFile(sourceJarFile);
ClassNodeProvider classNodeProvider =
new ClassNodeProvider() {
@Override
@@ -63,6 +112,23 @@ public class JarInstrumentor {
}
};
+ NativeCallHandler nativeCallHandler;
+ final boolean writeNativesFile = destNativesFile != null;
+
+ if (destNativesFile == null) {
+ destNativesFile =
+ new File(
+ sourceJarFile.getParentFile(),
+ sourceJarFile.getName().replace(".jar", "-natives.txt"));
+ }
+
+ try {
+ nativeCallHandler = new NativeCallHandler(destNativesFile, writeNativesFile, throwOnNatives);
+ classInstrumentor.setNativeCallHandler(nativeCallHandler);
+ } catch (IOException e) {
+ throw new AssertionError("Unable to load native exemptions file", e);
+ }
+
int nonClassCount = 0;
int classCount = 0;
@@ -70,11 +136,11 @@ public class JarInstrumentor {
try {
classInstrumentor.setAndroidJarSDKVersion(getJarAndroidSDKVersion(jarFile));
} catch (Exception e) {
- throw new AssertionError("Unable to get Android SDK version from Jar File", e);
+ throw new AssertionError("Unable to get Android SDK version from Jar file", e);
}
try (JarOutputStream jarOut =
- new JarOutputStream(new BufferedOutputStream(new FileOutputStream(destFile), ONE_MB))) {
+ new JarOutputStream(new BufferedOutputStream(new FileOutputStream(destJarFile), ONE_MB))) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
@@ -109,6 +175,11 @@ public class JarInstrumentor {
}
}
}
+
+ if (writeNativesFile) {
+ nativeCallHandler.writeExemptionsList();
+ }
+
long elapsedNs = System.nanoTime() - startNs;
System.out.println(
String.format(
diff --git a/preinstrumented/src/test/java/org/robolectric/preinstrumented/JarInstrumentorTest.java b/preinstrumented/src/test/java/org/robolectric/preinstrumented/JarInstrumentorTest.java
new file mode 100644
index 000000000..6b6d169e1
--- /dev/null
+++ b/preinstrumented/src/test/java/org/robolectric/preinstrumented/JarInstrumentorTest.java
@@ -0,0 +1,71 @@
+package org.robolectric.preinstrumented;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import java.io.File;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link JarInstrumentor}. */
+@RunWith(JUnit4.class)
+public class JarInstrumentorTest {
+
+ private JarInstrumentor spyDummyInstrumentor;
+
+ @Before
+ public void setUp() throws Exception {
+ JarInstrumentor dummyInstrumentor =
+ new JarInstrumentor() {
+ @Override
+ protected void instrumentJar(
+ File sourceJarFile, File destJarFile, File destNativesFile, boolean throwOnNatives) {
+ // No-op. We only want to test the command line processing. Stub the actual
+ // instrumention.
+ }
+
+ @Override
+ protected void exit(int status) {
+ // No-op. Tests should never call system.exit().
+ }
+ };
+ spyDummyInstrumentor = spy(dummyInstrumentor);
+ }
+
+ @Test
+ public void processCommandLine_legacyUsage() throws Exception {
+ spyDummyInstrumentor.processCommandLine(new String[] {"source.jar", "dest.jar"});
+ verify(spyDummyInstrumentor)
+ .instrumentJar(new File("source.jar"), new File("dest.jar"), null, false);
+ }
+
+ @Test
+ public void processCommandLine_throwOnNatives() throws Exception {
+ spyDummyInstrumentor.processCommandLine(
+ new String[] {"source.jar", "dest.jar", "--throw-on-natives"});
+ verify(spyDummyInstrumentor)
+ .instrumentJar(new File("source.jar"), new File("dest.jar"), null, true);
+ }
+
+ @Test
+ public void processCommandLine_writeNativesExemptionFile() throws Exception {
+ spyDummyInstrumentor.processCommandLine(
+ new String[] {"source.jar", "dest.jar", "--write-natives=natives.txt"});
+ verify(spyDummyInstrumentor)
+ .instrumentJar(
+ new File("source.jar"), new File("dest.jar"), new File("natives.txt"), false);
+ }
+
+ @Test
+ public void processCommandLine_unknownArguments() throws Exception {
+ spyDummyInstrumentor.processCommandLine(new String[] {"source.jar", "dest.jar", "--some-flag"});
+ verify(spyDummyInstrumentor, never())
+ .instrumentJar(any(File.class), any(File.class), any(File.class), anyBoolean());
+ verify(spyDummyInstrumentor).exit(1);
+ }
+}
diff --git a/processor/Android.bp b/processor/Android.bp
index 90a49815e..607486ba7 100644
--- a/processor/Android.bp
+++ b/processor/Android.bp
@@ -24,6 +24,7 @@ java_library_host {
static_libs: [
"Robolectric_annotations_upstream",
"Robolectric_shadowapi_upstream",
+ "Robolectric_shadows_versioning_upstream",
"auto_service_annotations",
"asm-commons-9.2",
"guava",
diff --git a/processor/build.gradle b/processor/build.gradle
index 9185d088a..c2f00d77d 100644
--- a/processor/build.gradle
+++ b/processor/build.gradle
@@ -34,6 +34,7 @@ tasks['classes'].dependsOn(generateSdksFile)
dependencies {
api project(":annotations")
api project(":shadowapi")
+ implementation project(":shadows:versioning")
compileOnly libs.findbugs.jsr305
api libs.asm
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java
index 05822d783..e08b3acd1 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java
@@ -22,7 +22,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarFile;
@@ -40,6 +39,7 @@ import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.robolectric.annotation.Implementation;
+import org.robolectric.versioning.AndroidVersionInitTools;
/** Encapsulates a collection of Android framework jars. */
public class SdkStore {
@@ -248,26 +248,14 @@ public class SdkStore {
/**
* Determine the API level for this SDK jar by inspecting its {@code build.prop} file.
*
- * <p>If the {@code ro.build.version.codename} value isn't {@code REL}, this is an unreleased
- * SDK, which is represented as 10000 (see {@link
- * android.os.Build.VERSION_CODES#CUR_DEVELOPMENT}.
- *
- * @return the API level, or 10000
+ * @return the API level
*/
private int readSdkInt() {
- Properties properties = new Properties();
- try (InputStream inputStream = jarFile.getInputStream(jarFile.getJarEntry("build.prop"))) {
- properties.load(inputStream);
+ try {
+ return AndroidVersionInitTools.computeReleaseVersion(jarFile).getSdkInt();
} catch (IOException e) {
throw new RuntimeException("failed to read build.prop from " + path);
}
- int sdkInt = Integer.parseInt(properties.getProperty("ro.build.version.sdk"));
- String codename = properties.getProperty("ro.build.version.codename");
- if (!"REL".equals(codename)) {
- sdkInt = 10000;
- }
-
- return sdkInt;
}
private JarFile ensureJar() {
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 edc1a0c86..9a696b17e 100644
--- a/resources/src/main/java/org/robolectric/res/android/ResTable.java
+++ b/resources/src/main/java/org/robolectric/res/android/ResTable.java
@@ -1268,13 +1268,30 @@ public class ResTable {
}
int findEntry(PackageGroup group, int typeIndex, String name, Ref<Integer> outTypeSpecFlags) {
+ // const TypeList& typeList = group->types[typeIndex];
List<Type> typeList = getOrDefault(group.types, typeIndex, Collections.emptyList());
+ // const size_t typeCount = typeList.size();
+ // for (size_t i = 0; i < typeCount; i++) {
for (Type type : typeList) {
+ // const Type* t = typeList[i];
+ // const base::expected<size_t, NullOrIOError> ei =
+ // t->package->keyStrings.indexOfString(name, nameLen);
int ei = type._package_.keyStrings.indexOfString(name);
+ // if (!ei.has_value()) {
if (ei < 0) {
continue;
}
+ // const size_t configCount = t->configs.size();
+ // for (size_t j = 0; j < configCount; j++) {
for (ResTable_type resTableType : type.configs) {
+ // const TypeVariant tv(t->configs[j]);
+ // for (TypeVariant::iterator iter = tv.beginEntries();
+ // iter != tv.endEntries();
+ // iter++) {
+ // const ResTable_entry* entry = *iter;
+ // if (entry == NULL) {
+ // continue;
+ // }
int entryIndex = resTableType.findEntryByResName(ei);
if (entryIndex >= 0) {
int resId = Res_MAKEID(group.id - 1, typeIndex, entryIndex);
diff --git a/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java b/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java
index 17c9dd91c..bab7f9670 100644
--- a/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java
+++ b/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java
@@ -8,6 +8,7 @@ import static org.robolectric.res.android.Util.SIZEOF_INT;
import static org.robolectric.res.android.Util.SIZEOF_SHORT;
import static org.robolectric.res.android.Util.dtohl;
import static org.robolectric.res.android.Util.dtohs;
+import static org.robolectric.res.android.Util.isTruthy;
import java.nio.Buffer;
import java.nio.ByteBuffer;
@@ -1213,14 +1214,23 @@ public static class ResTable_ref
int entryOffset(int entryIndex) {
ByteBuffer byteBuffer = myBuf();
int offset = myOffset();
- boolean isOffset16 = (flags & ResTable_type.FLAG_OFFSET16) == ResTable_type.FLAG_OFFSET16;
- if (isOffset16) {
+ if (isTruthy(flags & ResTable_type.FLAG_OFFSET16)) {
short off16 = byteBuffer.getShort(offset + header.headerSize + entryIndex * 2);
if (off16 == -1) {
return -1;
}
// Check for no entry (0xffff short)
- return dtohs(off16) == 0xffff ? ResTable_type.NO_ENTRY : dtohs(off16) * 4;
+ return dtohs(off16) == -1 ? ResTable_type.NO_ENTRY : dtohs(off16) * 4;
+ } else if (isTruthy(flags & ResTable_type.FLAG_SPARSE)) {
+ ResTable_sparseTypeEntry sparse_entry =
+ new ResTable_sparseTypeEntry(
+ myBuf(), myOffset() + entryIndex * ResTable_sparseTypeEntry.SIZEOF);
+ // if (!sparse_entry) {
+ // return base::unexpected(IOError::PAGES_MISSING);
+ // }
+ // TODO: implement above
+ // offset = dtohs(sparse_entry->offset) * 4u;
+ return dtohs(sparse_entry.offset) * 4;
} else {
return byteBuffer.getInt(offset + header.headerSize + entryIndex * 4);
}
@@ -1236,11 +1246,13 @@ public static class ResTable_ref
int offset = myOffset();
// from ResTable cpp:
-// const uint32_t* const eindex = reinterpret_cast<const uint32_t*>(
-// reinterpret_cast<const uint8_t*>(thisType) + dtohs(thisType->header.headerSize));
-//
-// uint32_t thisOffset = dtohl(eindex[realEntryIndex]);
- int entryOffset = byteBuffer.getInt(offset + header.headerSize + entryIndex * 4);
+ // const uint32_t* const eindex = reinterpret_cast<const uint32_t*>(
+ // reinterpret_cast<const uint8_t*>(thisType) +
+ // dtohs(thisType->header.headerSize));
+ //
+ // uint32_t thisOffset = dtohl(eindex[realEntryIndex]);
+
+ int entryOffset = entryOffset(entryIndex);
if (entryOffset == -1) {
return -1;
}
diff --git a/robolectric/Android.bp b/robolectric/Android.bp
index e690895a3..822c32288 100644
--- a/robolectric/Android.bp
+++ b/robolectric/Android.bp
@@ -63,6 +63,7 @@ java_test_host {
"Robolectric_shadows_framework_upstream",
"Robolectric_annotations_upstream",
"Robolectric_shadowapi_upstream",
+ "Robolectric_shadows_versioning_upstream",
"Robolectric_resources_upstream",
"Robolectric_sandbox_upstream",
"Robolectric_junit_upstream",
diff --git a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
index 519f42c09..d9dbbd46d 100644
--- a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
+++ b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
@@ -76,6 +76,10 @@ public class RobolectricTestRunner extends SandboxTestRunner {
new SecureRandom();
// Fixes an issue using AWT-backed graphics shadows when using X11 forwarding.
System.setProperty("java.awt.headless", "true");
+ // Fixes a performance regression in caused by the addition of RSA modulus
+ // validation introduced in Bouncy Castle 1.71.
+ // https://github.com/bcgit/bc-java/issues/1144
+ System.setProperty("org.bouncycastle.rsa.max_mr_tests", "0");
}
protected static Injector.Builder defaultInjector() {
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 f250381f4..2f66c7d88 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
@@ -86,6 +86,7 @@ import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowPackageParser;
import org.robolectric.shadows.ShadowPackageParser._Package_;
+import org.robolectric.shadows.ShadowPausedLooper;
import org.robolectric.shadows.ShadowView;
import org.robolectric.util.Logger;
import org.robolectric.util.PerfStatsCollector;
@@ -189,13 +190,13 @@ public class AndroidTestEnvironment implements TestEnvironment {
: androidConfiguration.locale;
Locale.setDefault(locale);
- // Looper needs to be prepared before the activity thread is created
- if (Looper.myLooper() == null) {
- Looper.prepareMainLooper();
- }
if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
+ if (Looper.myLooper() == null) {
+ Looper.prepareMainLooper();
+ }
ShadowLooper.getShadowMainLooper().resetScheduler();
} else {
+ ShadowPausedLooper.resetLoopers();
RuntimeEnvironment.setMasterScheduler(new LooperDelegatingScheduler(Looper.getMainLooper()));
}
@@ -203,9 +204,6 @@ public class AndroidTestEnvironment implements TestEnvironment {
RuntimeEnvironment.setAndroidFrameworkJarPath(sdkJarPath);
Bootstrap.setDisplayConfiguration(androidConfiguration, displayMetrics);
- RuntimeEnvironment.setActivityThread(ReflectionHelpers.callConstructor(ActivityThread.class));
- ReflectionHelpers.setStaticField(
- ActivityThread.class, "sMainThreadHandler", new Handler(Looper.myLooper()));
Instrumentation instrumentation = createInstrumentation();
InstrumentationRegistry.registerInstance(instrumentation, new Bundle());
@@ -284,6 +282,9 @@ public class AndroidTestEnvironment implements TestEnvironment {
Package parsedPackage = loadAppPackage(config, appManifest);
ApplicationInfo applicationInfo = parsedPackage.applicationInfo;
+ Class<? extends Application> applicationClass =
+ getApplicationClass(appManifest, config, applicationInfo);
+ applicationInfo.className = applicationClass.getName();
ComponentName actualComponentName =
new ComponentName(
@@ -311,8 +312,8 @@ public class AndroidTestEnvironment implements TestEnvironment {
Bootstrap.setUpDisplay();
activityThread.applyConfigurationToResources(androidConfiguration);
- Application application = createApplication(appManifest, config, applicationInfo);
- RuntimeEnvironment.setConfiguredApplicationClass(application.getClass());
+ Application application = ReflectionHelpers.callConstructor(applicationClass);
+ RuntimeEnvironment.setConfiguredApplicationClass(applicationClass);
RuntimeEnvironment.application = application;
@@ -479,13 +480,7 @@ public class AndroidTestEnvironment implements TestEnvironment {
}
@VisibleForTesting
- static Application createApplication(
- AndroidManifest appManifest, Config config, ApplicationInfo applicationInfo) {
- return ReflectionHelpers.callConstructor(
- getApplicationClass(appManifest, config, applicationInfo));
- }
-
- private static Class<? extends Application> getApplicationClass(
+ static Class<? extends Application> getApplicationClass(
AndroidManifest appManifest, Config config, ApplicationInfo applicationInfo) {
Class<? extends Application> applicationClass = null;
if (config != null && !Config.Builder.isDefaultApplication(config.application())) {
@@ -554,35 +549,39 @@ public class AndroidTestEnvironment implements TestEnvironment {
}
private Instrumentation createInstrumentation() {
- final ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
- final _ActivityThread_ activityThreadReflector =
- reflector(_ActivityThread_.class, activityThread);
-
Instrumentation androidInstrumentation = new RoboMonitoringInstrumentation();
- activityThreadReflector.setInstrumentation(androidInstrumentation);
-
- Application dummyInitialApplication = new Application();
- final ComponentName dummyInitialComponent =
- new ComponentName("", androidInstrumentation.getClass().getSimpleName());
- // TODO Move the API check into a helper method inside ShadowInstrumentation
- if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.JELLY_BEAN_MR1) {
- reflector(_Instrumentation_.class, androidInstrumentation)
- .init(
- activityThread,
- dummyInitialApplication,
- dummyInitialApplication,
- dummyInitialComponent,
- null);
- } else {
- reflector(_Instrumentation_.class, androidInstrumentation)
- .init(
- activityThread,
- dummyInitialApplication,
- dummyInitialApplication,
- dummyInitialComponent,
- null,
- null);
- }
+ androidInstrumentation.runOnMainSync(
+ () -> {
+ ActivityThread activityThread = ReflectionHelpers.callConstructor(ActivityThread.class);
+ ReflectionHelpers.setStaticField(
+ ActivityThread.class, "sMainThreadHandler", new Handler(Looper.getMainLooper()));
+ reflector(_ActivityThread_.class, activityThread)
+ .setInstrumentation(androidInstrumentation);
+ RuntimeEnvironment.setActivityThread(activityThread);
+
+ Application dummyInitialApplication = new Application();
+ final ComponentName dummyInitialComponent =
+ new ComponentName("", androidInstrumentation.getClass().getSimpleName());
+ // TODO Move the API check into a helper method inside ShadowInstrumentation
+ if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.JELLY_BEAN_MR1) {
+ reflector(_Instrumentation_.class, androidInstrumentation)
+ .init(
+ activityThread,
+ dummyInitialApplication,
+ dummyInitialApplication,
+ dummyInitialComponent,
+ null);
+ } else {
+ reflector(_Instrumentation_.class, androidInstrumentation)
+ .init(
+ activityThread,
+ dummyInitialApplication,
+ dummyInitialApplication,
+ dummyInitialComponent,
+ null,
+ null);
+ }
+ });
androidInstrumentation.onCreate(new Bundle());
return androidInstrumentation;
@@ -604,7 +603,7 @@ public class AndroidTestEnvironment implements TestEnvironment {
@Override
public void tearDownApplication() {
if (RuntimeEnvironment.application != null) {
- RuntimeEnvironment.application.onTerminate();
+ ShadowInstrumentation.runOnMainSyncNoIdle(RuntimeEnvironment.getApplication()::onTerminate);
ShadowInstrumentation.getInstrumentation().finish(1, new Bundle());
}
}
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java
index 29de83dec..4329e8c3e 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java
@@ -2,6 +2,7 @@ package org.robolectric.android.internal;
import static com.google.common.base.Preconditions.checkNotNull;
+import androidx.test.internal.platform.util.TestOutputEmitter;
import com.google.common.annotations.Beta;
import java.util.List;
import java.util.Locale;
@@ -15,10 +16,12 @@ import java.util.Locale;
* <p>Note: This API may be removed in the future in favor of using espresso's exception directly.
*/
@Beta
+@SuppressWarnings("RestrictTo")
public final class IdlingResourceTimeoutException extends RuntimeException {
public IdlingResourceTimeoutException(List<String> resourceNames) {
super(
String.format(
Locale.ROOT, "Wait for %s to become idle timed out", checkNotNull(resourceNames)));
+ TestOutputEmitter.dumpThreadStates("ThreadState-IdlingResTimeoutExcep.txt");
}
}
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java
index fc10913d8..1bc207383 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java
@@ -17,6 +17,7 @@ import javax.annotation.Nullable;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowActivity;
+import org.robolectric.shadows.ShadowInstrumentation;
/**
* An {@link ActivityInvoker} that drives {@link Activity} lifecycles manually.
@@ -69,89 +70,105 @@ public class LocalActivityInvoker implements ActivityInvoker {
public void resumeActivity(Activity activity) {
checkNotNull(controller);
checkState(controller.get() == activity);
- Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
- switch (stage) {
- case RESUMED:
- return;
- case PAUSED:
- controller.resume().topActivityResumed(true);
- return;
- case STOPPED:
- controller.restart().resume().topActivityResumed(true);
- return;
- default:
- throw new IllegalStateException(
- String.format(
- "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage));
- }
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ Stage stage =
+ ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
+ switch (stage) {
+ case RESUMED:
+ return;
+ case PAUSED:
+ controller.resume().topActivityResumed(true);
+ return;
+ case STOPPED:
+ controller.restart().resume().topActivityResumed(true);
+ return;
+ default:
+ throw new IllegalStateException(
+ String.format(
+ "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage));
+ }
+ });
}
@Override
public void pauseActivity(Activity activity) {
checkNotNull(controller);
checkState(controller.get() == activity);
- Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
- switch (stage) {
- case RESUMED:
- controller.topActivityResumed(false).pause();
- return;
- case PAUSED:
- return;
- default:
- throw new IllegalStateException(
- String.format("Activity's stage must be RESUMED or PAUSED but was %s.", stage));
- }
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ Stage stage =
+ ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
+ switch (stage) {
+ case RESUMED:
+ controller.topActivityResumed(false).pause();
+ return;
+ case PAUSED:
+ return;
+ default:
+ throw new IllegalStateException(
+ String.format("Activity's stage must be RESUMED or PAUSED but was %s.", stage));
+ }
+ });
}
@Override
public void stopActivity(Activity activity) {
checkNotNull(controller);
checkState(controller.get() == activity);
- Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
- switch (stage) {
- case RESUMED:
- controller.topActivityResumed(false).pause().stop();
- return;
- case PAUSED:
- controller.stop();
- return;
- case STOPPED:
- return;
- default:
- throw new IllegalStateException(
- String.format(
- "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage));
- }
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ Stage stage =
+ ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
+ switch (stage) {
+ case RESUMED:
+ controller.topActivityResumed(false).pause().stop();
+ return;
+ case PAUSED:
+ controller.stop();
+ return;
+ case STOPPED:
+ return;
+ default:
+ throw new IllegalStateException(
+ String.format(
+ "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage));
+ }
+ });
}
@Override
public void recreateActivity(Activity activity) {
checkNotNull(controller);
checkState(controller.get() == activity);
- controller.recreate();
+ ShadowInstrumentation.runOnMainSyncNoIdle(() -> controller.recreate());
}
@Override
public void finishActivity(Activity activity) {
checkNotNull(controller);
checkState(controller.get() == activity);
- activity.finish();
- Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
- switch (stage) {
- case RESUMED:
- controller.topActivityResumed(false).pause().stop().destroy();
- return;
- case PAUSED:
- controller.stop().destroy();
- return;
- case STOPPED:
- controller.destroy();
- return;
- default:
- throw new IllegalStateException(
- String.format(
- "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage));
- }
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ activity.finish();
+ Stage stage =
+ ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
+ switch (stage) {
+ case RESUMED:
+ controller.topActivityResumed(false).pause().stop().destroy();
+ return;
+ case PAUSED:
+ controller.stop().destroy();
+ return;
+ case STOPPED:
+ controller.destroy();
+ return;
+ default:
+ throw new IllegalStateException(
+ String.format(
+ "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage));
+ }
+ });
}
// This implementation makes sure, that the activity you are trying to launch exists
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/NoOpThreadChecker.java b/robolectric/src/main/java/org/robolectric/android/internal/NoOpThreadChecker.java
deleted file mode 100644
index 1f7f7c37c..000000000
--- a/robolectric/src/main/java/org/robolectric/android/internal/NoOpThreadChecker.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.robolectric.android.internal;
-
-import androidx.test.internal.platform.ThreadChecker;
-
-/**
- * In Robolectric environment, everything is executed on the main thread except for when you
- * manually create and run your code on worker thread.
- */
-@SuppressWarnings("RestrictTo")
-public class NoOpThreadChecker implements ThreadChecker {
- @Override
- public void checkMainThread() {}
-
- @Override
- public void checkNotMainThread() {}
-}
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 0d8d15be2..b41f01504 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java
@@ -2,7 +2,6 @@ package org.robolectric.android.internal;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadow.api.Shadow.extract;
-import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import android.app.Activity;
import android.app.Application;
@@ -33,11 +32,18 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowActivity;
+import org.robolectric.shadows.ShadowInstrumentation;
+import org.robolectric.shadows.ShadowLooper;
/**
* A Robolectric instrumentation that acts like a slimmed down {@link
@@ -48,7 +54,6 @@ public class RoboMonitoringInstrumentation extends Instrumentation {
private static final String TAG = "RoboInstrumentation";
- private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
private final ActivityLifecycleMonitorImpl lifecycleMonitor = new ActivityLifecycleMonitorImpl();
private final ApplicationLifecycleMonitorImpl applicationMonitor =
new ApplicationLifecycleMonitorImpl();
@@ -79,7 +84,7 @@ public class RoboMonitoringInstrumentation extends Instrumentation {
@Override
public void waitForIdleSync() {
- shadowMainLooper().idle();
+ shadowOf(Looper.getMainLooper()).idle();
}
@Override
@@ -108,22 +113,28 @@ public class RoboMonitoringInstrumentation extends Instrumentation {
} catch (ClassNotFoundException e) {
throw new RuntimeException("Could not load activity " + ai.name, e);
}
-
- ActivityController<? extends Activity> controller =
- Robolectric.buildActivity(activityClass, intent, activityOptions).create();
- if (controller.get().isFinishing()) {
- controller.destroy();
- } else {
- createdActivities.add(controller);
- controller
- .start()
- .postCreate(null)
- .resume()
- .visible()
- .windowFocusChanged(true)
- .topActivityResumed(true);
- }
- return controller;
+ AtomicReference<ActivityController<? extends Activity>> activityControllerReference =
+ new AtomicReference<>();
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ ActivityController<? extends Activity> controller =
+ Robolectric.buildActivity(activityClass, intent, activityOptions);
+ activityControllerReference.set(controller);
+ controller.create();
+ if (controller.get().isFinishing()) {
+ controller.destroy();
+ } else {
+ createdActivities.add(controller);
+ controller
+ .start()
+ .postCreate(null)
+ .resume()
+ .visible()
+ .windowFocusChanged(true)
+ .topActivityResumed(true);
+ }
+ });
+ return activityControllerReference.get();
}
@Override
@@ -136,10 +147,47 @@ public class RoboMonitoringInstrumentation extends Instrumentation {
applicationMonitor.signalLifecycleChange(app, ApplicationStage.CREATED);
}
+ /**
+ * Executes a runnable on the main thread, blocking until it is complete.
+ *
+ * <p>When in INSTUMENTATION_TEST Looper mode, the runnable is posted to the main handler and the
+ * caller's thread blocks until that runnable has finished. When a Throwable is thrown in the
+ * runnable, the exception is propagated back to the caller's thread. If it is an unchecked
+ * throwable, it will be rethrown as is. If it is a checked exception, it will be rethrown as a
+ * {@link RuntimeException}.
+ *
+ * <p>For other Looper modes, the main looper is idled and then the runnable is executed in the
+ * caller's thread.
+ *
+ * @param runnable a runnable to be executed on the main thread
+ */
@Override
- public void runOnMainSync(Runnable runner) {
- shadowMainLooper().idle();
- runner.run();
+ public void runOnMainSync(Runnable runnable) {
+ if (ShadowLooper.looperMode() != LooperMode.Mode.INSTRUMENTATION_TEST) {
+ waitForIdleSync();
+ runnable.run();
+ return;
+ }
+
+ FutureTask<Void> wrappedRunnable = new FutureTask<>(runnable, null);
+ new Handler(Looper.getMainLooper()).post(wrappedRunnable);
+ 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);
+ }
}
/** {@inheritDoc} */
@@ -218,21 +266,48 @@ public class RoboMonitoringInstrumentation extends Instrumentation {
private void postDispatchActivityResult(
ShadowActivity shadowActivity, String target, int requestCode, ActivityResult ar) {
- mainThreadHandler.post(
- new Runnable() {
- @Override
- public void run() {
- shadowActivity.internalCallDispatchActivityResult(
- target, requestCode, ar.getResultCode(), ar.getResultData());
- }
- });
+ new Handler(Looper.getMainLooper())
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ shadowActivity.internalCallDispatchActivityResult(
+ target, requestCode, ar.getResultCode(), ar.getResultData());
+ }
+ });
}
private ActivityResult stubResultFor(Intent intent) {
- if (IntentStubberRegistry.isLoaded()) {
- return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent);
+ if (!IntentStubberRegistry.isLoaded()) {
+ return null;
+ }
+
+ FutureTask<ActivityResult> task =
+ new FutureTask<ActivityResult>(
+ new Callable<ActivityResult>() {
+ @Override
+ public ActivityResult call() throws Exception {
+ return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent);
+ }
+ });
+ ShadowInstrumentation.runOnMainSyncNoIdle(task);
+
+ try {
+ return task.get();
+ } catch (ExecutionException e) {
+ String msg = String.format("Could not retrieve stub result for intent %s", intent);
+ // Preserve original exception
+ if (e.getCause() instanceof RuntimeException) {
+ Log.w(TAG, msg, e);
+ throw (RuntimeException) e.getCause();
+ } else if (e.getCause() != null) {
+ throw new RuntimeException(msg, e.getCause());
+ } else {
+ throw new RuntimeException(msg, e);
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
}
- return null;
}
/** {@inheritDoc} */
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/RobolectricThreadChecker.java b/robolectric/src/main/java/org/robolectric/android/internal/RobolectricThreadChecker.java
new file mode 100644
index 000000000..b11e3d8cb
--- /dev/null
+++ b/robolectric/src/main/java/org/robolectric/android/internal/RobolectricThreadChecker.java
@@ -0,0 +1,38 @@
+package org.robolectric.android.internal;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import android.os.Looper;
+import androidx.test.internal.platform.ThreadChecker;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.shadows.ShadowLooper;
+
+/**
+ * Performs thread checking when in INSTRUMENTAION_TEST Looper Mode where the test thread is
+ * distinct from the main thread. No-op for other modes because everything is executed on the main
+ * thread (except for manually created worker threads).
+ */
+@SuppressWarnings("RestrictTo")
+public class RobolectricThreadChecker implements ThreadChecker {
+ @Override
+ public void checkMainThread() {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) {
+ checkState(
+ Thread.currentThread().equals(Looper.getMainLooper().getThread()),
+ "Method cannot be called off the main application thread (on: %s) when running in"
+ + " LooperMode.INSTRUMENTATION_TEST",
+ Thread.currentThread().getName());
+ }
+ }
+
+ @Override
+ public void checkNotMainThread() {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) {
+ checkState(
+ !Thread.currentThread().equals(Looper.getMainLooper().getThread()),
+ "Method cannot be called on the main application thread (on: %s) when running in"
+ + " LooperMode.INSTRUMENTATION_TEST",
+ Thread.currentThread().getName());
+ }
+ }
+}
diff --git a/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java b/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java
index 1a1b7e1f8..359cb554e 100644
--- a/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java
+++ b/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java
@@ -33,7 +33,10 @@ import org.robolectric.android.util.concurrent.BackgroundExecutor;
* assertThat(Looper.myLooper()).isEqualTo(Looper.getMainLooper());
* }
* </pre>
+ *
+ * @deprecated use LooperMode.Mode.INSTRUMENTATION_TEST instead
*/
+@Deprecated
public final class BackgroundTestRule implements TestRule {
/** Annotation for test methods that need to be executed in a background thread. */
diff --git a/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker b/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker
index 55104eac2..341f4d97c 100644
--- a/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker
+++ b/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker
@@ -1 +1 @@
-org.robolectric.android.internal.NoOpThreadChecker
+org.robolectric.android.internal.RobolectricThreadChecker
diff --git a/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java b/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java
index 1837a7678..0ed5f056a 100644
--- a/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java
+++ b/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java
@@ -3,6 +3,7 @@ package org.robolectric;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.asList;
import static org.junit.Assert.fail;
+import static org.robolectric.annotation.Config.NEWEST_SDK;
import static org.robolectric.res.AttributeResource.ANDROID_NS;
import static org.robolectric.res.AttributeResource.ANDROID_RES_NS_PREFIX;
import static org.robolectric.res.AttributeResource.RES_AUTO_NS_URI;
@@ -13,6 +14,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
import org.robolectric.res.AttributeResource;
/** Tests for {@link Robolectric#buildAttributeSet()} */
@@ -50,7 +52,9 @@ public class AttributeSetBuilderTest {
.addAttribute(android.R.attr.text, AttributeResource.NULL_VALUE)
.build();
- assertThat(roboAttributeSet.getAttributeResourceValue(ANDROID_RES_NS_PREFIX + "com.some.namespace", "text", 0))
+ assertThat(
+ roboAttributeSet.getAttributeResourceValue(
+ ANDROID_RES_NS_PREFIX + "com.some.namespace", "text", 0))
.isEqualTo(0);
}
@@ -60,7 +64,9 @@ public class AttributeSetBuilderTest {
.addAttribute(android.R.attr.id, "@+id/text1")
.build();
- assertThat(roboAttributeSet.getAttributeResourceValue(ANDROID_RES_NS_PREFIX + "com.some.other.namespace", "id", 0))
+ assertThat(
+ roboAttributeSet.getAttributeResourceValue(
+ ANDROID_RES_NS_PREFIX + "com.some.other.namespace", "id", 0))
.isEqualTo(0);
}
@@ -128,7 +134,9 @@ public class AttributeSetBuilderTest {
AttributeSet roboAttributeSet = Robolectric.buildAttributeSet()
.build();
- assertThat(roboAttributeSet.getAttributeBooleanValue(ANDROID_RES_NS_PREFIX + "com.some.namespace", "isSugary", true))
+ assertThat(
+ roboAttributeSet.getAttributeBooleanValue(
+ ANDROID_RES_NS_PREFIX + "com.some.namespace", "isSugary", true))
.isTrue();
}
@@ -403,4 +411,15 @@ public class AttributeSetBuilderTest {
}
}
+ @Test
+ // buildAttributeSet always uses resource table from latest SDK
+ @Config(sdk = NEWEST_SDK)
+ public void attrWithIconReference() {
+ AttributeSet roboAttributeSet =
+ Robolectric.buildAttributeSet()
+ .addAttribute(R.attr.loaderIcon, "@android:drawable/ic_menu_save")
+ .build();
+
+ assertThat(roboAttributeSet.getAttributeNameResource(0)).isEqualTo(R.attr.loaderIcon);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/R.java b/robolectric/src/test/java/org/robolectric/R.java
index 880955aac..9f3708e69 100644
--- a/robolectric/src/test/java/org/robolectric/R.java
+++ b/robolectric/src/test/java/org/robolectric/R.java
@@ -36,7 +36,7 @@ public final class R {
or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
<p>May be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character.
*/
- public static final int altTitle=0x7f010025;
+ public static final int altTitle=0x7f010026;
/** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>"
or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
*/
@@ -71,7 +71,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size),
in (inches), mm (millimeters).
*/
- public static final int averageSheepWidth=0x7f01001f;
+ public static final int averageSheepWidth=0x7f010020;
/** <p>Must be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character.
<p>This may also be a reference to a resource (in the form
"<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or
@@ -115,7 +115,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
<p>May be a boolean value, either "<code>true</code>" or "<code>false</code>".
*/
- public static final int isSugary=0x7f010020;
+ public static final int isSugary=0x7f010021;
/** <p>Must be one of the following constant values.</p>
<table>
<colgroup align="left" />
@@ -138,13 +138,17 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
</table>
*/
public static final int keycode=0x7f01000f;
+ /** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>"
+or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
+ */
+ public static final int loaderIcon=0x7f01001e;
/** <p>May be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>"
or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
<p>May be a dimension value, which is a floating point number appended with a unit such as "<code>14.5sp</code>".
Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size),
in (inches), mm (millimeters).
*/
- public static final int logoHeight=0x7f010021;
+ public static final int logoHeight=0x7f010022;
/** <p>Must be a dimension value, which is a floating point number appended with a unit such as "<code>14.5sp</code>".
Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size),
in (inches), mm (millimeters).
@@ -154,7 +158,7 @@ theme attribute (in the form
"<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>")
containing a value of this type.
*/
- public static final int logoWidth=0x7f010022;
+ public static final int logoWidth=0x7f010023;
/** <p>Must be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character.
<p>This may also be a reference to a resource (in the form
"<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or
@@ -228,7 +232,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
/** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>"
or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
*/
- public static final int snail=0x7f010024;
+ public static final int snail=0x7f010025;
/** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>"
or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
*/
@@ -244,7 +248,7 @@ theme attribute (in the form
"<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>")
containing a value of this type.
*/
- public static final int stateFoo=0x7f01001e;
+ public static final int stateFoo=0x7f01001f;
/** <p>Must be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character.
<p>This may also be a reference to a resource (in the form
"<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or
@@ -280,7 +284,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
/** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>"
or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>".
*/
- public static final int styleReference=0x7f010023;
+ public static final int styleReference=0x7f010024;
/** <p>Must be an integer value, such as "<code>100</code>".
<p>This may also be a reference to a resource (in the form
"<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or
@@ -644,7 +648,7 @@ containing a value of this type.
@see #CustomStateView_stateFoo
*/
public static final int[] CustomStateView = {
- 0x7f01001e
+ 0x7f01001f
};
/**
<p>This symbol is the offset where the {@link org.robolectric.R.attr#stateFoo}
@@ -966,8 +970,8 @@ containing a value of this type.
@see #Theme_AnotherTheme_Attributes_styleReference
*/
public static final int[] Theme_AnotherTheme_Attributes = {
- 0x7f01001f, 0x7f010020, 0x7f010021, 0x7f010022,
- 0x7f010023, 0x7f010024, 0x7f010025
+ 0x7f010020, 0x7f010021, 0x7f010022, 0x7f010023,
+ 0x7f010024, 0x7f010025, 0x7f010026
};
/**
<p>This symbol is the offset where the {@link org.robolectric.R.attr#altTitle}
diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
index 737af4849..db4058d11 100644
--- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
+++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
@@ -257,8 +257,8 @@ public class RobolectricTestRunnerTest {
RobolectricTestRunner runner =
new SingleSdkRobolectricTestRunner(
TestWithTwoMethods.class,
- RobolectricTestRunner.defaultInjector()
- .bind(PerfStatsReporter[].class, new PerfStatsReporter[]{reporter})
+ SingleSdkRobolectricTestRunner.defaultInjector()
+ .bind(PerfStatsReporter[].class, new PerfStatsReporter[] {reporter})
.build());
runner.run(notifier);
@@ -275,7 +275,7 @@ public class RobolectricTestRunnerTest {
RobolectricTestRunner runner =
new SingleSdkRobolectricTestRunner(
TestThatFails.class,
- RobolectricTestRunner.defaultInjector()
+ SingleSdkRobolectricTestRunner.defaultInjector()
.bind(PerfStatsReporter[].class, new PerfStatsReporter[] {reporter})
.build());
diff --git a/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java b/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java
index 9a7fb0ddc..f2395b788 100644
--- a/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java
@@ -7,6 +7,7 @@ import android.content.res.Configuration;
import android.os.Build.VERSION_CODES;
import android.util.DisplayMetrics;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.Locale;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -34,61 +35,100 @@ public class DeviceConfigTest {
: "";
}
- @Test @Config(minSdk = VERSION_CODES.JELLY_BEAN_MR1)
- public void applyToConfiguration() throws Exception {
+ @Test
+ @Config(minSdk = VERSION_CODES.JELLY_BEAN_MR1)
+ public void applyToConfiguration() {
applyQualifiers("en-rUS-w400dp-h800dp-notround");
assertThat(asQualifierString())
.isEqualTo("en-rUS-ldltr-w400dp-h800dp-notround");
}
@Test
- public void applyToConfiguration_isCumulative() throws Exception {
- applyQualifiers("en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ public void applyToConfiguration_isCumulative() {
+ applyQualifiers(
+ "en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
applyQualifiers("fr-land");
assertThat(asQualifierString())
- .isEqualTo("fr-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "fr-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-"
+ + optsForO
+ + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
applyQualifiers("w500dp-large-television-night-xxhdpi-notouch-keyshidden");
assertThat(asQualifierString())
- .isEqualTo("fr-ldltr-sw400dp-w500dp-large-notlong-notround-" + optsForO + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "fr-ldltr-sw400dp-w500dp-large-notlong-notround-"
+ + optsForO
+ + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav");
applyQualifiers("long");
assertThat(asQualifierString())
- .isEqualTo("fr-ldltr-sw400dp-w500dp-large-long-notround-" + optsForO + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "fr-ldltr-sw400dp-w500dp-large-long-notround-"
+ + optsForO
+ + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav");
applyQualifiers("round");
assertThat(asQualifierString())
- .isEqualTo("fr-ldltr-sw400dp-w500dp-large-long-round-" + optsForO + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "fr-ldltr-sw400dp-w500dp-large-long-round-"
+ + optsForO
+ + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav");
}
@Test
- public void applyRules_defaults() throws Exception {
+ public void applyRules_defaults() {
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
// todo: this fails on JELLY_BEAN and LOLLIPOP through M... why?
- @Test @Config(minSdk = VERSION_CODES.N)
- public void applyRules_rtlScript() throws Exception {
- applyQualifiers("he");
+ @Test
+ @Config(minSdk = VERSION_CODES.N)
+ public void applyRules_rtlScript() {
+ String language = "he";
+ applyQualifiers(language);
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
-
+ // Locale's constructor has always converted three language codes to their earlier, obsoleted
+ // forms: he maps to iw, yi maps to ji, and id maps to in. Since Java SE 17, this is no longer
+ // the case. Each language maps to its new form; iw maps to he, ji maps to yi, and in maps to
+ // id.
+ // See
+ // https://stackoverflow.com/questions/8202406/locale-code-for-hebrew-reference-to-other-locale-codes/70882234#70882234,
+ // and https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Locale.html.
+ // To make sure this test can work with different JDK versions, using the following workaround.
+ Locale locale = new Locale(language);
assertThat(asQualifierString())
- .isEqualTo("iw-ldrtl-sw320dp-w320dp-h470dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ locale.getLanguage()
+ + "-ldrtl-sw320dp-w320dp-h470dp-normal-notlong-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
@Test
- public void applyRules_heightWidth() throws Exception {
+ public void applyRules_heightWidth() {
applyQualifiers("w800dp-h400dp");
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw400dp-w800dp-h400dp-normal-long-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw400dp-w800dp-h400dp-normal-long-notround-"
+ + optsForO
+ + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
@Test
@@ -97,51 +137,70 @@ public class DeviceConfigTest {
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-long-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
@Test
- public void applyRules_sizeToDimens() throws Exception {
+ public void applyRules_sizeToDimens() {
applyQualifiers("large-land");
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw480dp-w640dp-h480dp-large-notlong-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw480dp-w640dp-h480dp-large-notlong-notround-"
+ + optsForO
+ + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
@Test
- public void applyRules_sizeFromDimens() throws Exception {
+ public void applyRules_sizeFromDimens() {
applyQualifiers("w800dp-h640dp");
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw640dp-w800dp-h640dp-large-notlong-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw640dp-w800dp-h640dp-large-notlong-notround-"
+ + optsForO
+ + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
@Test
- public void applyRules_longIncreasesHeight() throws Exception {
+ public void applyRules_longIncreasesHeight() {
applyQualifiers("long");
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h587dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw320dp-w320dp-h587dp-normal-long-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
@Test
- public void applyRules_greatHeightTriggersLong() throws Exception {
+ public void applyRules_greatHeightTriggersLong() {
applyQualifiers("h590dp");
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
- @Ignore("consider how to reset uiMode type") @Test
- public void shouldParseButNotDisplayNormal() throws Exception {
+ @Ignore("consider how to reset uiMode type")
+ @Test
+ public void shouldParseButNotDisplayNormal() {
applyQualifiers("car");
applyQualifiers("+normal");
assertThat(asQualifierString())
- .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
+ .isEqualTo(
+ "en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav");
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentApplicationInfoTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentApplicationInfoTest.java
new file mode 100644
index 000000000..be5870b31
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentApplicationInfoTest.java
@@ -0,0 +1,24 @@
+package org.robolectric.android.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Application;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@Config(application = AndroidTestEnvironmentApplicationInfoTest.ThisApplication.class)
+@RunWith(AndroidJUnit4.class)
+public final class AndroidTestEnvironmentApplicationInfoTest {
+
+ @Test
+ public void testApplicationInfoIncludesConfiguredAppClass() {
+ Application app = ApplicationProvider.getApplicationContext();
+ assertThat(app).isInstanceOf(ThisApplication.class);
+ assertThat(app.getApplicationInfo().className).isEqualTo(ThisApplication.class.getName());
+ }
+
+ public static final class ThisApplication extends Application {}
+}
diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
index dc6ac3138..0e6874c65 100644
--- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
@@ -26,6 +26,7 @@ import org.robolectric.annotation.Config;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.testing.TestApplication;
+import org.robolectric.util.ReflectionHelpers;
@RunWith(AndroidJUnit4.class)
public class AndroidTestEnvironmentCreateApplicationTest {
@@ -37,7 +38,7 @@ public class AndroidTestEnvironmentCreateApplicationTest {
assertThrows(
RuntimeException.class,
() ->
- AndroidTestEnvironment.createApplication(
+ createApplication(
newConfigWith(
"<application android:name=\"org.robolectric.BogusTestApplication\"/>)"),
null,
@@ -47,18 +48,18 @@ public class AndroidTestEnvironmentCreateApplicationTest {
@Test
public void shouldReturnDefaultAndroidApplicationWhenManifestDeclaresNoAppName()
throws Exception {
- Application application = AndroidTestEnvironment.createApplication(newConfigWith(""), null,
- new ApplicationInfo());
+ Application application = createApplication(newConfigWith(""), null, new ApplicationInfo());
assertThat(application.getClass()).isEqualTo(Application.class);
}
@Test
public void shouldReturnSpecifiedApplicationWhenManifestDeclaresAppName() throws Exception {
Application application =
- AndroidTestEnvironment.createApplication(
+ createApplication(
newConfigWith(
"<application android:name=\"org.robolectric.shadows.testing.TestApplication\"/>"),
- null, null);
+ null,
+ null);
assertThat(application.getClass()).isEqualTo(TestApplication.class);
}
@@ -85,8 +86,7 @@ public class AndroidTestEnvironmentCreateApplicationTest {
+ " </intent-filter>"
+ " </receiver>"
+ "</application>");
- Application application = AndroidTestEnvironment.createApplication(appManifest, null,
- new ApplicationInfo());
+ Application application = createApplication(appManifest, null, new ApplicationInfo());
shadowOf(application).callAttach(RuntimeEnvironment.systemContext);
registerBroadcastReceivers(application, appManifest, null);
@@ -109,27 +109,30 @@ public class AndroidTestEnvironmentCreateApplicationTest {
@Test
public void shouldLoadConfigApplicationIfSpecified() throws Exception {
Application application =
- AndroidTestEnvironment.createApplication(
+ createApplication(
newConfigWith("<application android:name=\"" + "ClassNameToIgnore" + "\"/>"),
- new Config.Builder().setApplication(TestFakeApp.class).build(), null);
+ new Config.Builder().setApplication(TestFakeApp.class).build(),
+ null);
assertThat(application.getClass()).isEqualTo(TestFakeApp.class);
}
@Test
public void shouldLoadConfigInnerClassApplication() throws Exception {
Application application =
- AndroidTestEnvironment.createApplication(
+ createApplication(
newConfigWith("<application android:name=\"" + "ClassNameToIgnore" + "\"/>"),
- new Config.Builder().setApplication(TestFakeAppInner.class).build(), null);
+ new Config.Builder().setApplication(TestFakeAppInner.class).build(),
+ null);
assertThat(application.getClass()).isEqualTo(TestFakeAppInner.class);
}
@Test
public void shouldLoadTestApplicationIfClassIsPresent() throws Exception {
Application application =
- AndroidTestEnvironment.createApplication(
+ createApplication(
newConfigWith("<application android:name=\"" + FakeApp.class.getName() + "\"/>"),
- null, null);
+ null,
+ null);
assertThat(application.getClass()).isEqualTo(TestFakeApp.class);
}
@@ -137,7 +140,7 @@ public class AndroidTestEnvironmentCreateApplicationTest {
public void shouldLoadPackageApplicationIfClassIsPresent() {
final ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.className = TestApplication.class.getCanonicalName();
- Application application = AndroidTestEnvironment.createApplication(null, null, applicationInfo);
+ Application application = createApplication(null, null, applicationInfo);
assertThat(application.getClass()).isEqualTo(TestApplication.class);
}
@@ -145,7 +148,7 @@ public class AndroidTestEnvironmentCreateApplicationTest {
public void shouldLoadTestPackageApplicationIfClassIsPresent() {
final ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.className = FakeApp.class.getCanonicalName();
- Application application = AndroidTestEnvironment.createApplication(null, null, applicationInfo);
+ Application application = createApplication(null, null, applicationInfo);
assertThat(application.getClass()).isEqualTo(TestFakeApp.class);
}
@@ -154,15 +157,15 @@ public class AndroidTestEnvironmentCreateApplicationTest {
try {
final ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.className = "org.robolectric.BogusTestApplication";
- AndroidTestEnvironment.createApplication(null, null, applicationInfo);
+ createApplication(null, null, applicationInfo);
fail();
- } catch (RuntimeException expected) { }
+ } catch (RuntimeException expected) {
+ }
}
@Test
public void whenNoAppManifestPresent_shouldCreateGenericApplication() {
- Application application = AndroidTestEnvironment.createApplication(null, null,
- new ApplicationInfo());
+ Application application = createApplication(null, null, new ApplicationInfo());
assertThat(application.getClass()).isEqualTo(Application.class);
}
@@ -190,4 +193,10 @@ public class AndroidTestEnvironmentCreateApplicationTest {
}
public static class TestFakeAppInner extends Application {}
+
+ private static Application createApplication(
+ AndroidManifest appManifest, Config config, ApplicationInfo applicationInfo) {
+ return ReflectionHelpers.callConstructor(
+ AndroidTestEnvironment.getApplicationClass(appManifest, config, applicationInfo));
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
index 375699417..54828ea5d 100644
--- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
@@ -121,7 +121,6 @@ public class AndroidTestEnvironmentTest {
@ConscryptMode(ON)
public void testWhenConscryptModeOn_ConscryptInstalled()
throws CertificateException, NoSuchAlgorithmException {
-
bootstrapWrapper.callSetUpApplicationState();
CertificateFactory factory = CertificateFactory.getInstance("X.509");
assertThat(factory.getProvider().getName()).isEqualTo("Conscrypt");
@@ -142,7 +141,6 @@ public class AndroidTestEnvironmentTest {
@ConscryptMode(OFF)
public void testWhenConscryptModeOff_ConscryptNotInstalled()
throws CertificateException, NoSuchAlgorithmException {
-
bootstrapWrapper.callSetUpApplicationState();
CertificateFactory factory = CertificateFactory.getInstance("X.509");
assertThat(factory.getProvider().getName()).isNotEqualTo("Conscrypt");
@@ -154,12 +152,12 @@ public class AndroidTestEnvironmentTest {
@Test
@ConscryptMode(OFF)
public void testWhenConscryptModeOff_BouncyCastleInstalled() throws GeneralSecurityException {
-
bootstrapWrapper.callSetUpApplicationState();
- MessageDigest digest = MessageDigest.getInstance("SHA256");
+ MessageDigest digest = MessageDigest.getInstance("SHA256", BouncyCastleProvider.PROVIDER_NAME);
assertThat(digest.getProvider().getName()).isEqualTo(BouncyCastleProvider.PROVIDER_NAME);
- Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
+ Cipher aesCipher =
+ Cipher.getInstance("AES/CBC/PKCS7Padding", BouncyCastleProvider.PROVIDER_NAME);
assertThat(aesCipher.getProvider().getName()).isEqualTo(BouncyCastleProvider.PROVIDER_NAME);
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java
index b1d848968..49dd13dbc 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java
@@ -9,6 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
@RunWith(AndroidJUnit4.class)
public final class AssociationInfoBuilderTest {
@@ -22,10 +23,20 @@ public final class AssociationInfoBuilderTest {
private static final boolean NOTIFY_ON_DEVICE_NEARBY = true;
private static final long APPROVED_MS = 1234L;
private static final long LAST_TIME_CONNECTED_MS = 5678L;
+ private static final int SYSTEM_DATA_SYNC_FALGS = 7;
@Test
@Config(minSdk = VERSION_CODES.TIRAMISU)
public void obtain() {
+ Object associatedDeviceValue = null;
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) {
+ try {
+ Class<?> associatedDeviceClazz = Class.forName("android.companion.AssociatedDevice");
+ associatedDeviceValue = ReflectionHelpers.newInstance(associatedDeviceClazz);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
AssociationInfo info =
AssociationInfoBuilder.newBuilder()
.setId(ID)
@@ -33,11 +44,13 @@ public final class AssociationInfoBuilderTest {
.setPackageName(PACKAGE_NAME)
.setDeviceMacAddress(DEVICE_MAC_ADDRESS)
.setDisplayName(DISPLAY_NAME)
+ .setAssociatedDevice(associatedDeviceValue)
.setDeviceProfile(DEVICE_PROFILE)
.setSelfManaged(SELF_MANAGED)
.setNotifyOnDeviceNearby(NOTIFY_ON_DEVICE_NEARBY)
.setApprovedMs(APPROVED_MS)
.setLastTimeConnectedMs(LAST_TIME_CONNECTED_MS)
+ .setSystemDataSyncFlags(SYSTEM_DATA_SYNC_FALGS)
.build();
assertThat(info.getId()).isEqualTo(ID);
@@ -50,5 +63,13 @@ public final class AssociationInfoBuilderTest {
assertThat(info.isNotifyOnDeviceNearby()).isEqualTo(NOTIFY_ON_DEVICE_NEARBY);
assertThat(info.getTimeApprovedMs()).isEqualTo(APPROVED_MS);
assertThat(info.getLastTimeConnectedMs()).isEqualTo(LAST_TIME_CONNECTED_MS);
+
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) {
+ Object associatedDevice = ReflectionHelpers.callInstanceMethod(info, "getAssociatedDevice");
+ assertThat(associatedDevice).isEqualTo(associatedDeviceValue);
+ int systemDataSyncFlags =
+ ReflectionHelpers.callInstanceMethod(info, "getSystemDataSyncFlags");
+ assertThat(systemDataSyncFlags).isEqualTo(SYSTEM_DATA_SYNC_FALGS);
+ }
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
index 06769c66e..f57b4842b 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
@@ -166,12 +166,39 @@ public class MediaCodecInfoBuilderTest {
@Test
@Config(minSdk = Q)
+ public void canCreateVideoEncoderCapabilities_supportedFormatResolutionRangeIsSet() {
+ MediaFormat formatWithResolutionRange = AVC_MEDIA_FORMAT;
+
+ final int kMinDimension = 64;
+ formatWithResolutionRange.setInteger(MediaFormat.KEY_WIDTH, kMinDimension);
+ formatWithResolutionRange.setInteger(MediaFormat.KEY_HEIGHT, kMinDimension);
+ formatWithResolutionRange.setInteger(MediaFormat.KEY_MAX_WIDTH, WIDTH);
+ formatWithResolutionRange.setInteger(MediaFormat.KEY_MAX_HEIGHT, HEIGHT);
+
+ CodecCapabilities codecCapabilities =
+ MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
+ .setMediaFormat(formatWithResolutionRange)
+ .setIsEncoder(true)
+ .setProfileLevels(AVC_PROFILE_LEVELS)
+ .setColorFormats(AVC_COLOR_FORMATS)
+ .build();
+
+ assertThat(codecCapabilities.getVideoCapabilities()).isNotNull();
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths())
+ .isEqualTo(new Range<>(kMinDimension, WIDTH));
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights())
+ .isEqualTo(new Range<>(kMinDimension, HEIGHT));
+ }
+
+ @Test
+ @Config(minSdk = Q)
public void canCreateVideoDecoderCapabilities() {
CodecCapabilities codecCapabilities =
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
.setMediaFormat(VP9_MEDIA_FORMAT)
.setProfileLevels(VP9_PROFILE_LEVELS)
.setColorFormats(VP9_COLOR_FORMATS)
+ .setRequiredFeatures(new String[] {CodecCapabilities.FEATURE_SecurePlayback})
.build();
assertThat(codecCapabilities.getMimeType()).isEqualTo(MIMETYPE_VIDEO_VP9);
@@ -180,6 +207,8 @@ public class MediaCodecInfoBuilderTest {
assertThat(codecCapabilities.getEncoderCapabilities()).isNull();
assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback))
.isTrue();
+ assertThat(codecCapabilities.isFeatureRequired(CodecCapabilities.FEATURE_SecurePlayback))
+ .isTrue();
assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_MultipleFrames))
.isTrue();
assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_DynamicTimestamp))
diff --git a/robolectric/src/test/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilderTest.java
new file mode 100644
index 000000000..0944ae35f
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilderTest.java
@@ -0,0 +1,122 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.telephony.CellIdentity;
+import android.telephony.DataSpecificRegistrationInfo;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.VoiceSpecificRegistrationInfo;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+
+/** Test for {@link NetworkRegistrationInfoTestBuilder}. */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Q)
+public class NetworkRegistrationInfoTestBuilderTest {
+
+ private final List<Integer> intList = new ArrayList<>();
+ @Mock protected CellIdentity cellIdentity;
+
+ @Test
+ public void testSetAccessNetworkTechnology_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setAccessNetworkTechnology(10).build();
+ assertThat(networkRegistrationInfo.getAccessNetworkTechnology()).isEqualTo(10);
+ }
+
+ @Test
+ public void testSetAvailableServices_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setAvailableServices(intList).build();
+ assertThat(networkRegistrationInfo.getAvailableServices()).isEqualTo(intList);
+ }
+
+ @Test
+ public void testSetCellIdentity_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setCellIdentity(cellIdentity).build();
+ assertThat(networkRegistrationInfo.getCellIdentity()).isEqualTo(cellIdentity);
+ }
+
+ @Test
+ public void testSetDataSpecificInfo_isSetInResultingObject() {
+ DataSpecificRegistrationInfo dataSpecificRegistrationInfo =
+ ReflectionHelpers.callConstructor(DataSpecificRegistrationInfo.class);
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder()
+ .setDataSpecificInfo(dataSpecificRegistrationInfo)
+ .build();
+ assertThat(networkRegistrationInfo.getDataSpecificInfo())
+ .isEqualTo(dataSpecificRegistrationInfo);
+ }
+
+ @Test
+ public void testSetDomain_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setDomain(10).build();
+ assertThat(networkRegistrationInfo.getDomain()).isEqualTo(10);
+ }
+
+ @Test
+ public void testSetEmergencyOnly_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setEmergencyOnly(true).build();
+ assertThat(networkRegistrationInfo.isEmergencyEnabled()).isEqualTo(true);
+ }
+
+ @Test
+ public void testSetRegistrationState_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setRegistrationState(10).build();
+ assertThat(networkRegistrationInfo.getRegistrationState()).isEqualTo(10);
+ }
+
+ @Test
+ public void testSetRejectCause_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setRejectCause(10).build();
+ assertThat(networkRegistrationInfo.getRejectCause()).isEqualTo(10);
+ }
+
+ @Test
+ public void testSetRoamingType_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setRoamingType(10).build();
+ assertThat(networkRegistrationInfo.getRoamingType()).isEqualTo(10);
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void testSetRegisteredPlmn_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setRegisteredPlmn("string").build();
+ assertThat(networkRegistrationInfo.getRegisteredPlmn()).isEqualTo("string");
+ }
+
+ @Test
+ public void testSetTransportType_isSetInResultingObject() {
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder().setTransportType(10).build();
+ assertThat(networkRegistrationInfo.getTransportType()).isEqualTo(10);
+ }
+
+ @Test
+ public void testSetVoiceSpecificInfo_isSetInResultingObject() {
+ VoiceSpecificRegistrationInfo voiceSpecificRegistrationInfo =
+ ReflectionHelpers.callConstructor(VoiceSpecificRegistrationInfo.class);
+ NetworkRegistrationInfo networkRegistrationInfo =
+ NetworkRegistrationInfoTestBuilder.newBuilder()
+ .setVoiceSpecificInfo(voiceSpecificRegistrationInfo)
+ .build();
+ assertThat(networkRegistrationInfo.getVoiceSpecificInfo())
+ .isEqualTo(voiceSpecificRegistrationInfo);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java
index 26d92bc5d..45541f5b4 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
import static com.google.common.truth.Truth.assertThat;
import android.os.Build;
@@ -14,7 +15,7 @@ import org.robolectric.annotation.Config;
/** Test for {@link PreciseDataConnectionStateBuilder} */
@RunWith(AndroidJUnit4.class)
-@Config(minSdk = Build.VERSION_CODES.R)
+@Config(minSdk = Build.VERSION_CODES.S)
public class PreciseDataConnectionStateBuilderTest {
@Test
public void build_preciseDataConnectionState() {
@@ -23,6 +24,7 @@ public class PreciseDataConnectionStateBuilderTest {
PreciseDataConnectionStateBuilder.newBuilder()
.setDataState(TelephonyManager.DATA_DISCONNECTED)
.setNetworkType(TelephonyManager.NETWORK_TYPE_LTE)
+ .setTransportType(TRANSPORT_TYPE_WWAN)
.setApnSetting(apnSetting)
.setDataFailCause(DataFailCause.IMEI_NOT_ACCEPTED)
.build();
@@ -30,6 +32,7 @@ public class PreciseDataConnectionStateBuilderTest {
assertThat(state).isNotNull();
assertThat(state.getState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED);
assertThat(state.getNetworkType()).isEqualTo(TelephonyManager.NETWORK_TYPE_LTE);
+ assertThat(state.getTransportType()).isEqualTo(TRANSPORT_TYPE_WWAN);
assertThat(state.getLastCauseCode()).isEqualTo(DataFailCause.IMEI_NOT_ACCEPTED);
assertThat(state.getApnSetting()).isEqualTo(apnSetting);
}
@@ -40,12 +43,14 @@ public class PreciseDataConnectionStateBuilderTest {
PreciseDataConnectionStateBuilder.newBuilder()
.setDataState(TelephonyManager.DATA_DISCONNECTED)
.setNetworkType(TelephonyManager.NETWORK_TYPE_LTE)
+ .setTransportType(TRANSPORT_TYPE_WWAN)
.setDataFailCause(DataFailCause.IMEI_NOT_ACCEPTED)
.build();
assertThat(state).isNotNull();
assertThat(state.getState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED);
assertThat(state.getNetworkType()).isEqualTo(TelephonyManager.NETWORK_TYPE_LTE);
+ assertThat(state.getTransportType()).isEqualTo(TRANSPORT_TYPE_WWAN);
assertThat(state.getLastCauseCode()).isEqualTo(DataFailCause.IMEI_NOT_ACCEPTED);
assertThat(state.getApnSetting()).isNull();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ServiceStateBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/ServiceStateBuilderTest.java
new file mode 100644
index 000000000..620ce51a4
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ServiceStateBuilderTest.java
@@ -0,0 +1,126 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+
+/** Test for {@link ShadowServiceState}. */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = P)
+public class ServiceStateBuilderTest {
+
+ static final int[] INT_ARRAY = {1, 2, 3};
+
+ @Test
+ public void testServiceStateBuilder_setVoiceRegStateAndBuild_isSetInResultingObject() {
+ // These public APIs expected to be available in all SDKs in range.
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setVoiceRegState(10).build();
+ assertThat(serviceState.getVoiceRegState()).isEqualTo(10);
+ }
+
+ @Test
+ public void testServiceStateBuilder_setDataRegStateAndBuild_isSetInResultingObject() {
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setDataRegState(10).build();
+ assertThat(serviceState.getDataRegState()).isEqualTo(10);
+ }
+
+ @Test
+ public void testServiceStateBuilder_setIsManualSelectionAndBuild_isSetInResultingObject() {
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setIsManualSelection(true).build();
+ assertThat(serviceState.getIsManualSelection()).isTrue();
+ }
+
+ @Test
+ public void testServiceStateBuilder_setRoamingAndBuild_isSetInResultingObject() {
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setRoaming(true).build();
+ assertThat(serviceState.getRoaming()).isTrue();
+ }
+
+ @Test
+ public void testServiceStateBuilder_setEmergencyOnlyAndBuild_isSetInResultingObject() {
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setEmergencyOnly(true).build();
+ assertThat(serviceState.isEmergencyOnly()).isTrue();
+ }
+
+ @Test
+ public void testServiceStateBuilder_setChannelNumberAndBuild_isSetInResultingObject() {
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setChannelNumber(10).build();
+ assertThat(serviceState.getChannelNumber()).isEqualTo(10);
+ }
+
+ @Test
+ public void testServiceStateBuilder_setCellBandwidthsAndBuild_isSetInResultingObject() {
+ ServiceState serviceState =
+ ServiceStateBuilder.newBuilder().setCellBandwidths(INT_ARRAY).build();
+ assertThat(serviceState.getCellBandwidths()).isEqualTo(INT_ARRAY);
+ }
+
+ @Config(minSdk = Q)
+ @Test
+ public void testServiceStateBuilder_setNrFrequencyRangeAndBuild_isSetInResultingObject() {
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setNrFrequencyRange(10).build();
+ assertThat(serviceState.getNrFrequencyRange()).isEqualTo(10);
+ }
+
+ @Config(minSdk = R)
+ @Test
+ public void testServiceStateBuilder_setOperatorNameAndBuild_isSetInResultingObject() {
+ ServiceState serviceState =
+ ServiceStateBuilder.newBuilder().setOperatorName("string", "string", "string").build();
+ assertThat(serviceState.getOperatorAlphaLong()).isEqualTo("string");
+ assertThat(serviceState.getOperatorAlphaShort()).isEqualTo("string");
+ assertThat(serviceState.getOperatorNumeric()).isEqualTo("string");
+ }
+
+ @Config(minSdk = R)
+ @Test
+ public void testServiceStateBuilder_setIwlanPreferredAndBuild_isSetInResultingObjectField() {
+ ServiceState serviceState = ServiceStateBuilder.newBuilder().setIwlanPreferred(true).build();
+ assertThat((boolean) ReflectionHelpers.getField(serviceState, "mIsIwlanPreferred")).isTrue();
+ }
+
+ @Config(minSdk = R)
+ @Test
+ public void
+ testServiceStateBuilder_setDataRoamingFromRegistrationAndBuild_isSetInResultingObjectField() {
+ ServiceState serviceState =
+ ServiceStateBuilder.newBuilder().setDataRoamingFromRegistration(true).build();
+ assertThat((boolean) ReflectionHelpers.getField(serviceState, "mIsDataRoamingFromRegistration"))
+ .isTrue();
+ }
+
+ @Config(sdk = {P})
+ @Test
+ public void
+ testServiceStateBuilder_setIsUsingCarrierAggregationAndBuild_isSetInResultingObjectField() {
+ ServiceState serviceState =
+ ServiceStateBuilder.newBuilder().setIsUsingCarrierAggregation(true).build();
+ assertThat((boolean) ReflectionHelpers.getField(serviceState, "mIsUsingCarrierAggregation"))
+ .isTrue();
+ }
+
+ @Config(minSdk = Q)
+ @Test
+ public void
+ testServiceStateBuilder_setNetworkRegistrationInfoListAndBuild_isSetInResultingObjectField() {
+ List<NetworkRegistrationInfo> networkRegistrationInfoList = new ArrayList<>();
+ networkRegistrationInfoList.add(new NetworkRegistrationInfo.Builder().build());
+ ServiceState serviceState =
+ ServiceStateBuilder.newBuilder()
+ .setNetworkRegistrationInfoList(networkRegistrationInfoList)
+ .build();
+ assertThat(serviceState.getNetworkRegistrationInfoList())
+ .isEqualTo(networkRegistrationInfoList);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataInputTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataInputTest.java
new file mode 100644
index 000000000..4b0359800
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataInputTest.java
@@ -0,0 +1,188 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataInputStream;
+import android.os.Build.VERSION_CODES;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = VERSION_CODES.LOLLIPOP)
+public final class ShadowBackupDataInputTest {
+
+ private static final String TEST_KEY_1 = "key_1";
+ private static final byte[] TEST_DATA_1 = {1, 2, 3, 4};
+ private static final String TEST_KEY_2 = "key_2";
+ private static final byte[] TEST_DATA_2 = {5, 6, 7, 8};
+
+ private final BackupDataInput backupDataInput =
+ BackupDataInputBuilder.newBuilder()
+ .addEntity(BackupDataEntity.create(TEST_KEY_1, TEST_DATA_1))
+ .addEntity(BackupDataEntity.create(TEST_KEY_2, TEST_DATA_2))
+ .build();
+
+ @Test
+ public void readNextHeader_onFirstItem_returnsTrue() throws IOException {
+ boolean result = backupDataInput.readNextHeader();
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void readNextHeader_afterReadData_returnsTrue() throws IOException {
+ backupDataInput.readNextHeader();
+ backupDataInput.readEntityData(new byte[TEST_DATA_1.length], 0, TEST_DATA_1.length);
+
+ boolean result = backupDataInput.readNextHeader();
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void readNextHeader_afterSkipData_returnsTrue() throws IOException {
+ backupDataInput.readNextHeader();
+ backupDataInput.skipEntityData();
+
+ boolean result = backupDataInput.readNextHeader();
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void readNextHeader_afterLastItem_returnsFalse() throws IOException {
+ backupDataInput.readNextHeader();
+ backupDataInput.skipEntityData();
+ backupDataInput.readNextHeader();
+ backupDataInput.skipEntityData();
+
+ boolean result = backupDataInput.readNextHeader();
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void readNextHeader_withoutReadOrSkipData_throwsException() throws IOException {
+ backupDataInput.readNextHeader();
+
+ assertThrows(IOException.class, backupDataInput::readNextHeader);
+ }
+
+ @Test
+ public void getKey_beforeReadNextHeader_throwsException() {
+ assertThrows(IllegalStateException.class, backupDataInput::getKey);
+ }
+
+ @Test
+ public void getKey_afterReadNextHeader_returnsKey() throws IOException {
+ backupDataInput.readNextHeader();
+
+ String key = backupDataInput.getKey();
+
+ assertThat(key).isEqualTo(TEST_KEY_1);
+ }
+
+ @Test
+ public void getDataSize_beforeReadNextHeader_throwsException() {
+ assertThrows(IllegalStateException.class, backupDataInput::getDataSize);
+ }
+
+ @Test
+ public void getDataSize_afterReadNextHeader_returnsArrayLength() throws IOException {
+ backupDataInput.readNextHeader();
+
+ int dataSize = backupDataInput.getDataSize();
+
+ assertThat(dataSize).isEqualTo(TEST_DATA_1.length);
+ }
+
+ @Test
+ public void readEntityData_afterLastItem_throwsException() throws IOException {
+ backupDataInput.readNextHeader();
+ backupDataInput.skipEntityData();
+ backupDataInput.readNextHeader();
+ backupDataInput.skipEntityData();
+ backupDataInput.readNextHeader();
+
+ assertThrows(
+ IllegalStateException.class,
+ () -> backupDataInput.readEntityData(TEST_DATA_1, 0, TEST_DATA_1.length));
+ }
+
+ @Test
+ public void readEntityData_afterAllBytesRead_returns0() throws IOException {
+ backupDataInput.readNextHeader();
+ backupDataInput.readEntityData(new byte[TEST_DATA_1.length], 0, TEST_DATA_1.length);
+
+ int result =
+ backupDataInput.readEntityData(new byte[TEST_DATA_1.length], 0, TEST_DATA_1.length);
+
+ assertThat(result).isEqualTo(0);
+ }
+
+ @Test
+ public void readEntityData_withSizeGreaterThanDestination_throwsException() throws IOException {
+ backupDataInput.readNextHeader();
+
+ assertThrows(
+ IOException.class,
+ () -> backupDataInput.readEntityData(new byte[2], 0, TEST_DATA_1.length));
+ }
+
+ @Test
+ public void readEntityData_withFullDataRead_copiesSourceData() throws IOException {
+ backupDataInput.readNextHeader();
+ byte[] data = new byte[TEST_DATA_1.length];
+
+ int result = backupDataInput.readEntityData(data, 0, data.length);
+
+ assertThat(result).isEqualTo(TEST_DATA_1.length);
+ assertThat(data).isEqualTo(TEST_DATA_1);
+ }
+
+ @Test
+ public void readEntityData_withPartialDataRead_copiesSourceData() throws IOException {
+ backupDataInput.readNextHeader();
+ byte[] data1 = new byte[2];
+ byte[] data2 = new byte[2];
+
+ int result1 = backupDataInput.readEntityData(data1, 0, 2);
+ int result2 = backupDataInput.readEntityData(data2, 0, 2);
+
+ assertThat(result1).isEqualTo(2);
+ assertThat(result2).isEqualTo(2);
+ assertThat(data1).isEqualTo(new byte[] {1, 2});
+ assertThat(data2).isEqualTo(new byte[] {3, 4});
+ }
+
+ @Test
+ public void readEntityData_usingBackupDataInputStream_matchesSourceData() throws IOException {
+ backupDataInput.readNextHeader();
+ InputStream inputStream =
+ new BufferedInputStream(
+ reflector(BackupDataInputStreamReflector.class).newInstance(backupDataInput), 1);
+ byte[] data = new byte[TEST_DATA_1.length];
+
+ int result = inputStream.read(data);
+
+ assertThat(result).isEqualTo(TEST_DATA_1.length);
+ assertThat(data).isEqualTo(TEST_DATA_1);
+ }
+
+ @ForType(BackupDataInputStream.class)
+ private interface BackupDataInputStreamReflector {
+
+ @Constructor
+ BackupDataInputStream newInstance(BackupDataInput backupDataInput);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataOutputTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataOutputTest.java
new file mode 100644
index 000000000..ac222a899
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataOutputTest.java
@@ -0,0 +1,103 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.app.backup.BackupDataOutput;
+import android.os.Build.VERSION_CODES;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.reflector.ForType;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = VERSION_CODES.LOLLIPOP)
+public final class ShadowBackupDataOutputTest {
+
+ private static final String TEST_PREFIX = "prefix";
+ private static final String TEST_KEY = "key";
+ private static final byte[] TEST_DATA = {1, 2, 3, 4};
+
+ private final BackupDataOutput backupDataOutput = BackupDataOutputFactory.newInstance();
+
+ @Test
+ public void writeEntityHeader_withUnfinishedData_throwsException() throws IOException {
+ backupDataOutput.writeEntityHeader(TEST_KEY, TEST_DATA.length);
+
+ assertThrows(IOException.class, () -> backupDataOutput.writeEntityHeader("key_2", 5));
+ }
+
+ @Test
+ public void writeEntityData_withKeyPrefix_hasPrefixInEntityKey() throws IOException {
+ reflector(BackupDataOutputReflector.class, backupDataOutput).setKeyPrefix(TEST_PREFIX);
+ backupDataOutput.writeEntityHeader(TEST_KEY, 0);
+
+ assertThat(shadowOf(backupDataOutput).getEntities())
+ .containsExactly(
+ BackupDataEntity.create(
+ TEST_PREFIX + ShadowBackupDataOutput.KEY_PREFIX_JOINER + TEST_KEY, new byte[0]));
+ }
+
+ @Test
+ public void writeEntityData_withNonNegativeDataSize_addsEntityOfSize() throws IOException {
+ backupDataOutput.writeEntityHeader(TEST_KEY, 5);
+
+ assertThat(shadowOf(backupDataOutput).getEntities())
+ .containsExactly(BackupDataEntity.create(TEST_KEY, new byte[5]));
+ }
+
+ @Test
+ public void writeEntityData_withNegativeDataSize_addsDeletedEntity() throws IOException {
+ backupDataOutput.writeEntityHeader(TEST_KEY, -1);
+
+ assertThat(shadowOf(backupDataOutput).getEntities())
+ .containsExactly(BackupDataEntity.createDeletedEntity(TEST_KEY));
+ }
+
+ @Test
+ public void writeEntityData_withGreaterSizeThanSource_throwsException() throws IOException {
+ backupDataOutput.writeEntityHeader(TEST_KEY, 10);
+
+ assertThrows(IOException.class, () -> backupDataOutput.writeEntityData(new byte[5], 10));
+ }
+
+ @Test
+ public void writeEntityData_withGreaterSizeThanDestination_throwsException() throws IOException {
+ backupDataOutput.writeEntityHeader(TEST_KEY, 2);
+
+ assertThrows(
+ IOException.class, () -> backupDataOutput.writeEntityData(TEST_DATA, TEST_DATA.length));
+ }
+
+ @Test
+ public void writeEntityData_withFullDataWrite_addsCorrectEntityData() throws IOException {
+ backupDataOutput.writeEntityHeader(TEST_KEY, TEST_DATA.length);
+
+ backupDataOutput.writeEntityData(TEST_DATA, TEST_DATA.length);
+
+ assertThat(shadowOf(backupDataOutput).getEntities())
+ .containsExactly(BackupDataEntity.create(TEST_KEY, TEST_DATA));
+ }
+
+ @Test
+ public void writeEntityData_withPartialDataWrites_addsCorrectEntityData() throws IOException {
+ backupDataOutput.writeEntityHeader(TEST_KEY, 4);
+
+ backupDataOutput.writeEntityData(Arrays.copyOfRange(TEST_DATA, 0, 2), 2);
+ backupDataOutput.writeEntityData(Arrays.copyOfRange(TEST_DATA, 2, 4), 2);
+
+ assertThat(shadowOf(backupDataOutput).getEntities())
+ .containsExactly(BackupDataEntity.create(TEST_KEY, TEST_DATA));
+ }
+
+ @ForType(BackupDataOutput.class)
+ private interface BackupDataOutputReflector {
+
+ void setKeyPrefix(String prefix);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
index 9fa2ef2b9..b94d94e94 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
@@ -13,32 +13,42 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
+import android.Manifest.permission;
import android.app.Application;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.le.BluetoothLeScanner;
import android.content.Intent;
+import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.time.Duration;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.LinkedBlockingQueue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.versioning.AndroidVersions.U;
/** Unit tests for {@link ShadowBluetoothAdapter} */
@RunWith(AndroidJUnit4.class)
@@ -600,7 +610,7 @@ public class ShadowBluetoothAdapterTest {
getApplicationContext(),
/* requestCode= */ 0,
new Intent("com.dummy.action.DUMMY_ACTION")
- .setPackage(getApplicationContext().getPackageName()),
+ .setPackage(getApplicationContext().getPackageName()),
/* flags= */ PendingIntent.FLAG_MUTABLE)));
}
@@ -777,6 +787,76 @@ public class ShadowBluetoothAdapterTest {
.isEqualTo(BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED);
}
+ @Config(minSdk = U.SDK_INT)
+ @Test
+ public void getProfileProxy_serviceListenerInvoked() throws Exception {
+ shadowOf((Application) getApplicationContext()).grantPermissions(permission.BLUETOOTH);
+ bluetoothAdapter.enable();
+ LinkedBlockingQueue<Integer> profileQueue = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<BluetoothProfile> proxyQueue = new LinkedBlockingQueue<>();
+ BluetoothProfile.ServiceListener listener =
+ new ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ profileQueue.add(profile);
+ proxyQueue.add(proxy);
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {}
+ };
+
+ assertThat(
+ bluetoothAdapter.getProfileProxy(
+ getApplicationContext(), listener, BluetoothProfile.HEADSET))
+ .isTrue();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(profileQueue.take()).isEqualTo(BluetoothProfile.HEADSET);
+ assertThat(proxyQueue.take()).isInstanceOf(BluetoothHeadset.class);
+ }
+
+ @Config(minSdk = U.SDK_INT)
+ @Test
+ public void getProfileProxy_adapterDisabled_serviceListenerNotInvoked() {
+ shadowOf((Application) getApplicationContext()).grantPermissions(permission.BLUETOOTH);
+ BluetoothProfile.ServiceListener listener =
+ Mockito.mock(BluetoothProfile.ServiceListener.class);
+
+ bluetoothAdapter.getProfileProxy(getApplicationContext(), listener, BluetoothProfile.HEADSET);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ verify(listener, never()).onServiceConnected(anyInt(), any(BluetoothProfile.class));
+ }
+
+ @Config(minSdk = U.SDK_INT)
+ @Test
+ public void disconnectProfileProxy_serviceListenerInvoked() throws Exception {
+ shadowOf((Application) getApplicationContext()).grantPermissions(permission.BLUETOOTH);
+ bluetoothAdapter.enable();
+ LinkedBlockingQueue<Integer> profileQueue = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<BluetoothHeadset> headsetProxies = new LinkedBlockingQueue<>();
+ BluetoothProfile.ServiceListener listener =
+ new ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ headsetProxies.add((BluetoothHeadset) proxy);
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ profileQueue.add(profile);
+ }
+ };
+
+ bluetoothAdapter.getProfileProxy(getApplicationContext(), listener, BluetoothProfile.HEADSET);
+ shadowOf(Looper.getMainLooper()).idle();
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, headsetProxies.take());
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(profileQueue.take()).isEqualTo(BluetoothProfile.HEADSET);
+ }
+
private PendingIntent createTestPendingIntent(Intent intent) {
return PendingIntent.getBroadcast(
getApplicationContext(), /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java
index 1216595c7..cdac21f20 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java
@@ -109,6 +109,16 @@ public class ShadowBluetoothGattServerTest {
}
@Test
+ public void test_getResponses_acceptsNull() {
+ shadowOf(server).sendResponse(device, 0, 0, 0, RESPONSE_VALUE1);
+ assertThat(shadowOf(server).getResponses()).hasSize(1);
+ shadowOf(server).sendResponse(device, 0, 0, 0, null);
+ assertThat(shadowOf(server).getResponses()).hasSize(2);
+ assertThat(shadowOf(server).getResponses().get(0)).isEqualTo(RESPONSE_VALUE1);
+ assertThat(shadowOf(server).getResponses().get(1)).isEqualTo(null);
+ }
+
+ @Test
public void test_isConnectedToDevice_initially() {
assertThat(shadowOf(server).isConnectedToDevice(device)).isFalse();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java
index 6586ad668..88ee76029 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java
@@ -17,6 +17,12 @@ import org.robolectric.annotation.Config;
public class ShadowBuildTest {
@Test
+ public void setBoard() {
+ ShadowBuild.setBoard("test_board");
+ assertThat(Build.BOARD).isEqualTo("test_board");
+ }
+
+ @Test
public void setDevice() {
ShadowBuild.setDevice("test_device");
assertThat(Build.DEVICE).isEqualTo("test_device");
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java
index e718ab2fb..88ff63cea 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import static android.content.ClipboardManager.OnPrimaryClipChangedListener;
+import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
@@ -10,8 +11,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
+import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.time.Duration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -126,4 +129,27 @@ public class ShadowClipboardManagerTest {
verify(listener).onPrimaryClipChanged();
}
+
+ @Test
+ @Config(minSdk = O)
+ public void shouldSetTimestampForClip() {
+ long currentUptimeMs = SystemClock.uptimeMillis();
+ ShadowSystemClock.advanceBy(Duration.ofSeconds(47));
+ ClipData clip = ClipData.newPlainText(null, "BLARG?");
+ clipboardManager.setPrimaryClip(clip);
+ assertThat(clipboardManager.getPrimaryClipDescription()).isNotNull();
+ assertThat(clipboardManager.getPrimaryClipDescription().getTimestamp())
+ .isEqualTo(currentUptimeMs + 47 * 1000);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void shouldSetTimestampForText() {
+ long currentUptimeMs = SystemClock.uptimeMillis();
+ ShadowSystemClock.advanceBy(Duration.ofSeconds(42));
+ clipboardManager.setText("BLARG!!!");
+ assertThat(clipboardManager.getPrimaryClipDescription()).isNotNull();
+ assertThat(clipboardManager.getPrimaryClipDescription().getTimestamp())
+ .isEqualTo(currentUptimeMs + 42 * 1000);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java
index e49cfcdc7..f70af70e7 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES;
import static android.os.Build.VERSION_CODES.O;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
@@ -7,10 +8,10 @@ import static org.junit.Assert.assertThrows;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
-import android.companion.AssociatedDevice;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
+import android.companion.DeviceNotAssociatedException;
import android.content.ComponentName;
import android.content.IntentSender;
import android.net.MacAddress;
@@ -30,16 +31,18 @@ import org.robolectric.util.ReflectionHelpers.ClassParameter;
public class ShadowCompanionDeviceManagerTest {
private static final String MAC_ADDRESS = "AA:BB:CC:DD:FF:EE";
+ private static final String PACKAGE_NAME = "org.robolectric";
+ private final Application application = getApplicationContext();
private CompanionDeviceManager companionDeviceManager;
private ShadowCompanionDeviceManager shadowCompanionDeviceManager;
private ComponentName componentName;
@Before
public void setUp() throws Exception {
- companionDeviceManager = getApplicationContext().getSystemService(CompanionDeviceManager.class);
+ companionDeviceManager = application.getSystemService(CompanionDeviceManager.class);
shadowCompanionDeviceManager = shadowOf(companionDeviceManager);
- componentName = new ComponentName(getApplicationContext(), Application.class);
+ componentName = new ComponentName(application, Application.class);
}
@Test
@@ -121,22 +124,67 @@ public class ShadowCompanionDeviceManagerTest {
@Test
@Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testAddAssociation_byAssociationInfo_defaultValue() {
+ AssociationInfoBuilder infoBuilder =
+ AssociationInfoBuilder.newBuilder()
+ .setId(1)
+ .setUserId(1)
+ .setDeviceMacAddress(MAC_ADDRESS)
+ .setDisplayName("displayName")
+ .setSystemDataSyncFlags(-1);
+ AssociationInfo info = infoBuilder.build();
+
+ AssociationInfoBuilder expectedInfoBuilder =
+ AssociationInfoBuilder.newBuilder()
+ .setId(1)
+ .setUserId(1)
+ .setDeviceMacAddress(MAC_ADDRESS)
+ .setDisplayName("displayName")
+ .setSelfManaged(false)
+ .setNotifyOnDeviceNearby(false)
+ .setApprovedMs(0)
+ .setLastTimeConnectedMs(0)
+ .setSystemDataSyncFlags(-1);
+ AssociationInfo expectedInfo = expectedInfoBuilder.build();
+ assertThat(companionDeviceManager.getAssociations()).isEmpty();
+ shadowCompanionDeviceManager.addAssociation(info);
+ assertThat(companionDeviceManager.getMyAssociations()).contains(expectedInfo);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
public void testAddAssociation_byAssociationInfo() {
- AssociationInfo info =
- new AssociationInfo(
- /* id= */ 1,
- /* userId= */ 1,
- "packageName",
- MacAddress.fromString(MAC_ADDRESS),
- "displayName",
- "deviceProfile",
- /* AssociatedDevice*/ null,
- /* selfManaged= */ false,
- /* notifyOnDeviceNearby= */ false,
- /* revoked */ false,
- /* timeApprovedMs= */ 0,
- /* lastTimeConnectedMs= */ 0,
- /* systemDataSyncFlags= */ -1);
+ AssociationInfoBuilder infoBuilder =
+ AssociationInfoBuilder.newBuilder()
+ .setId(1)
+ .setUserId(1)
+ .setPackageName("packageName")
+ .setDeviceMacAddress(MAC_ADDRESS)
+ .setDisplayName("displayName")
+ .setSelfManaged(false)
+ .setNotifyOnDeviceNearby(false)
+ .setApprovedMs(0)
+ .setLastTimeConnectedMs(0);
+ Object associatedDeviceValue = null;
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) {
+ try {
+ Class<?> associatedDeviceClazz = Class.forName("android.companion.AssociatedDevice");
+ associatedDeviceValue = ReflectionHelpers.newInstance(associatedDeviceClazz);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ infoBuilder = infoBuilder.setAssociatedDevice(associatedDeviceValue);
+ }
+ int systemDataSyncFlagsValue = 1;
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mSystemDataSyncFlags")) {
+ infoBuilder = infoBuilder.setSystemDataSyncFlags(systemDataSyncFlagsValue);
+ }
+ AssociationInfo info = infoBuilder.build();
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mSystemDataSyncFlags")) {
+ int systemDataSyncFlags =
+ ReflectionHelpers.callInstanceMethod(info, "getSystemDataSyncFlags");
+ assertThat(systemDataSyncFlags).isEqualTo(systemDataSyncFlagsValue);
+ }
assertThat(companionDeviceManager.getAssociations()).isEmpty();
shadowCompanionDeviceManager.addAssociation(info);
assertThat(companionDeviceManager.getMyAssociations()).contains(info);
@@ -201,6 +249,102 @@ public class ShadowCompanionDeviceManagerTest {
companionDeviceManager, "notifyDeviceAppeared", ClassParameter.from(int.class, 1));
}
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testStartObservingDevicePresence_deviceNotAssociated_throwsException() {
+ assertThrows(
+ DeviceNotAssociatedException.class,
+ () -> companionDeviceManager.startObservingDevicePresence(MAC_ADDRESS));
+ assertThat(shadowCompanionDeviceManager.getLastObservingDevicePresenceDeviceAddress())
+ .isEqualTo(MAC_ADDRESS);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testStartObservingDevicePresence_deviceAssociated_presenceObserved() {
+ shadowCompanionDeviceManager.addAssociation(MAC_ADDRESS);
+
+ companionDeviceManager.startObservingDevicePresence(MAC_ADDRESS);
+ assertThat(shadowCompanionDeviceManager.getLastObservingDevicePresenceDeviceAddress())
+ .isEqualTo(MAC_ADDRESS);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void
+ testGetLastObservingDevicePresenceDeviceAddress_startObservingDevicePresenceNotCalled_returnsNull() {
+ assertThat(shadowCompanionDeviceManager.getLastObservingDevicePresenceDeviceAddress()).isNull();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testAssociate_systemApi_deviceAssociated() {
+ MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS);
+ shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES);
+
+ companionDeviceManager.associate(PACKAGE_NAME, macAddress, new byte[] {0x01});
+ assertThat(companionDeviceManager.getAssociations()).contains(macAddress.toString());
+ assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress())
+ .isEqualTo(macAddress);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testGetLastSystemApiAssociationMacAddress_associateCalled_returnsLastMacAddress() {
+ MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS);
+ shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES);
+
+ companionDeviceManager.associate(PACKAGE_NAME, macAddress, new byte[] {0x01});
+ assertThat(companionDeviceManager.getAssociations()).contains(macAddress.toString());
+ assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress())
+ .isEqualTo(macAddress);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testGetLastSystemApiAssociationMacAddress_associateNotCalled_returnsNull() {
+ assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress()).isNull();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testAssociate_systemApi_permissionDeniedDeviceNotAssociated() {
+ MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS);
+ shadowOf(application).denyPermissions(ASSOCIATE_COMPANION_DEVICES);
+
+ assertThrows(
+ SecurityException.class,
+ () -> companionDeviceManager.associate(PACKAGE_NAME, macAddress, new byte[] {0x01}));
+ assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress())
+ .isEqualTo(macAddress);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testAssociate_systemApi_badPackageNameDeviceNotAssociated() {
+ MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS);
+ shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES);
+
+ assertThrows(
+ SecurityException.class,
+ () -> companionDeviceManager.associate("some.package", macAddress, new byte[] {0x01}));
+ assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress())
+ .isEqualTo(macAddress);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testAssociate_systemApi_badCertificateDeviceNotAssociated() {
+ MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS);
+ shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES);
+
+ assertThrows(
+ SecurityException.class,
+ () -> companionDeviceManager.associate(PACKAGE_NAME, macAddress, null));
+ assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress())
+ .isEqualTo(macAddress);
+ }
+
private CompanionDeviceManager.Callback createCallback() {
return new CompanionDeviceManager.Callback() {
@Override
@@ -210,4 +354,39 @@ public class ShadowCompanionDeviceManagerTest {
public void onFailure(CharSequence error) {}
};
}
+
+ /** Create {@link AssociationInfo}. */
+ private AssociationInfo createDefaultAssociationInfo() {
+ AssociationInfoBuilder aiBuilder = AssociationInfoBuilder.newBuilder()
+ .setId(1)
+ .setUserId(1)
+ .setPackageName("packageName")
+ .setDeviceMacAddress(MAC_ADDRESS)
+ .setDisplayName("displayName")
+ .setDeviceProfile("deviceProfile")
+ .setSelfManaged(false)
+ .setNotifyOnDeviceNearby(false)
+ .setApprovedMs(0)
+ .setLastTimeConnectedMs(0);
+
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mTag")) {
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder, "setTag", ClassParameter.from(String.class, "tag"));
+ }
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) {
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder,
+ "setAssociatedDevice",
+ ClassParameter.from(Object.class, null));
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder,
+ "setSystemDataSyncFlags",
+ ClassParameter.from(int.class, -1));
+ }
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) {
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder, "setRevoked", ClassParameter.from(boolean.class, false));
+ }
+ return aiBuilder.build();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java
index f9721e464..1459a2362 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static com.google.common.truth.Truth.assertThat;
import android.icu.text.DateFormat;
@@ -16,9 +15,10 @@ import android.text.format.DateIntervalFormat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
+import org.robolectric.versioning.AndroidVersions.U;
@RunWith(AndroidJUnit4.class)
-@Config(minSdk = UPSIDE_DOWN_CAKE)
+@Config(minSdk = U.SDK_INT)
public class ShadowDateIntervalFormatTest {
@Test
public void testDateInterval_FormatDateRange() throws ParseException {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java
index 9d8a03881..7dcc6ad10 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.graphics.Rect;
@@ -10,6 +11,8 @@ import android.view.displayhash.DisplayHashManager;
import android.view.displayhash.VerifiedDisplayHash;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,11 +34,26 @@ public final class ShadowDisplayHashManagerTest {
@Test
public void getSupportedHashAlgorithms() {
+ // Default value is PHASH
assertThat(displayHashManager.getSupportedHashAlgorithms()).containsExactly("PHASH");
+
+ ShadowDisplayHashManager.setSupportedHashAlgorithms(ImmutableSet.of("TESTHASH"));
+ assertThat(displayHashManager.getSupportedHashAlgorithms()).containsExactly("TESTHASH");
}
@Test
public void verifyDisplayHash() {
+ DisplayHash displayHash = createDisplayHash();
+
+ assertThat(displayHashManager.verifyDisplayHash(displayHash)).isNull();
+
+ VerifiedDisplayHash verifiedDisplayHash =
+ new VerifiedDisplayHash(54321L, new Rect(0, 0, 100, 100), "PHASH", new byte[8]);
+ ShadowDisplayHashManager.setVerifyDisplayHashResult(verifiedDisplayHash);
+ assertThat(displayHashManager.verifyDisplayHash(displayHash)).isEqualTo(verifiedDisplayHash);
+ }
+
+ private DisplayHash createDisplayHash() {
Parcel parcel = Parcel.obtain();
parcel.writeLong(12345L);
parcel.writeTypedObject(new Rect(0, 0, 100, 100), 0);
@@ -43,13 +61,17 @@ public final class ShadowDisplayHashManagerTest {
parcel.writeByteArray(new byte[15]);
parcel.writeByteArray(new byte[21]);
parcel.setDataPosition(0);
- DisplayHash displayHash = DisplayHash.CREATOR.createFromParcel(parcel);
+ return DisplayHash.CREATOR.createFromParcel(parcel);
+ }
- assertThat(displayHashManager.verifyDisplayHash(displayHash)).isNull();
+ @Test
+ public void testSetSupportedHashAlgorithmsToNull() {
+ Set<String> previousSupportedHashAlgorithms = displayHashManager.getSupportedHashAlgorithms();
+ ShadowDisplayHashManager.setSupportedHashAlgorithms(previousSupportedHashAlgorithms);
+ ShadowDisplayHashManager.setSupportedHashAlgorithms(null);
+ assertThrows(NullPointerException.class, () -> displayHashManager.getSupportedHashAlgorithms());
- VerifiedDisplayHash verifiedDisplayHash =
- new VerifiedDisplayHash(54321L, new Rect(0, 0, 100, 100), "PHASH", new byte[8]);
- ShadowDisplayHashManager.setVerifyDisplayHashResult(verifiedDisplayHash);
- assertThat(displayHashManager.verifyDisplayHash(displayHash)).isEqualTo(verifiedDisplayHash);
+ // Restore previous value
+ ShadowDisplayHashManager.setSupportedHashAlgorithms(previousSupportedHashAlgorithms);
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java
index 3cb4ae992..ad8d1c1d0 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java
@@ -6,6 +6,7 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -38,6 +39,30 @@ public class ShadowEnvironmentTest {
}
@Test
+ @Config(minSdk = R)
+ public void getStorageDirectory_storageDirectoryUnset_shouldReturnDefaultDirectory() {
+ assertThat(Environment.getStorageDirectory().getAbsolutePath()).isEqualTo("/storage");
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void setStorageDirectory_shouldReturnDirectory() {
+ // state prior to override
+ File defaultDir = Environment.getStorageDirectory();
+ // override
+ Path expectedPath = FileSystems.getDefault().getPath("/tmp", "foo");
+ ShadowEnvironment.setStorageDirectory(expectedPath);
+ File override = Environment.getStorageDirectory();
+ assertThat(override.getAbsolutePath()).isEqualTo(expectedPath.toAbsolutePath().toString());
+
+ // restore default value by supplying {@code null}
+ ShadowEnvironment.setStorageDirectory(null);
+
+ // verify default
+ assertThat(defaultDir).isEqualTo(Environment.getStorageDirectory());
+ }
+
+ @Test
public void getExternalStorageDirectory_shouldReturnDirectory() {
assertThat(Environment.getExternalStorageDirectory().exists()).isTrue();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowImageDecoderTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowImageDecoderTest.java
new file mode 100644
index 000000000..a4e4a5c49
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowImageDecoderTest.java
@@ -0,0 +1,76 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.ImageDecoder;
+import android.os.Build.VERSION_CODES;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.R;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.GraphicsMode;
+import org.robolectric.annotation.GraphicsMode.Mode;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = VERSION_CODES.P)
+@GraphicsMode(Mode.LEGACY) // Non-legacy native mode is tested in ShadowNativeImageDecoderTest
+public class ShadowImageDecoderTest {
+
+ private final Context context = RuntimeEnvironment.getApplication();
+
+ @Test
+ public void mimeType_png_returnsPng() throws Exception {
+ Bitmap bmp =
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(context.getResources(), R.drawable.an_image),
+ (imageDecoder, imageInfo, source) -> {
+ assertThat(imageInfo.getSize().getHeight()).isEqualTo(53);
+ assertThat(imageInfo.getSize().getWidth()).isEqualTo(64);
+ assertThat(imageInfo.getMimeType()).isEqualTo("image/png");
+ });
+ assertThat(bmp).isNotNull();
+ }
+
+ @Test
+ public void mimeType_jpg_returnsJpg() throws Exception {
+ Bitmap bmp =
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(context.getResources(), R.drawable.test_jpeg),
+ (imageDecoder, imageInfo, source) -> {
+ assertThat(imageInfo.getSize().getHeight()).isEqualTo(50);
+ assertThat(imageInfo.getSize().getWidth()).isEqualTo(50);
+ assertThat(imageInfo.getMimeType()).isEqualTo("image/jpeg");
+ });
+ assertThat(bmp).isNotNull();
+ }
+
+ @Test
+ public void mimeType_gif_returnsGif() throws Exception {
+ Bitmap bmp =
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(context.getResources(), R.drawable.an_other_image),
+ (imageDecoder, imageInfo, source) -> {
+ assertThat(imageInfo.getSize().getWidth()).isEqualTo(32);
+ assertThat(imageInfo.getSize().getHeight()).isEqualTo(18);
+ assertThat(imageInfo.getMimeType()).isEqualTo("image/gif");
+ });
+ assertThat(bmp).isNotNull();
+ }
+
+ @Test
+ public void mimeType_webp_returnsWebp() throws Exception {
+ Bitmap bmp =
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(context.getResources(), R.drawable.test_webp),
+ (imageDecoder, imageInfo, source) -> {
+ // The Legacy decoder doesn't support WebP images so it'll return unknown MIME type.
+ assertEquals("image/unknown", imageInfo.getMimeType());
+ });
+ assertThat(bmp).isNotNull();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
index f0196218f..cffc091b9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
@@ -8,6 +8,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.annotation.SuppressLint;
import android.os.Build.VERSION_CODES;
+import android.telephony.AccessNetworkConstants;
import android.telephony.ims.ImsException;
import android.telephony.ims.ImsMmTelManager;
import android.telephony.ims.ImsMmTelManager.CapabilityCallback;
@@ -17,22 +18,28 @@ import android.telephony.ims.RegistrationManager;
import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.ArraySet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
/** Tests for {@link ShadowImsMmTelManager} */
@RunWith(RobolectricTestRunner.class)
@Config(minSdk = VERSION_CODES.Q)
public class ShadowImsMmTelManagerTest {
+ private static final int SUBSCRIPTION_ID = 5;
+
private ShadowImsMmTelManager shadowImsMmTelManager;
@Before
public void setup() {
- shadowImsMmTelManager = new ShadowImsMmTelManager();
+ shadowImsMmTelManager =
+ Shadow.extract(ImsMmTelManager.createForSubscriptionId(SUBSCRIPTION_ID));
}
@Test
@@ -241,6 +248,47 @@ public class ShadowImsMmTelManagerTest {
}
@Test
+ public void getRegistrationState_setAsRegistered_returnsRegistrationStateRegistered() {
+ AtomicInteger registrationState = new AtomicInteger();
+ Consumer<Integer> stateCallback = registrationState::set;
+ ShadowImsMmTelManager.setRegistrationState(
+ SUBSCRIPTION_ID, RegistrationManager.REGISTRATION_STATE_REGISTERED);
+
+ shadowImsMmTelManager.getRegistrationState(Runnable::run, stateCallback);
+
+ assertThat(registrationState.intValue())
+ .isEqualTo(RegistrationManager.REGISTRATION_STATE_REGISTERED);
+ }
+
+ @Test
+ public void getRegistrationStateCallback() {
+ Consumer<Integer> stateCallback = state -> {};
+ shadowImsMmTelManager.getRegistrationState(Runnable::run, stateCallback);
+ assertThat(shadowImsMmTelManager.getRegistrationStateCallback()).isEqualTo(stateCallback);
+ }
+
+ @Test
+ public void getRegistrationTransportType_setAsWlan_returnsTransportTypeWlan() {
+ AtomicInteger registrationTransportType = new AtomicInteger();
+ Consumer<Integer> stateCallback = registrationTransportType::set;
+ ShadowImsMmTelManager.setRegistrationTransportType(
+ SUBSCRIPTION_ID, AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
+
+ shadowImsMmTelManager.getRegistrationTransportType(Runnable::run, stateCallback);
+
+ assertThat(registrationTransportType.intValue())
+ .isEqualTo(AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
+ }
+
+ @Test
+ public void getRegistrationTransportTypeCallback() {
+ Consumer<Integer> transportTypeCallback = state -> {};
+ shadowImsMmTelManager.getRegistrationTransportType(Runnable::run, transportTypeCallback);
+ assertThat(shadowImsMmTelManager.getRegistrationTransportTypeCallback())
+ .isEqualTo(transportTypeCallback);
+ }
+
+ @Test
public void
registerMmTelCapabilityCallback_imsRegistered_availabilityChange_onCapabilitiesStatusChangedInvoked()
throws ImsException {
@@ -474,15 +522,40 @@ public class ShadowImsMmTelManagerTest {
assertThat(imsMmTelManager1).isEqualTo(ShadowImsMmTelManager.createForSubscriptionId(1));
assertThat(imsMmTelManager2).isEqualTo(ShadowImsMmTelManager.createForSubscriptionId(2));
- ShadowImsMmTelManager.clearExistingInstances();
+ ShadowImsMmTelManager.clearExistingInstancesAndStates();
assertThat(imsMmTelManager1).isNotEqualTo(ShadowImsMmTelManager.createForSubscriptionId(1));
assertThat(imsMmTelManager2).isNotEqualTo(ShadowImsMmTelManager.createForSubscriptionId(2));
}
@Test
+ public void clearExistingInstancesAndStates_statesAreCleared() {
+ AtomicInteger registrationState = new AtomicInteger();
+ Consumer<Integer> stateCallback = registrationState::set;
+ ShadowImsMmTelManager.setRegistrationState(
+ SUBSCRIPTION_ID, RegistrationManager.REGISTRATION_STATE_REGISTERED);
+
+ ShadowImsMmTelManager.clearExistingInstancesAndStates();
+ shadowImsMmTelManager.getRegistrationState(Runnable::run, stateCallback);
+
+ assertThat(registrationState.intValue()).isEqualTo(0);
+ }
+
+ @Test
+ public void clearExistingInstancesAndStates_typesAreCleared() {
+ AtomicInteger registrationTransportType = new AtomicInteger();
+ Consumer<Integer> stateCallback = registrationTransportType::set;
+ ShadowImsMmTelManager.setRegistrationTransportType(
+ SUBSCRIPTION_ID, AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
+
+ ShadowImsMmTelManager.clearExistingInstancesAndStates();
+ shadowImsMmTelManager.getRegistrationTransportType(Runnable::run, stateCallback);
+
+ assertThat(registrationTransportType.intValue()).isEqualTo(0);
+ }
+
+ @Test
public void getSubscriptionId() {
- shadowImsMmTelManager.__constructor__(5);
- assertThat(shadowImsMmTelManager.getSubscriptionId()).isEqualTo(5);
+ assertThat(shadowImsMmTelManager.getSubscriptionId()).isEqualTo(SUBSCRIPTION_ID);
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java
index daff15fda..a707463d1 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java
@@ -86,9 +86,7 @@ public class ShadowInCallServiceTest {
@Test
@TargetApi(P)
- @Config(
- minSdk = P,
- shadows = {ShadowBluetoothDevice.class})
+ @Config(minSdk = P)
public void requestBluetoothAudio_getBluetoothAudio() {
BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance("00:11:22:33:AA:BB");
ShadowInCallService shadowInCallService = shadowOf(inCallService);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java
new file mode 100644
index 000000000..a9fb74699
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java
@@ -0,0 +1,111 @@
+package org.robolectric.shadows;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import java.time.Duration;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+
+@LooperMode(Mode.INSTRUMENTATION_TEST)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowInstrumentationTestLooperTest {
+
+ @Test
+ @Config(minSdk = 18)
+ public void testThreadIsNotMainThread() {
+ assertFalse(Looper.getMainLooper().isCurrentThread());
+ }
+
+ @Test
+ public void idle() throws InterruptedException {
+ ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper());
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ AtomicBoolean hasRun = new AtomicBoolean(false);
+ mainHandler.post(() -> hasRun.set(true));
+ shadowMainLooper.idle();
+ assertTrue(hasRun.get());
+ }
+
+ @Test
+ public void pauseMainLooper() {
+ ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper());
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ shadowMainLooper.pause();
+ AtomicBoolean hasRun = new AtomicBoolean(false);
+ mainHandler.post(() -> hasRun.set(true));
+ assertFalse(hasRun.get());
+ shadowMainLooper.idle();
+ assertTrue(hasRun.get());
+ }
+
+ @Test
+ public void unpauseMainLooper() throws InterruptedException {
+ ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper());
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ shadowMainLooper.pause();
+ CountDownLatch hasRun = new CountDownLatch(1);
+ mainHandler.post(hasRun::countDown);
+ assertEquals(1, hasRun.getCount());
+ shadowMainLooper.unPause();
+ assertTrue(hasRun.await(2, SECONDS));
+ }
+
+ @Test
+ public void idleFor() {
+ ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper());
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ AtomicBoolean hasRun = new AtomicBoolean(false);
+ mainHandler.postDelayed(() -> hasRun.set(true), 99);
+ assertFalse(hasRun.get());
+ shadowMainLooper.idleFor(Duration.ofMillis(100));
+ assertTrue(hasRun.get());
+ }
+
+ @Test
+ public void exceptionOnMainThreadPropagated() throws InterruptedException {
+ ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper());
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ mainHandler.post(
+ () -> {
+ throw new RuntimeException("Exception should be propagated!");
+ });
+ assertThrows(RuntimeException.class, () -> shadowMainLooper.idle());
+
+ // Restore main looper and main thread to avoid error at tear down
+ Looper.getMainLooper().getThread().join();
+ ShadowPausedLooper.resetLoopers();
+ }
+
+ @Test
+ public void backgroundLooperCrash() throws InterruptedException {
+ HandlerThread ht = new HandlerThread("backgroundLooperCrash");
+ ht.start();
+ Handler handler = new Handler(ht.getLooper());
+ handler.post(
+ () -> {
+ throw new RuntimeException();
+ });
+ ht.join();
+
+ assertThrows(IllegalStateException.class, () -> handler.post(() -> {}));
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java
index 8fca386d0..2e549b727 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java
@@ -18,25 +18,40 @@ import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
@Config(minSdk = LOLLIPOP)
public class ShadowJobServiceTest {
-
private JobService jobService;
- @Mock
- private JobParameters params;
+ @Mock private JobParameters params;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- jobService = new JobService() {
- @Override
- public boolean onStartJob(JobParameters params) {
- return false;
- }
-
- @Override
- public boolean onStopJob(JobParameters params) {
- return false;
- }
- };
+ jobService =
+ new JobService() {
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ return false;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return false;
+ }
+ };
+ }
+
+ @Test
+ @Config(minSdk = 34)
+ public void updateEstimatedNetworkBytes() {
+ shadowOf(jobService)
+ .updateEstimatedNetworkBytes(params, /* downloadBytes= */ 10L, /* uploadBytes= */ 0L);
+ // If we make it here, the call above did not throw
+ }
+
+ @Test
+ @Config(minSdk = 34)
+ public void updateTransferredNetworkBytes() {
+ shadowOf(jobService)
+ .updateEstimatedNetworkBytes(params, /* downloadBytes= */ 1000L, /* uploadBytes= */ 0L);
+ // If we make it here, the call above did not throw
}
@Test
@@ -57,4 +72,4 @@ public class ShadowJobServiceTest {
assertThat(shadow.getIsRescheduleNeeded()).isTrue();
assertThat(shadow.getIsJobFinished()).isTrue();
}
-} \ No newline at end of file
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
index 47c16b4f4..340cbaab0 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
@@ -401,7 +401,10 @@ public class ShadowLauncherAppsTest {
ClassParameter.from(UserHandle.class, userHandle));
} else if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
LauncherActivityInfoInternal launcherActivityInfoInternal =
- new LauncherActivityInfoInternal(info, null, userHandle);
+ ReflectionHelpers.callConstructor(
+ LauncherActivityInfoInternal.class,
+ ClassParameter.from(ActivityInfo.class, info),
+ ClassParameter.from(IncrementalStatesInfo.class, null));
return ReflectionHelpers.callConstructor(
LauncherActivityInfo.class,
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
index bd99b4c83..18d4527fc 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
@@ -153,6 +153,35 @@ public class ShadowMatrixTest {
}
@Test
+ public void testGetSetValues_withLargeArray() {
+ final Matrix matrix = new Matrix();
+ final float[] values = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f};
+ matrix.setValues(values);
+ final float[] matrixValues = new float[10];
+ matrix.getValues(matrixValues);
+ // First 9 elements should match.
+ for (int i = 0; i < 9; i++) {
+ assertThat(matrixValues[i]).isEqualTo(values[i]);
+ }
+ // The last element should not have been set.
+ assertThat(matrixValues[9]).isEqualTo(0);
+ }
+
+ @Test(expected = ArrayIndexOutOfBoundsException.class)
+ public void testGetValues_withSmallArray() {
+ final Matrix matrix = new Matrix();
+ final float[] matrixValues = new float[8];
+ matrix.getValues(matrixValues);
+ }
+
+ @Test(expected = ArrayIndexOutOfBoundsException.class)
+ public void testSetValues_withSmallArray() {
+ final Matrix matrix = new Matrix();
+ final float[] values = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f};
+ matrix.setValues(values);
+ }
+
+ @Test
public void testSet() {
final Matrix matrix1 = new Matrix();
matrix1.postScale(2.0f, 2.0f);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java
index a359e1e16..d40ce2540 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java
@@ -1,15 +1,23 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static android.provider.MediaStore.Images;
import static android.provider.MediaStore.Video;
import static com.google.common.truth.Truth.assertThat;
+import android.provider.MediaStore;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
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";
+
@Test
public void shouldInitializeFields() {
assertThat(Images.Media.EXTERNAL_CONTENT_URI.toString())
@@ -21,4 +29,45 @@ public class ShadowMediaStoreTest {
assertThat(Video.Media.INTERNAL_CONTENT_URI.toString())
.isEqualTo("content://media/internal/video/media");
}
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void notifyCloudMediaChangedEvent_storesCloudMediaChangedEvent() {
+ MediaStore.notifyCloudMediaChangedEvent(null, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID);
+
+ ImmutableList<ShadowMediaStore.CloudMediaChangedEvent> cloudMediaChangedEventList =
+ ShadowMediaStore.getCloudMediaChangedEvents();
+ assertThat(cloudMediaChangedEventList).hasSize(1);
+ assertThat(cloudMediaChangedEventList.get(0).authority()).isEqualTo(AUTHORITY);
+ assertThat(cloudMediaChangedEventList.get(0).currentMediaCollectionId())
+ .isEqualTo(CURRENT_MEDIA_COLLECTION_ID);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void clearCloudMediaChangedEventList_clearsCloudMediaChangedEventList() {
+ MediaStore.notifyCloudMediaChangedEvent(null, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID);
+ assertThat(ShadowMediaStore.getCloudMediaChangedEvents()).isNotEmpty();
+
+ ShadowMediaStore.clearCloudMediaChangedEventList();
+
+ assertThat(ShadowMediaStore.getCloudMediaChangedEvents()).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void isCurrentCloudMediaProviderAuthority_withCorrectAuthority_returnsTrue() {
+ ShadowMediaStore.setCurrentCloudMediaProviderAuthority(AUTHORITY);
+
+ assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(null, AUTHORITY)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void isCurrentCloudMediaProviderAuthority_withIncorrectAuthority_returnsFalse() {
+ ShadowMediaStore.setCurrentCloudMediaProviderAuthority(AUTHORITY);
+
+ assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(null, INCORRECT_AUTHORITY))
+ .isFalse();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java
index 36386f599..47ed360b2 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java
@@ -24,13 +24,15 @@ public class ShadowMimeTypeMapTest {
@Test
public void shouldResetStaticStateBetweenTests() {
assertFalse(MimeTypeMap.getSingleton().hasExtension(VIDEO_EXTENSION));
- shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
+ shadowOf(MimeTypeMap.getSingleton())
+ .addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
}
@Test
public void shouldResetStaticStateBetweenTests_anotherTime() {
assertFalse(MimeTypeMap.getSingleton().hasExtension(VIDEO_EXTENSION));
- shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
+ shadowOf(MimeTypeMap.getSingleton())
+ .addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
}
@Test
@@ -50,8 +52,8 @@ public class ShadowMimeTypeMapTest {
@Test
public void addingMappingShouldWorkCorrectly() {
ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton());
- shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
- shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
assertTrue(MimeTypeMap.getSingleton().hasExtension(VIDEO_EXTENSION));
assertTrue(MimeTypeMap.getSingleton().hasExtension(IMAGE_EXTENSION));
@@ -68,8 +70,8 @@ public class ShadowMimeTypeMapTest {
@Test
public void clearMappingsShouldRemoveAllMappings() {
ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton());
- shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
- shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
shadowMimeTypeMap.clearMappings();
@@ -82,8 +84,8 @@ public class ShadowMimeTypeMapTest {
@Test
public void unknownExtensionShouldProvideNothing() {
ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton());
- shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
- shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
assertFalse(MimeTypeMap.getSingleton().hasExtension("foo"));
assertNull(MimeTypeMap.getSingleton().getMimeTypeFromExtension("foo"));
@@ -92,8 +94,8 @@ public class ShadowMimeTypeMapTest {
@Test
public void unknownMimeTypeShouldProvideNothing() {
ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton());
- shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
- shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE);
+ shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE);
assertFalse(MimeTypeMap.getSingleton().hasMimeType("foo/bar"));
assertNull(MimeTypeMap.getSingleton().getExtensionFromMimeType("foo/bar"));
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
index d913c9927..bd64a107f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
@@ -1,12 +1,10 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
-import static org.robolectric.util.reflector.Reflector.reflector;
import android.app.Activity;
import android.app.Application;
@@ -16,7 +14,6 @@ import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.Build;
-import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
@@ -26,7 +23,6 @@ import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
-import org.robolectric.util.reflector.ForType;
@RunWith(AndroidJUnit4.class)
public class ShadowNfcAdapterTest {
@@ -205,13 +201,9 @@ public class ShadowNfcAdapterTest {
callback,
NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
/* extras= */ null);
- Tag tag = createMockTag();
+ Tag tag = ShadowNfcAdapter.createMockTag();
shadowOf(adapter).dispatchTagDiscovered(tag);
verify(callback).onTagDiscovered(same(tag));
}
-
- private static Tag createMockTag() {
- return Tag.createMockTag(new byte[0], new int[0], new Bundle[0], 0L);
- }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
index 7744b9507..77710bc3f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
@@ -145,4 +145,36 @@ public class ShadowPaintTest {
Paint paint = new Paint();
assertThat(paint.getTextScaleX()).isEqualTo(1f);
}
+
+ @Test
+ public void testSetFilterBitmapFlag() {
+ Paint paint = new Paint();
+ paint.setFlags(paint.getFlags() | Paint.FILTER_BITMAP_FLAG);
+ assertThat(paint.isFilterBitmap()).isTrue();
+ assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isNotEqualTo(0);
+ }
+
+ @Test
+ public void testClearFilterBitmapFlag() {
+ Paint paint = new Paint();
+ paint.setFlags(paint.getFlags() & ~Paint.FILTER_BITMAP_FLAG);
+ assertThat(paint.isFilterBitmap()).isFalse();
+ assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isEqualTo(0);
+ }
+
+ @Test
+ public void testSetFilterBitmap() {
+ Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ assertThat(paint.isFilterBitmap()).isTrue();
+ assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isNotEqualTo(0);
+ }
+
+ @Test
+ public void testClearFilterBitmap() {
+ Paint paint = new Paint();
+ paint.setFilterBitmap(false);
+ assertThat(paint.isFilterBitmap()).isFalse();
+ assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isEqualTo(0);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java
index d61a56504..eb2dd4850 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java
@@ -1,21 +1,27 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE;
import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.Charset.defaultCharset;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeThat;
import static org.robolectric.Shadows.shadowOf;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.io.Files;
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.util.concurrent.atomic.AtomicBoolean;
import org.hamcrest.Matchers;
import org.junit.After;
@@ -23,6 +29,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowParcelFileDescriptor.FileDescriptorFromParcelUnavailableException;
import org.robolectric.util.ReflectionHelpers;
@RunWith(AndroidJUnit4.class)
@@ -344,4 +351,87 @@ public class ShadowParcelFileDescriptorTest {
assertThrows(IllegalStateException.class, () -> pfd.getFd());
}
+
+ @Test
+ public void testCanMarshalUnmarshal_closedByFlagUponWrite() throws Exception {
+ Files.asCharSink(file, defaultCharset()).write("foo");
+ pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+ assertThat(file.delete()).isTrue();
+ ParcelFileDescriptor clone = dupViaParcel(pfd, PARCELABLE_WRITE_RETURN_VALUE);
+ assertThat(pfd.getFileDescriptor().valid()).isFalse();
+ assertThat(readLine(clone.getFileDescriptor())).isEqualTo("foo");
+ }
+
+ @Test
+ public void testCanMarshalUnmarshal_closedAfterWrite() throws Exception {
+ Files.asCharSink(file, defaultCharset()).write("foo");
+ pfd = ParcelFileDescriptor.open(file, 0);
+ assertThat(file.delete()).isTrue();
+ ParcelFileDescriptor clone = dupViaParcel(pfd, 0);
+ assertThat(pfd.getFileDescriptor().valid()).isTrue();
+ pfd.close();
+ assertThat(readLine(clone.getFileDescriptor())).isEqualTo("foo");
+ }
+
+ @Test
+ public void testCanMarshalUnmarshal_canClosePendingDup() throws Exception {
+ Files.asCharSink(file, defaultCharset()).write("foo");
+ pfd = ParcelFileDescriptor.open(file, 0);
+ assertThat(file.delete()).isTrue();
+ ParcelFileDescriptor clone = dupViaParcel(pfd, 0);
+ clone.close();
+ assertThat(readLine(pfd.getFileDescriptor())).isEqualTo("foo");
+ }
+
+ @Test
+ public void testCanMarshalUnmarshal_marshalTwice() throws Exception {
+ Files.asCharSink(file, defaultCharset()).write("bar");
+ pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+ assertThat(file.delete()).isTrue();
+ Parcel parcel = Parcel.obtain();
+ pfd.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ParcelFileDescriptor clone1 = ParcelFileDescriptor.CREATOR.createFromParcel(parcel);
+ parcel.setDataPosition(0);
+ ParcelFileDescriptor clone2 = ParcelFileDescriptor.CREATOR.createFromParcel(parcel);
+ pfd.close();
+ assertThat(readLine(clone1.getFileDescriptor())).isEqualTo("bar");
+ assertThrows(
+ FileDescriptorFromParcelUnavailableException.class, () -> clone2.getFileDescriptor());
+ parcel.recycle();
+ }
+
+ @Test
+ public void testCanMarshalUnmarshal_chained() throws Exception {
+ Files.asCharSink(file, defaultCharset()).write("foo");
+ pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+ assertThat(file.delete()).isTrue();
+
+ ParcelFileDescriptor pfd2 = dupViaParcel(pfd, 0);
+ ParcelFileDescriptor pfd3 = dupViaParcel(pfd2, 0);
+
+ pfd.close(); // Makes our data available to anyone downstream on the chain.
+
+ assertThat(readLine(pfd3.getFileDescriptor())).isEqualTo("foo");
+ assertThrows(
+ FileDescriptorFromParcelUnavailableException.class, () -> pfd2.getFileDescriptor());
+ }
+
+ private static String readLine(FileDescriptor fd) throws IOException {
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(new FileInputStream(fd), defaultCharset()))) {
+ return reader.readLine();
+ }
+ }
+
+ private static ParcelFileDescriptor dupViaParcel(ParcelFileDescriptor src, int flags) {
+ Parcel parcel = Parcel.obtain();
+ try {
+ src.writeToParcel(parcel, flags);
+ parcel.setDataPosition(0);
+ return ParcelFileDescriptor.CREATOR.createFromParcel(parcel);
+ } finally {
+ parcel.recycle();
+ }
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java
index f0fe96f72..9098c6b50 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java
@@ -92,6 +92,21 @@ public class ShadowPausedMessageQueueTest {
assertMainQueueEmptyAndAdd();
}
+ @Test
+ public void drainQueue_withMultipleMsg() {
+ Message msg1 = Message.obtain(new Handler(), 1);
+ shadowQueue.doEnqueueMessage(msg1, 1);
+ Message msg3 = Message.obtain(new Handler(), 3);
+ shadowQueue.doEnqueueMessage(msg3, 3);
+
+ shadowQueue.drainQueue(input -> true);
+
+ Message msg2 = Message.obtain(new Handler(), 2);
+ shadowQueue.doEnqueueMessage(msg2, 2);
+
+ assertThat(shadowQueue.getNextIgnoringWhen().what).isEqualTo(2);
+ }
+
private void assertMainQueueEmptyAndAdd() {
MessageQueue mainQueue = Looper.getMainLooper().getQueue();
ShadowPausedMessageQueue shadowPausedMessageQueue = Shadow.extract(mainQueue);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java
index 02219fe24..58d219cf7 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java
@@ -34,6 +34,7 @@ import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
+@SuppressWarnings("deprecation")
@RunWith(AndroidJUnit4.class)
public class ShadowPendingIntentTest {
@@ -47,7 +48,7 @@ public class ShadowPendingIntentTest {
@Test
public void getBroadcast_shouldCreateIntentForBroadcast() {
Intent intent = new Intent();
- PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 0);
ShadowPendingIntent shadow = shadowOf(pendingIntent);
assertThat(shadow.isActivityIntent()).isFalse();
@@ -57,7 +58,7 @@ public class ShadowPendingIntentTest {
assertThat(intent).isEqualTo(shadow.getSavedIntent());
assertThat(context).isEqualTo(shadow.getSavedContext());
assertThat(shadow.getRequestCode()).isEqualTo(99);
- assertThat(shadow.getFlags()).isEqualTo(100);
+ assertThat(shadow.getFlags()).isEqualTo(0);
}
@Test
@@ -66,7 +67,7 @@ public class ShadowPendingIntentTest {
Bundle bundle = new Bundle();
bundle.putInt("weight", 741);
bundle.putString("name", "Ada");
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100, bundle);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0, bundle);
ShadowPendingIntent shadow = shadowOf(pendingIntent);
assertThat(shadow.isActivityIntent()).isTrue();
@@ -76,7 +77,7 @@ public class ShadowPendingIntentTest {
assertThat(intent).isEqualTo(shadow.getSavedIntent());
assertThat(context).isEqualTo(shadow.getSavedContext());
assertThat(shadow.getRequestCode()).isEqualTo(99);
- assertThat(shadow.getFlags()).isEqualTo(100);
+ assertThat(shadow.getFlags()).isEqualTo(0);
assertThat(shadow.getOptions().getInt("weight")).isEqualTo(741);
assertThat(shadow.getOptions().getString("name")).isEqualTo("Ada");
}
@@ -84,7 +85,7 @@ public class ShadowPendingIntentTest {
@Test
public void getActivities_shouldCreateIntentForBroadcast() throws Exception {
Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)};
- PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0);
ShadowPendingIntent shadow = shadowOf(pendingIntent);
assertThat(shadow.getSavedIntents()).isEqualTo(intents);
@@ -102,7 +103,7 @@ public class ShadowPendingIntentTest {
Bundle bundle = new Bundle();
bundle.putInt("weight", 741);
bundle.putString("name", "Ada");
- PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100, bundle);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0, bundle);
ShadowPendingIntent shadow = shadowOf(pendingIntent);
assertThat(shadow.getSavedIntents()).isEqualTo(intents);
@@ -119,7 +120,7 @@ public class ShadowPendingIntentTest {
@Test
public void getService_shouldCreateIntentForBroadcast() {
Intent intent = new Intent().setPackage("dummy.package");
- PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 0);
ShadowPendingIntent shadow = shadowOf(pendingIntent);
assertThat(shadow.isActivityIntent()).isFalse();
@@ -129,14 +130,14 @@ public class ShadowPendingIntentTest {
assertThat(intent).isEqualTo(shadow.getSavedIntent());
assertThat(context).isEqualTo(shadow.getSavedContext());
assertThat(shadow.getRequestCode()).isEqualTo(99);
- assertThat(shadow.getFlags()).isEqualTo(100);
+ assertThat(shadow.getFlags()).isEqualTo(0);
}
@Test
@Config(minSdk = Build.VERSION_CODES.O)
public void getForegroundService_shouldCreateIntentForBroadcast() {
Intent intent = new Intent().setPackage("dummy.package");
- PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 0);
ShadowPendingIntent shadow = shadowOf(pendingIntent);
assertThat(shadow.isActivityIntent()).isFalse();
@@ -146,13 +147,13 @@ public class ShadowPendingIntentTest {
assertThat(intent).isEqualTo(shadow.getSavedIntent());
assertThat(context).isEqualTo(shadow.getSavedContext());
assertThat(shadow.getRequestCode()).isEqualTo(99);
- assertThat(shadow.getFlags()).isEqualTo(100);
+ assertThat(shadow.getFlags()).isEqualTo(0);
}
@Test
public void getActivities_nullIntent() {
try {
- PendingIntent.getActivities(context, 99, null, 100);
+ PendingIntent.getActivities(context, 99, null, 0);
fail("Expected NullPointerException when creating PendingIntent with null Intent[]");
} catch (NullPointerException ignore) {
// expected
@@ -162,7 +163,7 @@ public class ShadowPendingIntentTest {
@Test
public void getActivities_withBundle_nullIntent() {
try {
- PendingIntent.getActivities(context, 99, null, 100, Bundle.EMPTY);
+ PendingIntent.getActivities(context, 99, null, 0, Bundle.EMPTY);
fail("Expected NullPointerException when creating PendingIntent with null Intent[]");
} catch (NullPointerException ignore) {
// expected
@@ -173,7 +174,7 @@ public class ShadowPendingIntentTest {
public void send_shouldFillInIntentData() throws Exception {
Intent intent = new Intent("action");
Context context = Robolectric.setupActivity(Activity.class);
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0);
Activity otherContext = Robolectric.setupActivity(Activity.class);
Intent fillIntent = new Intent().putExtra("TEST", 23);
@@ -189,7 +190,7 @@ public class ShadowPendingIntentTest {
public void send_shouldNotReusePreviouslyFilledInIntentData() throws Exception {
Intent intent = new Intent("action");
Context context = Robolectric.setupActivity(Activity.class);
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0);
Activity otherContext = Robolectric.setupActivity(Activity.class);
Intent firstFillIntent = new Intent().putExtra("KEY1", 23).putExtra("KEY2", 24);
@@ -212,7 +213,7 @@ public class ShadowPendingIntentTest {
public void send_shouldFillInLastIntentData() throws Exception {
Intent[] intents = {new Intent("first"), new Intent("second")};
Context context = Robolectric.setupActivity(Activity.class);
- PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0);
Activity otherContext = Robolectric.setupActivity(Activity.class);
Intent fillIntent = new Intent();
@@ -232,10 +233,62 @@ public class ShadowPendingIntentTest {
}
@Test
+ @Config(minSdk = Build.VERSION_CODES.S)
+ public void send_shouldFillInIntentData_immutable() throws Exception {
+ Intent[] intents = {new Intent("first")};
+ Context context = Robolectric.setupActivity(Activity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, FLAG_IMMUTABLE);
+
+ Activity otherContext = Robolectric.setupActivity(Activity.class);
+ Intent fillIntent = new Intent();
+ fillIntent.putExtra("TEST", 23);
+ pendingIntent.send(otherContext, 0, fillIntent);
+
+ ShadowActivity shadowActivity = shadowOf(otherContext);
+ Intent first = shadowActivity.getNextStartedActivity();
+ assertThat(first).isNotNull();
+ assertThat(first.filterEquals(intents[0])).isTrue(); // Ignore extras.
+ assertThat(first.hasExtra("TEST")).isFalse();
+ }
+
+ @Test
+ public void send_shouldFillInIntentData_mutable_alreadySet() throws Exception {
+ Intent[] intents = {new Intent("first")};
+ Context context = Robolectric.setupActivity(Activity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0);
+
+ Activity otherContext = Robolectric.setupActivity(Activity.class);
+ Intent fillIntent = new Intent("first_and");
+ pendingIntent.send(otherContext, 0, fillIntent);
+
+ ShadowActivity shadowActivity = shadowOf(otherContext);
+ Intent first = shadowActivity.getNextStartedActivity();
+ assertThat(first).isNotNull();
+ assertThat(first.getAction()).isEqualTo("first");
+ }
+
+ @Test
+ public void send_shouldFillInIntentData_mutable_alreadySet_fillIn() throws Exception {
+ Intent[] intents = {new Intent("first")};
+ Context context = Robolectric.setupActivity(Activity.class);
+ PendingIntent pendingIntent =
+ PendingIntent.getActivities(context, 99, intents, Intent.FILL_IN_ACTION);
+
+ Activity otherContext = Robolectric.setupActivity(Activity.class);
+ Intent fillIntent = new Intent("first_and");
+ pendingIntent.send(otherContext, 0, fillIntent);
+
+ ShadowActivity shadowActivity = shadowOf(otherContext);
+ Intent first = shadowActivity.getNextStartedActivity();
+ assertThat(first).isNotNull();
+ assertThat(first.getAction()).isEqualTo("first_and");
+ }
+
+ @Test
public void send_shouldNotUsePreviouslyFilledInLastIntentData() throws Exception {
Intent[] intents = {new Intent("first"), new Intent("second")};
Context context = Robolectric.setupActivity(Activity.class);
- PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0);
Activity otherContext = Robolectric.setupActivity(Activity.class);
Intent firstFillIntent = new Intent();
@@ -305,7 +358,7 @@ public class ShadowPendingIntentTest {
@Test
public void getActivity_withFlagNoCreate_shouldReturnExistingIntent() {
Intent intent = new Intent();
- PendingIntent.getActivity(context, 99, intent, 100);
+ PendingIntent.getActivity(context, 99, intent, 0);
Intent identical = new Intent();
PendingIntent saved = PendingIntent.getActivity(context, 99, identical, FLAG_NO_CREATE);
@@ -316,7 +369,7 @@ public class ShadowPendingIntentTest {
@Test
public void getActivity_withNoFlags_shouldReturnExistingIntent() {
Intent intent = new Intent();
- PendingIntent.getActivity(context, 99, intent, 100);
+ PendingIntent.getActivity(context, 99, intent, 0);
Intent updated = new Intent();
PendingIntent saved = PendingIntent.getActivity(context, 99, updated, 0);
@@ -341,7 +394,7 @@ public class ShadowPendingIntentTest {
@Test
public void getActivities_withFlagNoCreate_shouldReturnExistingIntent() {
Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)};
- PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 100);
+ PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 0);
Intent[] identicalIntents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)};
PendingIntent saved =
@@ -353,7 +406,7 @@ public class ShadowPendingIntentTest {
@Test
public void getActivities_withNoFlags_shouldReturnExistingIntent() {
Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)};
- PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 100);
+ PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 0);
Intent[] identicalIntents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)};
PendingIntent saved = PendingIntent.getActivities(context, 99, identicalIntents, 0);
@@ -378,7 +431,7 @@ public class ShadowPendingIntentTest {
@Test
public void getBroadcast_withFlagNoCreate_shouldReturnExistingIntent() {
Intent intent = new Intent();
- PendingIntent.getBroadcast(context, 99, intent, 100);
+ PendingIntent.getBroadcast(context, 99, intent, 0);
Intent identical = new Intent();
PendingIntent saved = PendingIntent.getBroadcast(context, 99, identical, FLAG_NO_CREATE);
@@ -389,7 +442,7 @@ public class ShadowPendingIntentTest {
@Test
public void getBroadcast_withNoFlags_shouldReturnExistingIntent() {
Intent intent = new Intent();
- PendingIntent.getBroadcast(context, 99, intent, 100);
+ PendingIntent.getBroadcast(context, 99, intent, 0);
Intent identical = new Intent();
PendingIntent saved = PendingIntent.getBroadcast(context, 99, identical, 0);
@@ -414,7 +467,7 @@ public class ShadowPendingIntentTest {
@Test
public void getService_withFlagNoCreate_shouldReturnExistingIntent() {
Intent intent = new Intent().setPackage("dummy.package");
- PendingIntent.getService(context, 99, intent, 100);
+ PendingIntent.getService(context, 99, intent, 0);
Intent identical = new Intent().setPackage("dummy.package");
PendingIntent saved = PendingIntent.getService(context, 99, identical, FLAG_NO_CREATE);
@@ -425,7 +478,7 @@ public class ShadowPendingIntentTest {
@Test
public void getService_withNoFlags_shouldReturnExistingIntent() {
Intent intent = new Intent().setPackage("dummy.package");
- PendingIntent.getService(context, 99, intent, 100);
+ PendingIntent.getService(context, 99, intent, 0);
Intent identical = new Intent().setPackage("dummy.package");
PendingIntent saved = PendingIntent.getService(context, 99, identical, 0);
@@ -454,7 +507,7 @@ public class ShadowPendingIntentTest {
@Config(minSdk = Build.VERSION_CODES.O)
public void getForegroundService_withFlagNoCreate_shouldReturnExistingIntent() {
Intent intent = new Intent();
- PendingIntent.getForegroundService(context, 99, intent, 100);
+ PendingIntent.getForegroundService(context, 99, intent, 0);
Intent identical = new Intent();
PendingIntent saved =
@@ -467,7 +520,7 @@ public class ShadowPendingIntentTest {
@Config(minSdk = Build.VERSION_CODES.O)
public void getForegroundService_withNoFlags_shouldReturnExistingIntent() {
Intent intent = new Intent().setPackage("dummy.package");
- PendingIntent.getForegroundService(context, 99, intent, 100);
+ PendingIntent.getForegroundService(context, 99, intent, 0);
Intent identical = new Intent().setPackage("dummy.package");
PendingIntent saved = PendingIntent.getForegroundService(context, 99, identical, 0);
@@ -478,7 +531,7 @@ public class ShadowPendingIntentTest {
@Test
public void cancel_shouldRemovePendingIntentForBroadcast() {
Intent intent = new Intent();
- PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 0);
assertThat(pendingIntent).isNotNull();
pendingIntent.cancel();
@@ -488,7 +541,7 @@ public class ShadowPendingIntentTest {
@Test
public void cancel_shouldRemovePendingIntentForActivity() {
Intent intent = new Intent();
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0);
assertThat(pendingIntent).isNotNull();
pendingIntent.cancel();
@@ -498,7 +551,7 @@ public class ShadowPendingIntentTest {
@Test
public void cancel_shouldRemovePendingIntentForActivities() {
Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)};
- PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0);
assertThat(pendingIntent).isNotNull();
pendingIntent.cancel();
@@ -508,7 +561,7 @@ public class ShadowPendingIntentTest {
@Test
public void cancel_shouldRemovePendingIntentForService() {
Intent intent = new Intent().setPackage("dummy.package");
- PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 0);
assertThat(pendingIntent).isNotNull();
pendingIntent.cancel();
@@ -519,7 +572,7 @@ public class ShadowPendingIntentTest {
@Config(minSdk = Build.VERSION_CODES.O)
public void cancel_shouldRemovePendingIntentForForegroundService() {
Intent intent = new Intent();
- PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 100);
+ PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 0);
assertThat(pendingIntent).isNotNull();
pendingIntent.cancel();
@@ -530,35 +583,35 @@ public class ShadowPendingIntentTest {
@Config(minSdk = Build.VERSION_CODES.S)
public void isActivity_activityPendingIntent_returnsTrue() {
Intent intent = new Intent();
- assertThat(PendingIntent.getActivity(context, 99, intent, 100).isActivity()).isTrue();
+ assertThat(PendingIntent.getActivity(context, 99, intent, 0).isActivity()).isTrue();
}
@Test
@Config(minSdk = Build.VERSION_CODES.S)
public void isActivity_broadcastPendingIntent_returnsFalse() {
Intent intent = new Intent();
- assertThat(PendingIntent.getBroadcast(context, 99, intent, 100).isActivity()).isFalse();
+ assertThat(PendingIntent.getBroadcast(context, 99, intent, 0).isActivity()).isFalse();
}
@Test
@Config(minSdk = Build.VERSION_CODES.S)
public void isBroadcast_broadcastPendingIntent_returnsTrue() {
Intent intent = new Intent();
- assertThat(PendingIntent.getBroadcast(context, 99, intent, 100).isBroadcast()).isTrue();
+ assertThat(PendingIntent.getBroadcast(context, 99, intent, 0).isBroadcast()).isTrue();
}
@Test
@Config(minSdk = Build.VERSION_CODES.S)
public void isBroadcast_activityPendingIntent_returnsFalse() {
Intent intent = new Intent();
- assertThat(PendingIntent.getActivity(context, 99, intent, 100).isBroadcast()).isFalse();
+ assertThat(PendingIntent.getActivity(context, 99, intent, 0).isBroadcast()).isFalse();
}
@Test
@Config(minSdk = Build.VERSION_CODES.S)
public void isForegroundService_foregroundServicePendingIntent_returnsTrue() {
Intent intent = new Intent();
- assertThat(PendingIntent.getForegroundService(context, 99, intent, 100).isForegroundService())
+ assertThat(PendingIntent.getForegroundService(context, 99, intent, 0).isForegroundService())
.isTrue();
}
@@ -566,21 +619,21 @@ public class ShadowPendingIntentTest {
@Config(minSdk = Build.VERSION_CODES.S)
public void isForegroundService_normalServicePendingIntent_returnsFalse() {
Intent intent = new Intent();
- assertThat(PendingIntent.getService(context, 99, intent, 100).isForegroundService()).isFalse();
+ assertThat(PendingIntent.getService(context, 99, intent, 0).isForegroundService()).isFalse();
}
@Test
@Config(minSdk = Build.VERSION_CODES.S)
public void isService_servicePendingIntent_returnsTrue() {
Intent intent = new Intent();
- assertThat(PendingIntent.getService(context, 99, intent, 100).isService()).isTrue();
+ assertThat(PendingIntent.getService(context, 99, intent, 0).isService()).isTrue();
}
@Test
@Config(minSdk = Build.VERSION_CODES.S)
public void isService_foregroundServicePendingIntent_returnsFalse() {
Intent intent = new Intent();
- assertThat(PendingIntent.getForegroundService(context, 99, intent, 100).isService()).isFalse();
+ assertThat(PendingIntent.getForegroundService(context, 99, intent, 0).isService()).isFalse();
}
@Test
@@ -603,7 +656,7 @@ public class ShadowPendingIntentTest {
@Test
public void send_canceledPendingIntent_throwsCanceledException() throws CanceledException {
Intent intent = new Intent().setPackage("dummy.package");
- PendingIntent canceled = PendingIntent.getService(context, 99, intent, 100);
+ PendingIntent canceled = PendingIntent.getService(context, 99, intent, 0);
assertThat(canceled).isNotNull();
// Cancel the existing PendingIntent and create a new one in its place.
@@ -798,8 +851,7 @@ public class ShadowPendingIntentTest {
@Test
public void testEquals() {
- PendingIntent pendingIntent =
- PendingIntent.getActivity(context, 99, new Intent("activity"), 100);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0);
// Same type, requestCode and Intent action implies equality.
assertThat(PendingIntent.getActivity(context, 99, new Intent("activity"), FLAG_NO_CREATE))
@@ -823,7 +875,7 @@ public class ShadowPendingIntentTest {
@Test
public void testEquals_getActivities() {
Intent[] intents = {new Intent("activity"), new Intent("activity2")};
- PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100);
+ PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0);
Intent[] forward = {new Intent("activity"), new Intent("activity2")};
assertThat(PendingIntent.getActivities(context, 99, forward, FLAG_NO_CREATE))
@@ -844,8 +896,7 @@ public class ShadowPendingIntentTest {
@Test
@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
public void testGetCreatorPackage_nothingSet() {
- PendingIntent pendingIntent =
- PendingIntent.getActivity(context, 99, new Intent("activity"), 100);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0);
assertThat(pendingIntent.getCreatorPackage()).isEqualTo(context.getPackageName());
assertThat(pendingIntent.getTargetPackage()).isEqualTo(context.getPackageName());
}
@@ -854,8 +905,7 @@ public class ShadowPendingIntentTest {
@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
public void testGetCreatorPackage_explicitlySetPackage() {
String fakePackage = "some.fake.package";
- PendingIntent pendingIntent =
- PendingIntent.getActivity(context, 99, new Intent("activity"), 100);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0);
shadowOf(pendingIntent).setCreatorPackage(fakePackage);
assertThat(pendingIntent.getCreatorPackage()).isEqualTo(fakePackage);
assertThat(pendingIntent.getTargetPackage()).isEqualTo(fakePackage);
@@ -865,8 +915,7 @@ public class ShadowPendingIntentTest {
@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
public void testGetCreatorUid() {
int fakeUid = 123;
- PendingIntent pendingIntent =
- PendingIntent.getActivity(context, 99, new Intent("activity"), 100);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0);
shadowOf(pendingIntent).setCreatorUid(fakeUid);
assertThat(pendingIntent.getCreatorUid()).isEqualTo(fakeUid);
@@ -875,16 +924,16 @@ public class ShadowPendingIntentTest {
@Test
public void testHashCode() {
Context ctx = ApplicationProvider.getApplicationContext();
- PendingIntent pendingIntent1 = PendingIntent.getActivity(ctx, 99, new Intent("activity"), 100);
+ PendingIntent pendingIntent1 = PendingIntent.getActivity(ctx, 99, new Intent("activity"), 0);
assertThat(pendingIntent1.hashCode())
- .isEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity"), 100).hashCode());
+ .isEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity"), 0).hashCode());
assertThat(pendingIntent1.hashCode())
- .isNotEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity2"), 100).hashCode());
+ .isNotEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity2"), 0).hashCode());
assertThat(pendingIntent1.hashCode())
- .isNotEqualTo(PendingIntent.getActivity(ctx, 999, new Intent("activity"), 100).hashCode());
+ .isNotEqualTo(PendingIntent.getActivity(ctx, 999, new Intent("activity"), 0).hashCode());
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java
index 96ef647ed..3c5781720 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java
@@ -266,10 +266,12 @@ public class ShadowPowerManagerTest {
String rebootReason = "reason";
powerManager.reboot(rebootReason);
+ powerManager.reboot(null);
- assertThat(shadowOf(powerManager).getTimesRebooted()).isEqualTo(1);
- assertThat(shadowOf(powerManager).getRebootReasons()).hasSize(1);
- assertThat(shadowOf(powerManager).getRebootReasons()).contains(rebootReason);
+ assertThat(shadowOf(powerManager).getTimesRebooted()).isEqualTo(2);
+ assertThat(shadowOf(powerManager).getRebootReasons())
+ .containsExactly(rebootReason, null)
+ .inOrder();
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowStatsManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowStatsManagerTest.java
new file mode 100644
index 000000000..1c895bb27
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowStatsManagerTest.java
@@ -0,0 +1,80 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Build;
+import androidx.test.core.app.ApplicationProvider;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link ShadowStatsManager} */
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = Build.VERSION_CODES.P)
+public final class ShadowStatsManagerTest {
+
+ @Test
+ public void testGetMetadata() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ StatsManager statsManager = context.getSystemService(StatsManager.class);
+ byte[] metadataBytes = new byte[] {1, 2, 3, 4, 5};
+ ShadowStatsManager.setStatsMetadata(metadataBytes);
+
+ assertThat(statsManager.getMetadata()).isEqualTo(metadataBytes);
+ }
+
+ @Test
+ public void testGetReports_multipleReports() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ StatsManager statsManager = context.getSystemService(StatsManager.class);
+ long reportId1 = 1L;
+ long reportId2 = 2L;
+ byte[] report1Bytes = new byte[] {1, 2, 3, 4, 5};
+ byte[] report2Bytes = new byte[] {1, 2, 3};
+ ShadowStatsManager.addReportData(reportId1, report1Bytes);
+ ShadowStatsManager.addReportData(reportId2, report2Bytes);
+
+ assertThat(statsManager.getReports(reportId1)).isEqualTo(report1Bytes);
+ assertThat(statsManager.getReports(reportId2)).isEqualTo(report2Bytes);
+ }
+
+ @Test
+ public void testGetReports_clearsExistingReport() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ StatsManager statsManager = context.getSystemService(StatsManager.class);
+ long reportId1 = 1L;
+ byte[] report1Bytes = new byte[] {1, 2, 3, 4, 5};
+ ShadowStatsManager.addReportData(reportId1, report1Bytes);
+
+ assertThat(statsManager.getReports(reportId1)).isEqualTo(report1Bytes);
+ assertThat(statsManager.getReports(reportId1)).isEqualTo(new byte[] {});
+ }
+
+ @Test
+ public void testReset_clearsReports() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ StatsManager statsManager = context.getSystemService(StatsManager.class);
+ long reportId1 = 1L;
+ byte[] report1Bytes = new byte[] {1, 2, 3, 4, 5};
+ ShadowStatsManager.addReportData(reportId1, report1Bytes);
+
+ ShadowStatsManager.reset();
+
+ assertThat(statsManager.getReports(reportId1)).isEqualTo(new byte[] {});
+ }
+
+ @Test
+ public void testReset_clearsMetadata() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ StatsManager statsManager = context.getSystemService(StatsManager.class);
+ byte[] metadataBytes = new byte[] {1, 2, 3, 4, 5};
+ ShadowStatsManager.setStatsMetadata(metadataBytes);
+
+ ShadowStatsManager.reset();
+
+ assertThat(statsManager.getMetadata()).isEqualTo(new byte[] {});
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
index a527ec98f..e3fd11c48 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
@@ -3,6 +3,7 @@ package org.robolectric.shadows;
import static android.content.Context.TELEPHONY_SUBSCRIPTION_SERVICE;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.TIRAMISU;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
@@ -156,6 +157,16 @@ public class ShadowSubscriptionManagerTest {
}
@Test
+ public void getActiveSubscriptionInfo_shouldThrowExceptionWhenNoPermissions() {
+ shadowOf(subscriptionManager).setReadPhoneStatePermission(false);
+ assertThrows(
+ SecurityException.class,
+ () ->
+ shadowOf(subscriptionManager)
+ .getActiveSubscriptionInfo(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID));
+ }
+
+ @Test
public void getActiveSubscriptionInfoList_shouldReturnInfoList() {
SubscriptionInfo expectedSubscriptionInfo =
SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo();
@@ -373,6 +384,17 @@ public class ShadowSubscriptionManagerTest {
@Test
@Config(minSdk = TIRAMISU)
+ public void getPhoneNumber_shouldThrowExceptionWhenNoPermissions() {
+ shadowOf(subscriptionManager).setReadPhoneNumbersPermission(false);
+ assertThrows(
+ SecurityException.class,
+ () ->
+ shadowOf(subscriptionManager)
+ .getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
public void getPhoneNumberWithSource_phoneNumberNotSet_returnsEmptyString() {
assertThat(
subscriptionManager.getPhoneNumber(
@@ -413,6 +435,28 @@ public class ShadowSubscriptionManagerTest {
.isEqualTo("123");
}
+ @Test
+ @Config(minSdk = Q)
+ public void setIsOpportunistic_shouldReturnFalse() {
+ assertThat(
+ ShadowSubscriptionManager.SubscriptionInfoBuilder.newBuilder()
+ .setIsOpportunistic(false)
+ .buildSubscriptionInfo()
+ .isOpportunistic())
+ .isFalse();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void setIsOpportunistic_shouldReturnTrue() {
+ assertThat(
+ ShadowSubscriptionManager.SubscriptionInfoBuilder.newBuilder()
+ .setIsOpportunistic(true)
+ .buildSubscriptionInfo()
+ .isOpportunistic())
+ .isTrue();
+ }
+
private static class DummySubscriptionsChangedListener
extends SubscriptionManager.OnSubscriptionsChangedListener {
private int subscriptionChangedCount;
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java
index 67e4746f5..58d77d3c8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java
@@ -8,6 +8,7 @@ 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -93,6 +94,14 @@ public class ShadowTelecomManagerTest {
}
@Test
+ public void getPhoneAccount_noPermission_throwsSecurityException() {
+ shadowOf(telecomService).setReadPhoneStatePermission(false);
+
+ PhoneAccountHandle handler = createHandle("id");
+ assertThrows(SecurityException.class, () -> telecomService.getPhoneAccount(handler));
+ }
+
+ @Test
public void clearAccounts() {
PhoneAccountHandle anotherPackageHandle =
createHandle("some.other.package", "OtherConnectionService", "id");
@@ -156,6 +165,14 @@ public class ShadowTelecomManagerTest {
}
@Test
+ @Config(minSdk = M)
+ public void getCallCapablePhoneAccounts_noPermission_throwsSecurityException() {
+ shadowOf(telecomService).setReadPhoneStatePermission(false);
+
+ assertThrows(SecurityException.class, () -> telecomService.getCallCapablePhoneAccounts());
+ }
+
+ @Test
@Config(minSdk = O)
public void getSelfManagedPhoneAccounts() {
PhoneAccountHandle selfManagedPhoneAccountHandle = createHandle("id1");
@@ -266,6 +283,19 @@ public class ShadowTelecomManagerTest {
@Test
@Config(minSdk = M)
+ public void testPlaceCall_noPermission_throwsSecurityException() {
+ shadowOf(telecomService).setCallPhonePermission(false);
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, createHandle("id"));
+
+ assertThrows(
+ SecurityException.class,
+ () -> telecomService.placeCall(Uri.parse("tel:+1-201-555-0123"), extras));
+ }
+
+ @Test
+ @Config(minSdk = M)
public void testAllowPlaceCall() {
shadowOf(telecomService).setCallRequestMode(CallRequestMode.ALLOW_ALL);
@@ -414,6 +444,13 @@ public class ShadowTelecomManagerTest {
}
@Test
+ public void setTtySupported_noPermission_throwsSecurityException() {
+ shadowOf(telecomService).setReadPhoneStatePermission(false);
+
+ assertThrows(SecurityException.class, () -> telecomService.isTtySupported());
+ }
+
+ @Test
public void canSetAndGetIsInCall() {
shadowOf(telecomService).setIsInCall(true);
assertThat(telecomService.isInCall()).isTrue();
@@ -532,6 +569,59 @@ public class ShadowTelecomManagerTest {
assertThat(telecomService.getVoiceMailNumber(phoneAccountHandle)).isNull();
}
+ @Test
+ @Config(minSdk = LOLLIPOP_MR1)
+ public void getLine1Number() {
+ // Check initial state
+ PhoneAccountHandle phoneAccountHandle = createHandle("id1");
+ assertThat(telecomService.getLine1Number(phoneAccountHandle)).isNull();
+
+ // After setting
+ shadowOf(telecomService).setLine1Number(phoneAccountHandle, "123");
+ assertThat(telecomService.getLine1Number(phoneAccountHandle)).isEqualTo("123");
+
+ // After reset
+ shadowOf(telecomService).setLine1Number(phoneAccountHandle, null);
+ assertThat(telecomService.getLine1Number(phoneAccountHandle)).isNull();
+ }
+
+ @Test
+ @Config(minSdk = LOLLIPOP_MR1)
+ public void getLine1Number_noPermission_throwsSecurityException() {
+ shadowOf(telecomService).setReadPhoneStatePermission(false);
+
+ PhoneAccountHandle phoneAccountHandle = createHandle("id1");
+ assertThrows(SecurityException.class, () -> telecomService.getLine1Number(phoneAccountHandle));
+ }
+
+ @Test
+ public void handleMmi_defaultValueFalse() {
+ assertThat(telecomService.handleMmi("123")).isFalse();
+ }
+
+ @Test
+ public void handleMmi() {
+ shadowOf(telecomService).setHandleMmiValue(true);
+
+ assertThat(telecomService.handleMmi("123")).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void handleMmiWithHandle_defaultValueFalse() {
+ PhoneAccountHandle phoneAccountHandle = createHandle("id1");
+ assertThat(telecomService.handleMmi("123", phoneAccountHandle)).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void handleMmiWithHandle() {
+ shadowOf(telecomService).setHandleMmiValue(true);
+ PhoneAccountHandle phoneAccountHandle = createHandle("id1");
+
+ assertThat(telecomService.handleMmi("123", phoneAccountHandle)).isTrue();
+ }
+
private static PhoneAccountHandle createHandle(String id) {
return new PhoneAccountHandle(
new ComponentName(ApplicationProvider.getApplicationContext(), TestConnectionService.class),
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
index ad3adebcf..90c74d105 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -58,6 +58,7 @@ import android.os.PersistableBundle;
import android.telecom.PhoneAccountHandle;
import android.telephony.CellInfo;
import android.telephony.CellLocation;
+import android.telephony.PhoneCapability;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
@@ -172,6 +173,13 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ public void shouldGiveDeviceSoftwareVersion() {
+ String testSoftwareVersion = "getDeviceSoftwareVersion";
+ shadowOf(telephonyManager).setDeviceSoftwareVersion(testSoftwareVersion);
+ assertEquals(testSoftwareVersion, telephonyManager.getDeviceSoftwareVersion());
+ }
+
+ @Test
@Config(minSdk = O)
public void getImei() {
String testImei = "4test imei";
@@ -326,6 +334,13 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = O)
+ public void shouldGiveNetworkSpecifier() {
+ shadowOf(telephonyManager).setNetworkSpecifier("SomeSpecifier");
+ assertEquals("SomeSpecifier", telephonyManager.getNetworkSpecifier());
+ }
+
+ @Test
public void shouldGiveLine1Number() {
shadowOf(telephonyManager).setLine1Number("123-244-2222");
assertEquals("123-244-2222", telephonyManager.getLine1Number());
@@ -346,6 +361,23 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = M)
+ public void
+ getDeviceIdForSlot_shouldThrowSecurityExceptionWhenReadPhoneStatePermissionNotGranted()
+ throws Exception {
+ shadowOf(telephonyManager).setReadPhoneStatePermission(false);
+ assertThrows(SecurityException.class, () -> telephonyManager.getDeviceId(1));
+ }
+
+ @Test
+ public void
+ getDeviceSoftwareVersion_shouldThrowSecurityExceptionWhenReadPhoneStatePermissionNotGranted()
+ throws Exception {
+ shadowOf(telephonyManager).setReadPhoneStatePermission(false);
+ assertThrows(SecurityException.class, () -> telephonyManager.getDeviceSoftwareVersion());
+ }
+
+ @Test
public void shouldGivePhoneType() {
shadowOf(telephonyManager).setPhoneType(TelephonyManager.PHONE_TYPE_CDMA);
assertEquals(TelephonyManager.PHONE_TYPE_CDMA, telephonyManager.getPhoneType());
@@ -515,7 +547,7 @@ public class ShadowTelephonyManagerTest {
PhoneAccountHandle phoneAccountHandle =
new PhoneAccountHandle(
new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle");
- Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment = */ null);
+ Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment= */ null);
shadowOf(telephonyManager).setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri);
@@ -528,7 +560,7 @@ public class ShadowTelephonyManagerTest {
PhoneAccountHandle phoneAccountHandle =
new PhoneAccountHandle(
new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle");
- Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment = */ null);
+ Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment= */ null);
// Note: Using the real manager to set, instead of the shadow.
telephonyManager.setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri);
@@ -849,6 +881,15 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ public void setDataActivityChangesDataActivity() {
+ assertThat(telephonyManager.getDataActivity()).isEqualTo(TelephonyManager.DATA_ACTIVITY_NONE);
+ shadowOf(telephonyManager).setDataActivity(TelephonyManager.DATA_ACTIVITY_IN);
+ assertThat(telephonyManager.getDataActivity()).isEqualTo(TelephonyManager.DATA_ACTIVITY_IN);
+ shadowOf(telephonyManager).setDataActivity(TelephonyManager.DATA_ACTIVITY_OUT);
+ assertThat(telephonyManager.getDataActivity()).isEqualTo(TelephonyManager.DATA_ACTIVITY_OUT);
+ }
+
+ @Test
@Config(minSdk = Q)
public void setRttSupportedChangesIsRttSupported() {
shadowOf(telephonyManager).setRttSupported(false);
@@ -858,6 +899,33 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = M)
+ public void setTtyModeSupportedChangesIsTtyModeSupported() {
+ shadowOf(telephonyManager).setTtyModeSupported(false);
+ assertThat(telephonyManager.isTtyModeSupported()).isFalse();
+ shadowOf(telephonyManager).setTtyModeSupported(true);
+ assertThat(telephonyManager.isTtyModeSupported()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void
+ isTtyModeSupported_shouldThrowSecurityExceptionWhenReadPhoneStatePermissionNotGranted()
+ throws Exception {
+ shadowOf(telephonyManager).setReadPhoneStatePermission(false);
+ assertThrows(SecurityException.class, () -> telephonyManager.isTtyModeSupported());
+ }
+
+ @Test
+ @Config(minSdk = N)
+ public void hasCarrierPrivilegesWithSubId() {
+ int subId = 3;
+ assertThat(telephonyManager.hasCarrierPrivileges(subId)).isFalse();
+ shadowOf(telephonyManager).setHasCarrierPrivileges(subId, true);
+ assertThat(telephonyManager.hasCarrierPrivileges(subId)).isTrue();
+ }
+
+ @Test
@Config(minSdk = O)
public void sendDialerSpecialCode() {
shadowOf(telephonyManager).sendDialerSpecialCode("1234");
@@ -1034,6 +1102,16 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = S)
+ public void setPhoneCapability_returnsPhoneCapability() {
+ PhoneCapability phoneCapability = PhoneCapabilityFactory.create(2, 1, false, new int[0]);
+
+ shadowTelephonyManager.setPhoneCapability(phoneCapability);
+
+ assertThat(telephonyManager.getPhoneCapability()).isEqualTo(phoneCapability);
+ }
+
+ @Test
@Config(minSdk = O)
public void sendVisualVoicemailSms_shouldStoreLastSendSmsParameters() {
telephonyManager.sendVisualVoicemailSms("destAddress", 0, "message", null);
@@ -1137,4 +1215,16 @@ public class ShadowTelephonyManagerTest {
ImmutableMap.of(0, ImmutableList.of(emergencyNumber)));
assertThat(telephonyManager.getEmergencyNumberList().get(0)).containsExactly(emergencyNumber);
}
+
+ @Test
+ @Config(minSdk = R)
+ public void getSubscriptionIdForPhoneAccountHandle() {
+ int subscriptionId = 123;
+ PhoneAccountHandle phoneAccountHandle =
+ new PhoneAccountHandle(
+ new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle");
+ shadowOf(telephonyManager)
+ .setPhoneAccountHandleSubscriptionId(phoneAccountHandle, subscriptionId);
+ assertEquals(subscriptionId, telephonyManager.getSubscriptionId(phoneAccountHandle));
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java
index 9da4c416c..14aa24297 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java
@@ -15,7 +15,7 @@ import org.robolectric.annotation.Config;
/** Tests for {@link ShadowTimeManager} */
@RunWith(RobolectricTestRunner.class)
-@Config(sdk = Build.VERSION_CODES.S)
+@Config(minSdk = Build.VERSION_CODES.S)
public final class ShadowTimeManagerTest {
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java
index 5f19aafef..66f40b933 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java
@@ -17,6 +17,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
+import org.robolectric.annotation.LooperMode;
/** Test for {@link ShadowUiAutomation}. */
@Config(minSdk = JELLY_BEAN_MR2)
@@ -102,4 +103,16 @@ public class ShadowUiAutomationTest {
assertThat(Resources.getSystem().getConfiguration().orientation)
.isEqualTo(Configuration.ORIENTATION_PORTRAIT);
}
+
+ @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)
+ @Test
+ public void setAnimationScale_zero_instrumentationTestLooperMode() throws Exception {
+ setAnimationScale_zero();
+ }
+
+ @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)
+ @Test
+ public void setRotation_freeze90_rotatesToLandscape_instrumentationTestLooperMode() {
+ setRotation_freeze90_rotatesToLandscape();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java
index 18b3e92b6..39f8b6d5e 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java
@@ -5,6 +5,7 @@ import static android.os.Build.VERSION_CODES.Q;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
+import android.app.VoiceInteractor.CommandRequest;
import android.content.Intent;
import android.os.Bundle;
import android.service.voice.VoiceInteractionSession;
@@ -46,6 +47,16 @@ public class ShadowVoiceInteractionSessionTest {
}
@Test
+ @Config(minSdk = 34)
+ public void isWindowShowing_afterShowSdk34_returnsTrue() {
+ shadowSession.create();
+
+ session.show(new Bundle(), /* flags= */ 0);
+
+ assertThat(shadowSession.isWindowShowing()).isTrue();
+ }
+
+ @Test
public void isWindowShowing_afterShowThenHide_returnsFalse() {
shadowSession.create();
@@ -159,4 +170,71 @@ public class ShadowVoiceInteractionSessionTest {
public void isUiEnabled_belowAndroidO_throws() {
shadowSession.isUiEnabled();
}
+
+ @Test
+ public void sendCommandRequest_cancel_requestCanceled() {
+ TestCommandRequest commandRequest = new TestCommandRequest("test_command", new Bundle());
+ VoiceInteractionSession.CommandRequest receivedCommandRequest =
+ shadowSession.sendCommandRequest(commandRequest, "test_package", 123);
+
+ assertThat(receivedCommandRequest.isActive()).isTrue();
+ assertThat(receivedCommandRequest.getCommand()).isEqualTo("test_command");
+
+ receivedCommandRequest.cancel();
+
+ assertThat(commandRequest.isCancelled).isTrue();
+ }
+
+ @Test
+ public void sendCommandRequest_sendIntermediateResult_requestRemainsActive() {
+ TestCommandRequest commandRequest = new TestCommandRequest("test_command", new Bundle());
+ VoiceInteractionSession.CommandRequest receivedCommandRequest =
+ shadowSession.sendCommandRequest(commandRequest, "test_package", 123);
+
+ assertThat(receivedCommandRequest.isActive()).isTrue();
+ assertThat(receivedCommandRequest.getCommand()).isEqualTo("test_command");
+
+ Bundle result = new Bundle();
+ result.putBoolean("intermediate", true);
+ receivedCommandRequest.sendIntermediateResult(result);
+ assertThat(commandRequest.isCompleted).isFalse();
+ assertThat(commandRequest.result).isEqualTo(result);
+ }
+
+ @Test
+ public void sendCommandRequest_sendFinalResult_requestCompleted() {
+ TestCommandRequest commandRequest = new TestCommandRequest("test_command", new Bundle());
+ VoiceInteractionSession.CommandRequest receivedCommandRequest =
+ shadowSession.sendCommandRequest(commandRequest, "test_package", 123);
+
+ assertThat(receivedCommandRequest.isActive()).isTrue();
+ assertThat(receivedCommandRequest.getCommand()).isEqualTo("test_command");
+
+ Bundle result = new Bundle();
+ result.putBoolean("final", true);
+ receivedCommandRequest.sendResult(result);
+ assertThat(commandRequest.isCompleted).isTrue();
+ assertThat(commandRequest.result).isEqualTo(result);
+ }
+
+ private static class TestCommandRequest extends CommandRequest {
+ public boolean isCancelled = false;
+ public boolean isCompleted = false;
+ public Bundle result = null;
+
+ public TestCommandRequest(String command, Bundle args) {
+ super(command, args);
+ }
+
+ @Override
+ public void onCommandResult(boolean isCompleted, Bundle result) {
+ this.isCompleted = isCompleted;
+ this.result = result;
+ }
+
+ @Override
+ public void onCancel() {
+ this.isCancelled = true;
+ }
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index fc5a5e5da..26acedbb8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -642,6 +642,38 @@ public class ShadowWifiManagerTest {
@Test
@Config(minSdk = Q)
+ public void isWpa3SaeSupportedAndConfigurable() {
+ assertThat(wifiManager.isWpa3SaeSupported()).isFalse();
+ shadowOf(wifiManager).setWpa3SaeSupported(true);
+ assertThat(wifiManager.isWpa3SaeSupported()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void isWpa3SaePublicKeySupportedAndConfigurable() {
+ assertThat(wifiManager.isWpa3SaePublicKeySupported()).isFalse();
+ shadowOf(wifiManager).setWpa3SaePublicKeySupported(true);
+ assertThat(wifiManager.isWpa3SaePublicKeySupported()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void isWpa3SaeH2eSupportedAndConfigurable() {
+ assertThat(wifiManager.isWpa3SaeH2eSupported()).isFalse();
+ shadowOf(wifiManager).setWpa3SaeH2eSupported(true);
+ assertThat(wifiManager.isWpa3SaeH2eSupported()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void isWpa3SuiteBSupportedAndConfigurable() {
+ assertThat(wifiManager.isWpa3SuiteBSupported()).isFalse();
+ shadowOf(wifiManager).setWpa3SuiteBSupported(true);
+ assertThat(wifiManager.isWpa3SuiteBSupported()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q)
public void testAddOnWifiUsabilityStatsListener() {
// GIVEN
WifiManager.OnWifiUsabilityStatsListener mockListener =
diff --git a/robolectric/src/test/resources/res/values/attrs.xml b/robolectric/src/test/resources/res/values/attrs.xml
index b96d70613..aea211d93 100644
--- a/robolectric/src/test/resources/res/values/attrs.xml
+++ b/robolectric/src/test/resources/res/values/attrs.xml
@@ -65,6 +65,7 @@
<attr name="attributeReferencingAnAttribute" format="reference"/>
<attr name="circularReference" format="reference"/>
<attr name="title" format="string"/>
+ <attr name="loaderIcon" format="reference"/>
<declare-styleable name="CustomStateView">
<attr name="stateFoo" format="boolean" />
diff --git a/robolectric/src/test/resources/resources.ap_ b/robolectric/src/test/resources/resources.ap_
index 52d9aeaf6..b34ec69fd 100644
--- a/robolectric/src/test/resources/resources.ap_
+++ b/robolectric/src/test/resources/resources.ap_
Binary files differ
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
index 53d4e5724..5c77b9964 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -38,6 +38,7 @@ import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
+import org.robolectric.sandbox.NativeMethodNotFoundException;
import org.robolectric.util.PerfStatsCollector;
/**
@@ -53,6 +54,7 @@ public class ClassInstrumentor {
protected static final Type OBJECT_TYPE = Type.getType(Object.class);
private static final ShadowImpl SHADOW_IMPL = new ShadowImpl();
final Decorator decorator;
+ private NativeCallHandler nativeCallHandler;
static {
String className = Type.getInternalName(InvokeDynamicSupport.class);
@@ -538,6 +540,18 @@ public class ClassInstrumentor {
method.access = method.access & ~Opcodes.ACC_NATIVE;
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
+
+ if (nativeCallHandler != null) {
+ String descriptor =
+ String.format("%s#%s%s", mutableClass.getName(), method.name, method.desc);
+ nativeCallHandler.logNativeCall(descriptor);
+ if (nativeCallHandler.shouldThrow(descriptor)) {
+ String message =
+ nativeCallHandler.getExceptionMessage(descriptor, mutableClass.getName(), method.name);
+ generator.throwException(Type.getType(NativeMethodNotFoundException.class), message);
+ }
+ }
+
Type returnType = generator.getReturnType();
generator.pushDefaultReturnValueToStack(returnType);
generator.returnValue();
@@ -739,6 +753,10 @@ public class ClassInstrumentor {
return -1;
}
+ public void setNativeCallHandler(NativeCallHandler nativeCallHandler) {
+ this.nativeCallHandler = nativeCallHandler;
+ }
+
public interface Decorator {
void decorate(MutableClass mutableClass);
}
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java
new file mode 100644
index 000000000..89034d63f
--- /dev/null
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java
@@ -0,0 +1,137 @@
+package org.robolectric.internal.bytecode;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.annotation.Nonnull;
+
+/**
+ * Handler for native calls instrumented by ClassInstrumentor.
+ *
+ * <p>Native Calls can either be instrumented as no-op calls (returning a default value or 0 or
+ * null) or throw an exception. This helper class helps maintain a list of exemptions to indicates
+ * which native calls should be no-op and never throw.
+ */
+public class NativeCallHandler {
+
+ private final File exemptionsFile;
+ private final boolean writeExemptions;
+ private final boolean throwOnNatives;
+ private final Set<String> descriptors = new TreeSet<>();
+
+ /**
+ * Initializes the native calls handler.
+ *
+ * @param exemptionsFile The exemptions file to read from and/or to generate.
+ * @param writeExemptions When true, native calls are added to the exemption list.
+ * @param throwOnNatives Whether native calls should throw by default unless their signature is
+ * listed in the exemption list. When false, all native calls become no-op.
+ * @throws IOException if there's an issue reading an existing exemption list.
+ */
+ public NativeCallHandler(
+ @Nonnull File exemptionsFile, boolean writeExemptions, boolean throwOnNatives)
+ throws IOException {
+ this.exemptionsFile = exemptionsFile;
+ this.writeExemptions = writeExemptions;
+ this.throwOnNatives = throwOnNatives;
+
+ if (exemptionsFile.exists()) {
+ readExemptionsList(exemptionsFile);
+ }
+ }
+
+ private String getExemptionFileName() {
+ return exemptionsFile.getName();
+ }
+
+ private void readExemptionsList(File exemptionsFile) throws IOException {
+ try (BufferedReader reader =
+ new BufferedReader(new FileReader(exemptionsFile.getPath(), UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ // Sanitize input. Ignore empty lines and commented lines starting with #.
+ line = sanitize(line.trim());
+ if (line.isEmpty() || line.charAt(0) == '#') {
+ continue;
+ }
+ descriptors.add(line);
+ }
+ }
+ System.out.println(
+ "Loaded " + descriptors.size() + " exemptions from " + exemptionsFile.getPath());
+ }
+
+ public void writeExemptionsList() throws IOException {
+ try (BufferedWriter writer =
+ new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) {
+ for (String descriptor : descriptors) {
+ writer.write(descriptor);
+ writer.write('\n');
+ }
+ }
+ System.out.println(
+ "Wrote " + descriptors.size() + " exemptions to " + exemptionsFile.getPath());
+ }
+
+ /**
+ * Adds the method description to the native call exemption list if {@link #writeExemptions} is
+ * set.
+ */
+ public void logNativeCall(@Nonnull String descriptor) {
+ if (!writeExemptions) {
+ return;
+ }
+ descriptors.add(sanitize(descriptor));
+ }
+
+ /** Returns whether the ClassInstrumentor should generate an exception or a no-op bytecode. */
+ public boolean shouldThrow(@Nonnull String descriptor) {
+ return throwOnNatives && !descriptors.contains(sanitize(descriptor));
+ }
+
+ private String sanitize(String descriptor) {
+ // Post-processing of the exemptions files is made complicated by the presence of $ signs
+ // in the FQCN. Instead of escaping them, just replace them by another unused character
+ // that is not so sensitive to shell or make mangling.
+ return descriptor.replace('$', '^');
+ }
+
+ /**
+ * Returns the detailed message to be used by the ClassInstrumentor in the generated bytecode.
+ *
+ * @param descriptor The ASM descriptor as it should be written in the exemption file.
+ * @param className The fully qualified class name, used for the user description.
+ * @param methodName The method name, used for the user description.
+ */
+ public String getExceptionMessage(
+ @Nonnull String descriptor, @Nonnull String className, @Nonnull String methodName) {
+ // The shadow message is merely a hint based on the last component of the FQCN, which is
+ // typically the pattern used for shadow classes.
+ String shadowHint =
+ "Shadow" + className.replaceAll("[^.]+\\.", "").replaceAll("\\$.*", "") + ".java";
+ // The message below tries to educate the user that shadow overrides are not necessarily
+ // needed nor desired for trivial cases that are better covered by a no-op return operation.
+ return "Unexpected Robolectric native method call to '"
+ + className
+ + "#"
+ + methodName
+ + "()'.\n"
+ + "Option 1: If customizing this method is useful, add an implementation in "
+ + shadowHint
+ + ".\n"
+ + "Option 2: If this method just needs to trivially return 0 or null, please add an"
+ + " exemption entry for\n"
+ + " "
+ + sanitize(descriptor)
+ + "\n"
+ + "to exemption file "
+ + getExemptionFileName();
+ }
+}
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java
index 51953fdc9..30ee4f563 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java
@@ -23,6 +23,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Priority;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.sandbox.NativeMethodNotFoundException;
import org.robolectric.sandbox.ShadowMatcher;
import org.robolectric.util.Function;
import org.robolectric.util.PerfStatsCollector;
@@ -182,7 +183,21 @@ public class ShadowWrangler implements ClassHandler {
} else {
RobolectricInternals.performStaticInitialization(clazz);
}
- } catch (InvocationTargetException | IllegalAccessException e) {
+ } catch (InvocationTargetException e) {
+ // Note: target exception originates from the sandbox classloader.
+ // "instanceof" does not check class equality across classloaders (since they differ).
+ // A simple workaround is to check the class FQCN instead.
+ String nativeMethodNotFoundException = NativeMethodNotFoundException.class.getName();
+
+ for (Throwable t = e.getTargetException(); t != null; ) {
+ if (nativeMethodNotFoundException.equals(t.getClass().getName())) {
+ throw (RuntimeException) t;
+ }
+
+ t = t.getCause();
+ }
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
diff --git a/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java b/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java
new file mode 100644
index 000000000..ad04d959f
--- /dev/null
+++ b/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java
@@ -0,0 +1,18 @@
+package org.robolectric.sandbox;
+
+/**
+ * Thrown when a particular Robolectric native method cannot be found.
+ *
+ * <p>Instrumented native methods throw this exception when the NativeCallHandler is set to
+ * throw-on-native and that the dedicated method signature has not been exempted.
+ */
+public class NativeMethodNotFoundException extends RuntimeException {
+
+ public NativeMethodNotFoundException() {
+ super();
+ }
+
+ public NativeMethodNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java b/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java
new file mode 100644
index 000000000..fa208d95d
--- /dev/null
+++ b/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java
@@ -0,0 +1,158 @@
+package org.robolectric.internal.bytecode;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/** Test for {@link ClassInstrumentor}. */
+@RunWith(JUnit4.class)
+public class ClassInstrumentorTest {
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private ClassInstrumentor instrumentor;
+ private ClassNodeProvider classNodeProvider;
+
+ @Before
+ public void setUp() throws Exception {
+ instrumentor = new ClassInstrumentor();
+ classNodeProvider =
+ new ClassNodeProvider() {
+ @Override
+ protected byte[] getClassBytes(String className) {
+ return new byte[0];
+ }
+ };
+ }
+
+ @Test
+ public void instrumentNativeMethod_legacy() {
+ ClassNode classNode = new ClassNode();
+ classNode.name = "org/example/MyClass";
+
+ MethodNode methodNode = new MethodNode();
+ methodNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_NATIVE;
+ methodNode.name = "someFunction";
+ methodNode.desc = "()I";
+ methodNode.signature = "()";
+ methodNode.exceptions = ImmutableList.of();
+ methodNode.visibleAnnotations = ImmutableList.of();
+
+ classNode.methods.add(methodNode);
+
+ MutableClass clazz =
+ new MutableClass(
+ classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider);
+ instrumentor.instrument(clazz);
+
+ // Side effect: original method has been made private.
+ assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0);
+ // Side effect: original method has been renamed to a robolectric delegate
+ assertThat(methodNode.name).isEqualTo("$$robo$$org_example_MyClass$someFunction");
+ // Side effect: instructions have been rewritten to return 0.
+ assertThat(methodNode.instructions.size()).isEqualTo(2);
+ assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.ICONST_0);
+ assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.IRETURN);
+ }
+
+ @Test
+ public void instrumentNativeMethod_withoutExemption_generatesThrowException() throws IOException {
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+ try (BufferedWriter writer =
+ new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) {
+ writer.write("org.example.MyClass#someOtherMethod()V\n");
+ }
+
+ NativeCallHandler nativeCallHandler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true);
+ instrumentor.setNativeCallHandler(nativeCallHandler);
+
+ ClassNode classNode = new ClassNode();
+ classNode.name = "org/example/MyClass";
+
+ MethodNode methodNode = new MethodNode();
+ methodNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_NATIVE;
+ methodNode.name = "someFunction";
+ methodNode.desc = "()I";
+ methodNode.signature = "()";
+ methodNode.exceptions = ImmutableList.of();
+ methodNode.visibleAnnotations = ImmutableList.of();
+
+ classNode.methods.add(methodNode);
+
+ MutableClass clazz =
+ new MutableClass(
+ classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider);
+ instrumentor.instrument(clazz);
+
+ // Side effect: original method has been made private.
+ assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0);
+ // Side effect: original method has been renamed to a robolectric delegate
+ assertThat(methodNode.name).isEqualTo("$$robo$$org_example_MyClass$someFunction");
+ // Side effect: instructions have been rewritten to throw and return.
+ assertThat(methodNode.instructions.size()).isEqualTo(7);
+ assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.NEW);
+ assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.DUP);
+ assertThat(methodNode.instructions.get(2).getOpcode()).isEqualTo(Opcodes.LDC);
+ assertThat(methodNode.instructions.get(3).getOpcode()).isEqualTo(Opcodes.INVOKESPECIAL);
+ assertThat(methodNode.instructions.get(4).getOpcode()).isEqualTo(Opcodes.ATHROW);
+ assertThat(methodNode.instructions.get(5).getOpcode()).isEqualTo(Opcodes.ICONST_0);
+ assertThat(methodNode.instructions.get(6).getOpcode()).isEqualTo(Opcodes.IRETURN);
+ }
+
+ @Test
+ public void instrumentNativeMethod_withExemption_generatesNoOpReturn() throws IOException {
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+ try (BufferedWriter writer =
+ new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) {
+ writer.write("org.example.MyClass#someOtherMethod()V\n");
+ writer.write("org.example.MyClass#someFunction()I\n");
+ }
+
+ NativeCallHandler nativeCallHandler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true);
+ instrumentor.setNativeCallHandler(nativeCallHandler);
+
+ ClassNode classNode = new ClassNode();
+ classNode.name = "org/example/MyClass";
+
+ MethodNode methodNode = new MethodNode();
+ methodNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_NATIVE;
+ methodNode.name = "someFunction";
+ methodNode.desc = "()I";
+ methodNode.signature = "()";
+ methodNode.exceptions = ImmutableList.of();
+ methodNode.visibleAnnotations = ImmutableList.of();
+
+ classNode.methods.add(methodNode);
+
+ MutableClass clazz =
+ new MutableClass(
+ classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider);
+ instrumentor.instrument(clazz);
+
+ // Side effect: original method has been made private.
+ assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0);
+ // Side effect: original method has been renamed to a robolectric delegate
+ assertThat(methodNode.name).isEqualTo("$$robo$$org_example_MyClass$someFunction");
+ // Side effect: instructions have been rewritten to return 0.
+ assertThat(methodNode.instructions.size()).isEqualTo(2);
+ assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.ICONST_0);
+ assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.IRETURN);
+ }
+}
diff --git a/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java b/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java
new file mode 100644
index 000000000..04035346e
--- /dev/null
+++ b/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java
@@ -0,0 +1,218 @@
+package org.robolectric.internal.bytecode;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.Files;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link NativeCallHandler}. */
+@RunWith(JUnit4.class)
+public class NativeCallHandlerTest {
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void jarInstrumentorLegacyUsage() throws IOException {
+ // CUJ: Legacy jarInstrumentor usage; there is no exemption file, native methods do not throw.
+
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+ assertThat(exemptionsFile.delete()).isTrue();
+
+ // Create handler, which loads exemptions from file. It's fine for the file to be missing.
+ NativeCallHandler handler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ false);
+
+ // No method descriptor should throw.
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse();
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isFalse();
+ }
+
+ @Test
+ public void jarInstrumentorUsage_throwOnNativesEnabled() throws IOException {
+ // CUJ: jarInstrumentor usage with an exemption list and non-exempted native methods should
+ // throw.
+
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+ try (BufferedWriter writer =
+ new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) {
+ writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n");
+ writer.write("libcore.io.Linux#chmod(Ljava/lang/String;I)V\n");
+ writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n");
+ writer.write("android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n");
+ writer.write("org.example.MyClass#someOtherMethod()V\n");
+ // empty or white-space lines are ignored
+ writer.write("\n");
+ writer.write(" \t \n");
+ // A # prefix denotes a comment and is ignored too
+ writer.write("# org.example.Ignored#comment()V\n");
+ writer.write(" # org.example.Ignored#thisIsACommentToo()V \n");
+ }
+
+ // Create handler, which loads exemptions from file. ThrowOnNatives is enabled.
+ NativeCallHandler handler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true);
+
+ // Test exempted methods
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse();
+
+ // Test non-exempted methods
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isTrue();
+
+ // Empty lines and comments are ignored and not present in the exemption list.
+ assertThat(handler.shouldThrow("")).isTrue();
+ assertThat(handler.shouldThrow(" \t ")).isTrue();
+ assertThat(handler.shouldThrow("# org.example.Ignored#comment()V")).isTrue();
+ assertThat(handler.shouldThrow(" # org.example.Ignored#thisIsACommentToo()V ")).isTrue();
+ }
+
+ @Test
+ public void jarInstrumentorUsage_throwOnNativesDisabled() throws IOException {
+ // CUJ: jarInstrumentor usage with an exemption list and non-exempted native methods should
+ // throw.
+
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+ try (BufferedWriter writer =
+ new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) {
+ writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n");
+ writer.write("libcore.io.Linux#chmod(Ljava/lang/String;I)V\n");
+ writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n");
+ writer.write("android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n");
+ writer.write("org.example.MyClass#someOtherMethod()V\n");
+ }
+
+ // Create handler, which loads exemptions from file. ThrowOnNatives is disabled.
+ NativeCallHandler handler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ false);
+
+ // Test exempted methods
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse();
+
+ // Test non-exempted methods
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isFalse();
+ }
+
+ @Test
+ public void jarInstrumentorUsage_logNativeCall_ignored() throws IOException {
+ // When not writing the exemption list, logNativeCall calls are no-op.
+
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+
+ // Create handler, which loads exemptions from file. ThrowOnNatives is enabled.
+ NativeCallHandler handler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true);
+
+ // No methods are exempted -- initial list is empty.
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue();
+ assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue();
+
+ handler.logNativeCall("org.example.MyClass#someOtherMethod()V");
+ handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V");
+
+ // LogNativeCall did not capture. These methods are still not exempted.
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue();
+ assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue();
+ }
+
+ @Test
+ public void exemptionListGeneratorUsage_logNativeCall_capturesCalls() throws IOException {
+ // CUJ: jarInstrumentor called to generate the exemption list.
+
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+
+ // Create handler, which loads exemptions from file. ThrowOnNatives is enabled.
+ NativeCallHandler handler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ true, /* throwOnNatives= */ true);
+
+ // No methods are exempted -- initial list is empty.
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue();
+ assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue();
+
+ handler.logNativeCall("org.example.MyClass#someOtherMethod()V");
+ handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V");
+
+ // These methods are now exempted.
+ assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse();
+ assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isFalse();
+ }
+
+ @Test
+ public void exemptionListGeneratorUsage_writeExemptionFile() throws IOException {
+ // CUJ: jarInstrumentor called to generate the exemption list.
+
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+ try (BufferedWriter writer =
+ new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) {
+ writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n");
+ writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n");
+ }
+
+ // Create handler, which loads exemptions from file. ThrowOnNatives is disabled.
+ NativeCallHandler handler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ true, /* throwOnNatives= */ false);
+
+ handler.logNativeCall("org.example.MyClass#someOtherMethod()V");
+ // Multiple calls with same value are idempotent.
+ handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V");
+ handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V");
+ handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V");
+ handler.logNativeCall("org.example.MyClass#someOtherMethod(II)V");
+ handler.logNativeCall("libcore.io.Linux#chmod(Ljava/lang/String;I)V");
+ // Case of a nested class with $ in the FQCN.
+ handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V");
+
+ handler.writeExemptionsList();
+
+ // Note: due to how the generated files are manipulated in the shell/makefile build system,
+ // '$' characters are a problem and would need to be escaped (and potentially differently for
+ // shell vs makefiles). The workaround is to have '$' rewritten as '^'.
+
+ assertThat(Files.asCharSource(exemptionsFile, UTF_8).read())
+ .isEqualTo(
+ "android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n"
+ // Font$Builder gets written as Font^Builder.
+ + "android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n"
+ + "libcore.io.Linux#chmod(Ljava/lang/String;I)V\n"
+ + "libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n"
+ + "org.example.MyClass#someOtherMethod()V\n"
+ + "org.example.MyClass#someOtherMethod(I)V\n"
+ + "org.example.MyClass#someOtherMethod(II)V\n");
+ }
+
+ @Test
+ public void getExceptionMessage() throws IOException {
+ File exemptionsFile = tempFolder.newFile("natives.txt");
+ NativeCallHandler handler =
+ new NativeCallHandler(
+ exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true);
+
+ // Test generated exception message for non-exempted methods.
+ assertThat(
+ handler.getExceptionMessage(
+ "org.example.MyClass$1#someOtherMethod(II)V",
+ "org.example.MyClass$1",
+ "someOtherMethod"))
+ .isEqualTo(
+ "Unexpected Robolectric native method call to"
+ + " 'org.example.MyClass$1#someOtherMethod()'.\n"
+ + "Option 1: If customizing this method is useful, add an implementation in"
+ + " ShadowMyClass.java.\n"
+ + "Option 2: If this method just needs to trivially return 0 or null, please add an"
+ + " exemption entry for\n"
+ + " org.example.MyClass^1#someOtherMethod(II)V\n"
+ + "to exemption file natives.txt");
+ }
+}
diff --git a/shadows/framework/Android.bp b/shadows/framework/Android.bp
index 04ba8ff71..8c050b589 100644
--- a/shadows/framework/Android.bp
+++ b/shadows/framework/Android.bp
@@ -35,6 +35,7 @@ java_library_host {
"Robolectric_pluginapi_upstream",
"Robolectric_sandbox_upstream",
"Robolectric_shadowapi_upstream",
+ "Robolectric_shadows_versioning_upstream",
"Robolectric_utils_upstream",
"Robolectric_utils_reflector_upstream",
"auto_value_annotations",
diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle
index a2230b0fe..57d2679d6 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -17,7 +17,7 @@ configurations {
def sqlite4javaVersion = libs.versions.sqlite4java.get()
-task copySqliteNatives(type: Copy) {
+tasks.register('copySqliteNatives', Copy) {
from project.configurations.sqlite4java {
include '**/*.dll'
include '**/*.so'
@@ -40,6 +40,10 @@ jar {
dependsOn copySqliteNatives
}
+javadoc {
+ dependsOn copySqliteNatives
+}
+
dependencies {
api project(":annotations")
api project(":nativeruntime")
diff --git a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
index 0385636a8..a58fd7069 100644
--- a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
+++ b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
@@ -21,6 +21,7 @@ import org.robolectric.android.Bootstrap;
import org.robolectric.android.ConfigurationV25;
import org.robolectric.res.ResourceTable;
import org.robolectric.shadows.ShadowDisplayManager;
+import org.robolectric.shadows.ShadowInstrumentation;
import org.robolectric.shadows.ShadowView;
import org.robolectric.util.Scheduler;
import org.robolectric.util.TempDirectory;
@@ -39,10 +40,10 @@ public class RuntimeEnvironment {
* incompatible with {@link org.robolectric.annotation.experimental.LazyApplication} and
* Robolectric makes no guarantees if a test *modifies* this field during execution.
*/
- @Deprecated public static Application application;
+ @Deprecated public static volatile Application application;
private static volatile Thread mainThread;
- private static Object activityThread;
+ private static volatile Object activityThread;
private static int apiLevel;
private static Scheduler masterScheduler;
private static ResourceTable systemResourceTable;
@@ -76,7 +77,7 @@ public class RuntimeEnvironment {
if (application == null) {
synchronized (supplierLock) {
if (applicationSupplier != null) {
- application = applicationSupplier.get();
+ ShadowInstrumentation.runOnMainSyncNoIdle(() -> application = applicationSupplier.get());
}
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java b/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java
index 09f737979..367ca62e1 100644
--- a/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java
+++ b/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java
@@ -20,7 +20,6 @@ public abstract class ComponentController<C extends ComponentController<C, T>, T
protected boolean attached;
- @SuppressWarnings("unchecked")
public ComponentController(T component, Intent intent) {
this(component);
this.intent = intent;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
index e2b8f0df3..ceca51e0e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
@@ -7,6 +7,7 @@ import android.net.MacAddress;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.versioning.AndroidVersions.U;
/** Builder for {@link AssociationInfo}. */
public class AssociationInfoBuilder {
@@ -16,10 +17,12 @@ public class AssociationInfoBuilder {
private String deviceMacAddress;
private CharSequence displayName;
private String deviceProfile;
+ private Object associatedDevice;
private boolean selfManaged;
private boolean notifyOnDeviceNearby;
private long approvedMs;
private long lastTimeConnectedMs;
+ private int systemDataSyncFlags;
private AssociationInfoBuilder() {}
@@ -57,6 +60,11 @@ public class AssociationInfoBuilder {
return this;
}
+ public AssociationInfoBuilder setAssociatedDevice(Object associatedDevice) {
+ this.associatedDevice = associatedDevice;
+ return this;
+ }
+
public AssociationInfoBuilder setSelfManaged(boolean selfManaged) {
this.selfManaged = selfManaged;
return this;
@@ -77,8 +85,15 @@ public class AssociationInfoBuilder {
return this;
}
+ public AssociationInfoBuilder setSystemDataSyncFlags(int systemDataSyncFlags) {
+ this.systemDataSyncFlags = systemDataSyncFlags;
+ return this;
+ }
+
public AssociationInfo build() {
try {
+ MacAddress macAddress =
+ deviceMacAddress == null ? null : MacAddress.fromString(deviceMacAddress);
if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
// We have two different constructors for AssociationInfo across
// T branches. aosp has the constructor that takes a new "revoked" parameter.
@@ -92,7 +107,7 @@ public class AssociationInfoBuilder {
ClassParameter.from(int.class, id),
ClassParameter.from(int.class, userId),
ClassParameter.from(String.class, packageName),
- ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+ ClassParameter.from(MacAddress.class, macAddress),
ClassParameter.from(CharSequence.class, displayName),
ClassParameter.from(String.class, deviceProfile),
ClassParameter.from(boolean.class, selfManaged),
@@ -106,11 +121,45 @@ public class AssociationInfoBuilder {
ClassParameter.from(int.class, id),
ClassParameter.from(int.class, userId),
ClassParameter.from(String.class, packageName),
- ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+ ClassParameter.from(MacAddress.class, macAddress),
+ ClassParameter.from(CharSequence.class, displayName),
+ ClassParameter.from(String.class, deviceProfile),
+ ClassParameter.from(boolean.class, selfManaged),
+ ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+ ClassParameter.from(long.class, approvedMs),
+ ClassParameter.from(long.class, lastTimeConnectedMs));
+ }
+ } else if (RuntimeEnvironment.getApiLevel() <= U.SDK_INT) {
+ // AOSP does not yet contains the new fields - mAssociatedDevice & mSystemDataSyncFlags yet
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) {
+ return ReflectionHelpers.callConstructor(
+ AssociationInfo.class,
+ ClassParameter.from(int.class, id),
+ ClassParameter.from(int.class, userId),
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(MacAddress.class, macAddress),
+ ClassParameter.from(CharSequence.class, displayName),
+ ClassParameter.from(String.class, deviceProfile),
+ ClassParameter.from(
+ Class.forName("android.companion.AssociatedDevice"), associatedDevice),
+ ClassParameter.from(boolean.class, selfManaged),
+ ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+ ClassParameter.from(boolean.class, false /*revoked*/),
+ ClassParameter.from(long.class, approvedMs),
+ ClassParameter.from(long.class, lastTimeConnectedMs),
+ ClassParameter.from(int.class, systemDataSyncFlags));
+ } else {
+ return ReflectionHelpers.callConstructor(
+ AssociationInfo.class,
+ ClassParameter.from(int.class, id),
+ ClassParameter.from(int.class, userId),
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(MacAddress.class, macAddress),
ClassParameter.from(CharSequence.class, displayName),
ClassParameter.from(String.class, deviceProfile),
ClassParameter.from(boolean.class, selfManaged),
ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+ ClassParameter.from(boolean.class, false /*revoked*/),
ClassParameter.from(long.class, approvedMs),
ClassParameter.from(long.class, lastTimeConnectedMs));
}
@@ -120,16 +169,18 @@ public class AssociationInfoBuilder {
ClassParameter.from(int.class, id),
ClassParameter.from(int.class, userId),
ClassParameter.from(String.class, packageName),
- ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+ ClassParameter.from(String.class, null /* tag */),
+ ClassParameter.from(MacAddress.class, macAddress),
ClassParameter.from(CharSequence.class, displayName),
ClassParameter.from(String.class, deviceProfile),
- ClassParameter.from(Class.forName("android.companion.AssociatedDevice"), null),
+ ClassParameter.from(
+ Class.forName("android.companion.AssociatedDevice"), associatedDevice),
ClassParameter.from(boolean.class, selfManaged),
ClassParameter.from(boolean.class, notifyOnDeviceNearby),
ClassParameter.from(boolean.class, false /*revoked*/),
ClassParameter.from(long.class, approvedMs),
ClassParameter.from(long.class, lastTimeConnectedMs),
- ClassParameter.from(int.class, 0 /*systemDataSyncFlags*/));
+ ClassParameter.from(int.class, systemDataSyncFlags));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java
index 54cccf067..c13b68678 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java
@@ -30,8 +30,7 @@ public class AudioDeviceInfoBuilder {
* Sets the device type.
*
* @param type The device type. The possible values are the constants defined as <a
- * href=https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/AudioDeviceInfo.java?q=AudioDeviceType>
- * {@code AudioDeviceInfo.AudioDeviceType}</a>
+ * href="https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/AudioDeviceInfo.java?q=AudioDeviceType">AudioDeviceInfo.AudioDeviceType</a>
*/
public AudioDeviceInfoBuilder setType(int type) {
this.type = type;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataEntity.java b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataEntity.java
new file mode 100644
index 000000000..38bf3f0cb
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataEntity.java
@@ -0,0 +1,58 @@
+package org.robolectric.shadows;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.auto.value.AutoValue;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * Represents a key value pair in {@link ShadowBackupDataInput} and {@link ShadowBackupDataOutput}.
+ */
+@AutoValue
+public abstract class BackupDataEntity {
+
+ /** The header key for a backup entity. */
+ public abstract String key();
+
+ /** The size of data in a backup entity. */
+ public abstract int dataSize();
+
+ /** The byte array of data in a backup entity. */
+ @SuppressWarnings("mutable")
+ public abstract byte[] data();
+
+ /**
+ * Constructs a new entity with the given key but a negative size. This represents a deleted pair.
+ */
+ public static BackupDataEntity createDeletedEntity(String key) {
+ return new AutoValue_BackupDataEntity(
+ checkNotNull(key), /* dataSize= */ -1, /* data= */ new byte[0]);
+ }
+
+ /**
+ * Constructs a pair with a string value. The value will be converted to a byte array in {@link
+ * StandardCharsets#UTF_8}.
+ */
+ public static BackupDataEntity create(String key, String data) {
+ return create(key, data.getBytes(UTF_8));
+ }
+
+ /** Constructs a new entity where the size of the value is the entire array. */
+ public static BackupDataEntity create(String key, byte[] data) {
+ return create(key, data, data.length);
+ }
+
+ /**
+ * Constructs a new entity.
+ *
+ * @param key the key of the pair
+ * @param data the value to associate with the key
+ * @param dataSize the length of the value in bytes
+ */
+ public static BackupDataEntity create(String key, byte[] data, int dataSize) {
+ return new AutoValue_BackupDataEntity(
+ checkNotNull(key), dataSize, Arrays.copyOf(data, dataSize));
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataInputBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataInputBuilder.java
new file mode 100644
index 000000000..8b91f6a73
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataInputBuilder.java
@@ -0,0 +1,46 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.app.backup.BackupDataInput;
+import com.google.common.collect.ImmutableList;
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for a {@link BackupDataInput} object. */
+public class BackupDataInputBuilder {
+
+ private final List<BackupDataEntity> entities = new ArrayList<>();
+
+ private BackupDataInputBuilder() {}
+
+ /** Creates a new builder for {@link BackupDataInput}. */
+ public static BackupDataInputBuilder newBuilder() {
+ return new BackupDataInputBuilder();
+ }
+
+ /** Adds the given entity to the input. */
+ public BackupDataInputBuilder addEntity(BackupDataEntity entity) {
+ entities.add(entity);
+ return this;
+ }
+
+ /** Builds the {@link BackupDataInput} instance with the added entities. */
+ public BackupDataInput build() {
+ BackupDataInput data =
+ reflector(BackupDataInputReflector.class).newBackupDataInput(new FileDescriptor());
+ shadowOf(data).setEntities(ImmutableList.copyOf(entities));
+ return data;
+ }
+
+ @ForType(BackupDataInput.class)
+ private interface BackupDataInputReflector {
+
+ @Constructor
+ BackupDataInput newBackupDataInput(FileDescriptor fd);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataOutputFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataOutputFactory.java
new file mode 100644
index 000000000..eb8c888a0
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataOutputFactory.java
@@ -0,0 +1,48 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.app.backup.BackupDataOutput;
+import android.os.Build.VERSION_CODES;
+import androidx.annotation.RequiresApi;
+import java.io.FileDescriptor;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Factory for instances of {@link BackupDataOutput}. */
+public class BackupDataOutputFactory {
+
+ private BackupDataOutputFactory() {}
+
+ /** Returns a new instance of {@link BackupDataOutput}. */
+ public static BackupDataOutput newInstance() {
+ return reflector(BackupDataOutputReflector.class).newBackupDataOutput(new FileDescriptor());
+ }
+
+ /** Returns a new instance of {@link BackupDataOutput}. */
+ @RequiresApi(VERSION_CODES.O)
+ public static BackupDataOutput newInstance(long quota) {
+ return reflector(BackupDataOutputReflector.class)
+ .newBackupDataOutput(new FileDescriptor(), quota);
+ }
+
+ /** Returns a new instance of {@link BackupDataOutput}. */
+ @RequiresApi(VERSION_CODES.P)
+ public static BackupDataOutput newInstance(long quota, int transportFlags) {
+ return reflector(BackupDataOutputReflector.class)
+ .newBackupDataOutput(new FileDescriptor(), quota, transportFlags);
+ }
+
+ @ForType(BackupDataOutput.class)
+ private interface BackupDataOutputReflector {
+
+ @Constructor
+ BackupDataOutput newBackupDataOutput(FileDescriptor fd);
+
+ @Constructor
+ BackupDataOutput newBackupDataOutput(FileDescriptor fd, long quota);
+
+ @Constructor
+ BackupDataOutput newBackupDataOutput(FileDescriptor fd, long quota, int transportFlags);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
index c9a723ca2..14c3a224a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
@@ -18,6 +18,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
+import java.util.Locale;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
@@ -33,7 +34,30 @@ public class ImageUtil {
private static final String FORMAT_NAME_PNG = "png";
private static boolean initialized;
+ /** Image information descriptor. */
+ public static class ImageInfo {
+
+ public final int width;
+ public final int height;
+ public final String mimeType;
+
+ ImageInfo(int width, int height, String mimeType) {
+ this.width = width;
+ this.height = height;
+ this.mimeType = mimeType;
+ }
+ }
+
static Point getImageSizeFromStream(InputStream is) {
+ ImageInfo info = getImageInfoFromStream(is);
+ if (info == null) {
+ return null;
+ } else {
+ return new Point(info.width, info.height);
+ }
+ }
+
+ static ImageInfo getImageInfoFromStream(InputStream is) {
if (!initialized) {
// Stops ImageIO from creating temp files when reading images
// from input stream.
@@ -49,7 +73,10 @@ public class ImageUtil {
ImageReader reader = readers.next();
try {
reader.setInput(imageStream);
- return new Point(reader.getWidth(0), reader.getHeight(0));
+ return new ImageInfo(
+ reader.getWidth(0),
+ reader.getHeight(0),
+ "image/" + reader.getFormatName().toLowerCase(Locale.US));
} finally {
reader.dispose();
}
@@ -84,7 +111,7 @@ public class ImageUtil {
format = reader.getFormatName();
int minIndex = reader.getMinIndex();
BufferedImage image = reader.read(minIndex);
- return RobolectricBufferedImage.create(image, ("image/" + format).toLowerCase());
+ return RobolectricBufferedImage.create(image, ("image/" + format).toLowerCase(Locale.US));
} finally {
reader.dispose();
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java
index 5c12213af..56a8fc9ee 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import org.robolectric.annotation.LooperMode;
import org.robolectric.shadow.api.ShadowPicker;
public class LooperShadowPicker<T> implements ShadowPicker<T> {
@@ -15,11 +14,15 @@ public class LooperShadowPicker<T> implements ShadowPicker<T> {
}
@Override
+ @SuppressWarnings("deprecation") // This is Robolectric library code
public Class<? extends T> pickShadowClass() {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return pausedShadowClass;
- } else {
- return legacyShadowClass;
+ switch (ShadowLooper.looperMode()) {
+ case LEGACY:
+ return legacyShadowClass;
+ case PAUSED:
+ case INSTRUMENTATION_TEST:
+ return pausedShadowClass;
}
+ throw new UnsupportedOperationException("Unrecognized looperMode " + ShadowLooper.looperMode());
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
index 459340273..95c079190 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
@@ -2,6 +2,7 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.Q;
+import static java.util.Arrays.asList;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.AudioCapabilities;
@@ -12,6 +13,7 @@ import android.media.MediaCodecInfo.VideoCapabilities;
import android.media.MediaFormat;
import android.util.Range;
import com.google.common.base.Preconditions;
+import java.util.HashSet;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
@@ -178,6 +180,7 @@ public class MediaCodecInfoBuilder {
private boolean isEncoder;
private CodecProfileLevel[] profileLevels = new CodecProfileLevel[0];
private int[] colorFormats;
+ private String[] requiredFeatures = new String[0];
private CodecCapabilitiesBuilder() {}
@@ -190,7 +193,12 @@ public class MediaCodecInfoBuilder {
* Sets media format.
*
* @param mediaFormat a {@link MediaFormat} supported by the codec. It is a requirement for
- * mediaFormat to have {@link MediaFormat.KEY_MIME} set. Other keys are optional.
+ * mediaFormat to have {@link MediaFormat.KEY_MIME} set. Other keys are optional. Setting
+ * {@link MediaFormat.KEY_WIDTH}, {@link MediaFormat.KEY_MAX_WIDTH} and {@link
+ * MediaFormat.KEY_HEIGHT}, {@link MediaFormat.KEY_MAX_HEIGHT} will set the minimum and
+ * maximum width, height respectively. For backwards compatibility, setting only {@link
+ * MediaFormat.KEY_WIDTH}, {@link MediaFormat.KEY_HEIGHT} will only set the maximum width,
+ * height respectively.
* @throws {@link NullPointerException} if mediaFormat is null.
* @throws {@link IllegalArgumentException} if mediaFormat does not have {@link
* MediaFormat.KEY_MIME}.
@@ -205,6 +213,16 @@ public class MediaCodecInfoBuilder {
}
/**
+ * Sets required features.
+ *
+ * @param requiredFeatures An array of {@link CodecCapabilities} FEATURE strings.
+ */
+ public CodecCapabilitiesBuilder setRequiredFeatures(String[] requiredFeatures) {
+ this.requiredFeatures = requiredFeatures;
+ return this;
+ }
+
+ /**
* Sets codec role.
*
* @param isEncoder a boolean to indicate whether the codec is an encoder or a decoder. Default
@@ -220,7 +238,7 @@ public class MediaCodecInfoBuilder {
*
* @param profileLevels an array of {@link MediaCodecInfo.CodecProfileLevel} supported by the
* codec.
- * @throws {@link NullPointerException} if profileLevels is null.
+ * @throws NullPointerException if profileLevels is null.
*/
public CodecCapabilitiesBuilder setProfileLevels(CodecProfileLevel[] profileLevels) {
this.profileLevels = Preconditions.checkNotNull(profileLevels);
@@ -265,6 +283,9 @@ public class MediaCodecInfoBuilder {
@Accessor("mFlagsSupported")
void setFlagsSupported(int flagsSupported);
+
+ @Accessor("mFlagsRequired")
+ void setFlagsRequired(int flagsRequired);
}
/** Accessor interface for {@link VideoCapabilities}'s internals. */
@@ -312,14 +333,27 @@ public class MediaCodecInfoBuilder {
VideoCapabilities videoCaps = createDefaultVideoCapabilities(caps, mediaFormat);
VideoCapabilitiesReflector videoCapsReflector =
Reflector.reflector(VideoCapabilitiesReflector.class, videoCaps);
- if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) {
+ if (mediaFormat.containsKey(MediaFormat.KEY_MAX_WIDTH)
+ && mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) {
+ videoCapsReflector.setWidthRange(
+ new Range<>(
+ mediaFormat.getInteger(MediaFormat.KEY_WIDTH),
+ mediaFormat.getInteger(MediaFormat.KEY_MAX_WIDTH)));
+ } else if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) {
videoCapsReflector.setWidthRange(
new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_WIDTH)));
}
- if (mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
+ if (mediaFormat.containsKey(MediaFormat.KEY_MAX_HEIGHT)
+ && mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
+ videoCapsReflector.setHeightRange(
+ new Range<>(
+ mediaFormat.getInteger(MediaFormat.KEY_HEIGHT),
+ mediaFormat.getInteger(MediaFormat.KEY_MAX_HEIGHT)));
+ } else if (mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
videoCapsReflector.setHeightRange(
new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)));
}
+
capsReflector.setVideoCaps(videoCaps);
} else {
AudioCapabilities audioCaps = createDefaultAudioCapabilities(caps, mediaFormat);
@@ -334,6 +368,9 @@ public class MediaCodecInfoBuilder {
if (RuntimeEnvironment.getApiLevel() >= Q) {
int flagsSupported = getSupportedFeatures(caps, mediaFormat);
capsReflector.setFlagsSupported(flagsSupported);
+
+ int flagsRequired = getRequiredFeatures(caps, requiredFeatures);
+ capsReflector.setFlagsRequired(flagsRequired);
}
return caps;
@@ -386,5 +423,23 @@ public class MediaCodecInfoBuilder {
}
return flagsSupported;
}
+
+ /**
+ * Read codec features from a given array of feature strings and convert them to values
+ * recognized by {@link CodecCapabilities}.
+ */
+ private static int getRequiredFeatures(CodecCapabilities parent, String[] requiredFeatures) {
+ int flagsRequired = 0;
+ Object[] validFeatures = ReflectionHelpers.callInstanceMethod(parent, "getValidFeatures");
+ HashSet<String> requiredFeaturesSet = new HashSet<>(asList(requiredFeatures));
+ for (Object validFeature : validFeatures) {
+ String featureName = (String) ReflectionHelpers.getField(validFeature, "mName");
+ int featureValue = (int) ReflectionHelpers.getField(validFeature, "mValue");
+ if (requiredFeaturesSet.contains(featureName)) {
+ flagsRequired |= featureValue;
+ }
+ }
+ return flagsRequired;
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java b/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java
index d04421c9b..d136be83d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java
@@ -34,8 +34,10 @@ import org.robolectric.res.android.Ref;
* Java representation of framework native input Transliterated from oreo-mr1 (SDK 27)
* frameworks/native/include/input/Input.h and libs/input/Input.cpp
*
- * @see <a href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/include/input/Input.h">include/input/Input.h</a>
- * @see <a href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/libs/input/Input.cpp>libs/input/Input.cpp</a>
+ * @see <a
+ * href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/include/input/Input.h">include/input/Input.h</a>
+ * @see <a
+ * href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/libs/input/Input.cpp">libs/input/Input.cpp</a>
*/
public class NativeInput {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilder.java
new file mode 100644
index 000000000..44706fba2
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilder.java
@@ -0,0 +1,138 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build.VERSION;
+import android.telephony.CellIdentity;
+import android.telephony.DataSpecificRegistrationInfo;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.VoiceSpecificRegistrationInfo;
+import androidx.annotation.RequiresApi;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/**
+ * Builder class to create instance of {@link NetworkRegistrationInfo}.
+ *
+ * <p>NRI was first made a @SystemApi in Q then finally exposed as public in R.
+ *
+ * <p>This builder class does not extend {@link NetworkRegistrationInfo.Builder}. It uses {@link
+ * NetworkRegistrationInfo.Builder} and some additional APIs to set NRI private fields.
+ */
+@RequiresApi(Q)
+public class NetworkRegistrationInfoTestBuilder {
+
+ private final NetworkRegistrationInfo.Builder buider = new NetworkRegistrationInfo.Builder();
+
+ private VoiceSpecificRegistrationInfo voiceSpecificInfo;
+ private DataSpecificRegistrationInfo dataSpecificInfo;
+ private int roamingType;
+
+ public static NetworkRegistrationInfoTestBuilder newBuilder() {
+ return new NetworkRegistrationInfoTestBuilder();
+ }
+
+ public NetworkRegistrationInfo build() {
+ NetworkRegistrationInfo networkRegistrationInfo = buider.build();
+ if (VERSION.SDK_INT < Q) {
+ throw new IllegalStateException(
+ "NetworkRegistrationInfo not available on SDK : " + RuntimeEnvironment.getApiLevel());
+ } else if (VERSION.SDK_INT < TIRAMISU) {
+ reflector(NetworkRegistrationInfoReflector.class, networkRegistrationInfo)
+ .setVoiceSpecificInfo(voiceSpecificInfo);
+ reflector(NetworkRegistrationInfoReflector.class, networkRegistrationInfo)
+ .setDataSpecificInfo(dataSpecificInfo);
+ }
+ networkRegistrationInfo.setRoamingType(roamingType);
+ return networkRegistrationInfo;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setAccessNetworkTechnology(int value) {
+ buider.setAccessNetworkTechnology(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setAvailableServices(List<Integer> value) {
+ buider.setAvailableServices(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setCellIdentity(CellIdentity value) {
+ buider.setCellIdentity(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setDomain(int value) {
+ buider.setDomain(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setEmergencyOnly(boolean value) {
+ buider.setEmergencyOnly(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setRegisteredPlmn(String value) {
+ if (VERSION.SDK_INT == Q) {
+ throw new IllegalStateException(
+ "Registered PLMN is not available on SDK : " + RuntimeEnvironment.getApiLevel());
+ } else {
+ buider.setRegisteredPlmn(value);
+ }
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setRegistrationState(int value) {
+ buider.setRegistrationState(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setRejectCause(int value) {
+ buider.setRejectCause(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setTransportType(int value) {
+ buider.setTransportType(value);
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setDataSpecificInfo(
+ DataSpecificRegistrationInfo value) {
+ if (VERSION.SDK_INT >= TIRAMISU) {
+ buider.setDataSpecificInfo(value);
+ } else {
+ dataSpecificInfo = value;
+ }
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setVoiceSpecificInfo(
+ VoiceSpecificRegistrationInfo value) {
+ if (VERSION.SDK_INT >= TIRAMISU) {
+ buider.setVoiceSpecificInfo(value);
+ } else {
+ voiceSpecificInfo = value;
+ }
+ return this;
+ }
+
+ public NetworkRegistrationInfoTestBuilder setRoamingType(int value) {
+ roamingType = value;
+ return this;
+ }
+
+ @ForType(NetworkRegistrationInfo.class)
+ private interface NetworkRegistrationInfoReflector {
+
+ @Accessor("mDataSpecificInfo")
+ public void setDataSpecificInfo(DataSpecificRegistrationInfo value);
+
+ @Accessor("mVoiceSpecificInfo")
+ public void setVoiceSpecificInfo(VoiceSpecificRegistrationInfo value);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/PhoneCapabilityFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/PhoneCapabilityFactory.java
new file mode 100644
index 000000000..0252c12c0
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/PhoneCapabilityFactory.java
@@ -0,0 +1,26 @@
+package org.robolectric.shadows;
+
+import android.telephony.PhoneCapability;
+import java.util.ArrayList;
+
+/** Factory to create PhoneCapability. */
+public final class PhoneCapabilityFactory {
+
+ /** Creates PhoneCapability. */
+ public static PhoneCapability create(
+ int maxActiveVoiceSubscriptions,
+ int maxActiveDataSubscriptions,
+ boolean networkValidationBeforeSwitchSupported,
+ int[] deviceNrCapabilities) {
+ return new PhoneCapability(
+ maxActiveVoiceSubscriptions,
+ maxActiveDataSubscriptions,
+ // Since ModemInfo is an @hide object, there is no reason for an external object to be able
+ // to declare it, using an empty ArrayList as the parameter here.
+ /* List<ModemInfo> */ new ArrayList<>(),
+ networkValidationBeforeSwitchSupported,
+ deviceNrCapabilities);
+ }
+
+ private PhoneCapabilityFactory() {}
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java
index 1fa638b3b..ddabcaf66 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java
@@ -36,7 +36,7 @@ public class PreciseDataConnectionStateBuilder {
}
public PreciseDataConnectionStateBuilder setTransportType(int transportType) {
- this.transportType = networkType;
+ this.transportType = transportType;
return this;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ServiceStateBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ServiceStateBuilder.java
new file mode 100644
index 000000000..2417bf8a7
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ServiceStateBuilder.java
@@ -0,0 +1,141 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build.VERSION;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import androidx.annotation.RequiresApi;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder class to create instance of {@link ServiceState}. */
+public class ServiceStateBuilder {
+ private ServiceState serviceState = new ServiceState();
+
+ public static ServiceStateBuilder newBuilder() {
+ return new ServiceStateBuilder();
+ }
+
+ public static ServiceStateBuilder newBuilder(ServiceState serviceState) {
+ ServiceStateBuilder builder = new ServiceStateBuilder();
+ builder.serviceState = serviceState;
+ return builder;
+ }
+
+ public ServiceState build() {
+ return serviceState;
+ }
+
+ public ServiceStateBuilder setVoiceRegState(int value) {
+ serviceState.setVoiceRegState(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setDataRegState(int value) {
+ serviceState.setDataRegState(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setNrFrequencyRange(int value) {
+ assertIsAtLeast(Q);
+ serviceState.setNrFrequencyRange(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setIsManualSelection(boolean value) {
+ serviceState.setIsManualSelection(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setOperatorName(String longName, String shortName, String numeric) {
+ assertIsAtLeast(R);
+ serviceState.setOperatorName(longName, shortName, numeric);
+ return this;
+ }
+
+ public ServiceStateBuilder setIwlanPreferred(boolean value) {
+ assertIsAtLeast(R);
+ serviceState.setIwlanPreferred(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setEmergencyOnly(boolean value) {
+ serviceState.setEmergencyOnly(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setDataRoamingFromRegistration(boolean value) {
+ assertIsAtLeast(R);
+ serviceState.setDataRoamingFromRegistration(value);
+ return this;
+ }
+
+ /**
+ * Use this method to control return value of {@link ServiceState#isUsingCarrierAggregation()} (up
+ * to P). On APIs > P, use {@link ServiceStateBuilder#setNetworkRegistrationInfoList()}.
+ */
+ public ServiceStateBuilder setIsUsingCarrierAggregation(boolean value) {
+ assertIsAtLeast(P);
+ // {@link NetworkRegistrationInfo} was first made a @SystemApi in Q then finally exposed as
+ // public in R. For SDK later than Q, call
+ // {@link ServiceStateBuilder#setNetworkRegistrationInfoList} to set this value. Downstream test
+ // code will have to specify NRIs in the builder to set this value. But the "actual"
+ // implementation code under test would still be looking at the non-NRI getters
+ // on {@link ServiceState}, assuming it's restricted to only public APIs.
+ if (VERSION.SDK_INT >= Q) {
+ throw new UnsupportedOperationException(
+ "Newer SDKs must specify carrier aggregation by constructing an appropriate "
+ + "NetworkRegistrationInfo and calling #setNetworkRegistrationInfoList instead");
+ } else {
+ reflector(ServiceStateReflector.class, serviceState).setIsUsingCarrierAggregation(value);
+ }
+ return this;
+ }
+
+ @RequiresApi(Q)
+ public ServiceStateBuilder setNetworkRegistrationInfoList(List<NetworkRegistrationInfo> value) {
+ assertIsAtLeast(Q);
+ reflector(ServiceStateReflector.class, serviceState).setNetworkRegistrationInfos(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setRoaming(boolean value) {
+ serviceState.setRoaming(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setChannelNumber(int value) {
+ serviceState.setChannelNumber(value);
+ return this;
+ }
+
+ public ServiceStateBuilder setCellBandwidths(int[] value) {
+ serviceState.setCellBandwidths(value);
+ return this;
+ }
+
+ // TODO Find a proper way to set radio tech values.
+
+ private void assertIsAtLeast(int sdk) {
+ if (VERSION.SDK_INT < sdk) {
+ throw new IllegalStateException(
+ "This method is not available on SDK : " + RuntimeEnvironment.getApiLevel());
+ }
+ }
+
+ @ForType(ServiceState.class)
+ private interface ServiceStateReflector {
+
+ @Accessor("mIsUsingCarrierAggregation")
+ public void setIsUsingCarrierAggregation(boolean value);
+
+ @Accessor("mNetworkRegistrationInfos")
+ public void setNetworkRegistrationInfos(List<NetworkRegistrationInfo> value);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java
index c28bdc44f..bd588217a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java
@@ -207,7 +207,7 @@ public class ShadowActivityManager {
* Sets the values to be returned by {@link #getAppTasks()}.
*
* @see #getAppTasks()
- * @param tasks List of app tasks.
+ * @param appTasks List of app tasks.
*/
public void setAppTasks(List<ActivityManager.AppTask> appTasks) {
this.appTasks.clear();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
index 852919a4d..8b55be124 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
@@ -14,10 +14,12 @@ import android.app.ActivityThread.ActivityClientRecord;
import android.app.Application;
import android.app.Instrumentation;
import android.app.ResultInfo;
+import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ComponentInfoFlags;
import android.content.res.Configuration;
import android.os.IBinder;
import com.android.internal.content.ReferrerIntent;
@@ -79,12 +81,29 @@ public class ShadowActivityThread {
} else if (method.getName().equals("notifyPackageUse")) {
return null;
} else if (method.getName().equals("getPackageInstaller")) {
- return null;
+ try {
+ Class<?> iPackageInstallerClass =
+ classLoader.loadClass("android.content.pm.IPackageInstaller");
+ return ReflectionHelpers.createNullProxy(iPackageInstallerClass);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
} else if (method.getName().equals("hasSystemFeature")) {
String featureName = (String) args[0];
return RuntimeEnvironment.getApplication()
.getPackageManager()
.hasSystemFeature(featureName);
+ } else if (method.getName().equals("getServiceInfo")) {
+ ComponentName componentName = (ComponentName) args[0];
+ if (args[1] instanceof ComponentInfoFlags) {
+ return RuntimeEnvironment.getApplication()
+ .getPackageManager()
+ .getServiceInfo(componentName, (ComponentInfoFlags) args[1]);
+ } else {
+ return RuntimeEnvironment.getApplication()
+ .getPackageManager()
+ .getServiceInfo(componentName, ((Number) args[1]).intValue());
+ }
}
throw new UnsupportedOperationException("sorry, not supporting " + method + " yet!");
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
index 8771d6adb..d0e187fa7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
@@ -5,13 +5,15 @@ import android.annotation.Nullable;
import android.content.res.AssetManager;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.versioning.AndroidVersions.U;
+
// TODO: update path to released version.
// transliterated from
// https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_rXX/core/jni/android_util_AssetManager.cpp
@Implements(
value = AssetManager.class,
- minSdk = ShadowBuild.UPSIDE_DOWN_CAKE,
+ minSdk = U.SDK_INT,
shadowPicker = ShadowAssetManager.Picker.class)
@SuppressWarnings("NewApi")
public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 {
@@ -25,7 +27,7 @@ public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 {
// jint smallest_screen_width_dp, jint screen_width_dp,
// jint screen_height_dp, jint screen_layout, jint ui_mode,
// jint color_mode, jint major_version) {
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected static void nativeSetConfiguration(
long ptr,
int mcc,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java
index 7ce0540ae..17357d66e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java
@@ -6,8 +6,10 @@ import org.robolectric.annotation.Implements;
/**
* The shadow API for {@link AsyncTaskLoader}.
*
- * Different shadow implementations will be used based on the current {@link LooperMode.Mode}.
- * @see ShadowLegacyAsyncTaskLoader, ShadowPausedAsyncTaskLoader
+ * <p>Different shadow implementations will be used based on the current {@link LooperMode.Mode}.
+ *
+ * @see ShadowLegacyAsyncTaskLoader
+ * @see ShadowPausedAsyncTaskLoader
*/
@Implements(value = AsyncTaskLoader.class, shadowPicker = ShadowAsyncTaskLoader.Picker.class)
public abstract class ShadowAsyncTaskLoader<D> {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
index 6408ce87c..ecf5f16ea 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
@@ -37,6 +37,7 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
+import org.robolectric.versioning.AndroidVersions.U;
/**
* Implementation of a couple methods in {@link AudioTrack}. Only a couple methods are supported,
@@ -245,7 +246,7 @@ public class ShadowAudioTrack {
return AudioTrack.SUCCESS;
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected int native_setup(
Object /*WeakReference<AudioTrack>*/ audioTrack,
Object /*AudioAttributes*/ attributes,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataInput.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataInput.java
new file mode 100644
index 000000000..eb9445101
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataInput.java
@@ -0,0 +1,107 @@
+package org.robolectric.shadows;
+
+import static java.lang.Math.min;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.app.backup.BackupDataInput;
+import android.os.Build.VERSION_CODES;
+import com.google.common.collect.ImmutableList;
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow for BackupDataInput. */
+@Implements(value = BackupDataInput.class, minSdk = VERSION_CODES.LOLLIPOP, looseSignatures = true)
+public class ShadowBackupDataInput {
+
+ private List<BackupDataEntity> entities = new ArrayList<>();
+ private int currentEntityIndex = -1;
+ private int currentBytesRead = 0;
+ private int currentBytesToRead = 0;
+
+ /**
+ * Sets the entities to return when reading from this {@link BackupDataInput}. Use {@link
+ * org.robolectric.shadows.BackupDataInputBuilder} to get a populated instance.
+ */
+ void setEntities(ImmutableList<BackupDataEntity> entities) {
+ this.entities = entities;
+ }
+
+ @Implementation
+ protected static long ctor(FileDescriptor fd) {
+ // Return value greater than 0 to indicate successful allocation of backup reader. The real
+ // implementation would return an allocated pointer address, but since the methods are not
+ // static, we do not need a native object in this shadow implementation, and can return a fixed
+ // value instead.
+ return 1;
+ }
+
+ // Using loose signature because EntityHeader is a private nested class.
+ @Implementation
+ protected int readNextHeader_native(Object backupReader, Object entity) {
+ if (currentBytesRead < currentBytesToRead) {
+ // Return failure to read header due to unread data bytes.
+ return -1;
+ }
+
+ currentEntityIndex++;
+
+ if (currentEntityIndex >= entities.size()) {
+ // Return end of backup input data.
+ return 1;
+ }
+
+ BackupDataEntity shadowEntity = entities.get(currentEntityIndex);
+
+ currentBytesRead = 0;
+ currentBytesToRead = shadowEntity.dataSize();
+
+ // Accessing using reflection because EntityHeader is a private nested class.
+ reflector(EntityHeaderReflector.class, entity).setKey(shadowEntity.key());
+ reflector(EntityHeaderReflector.class, entity).setDataSize(shadowEntity.dataSize());
+ return 0;
+ }
+
+ @Implementation
+ protected int readEntityData_native(long backupReader, byte[] data, int offset, int size) {
+ if (currentEntityIndex >= entities.size() || currentBytesRead >= currentBytesToRead) {
+ // Return end of data.
+ return 0;
+ }
+
+ if (offset + size > data.length) {
+ // Return error reading data.
+ return -1;
+ }
+
+ byte[] shadowData = entities.get(currentEntityIndex).data();
+ int remainingBytes = currentBytesToRead - currentBytesRead;
+ int bytesToRead = min(size, remainingBytes);
+
+ System.arraycopy(shadowData, currentBytesRead, data, offset, bytesToRead);
+ currentBytesRead += bytesToRead;
+ return bytesToRead;
+ }
+
+ @Implementation
+ protected int skipEntityData_native(long backupReader) {
+ if (currentEntityIndex < entities.size()) {
+ currentBytesRead = entities.get(currentEntityIndex).dataSize();
+ }
+ return 0;
+ }
+
+ @ForType(className = "android.app.backup.BackupDataInput$EntityHeader")
+ private interface EntityHeaderReflector {
+
+ @Accessor("key")
+ void setKey(String key);
+
+ @Accessor("dataSize")
+ void setDataSize(int dataSize);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataOutput.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataOutput.java
new file mode 100644
index 000000000..9c48306f2
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataOutput.java
@@ -0,0 +1,109 @@
+package org.robolectric.shadows;
+
+import android.app.backup.BackupDataOutput;
+import android.os.Build.VERSION_CODES;
+import androidx.annotation.Nullable;
+import com.google.common.collect.ImmutableList;
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.res.android.NativeObjRegistry;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow for BackupDataOutput. */
+@Implements(value = BackupDataOutput.class, minSdk = VERSION_CODES.LOLLIPOP)
+public class ShadowBackupDataOutput {
+
+ protected static final String KEY_PREFIX_JOINER = ":";
+
+ private static final NativeObjRegistry<NativeBackupDataOutput> nativeObjectRegistry =
+ new NativeObjRegistry<>(NativeBackupDataOutput.class);
+
+ @ReflectorObject private BackupDataOutputReflector backupDataOutputReflector;
+
+ /** Gets a list of all data written to the {@link BackupDataOutput}. */
+ public ImmutableList<BackupDataEntity> getEntities() {
+ return ImmutableList.copyOf(
+ nativeObjectRegistry.getNativeObject(backupDataOutputReflector.getBackupWriter()).entities);
+ }
+
+ @Implementation
+ protected static int writeEntityHeader_native(long mBackupWriter, String key, int dataSize) {
+ NativeBackupDataOutput nativeObject = nativeObjectRegistry.getNativeObject(mBackupWriter);
+
+ if (nativeObject.currentEntity != null
+ && nativeObject.currentBytesWritten < nativeObject.currentEntity.dataSize()) {
+ // Return failed due to write due to unfinished previous record.
+ return -1;
+ }
+
+ String prefixedKey =
+ nativeObject.keyPrefix != null ? nativeObject.keyPrefix + KEY_PREFIX_JOINER + key : key;
+ if (dataSize >= 0) {
+ nativeObject.currentEntity = BackupDataEntity.create(prefixedKey, new byte[dataSize]);
+ } else {
+ nativeObject.currentEntity = BackupDataEntity.createDeletedEntity(prefixedKey);
+ }
+ nativeObject.currentBytesWritten = 0;
+
+ nativeObject.entities.add(nativeObject.currentEntity);
+
+ // Return bytes written (1 byte per char in key plus 1 for the size int).
+ return key.length() + 1;
+ }
+
+ @Implementation
+ protected static int writeEntityData_native(long mBackupWriter, byte[] data, int size) {
+ NativeBackupDataOutput nativeObject = nativeObjectRegistry.getNativeObject(mBackupWriter);
+
+ if (nativeObject.currentEntity == null) {
+ // Return error writing due to missing header.
+ return -1;
+ }
+
+ if (size > data.length
+ || nativeObject.currentBytesWritten + size > nativeObject.currentEntity.dataSize()) {
+ // Return error writing due to size exceeding of one of the arrays.
+ return -1;
+ }
+
+ System.arraycopy(
+ data, 0, nativeObject.currentEntity.data(), nativeObject.currentBytesWritten, size);
+ nativeObject.currentBytesWritten += size;
+
+ return size;
+ }
+
+ @Implementation
+ protected static void setKeyPrefix_native(long mBackupWriter, String keyPrefix) {
+ nativeObjectRegistry.getNativeObject(mBackupWriter).keyPrefix = keyPrefix;
+ }
+
+ @Implementation
+ protected static long ctor(FileDescriptor fd) {
+ return nativeObjectRegistry.register(new NativeBackupDataOutput());
+ }
+
+ @Implementation
+ protected static void dtor(long mBackupWriter) {
+ nativeObjectRegistry.unregister(mBackupWriter);
+ }
+
+ @ForType(BackupDataOutput.class)
+ private interface BackupDataOutputReflector {
+
+ @Accessor("mBackupWriter")
+ long getBackupWriter();
+ }
+
+ private static final class NativeBackupDataOutput {
+ final List<BackupDataEntity> entities = new ArrayList<>();
+ @Nullable String keyPrefix = null;
+ @Nullable BackupDataEntity currentEntity = null;
+ int currentBytesWritten = 0;
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java
index 6dabdf569..21e7d51f3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java
@@ -110,7 +110,7 @@ public class ShadowBiometricManager {
*
* @param type to set the authenticatorType
* @see <a
- * href="https://developer.android.com/reference/android/hardware/biometrics/BiometricManager#canAuthenticate(int)"
+ * href="https://developer.android.com/reference/android/hardware/biometrics/BiometricManager#canAuthenticate(int)">BiometricManager#canAuthenticate(int)</a>
*/
public void setAuthenticatorType(int type) {
authenticatorType = type;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
index 74133081c..ca55a0657 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
@@ -1,16 +1,27 @@
package org.robolectric.shadows;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
import android.graphics.Bitmap;
import android.graphics.Matrix;
+import com.google.common.base.Preconditions;
import java.io.InputStream;
+import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowBitmap.Picker;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.versioning.AndroidVersions.U;
/** Base class for {@link Bitmap} shadows. */
-@Implements(value = Bitmap.class, shadowPicker = Picker.class)
+@Implements(value = Bitmap.class, shadowPicker = Picker.class, looseSignatures = true)
public abstract class ShadowBitmap {
+ @RealObject Bitmap realBitmap;
+
/**
* Returns a textual representation of the appearance of the object.
*
@@ -124,6 +135,33 @@ public abstract class ShadowBitmap {
public abstract void setDescription(String s);
+ @Implementation(minSdk = U.SDK_INT)
+ protected void setGainmap(Object gainmap) {
+ Preconditions.checkState(!realBitmap.isRecycled(), "Bitmap is recycled");
+ reflector(BitmapReflector.class, realBitmap).setGainmap(gainmap);
+ }
+
+ @Implementation(minSdk = U.SDK_INT)
+ protected boolean hasGainmap() {
+ Preconditions.checkState(!realBitmap.isRecycled(), "Bitmap is recycled");
+ return reflector(BitmapReflector.class, realBitmap).getGainmap() != null;
+ }
+
+ /** Reflector for {@link Bitmap}. */
+ @ForType(Bitmap.class)
+ protected interface BitmapReflector {
+ void checkRecycled(String errorMessage);
+
+ @Accessor("mNativePtr")
+ long getNativePtr();
+
+ @Accessor("mGainmap")
+ void setGainmap(Object gainmap);
+
+ @Direct
+ Object getGainmap();
+ }
+
/** Shadow picker for {@link Bitmap}. */
public static final class Picker extends GraphicsShadowPicker<Object> {
public Picker() {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
index f8725068e..4dbf20915 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
@@ -22,6 +22,7 @@ import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothProfileServiceConnection;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.bluetooth.le.BluetoothLeScanner;
import android.content.AttributionSource;
@@ -29,6 +30,7 @@ import android.content.Context;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.ParcelUuid;
+import android.os.RemoteException;
import android.provider.Settings;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
@@ -129,7 +131,9 @@ public class ShadowBluetoothAdapter {
/** Requires LooseSignatures because of {@link AttributionSource} parameter */
@Implementation(minSdk = VERSION_CODES.TIRAMISU)
protected static Object createAdapter(Object attributionSource) {
- IBluetoothManager service = ReflectionHelpers.createNullProxy(IBluetoothManager.class);
+ IBluetoothManager service =
+ ReflectionHelpers.createDelegatingProxy(
+ IBluetoothManager.class, new BluetoothManagerDelegate());
return ReflectionHelpers.callConstructor(
BluetoothAdapter.class,
ClassParameter.from(IBluetoothManager.class, service),
@@ -304,6 +308,11 @@ public class ShadowBluetoothAdapter {
}
@Implementation
+ protected boolean disable(boolean persist) {
+ return disable();
+ }
+
+ @Implementation
protected String getAddress() {
return this.address;
}
@@ -738,4 +747,39 @@ public class ShadowBluetoothAdapter {
@Static
void setSBluetoothLeScanner(BluetoothLeScanner scanner);
}
+
+ // Any BluetoothAdapter calls which need to invoke BluetoothManager methods can delegate those
+ // calls to this class. The default behavior for any methods not defined in this class is a no-op.
+ @SuppressWarnings("unused")
+ private static class BluetoothManagerDelegate {
+ /**
+ * Allows the internal BluetoothProfileConnector associated with a {@link BluetoothProfile} to
+ * automatically invoke the service connected callback.
+ */
+ public boolean bindBluetoothProfileService(
+ int bluetoothProfile, String serviceName, IBluetoothProfileServiceConnection proxy) {
+ if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
+ return false;
+ }
+ try {
+ proxy.onServiceConnected(null, null);
+ } catch (RemoteException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Allows the internal BluetoothProfileConnector associated with a {@link BluetoothProfile} to
+ * automatically invoke the service disconnected callback.
+ */
+ public void unbindBluetoothProfileService(
+ int bluetoothProfile, IBluetoothProfileServiceConnection proxy) {
+ try {
+ proxy.onServiceDisconnected(null);
+ } catch (RemoteException e) {
+ // nothing to do
+ }
+ }
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java
index de2e76e40..7927da22c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java
@@ -116,7 +116,11 @@ public class ShadowBluetoothGattServer {
public List<byte[]> getResponses() {
List<byte[]> responsesCopy = new ArrayList<>();
for (byte[] response : this.responses) {
- responsesCopy.add(response.clone());
+ if (response != null) {
+ responsesCopy.add(response.clone());
+ } else {
+ responsesCopy.add(null);
+ }
}
return responsesCopy;
}
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 c1c887acc..b6b1b8309 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
@@ -23,10 +23,13 @@ public class ShadowBuild {
private static String serialOverride = Build.UNKNOWN;
/**
- * Temporary constant for VERSION_CODES.UPSIDE_DOWN_CAKE. Will be removed and replaced once the
- * constant is available upstream.
+ * Sets the value of the {@link Build#BOARD} field.
+ *
+ * <p>It will be reset for the next test.
*/
- public static final int UPSIDE_DOWN_CAKE = 34;
+ public static void setBoard(String board) {
+ ReflectionHelpers.setStaticField(Build.class, "BOARD", board);
+ }
/**
* Sets the value of the {@link Build#DEVICE} field.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
index 19be93acc..1c8d78ba5 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
@@ -32,6 +32,7 @@ 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 class for {@link CameraManager} */
@Implements(value = CameraManager.class, minSdk = VERSION_CODES.LOLLIPOP)
@@ -77,7 +78,7 @@ public class ShadowCameraManager {
cameraTorches.put(cameraId, enabled);
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected CameraDevice openCameraDeviceUserAsync(
String cameraId,
CameraDevice.StateCallback callback,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
index 39b942857..9370a7ed2 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
@@ -6,10 +6,12 @@ import static android.os.Build.VERSION_CODES.Q;
import android.annotation.Nullable;
import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
+import com.google.common.base.Preconditions;
import java.util.HashMap;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.versioning.AndroidVersions.U;
@Implements(value = CarrierConfigManager.class, minSdk = M)
public class ShadowCarrierConfigManager {
@@ -35,6 +37,19 @@ public class ShadowCarrierConfigManager {
return new PersistableBundle();
}
+ /**
+ * @see #getConfigForSubId(int). Currently the 'keys' parameter is ignored.
+ */
+ @Implementation(minSdk = U.SDK_INT)
+ protected PersistableBundle getConfigForSubId(int subId, String... keys) {
+ // TODO: consider implementing the logic in telephony service
+ // CarrierConfigLoader#getConfigSubsetForSubIdWithFeature
+ Preconditions.checkNotNull(keys);
+ Preconditions.checkArgument(
+ keys.length == 0, "filtering by keys is not currently supported in Robolectric");
+ return getConfigForSubId(subId);
+ }
+
public void setReadPhoneStatePermission(boolean readPhoneStatePermission) {
this.readPhoneStatePermission = readPhoneStatePermission;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java
index 0d13c918d..1ff824105 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java
@@ -2,6 +2,7 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static org.robolectric.RuntimeEnvironment.getApiLevel;
import static org.robolectric.util.reflector.Reflector.reflector;
@@ -10,12 +11,14 @@ import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.ClipboardManager.OnPrimaryClipChangedListener;
+import android.os.SystemClock;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.ForType;
@SuppressWarnings("UnusedDeclaration")
@@ -28,6 +31,19 @@ public class ShadowClipboardManager {
@Implementation
protected void setPrimaryClip(ClipData clip) {
+ if (getApiLevel() >= O) {
+ if (clip != null) {
+ final ClipDescription description = clip.getDescription();
+ if (description != null) {
+ final long currentTimeMillis = SystemClock.uptimeMillis();
+ ReflectionHelpers.callInstanceMethod(
+ ClipDescription.class,
+ description,
+ "setTimestamp",
+ ClassParameter.from(long.class, currentTimeMillis));
+ }
+ }
+ }
if (getApiLevel() >= N) {
if (clip != null) {
clip.prepareToLeaveProcess(true);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java
index 89609cc52..f18cf22ba 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java
@@ -29,7 +29,17 @@ public class ShadowColor {
@Implementation
protected static int HSVToColor(int alpha, float hsv[]) {
- int rgb = java.awt.Color.HSBtoRGB(hsv[0] / 360, hsv[1], hsv[2]);
+ int rgb = java.awt.Color.HSBtoRGB(hsv[0] / 360, pin(hsv[1]), pin(hsv[2]));
return Color.argb(alpha, Color.red(rgb), Color.green(rgb), Color.blue(rgb));
}
+
+ private static float pin(float value) {
+ if (value < 0.0f) {
+ return 0.0f;
+ }
+ if (value > 1.0) {
+ return 1.0f;
+ }
+ return value;
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java
index 6201869ae..5c45d1f4a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java
@@ -1,14 +1,15 @@
package org.robolectric.shadows;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
-import android.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.companion.AssociatedDevice;
+import android.Manifest.permission;
+import android.app.ActivityThread;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
+import android.companion.DeviceNotAssociatedException;
import android.content.ComponentName;
import android.net.MacAddress;
import android.os.Build.VERSION_CODES;
@@ -22,12 +23,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.util.reflector.ForType;
-import org.robolectric.util.reflector.Accessor;
-import org.robolectric.util.reflector.Reflector;
-
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
/** Shadow for CompanionDeviceManager. */
@Implements(value = CompanionDeviceManager.class, minSdk = VERSION_CODES.O)
@@ -37,7 +38,11 @@ public class ShadowCompanionDeviceManager {
protected final Set<ComponentName> hasNotificationAccess = new HashSet<>();
protected ComponentName lastRequestedNotificationAccess;
protected AssociationRequest lastAssociationRequest;
+ protected MacAddress lastSystemApiAssociationMacAddress;
protected CompanionDeviceManager.Callback lastAssociationCallback;
+ protected String lastObservingDevicePresenceDeviceAddress;
+
+ private static final int DEFAULT_SYSTEMDATASYNCFLAGS = -1;
@Implementation
@SuppressWarnings("JdkCollectors") // toImmutableList is only supported in Java 8+.
@@ -109,18 +114,95 @@ public class ShadowCompanionDeviceManager {
associate(request, callback, /* handler= */ null);
}
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected void associate(String packageName, MacAddress macAddress, byte[] certificate) {
+ lastSystemApiAssociationMacAddress = macAddress;
+ if (!checkPermission(permission.ASSOCIATE_COMPANION_DEVICES)) {
+ throw new SecurityException("Permission ASSOCIATE_COMPANION_DEVICES not granted");
+ }
+ if (!RuntimeEnvironment.getApplication().getPackageName().equals(packageName)) {
+ throw new SecurityException("Calling application package does not equal packageName");
+ }
+ if (certificate == null) {
+ // Check the null case for now as {@link PackageManager#hasSigningCertificate} is not yet
+ // supported.
+ throw new SecurityException("Certificate is null");
+ }
+ associations.add(
+ RoboAssociationInfo.builder().setDeviceMacAddress(macAddress.toString()).build());
+ }
+
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected void startObservingDevicePresence(String deviceAddress) {
+ lastObservingDevicePresenceDeviceAddress = deviceAddress;
+ for (RoboAssociationInfo association : associations) {
+ if (Ascii.equalsIgnoreCase(deviceAddress, association.deviceMacAddress())) {
+ return;
+ }
+ }
+ throw new DeviceNotAssociatedException("Association does not exist");
+ }
+
+ /**
+ * This method will return the last {@link AssociationRequest} passed to {@code
+ * CompanionDeviceManager#associate(AssociationRequest, CompanionDeviceManager.Callback, Handler)}
+ * or {@code CompanionDeviceManager#associate(AssociationRequest, Executor,
+ * CompanionDeviceManager.Callback, Handler)}.
+ *
+ * <p>Note that the value returned is only changed when calling {@code associate} and will be set
+ * if that method throws an exception. Moreover, this value will unchanged if disassociate is
+ * called.
+ */
public AssociationRequest getLastAssociationRequest() {
return lastAssociationRequest;
}
+ /**
+ * This method will return the last {@link CompanionDeviceManager.Callback} passed to {@code
+ * CompanionDeviceManager#associate(AssociationRequest, CompanionDeviceManager.Callback, Handler)}
+ * or {@code CompanionDeviceManager#associate(AssociationRequest, Executor,
+ * CompanionDeviceManager.Callback, Handler)}.
+ *
+ * <p>Note that the value returned is only changed when calling {@code associate} and will be set
+ * if that method throws an exception. Moreover, this value will unchanged if disassociate is
+ * called.
+ */
public CompanionDeviceManager.Callback getLastAssociationCallback() {
return lastAssociationCallback;
}
+ /**
+ * If an association is set, this method will return the last {@link ComponentName} passed to
+ * {@code CompanionDeviceManager#requestNotificationAccess(ComponentName)}.
+ */
public ComponentName getLastRequestedNotificationAccess() {
return lastRequestedNotificationAccess;
}
+ /**
+ * Returns the last {@link MacAddress} passed to systemApi {@code associate}.
+ *
+ * <p>Note that the value returned is only changed when calling {@code associate} and will be set
+ * if that method throws an exception. Moreover, this value will unchanged if disassociate is
+ * called.
+ */
+ public MacAddress getLastSystemApiAssociationMacAddress() {
+ return lastSystemApiAssociationMacAddress;
+ }
+
+ /**
+ * Returns the last device address passed to {@link
+ * CompanionDeviceManager#startObservingDevicePresence(String)}.
+ *
+ * <p>Note that the value returned is only changed when calling {@link
+ * CompanionDeviceManager#startObservingDevicePresence(String)} and will still be set in the event
+ * that this method throws an exception. Moreover, this value will unchanged if disassociate is
+ * called.
+ */
+ public String getLastObservingDevicePresenceDeviceAddress() {
+ return lastObservingDevicePresenceDeviceAddress;
+ }
+
private void checkHasAssociation() {
if (associations.isEmpty()) {
throw new IllegalStateException("App must have an association before calling this API");
@@ -136,45 +218,79 @@ public class ShadowCompanionDeviceManager {
/** Convert {@link RoboAssociationInfo} to actual {@link AssociationInfo}. */
private AssociationInfo createAssociationInfo(RoboAssociationInfo info) {
- return new AssociationInfo(
- info.id(),
- info.userId(),
- info.packageName(),
- MacAddress.fromString(info.deviceMacAddress()),
- info.displayName(),
- info.deviceProfile(),
- info.associatedDevice(),
- info.selfManaged(),
- info.notifyOnDeviceNearby(),
- info.revoked(),
- info.timeApprovedMs(),
- info.lastTimeConnectedMs(),
- info.systemDataSyncFlags());
+ AssociationInfoBuilder aiBuilder = AssociationInfoBuilder.newBuilder()
+ .setId(info.id())
+ .setUserId(info.userId())
+ .setPackageName(info.packageName())
+ .setDeviceMacAddress(info.deviceMacAddress())
+ .setDisplayName(info.displayName())
+ .setDeviceProfile(info.deviceProfile())
+ .setAssociatedDevice(info.associatedDevice())
+ .setSelfManaged(info.selfManaged())
+ .setNotifyOnDeviceNearby(info.notifyOnDeviceNearby())
+ .setApprovedMs(info.timeApprovedMs())
+ .setLastTimeConnectedMs(info.lastTimeConnectedMs());
+
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mTag")) {
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder, "setTag", ClassParameter.from(String.class, info.tag()));
+ }
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) {
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder,
+ "setAssociatedDevice",
+ ClassParameter.from(Object.class, info.associatedDevice()));
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder,
+ "setSystemDataSyncFlags",
+ ClassParameter.from(int.class, info.systemDataSyncFlags()));
+ }
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) {
+ ReflectionHelpers.callInstanceMethod(
+ aiBuilder, "setRevoked", ClassParameter.from(boolean.class, info.revoked()));
+ }
+ return aiBuilder.build();
}
private RoboAssociationInfo createShadowAssociationInfo(AssociationInfo info) {
- var ref_info = Reflector.reflector(_AssociationInfo_.class, info);
+ Object associatedDevice = null;
+ int systemDataSyncFlags = DEFAULT_SYSTEMDATASYNCFLAGS;
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) {
+ associatedDevice = ReflectionHelpers.callInstanceMethod(info, "getAssociatedDevice");
+ systemDataSyncFlags = ReflectionHelpers.callInstanceMethod(info, "getSystemDataSyncFlags");
+ }
+ boolean revoked = false;
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) {
+ revoked = ReflectionHelpers.callInstanceMethod(info, "revoked");
+ }
+ String tag = "";
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mTag")) {
+ tag = ReflectionHelpers.callInstanceMethod(info, "getTag");
+ }
return RoboAssociationInfo.create(
info.getId(),
info.getUserId(),
info.getPackageName(),
- info.getDeviceMacAddress().toString(),
+ tag,
+ info.getDeviceMacAddress() == null ? null : info.getDeviceMacAddress().toString(),
info.getDisplayName(),
info.getDeviceProfile(),
- ref_info.getFullAssociatedDevice(),
+ associatedDevice,
info.isSelfManaged(),
info.isNotifyOnDeviceNearby(),
- info.isRevoked(),
+ revoked,
info.getTimeApprovedMs(),
info.getLastTimeConnectedMs(),
- info.getSystemDataSyncFlags());
+ systemDataSyncFlags);
}
- @ForType(AssociationInfo.class)
- public interface _AssociationInfo_ {
-
- @Accessor("mAssociatedDevice")
- AssociatedDevice getFullAssociatedDevice();
+ private static boolean checkPermission(String permission) {
+ ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
+ ShadowInstrumentation shadowInstrumentation =
+ Shadow.extract(activityThread.getInstrumentation());
+ return shadowInstrumentation.checkPermission(
+ permission, android.os.Process.myPid(), android.os.Process.myUid())
+ == PERMISSION_GRANTED;
}
/**
@@ -191,6 +307,9 @@ public class ShadowCompanionDeviceManager {
public abstract String packageName();
@Nullable
+ public abstract String tag();
+
+ @Nullable
public abstract String deviceMacAddress();
@Nullable
@@ -199,7 +318,8 @@ public class ShadowCompanionDeviceManager {
@Nullable
public abstract String deviceProfile();
- public abstract AssociatedDevice associatedDevice();
+ @Nullable
+ public abstract Object associatedDevice();
public abstract boolean selfManaged();
@@ -221,17 +341,18 @@ public class ShadowCompanionDeviceManager {
.setNotifyOnDeviceNearby(false)
.setTimeApprovedMs(0)
.setLastTimeConnectedMs(0)
- .setSystemDataSyncFlags(-1);
+ .setSystemDataSyncFlags(DEFAULT_SYSTEMDATASYNCFLAGS);
}
public static RoboAssociationInfo create(
int id,
int userId,
String packageName,
+ String tag,
String deviceMacAddress,
CharSequence displayName,
String deviceProfile,
- AssociatedDevice associatedDevice,
+ Object associatedDevice,
boolean selfManaged,
boolean notifyOnDeviceNearby,
boolean revoked,
@@ -242,14 +363,15 @@ public class ShadowCompanionDeviceManager {
.setId(id)
.setUserId(userId)
.setPackageName(packageName)
+ .setTag(tag)
.setDeviceMacAddress(deviceMacAddress)
.setDisplayName(displayName)
.setDeviceProfile(deviceProfile)
- .setAssociatedDevice(associatedDevice)
+ .setAssociatedDevice(associatedDevice)
.setSelfManaged(selfManaged)
.setNotifyOnDeviceNearby(notifyOnDeviceNearby)
.setTimeApprovedMs(timeApprovedMs)
- .setRevoked(revoked)
+ .setRevoked(revoked)
.setLastTimeConnectedMs(lastTimeConnectedMs)
.setSystemDataSyncFlags(systemDataSyncFlags)
.build();
@@ -264,6 +386,8 @@ public class ShadowCompanionDeviceManager {
public abstract Builder setPackageName(String packageName);
+ public abstract Builder setTag(String tag);
+
public abstract Builder setDeviceMacAddress(String deviceMacAddress);
public abstract Builder setDisplayName(CharSequence displayName);
@@ -272,7 +396,7 @@ public class ShadowCompanionDeviceManager {
public abstract Builder setSelfManaged(boolean selfManaged);
- public abstract Builder setAssociatedDevice(AssociatedDevice device);
+ public abstract Builder setAssociatedDevice(Object device);
public abstract Builder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCryptoObject.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCryptoObject.java
new file mode 100644
index 000000000..d338d98cb
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCryptoObject.java
@@ -0,0 +1,31 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import android.hardware.biometrics.CryptoObject;
+import org.robolectric.annotation.HiddenApi;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@SuppressWarnings("UnusedDeclaration")
+@Implements(value = CryptoObject.class, isInAndroidSdk = false, minSdk = P)
+public class ShadowCryptoObject {
+
+ /**
+ * The shadow method of CryptoObject#getOpId.
+ *
+ * <p>The CryptoObject#getOpId implementation in AOSP calls javax.crypto.CipherSpi#getCurrentSpi
+ * to retrieve javax.crypto.Cipher, but this API is added by Android JDK implementation, and not
+ * supported by OpenJDK. To avoid this issue, we shadow CryptoObject#getOpId to intercept
+ * call-chain early. Related issue: <a
+ * href="https://github.com/robolectric/robolectric/issues/8242">java.lang.NoSuchMethodError:
+ * 'javax.crypto.CipherSpi javax.crypto.Cipher.getCurrentSpi()</a>.
+ *
+ * @return 0L as default value.
+ */
+ @Implementation
+ @HiddenApi
+ protected final long getOpId() {
+ return 0L;
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java
index ac435c46f..b36762501 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java
@@ -1,7 +1,6 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
-import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+import org.robolectric.versioning.AndroidVersions.U;
import java.text.FieldPosition;
import java.util.HashMap;
@@ -11,7 +10,7 @@ import android.text.format.DateIntervalFormat;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-@Implements(value = DateIntervalFormat.class, isInAndroidSdk = false, minSdk = UPSIDE_DOWN_CAKE)
+@Implements(value = DateIntervalFormat.class, isInAndroidSdk = false, minSdk = U.SDK_INT)
public class ShadowDateIntervalFormatU {
private static long address;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
index 00bb9558c..145a37736 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
@@ -35,6 +35,7 @@ import org.robolectric.util.reflector.Constructor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;
+import org.robolectric.versioning.AndroidVersions.U;
/**
* Shadow of {@link DisplayEventReceiver}. The {@link Choreographer} is a subclass of {@link
@@ -95,7 +96,7 @@ public class ShadowDisplayEventReceiver {
return nativeInit(receiver, msgQueue);
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected static long nativeInit(
WeakReference<DisplayEventReceiver> receiver,
WeakReference<Object> vsyncEventData,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java
index bb9fc5538..f0211a180 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java
@@ -3,7 +3,9 @@ package org.robolectric.shadows;
import android.view.displayhash.DisplayHash;
import android.view.displayhash.DisplayHashManager;
import android.view.displayhash.VerifiedDisplayHash;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
import java.util.Set;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -13,6 +15,7 @@ import org.robolectric.annotation.Implements;
public class ShadowDisplayHashManager {
private static VerifiedDisplayHash verifyDisplayHashResult;
+ private static Set<String> supportedHashAlgorithms = ImmutableSet.of("PHASH");
/**
* Sets the {@link VerifiedDisplayHash} that's going to be returned by following
@@ -22,9 +25,23 @@ public class ShadowDisplayHashManager {
ShadowDisplayHashManager.verifyDisplayHashResult = verifyDisplayHashResult;
}
+ /**
+ * Sets the return value of #getSupportedHashAlgorithms.
+ *
+ * <p>If null is provided, getSupportedHashAlgorithms will throw a RuntimeException.
+ */
+ public static void setSupportedHashAlgorithms(Collection<String> supportedHashAlgorithms) {
+ if (supportedHashAlgorithms == null) {
+ ShadowDisplayHashManager.supportedHashAlgorithms = null;
+ } else {
+ ShadowDisplayHashManager.supportedHashAlgorithms =
+ ImmutableSet.copyOf(supportedHashAlgorithms);
+ }
+ }
+
@Implementation(minSdk = 31)
protected Set<String> getSupportedHashAlgorithms() {
- return ImmutableSet.of("PHASH");
+ return Preconditions.checkNotNull(supportedHashAlgorithms);
}
@Implementation(minSdk = 31)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java
index c684b69fb..9d755d4ef 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java
@@ -6,6 +6,7 @@ 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.Q;
+import static android.os.Build.VERSION_CODES.R;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.os.Environment;
@@ -24,7 +25,9 @@ import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
@Implements(Environment.class)
@SuppressWarnings("NewApi")
@@ -37,6 +40,7 @@ public class ShadowEnvironment {
private static Path tmpExternalFilesDirBase;
private static final List<File> externalDirs = new ArrayList<>();
private static Map<Path, String> storageState = new HashMap<>();
+ private static Path rootStorageDirectory;
static Path EXTERNAL_CACHE_DIR;
static Path EXTERNAL_FILES_DIR;
@@ -74,11 +78,33 @@ public class ShadowEnvironment {
}
/**
- * Sets the return value of {@link #getExternalStorageDirectory()}. Note that
- * the default value provides a directory that is usable in the test environment.
- * If the test app uses this method to override that default directory, please
- * clean up any files written to that directory, as the Robolectric environment
- * will not purge that directory when the test ends.
+ * Sets the return value of {@link #getStorageDirectory()}. This can be used for example, when
+ * testing code paths that need to perform regex matching on this directory.
+ *
+ * <p>Note that the default value provides a directory that is usable in the test environment. If
+ * the test app uses this method to override that default directory, please clean up any files
+ * written to that directory, as the Robolectric environment will not purge that directory when
+ * the test ends.
+ *
+ * @param directory Path to return from {@link #getStorageDirectory()}.
+ */
+ public static void setStorageDirectory(Path directory) {
+ rootStorageDirectory = directory;
+ }
+
+ @Implementation(minSdk = R)
+ protected static File getStorageDirectory() {
+ if (rootStorageDirectory == null) {
+ return reflector(EnvironmentReflector.class).getStorageDirectory();
+ }
+ return rootStorageDirectory.toFile();
+ }
+
+ /**
+ * Sets the return value of {@link #getExternalStorageDirectory()}. Note that the default value
+ * provides a directory that is usable in the test environment. If the test app uses this method
+ * to override that default directory, please clean up any files written to that directory, as the
+ * Robolectric environment will not purge that directory when the test ends.
*
* @param directory Path to return from {@link #getExternalStorageDirectory()}.
*/
@@ -140,6 +166,7 @@ public class ShadowEnvironment {
@Resetter
public static void reset() {
+ rootStorageDirectory = null;
EXTERNAL_CACHE_DIR = null;
EXTERNAL_FILES_DIR = null;
@@ -301,4 +328,11 @@ public class ShadowEnvironment {
@Accessor("mExternalStorageAndroidData")
void setExternalStorageAndroidData(File file);
}
+
+ @ForType(Environment.class)
+ interface EnvironmentReflector {
+ @Static
+ @Direct
+ File getStorageDirectory();
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
index 58cd55818..9f9722522 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
@@ -14,7 +14,6 @@ import android.graphics.ColorSpace.Named;
import android.graphics.ImageDecoder;
import android.graphics.ImageDecoder.DecodeException;
import android.graphics.ImageDecoder.Source;
-import android.graphics.Point;
import android.graphics.Rect;
import android.util.Size;
import java.io.ByteArrayInputStream;
@@ -43,12 +42,14 @@ public class ShadowImageDecoder {
private final int height;
private final boolean animated = false;
private final boolean ninePatch;
+ private final String mimeType;
ImgStream() {
InputStream inputStream = getInputStream();
- final Point size = ImageUtil.getImageSizeFromStream(inputStream);
- this.width = size == null ? 10 : size.x;
- this.height = size == null ? 10 : size.y;
+ final ImageUtil.ImageInfo info = ImageUtil.getImageInfoFromStream(inputStream);
+ this.width = info == null ? 10 : info.width;
+ this.height = info == null ? 10 : info.height;
+ this.mimeType = info == null ? "image/unknown" : info.mimeType;
if (inputStream instanceof AssetManager.AssetInputStream) {
ShadowAssetInputStream sis = Shadow.extract(inputStream);
this.ninePatch = sis.isNinePatch();
@@ -74,6 +75,10 @@ public class ShadowImageDecoder {
boolean isNinePatch() {
return ninePatch;
}
+
+ String mimeType() {
+ return mimeType;
+ }
}
private static final class CppImageDecoder {
@@ -84,6 +89,9 @@ public class ShadowImageDecoder {
this.imgStream = imgStream;
}
+ public String getMimeType() {
+ return imgStream.mimeType();
+ }
}
private static final NativeObjRegistry<CppImageDecoder> NATIVE_IMAGE_DECODER_REGISTRY =
@@ -247,9 +255,7 @@ public class ShadowImageDecoder {
static String ImageDecoder_nGetMimeType(long nativePtr) {
CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
- // return encodedFormatToString(decoder.mCodec.getEncodedFormat());
- // TODO: fix this properly. Just hardcode to png for now or just remove GraphicsMode.LEGACY
- return "image/png";
+ return decoder.getMimeType();
}
static ColorSpace ImageDecoder_nGetColorSpace(long nativePtr) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
index b8564ca40..140f664be 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
@@ -24,6 +24,7 @@ import org.robolectric.shadow.api.Shadow;
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 {@link android.media.ImageReader} */
@Implements(value = ImageReader.class, looseSignatures = true)
@@ -69,7 +70,7 @@ public class ShadowImageReader {
return nativeImageSetup(image);
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected int nativeImageSetup(Object /* Image */ image) {
return nativeImageSetup((Image) image);
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
index 1675a025a..8d447ed7c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
@@ -21,9 +21,13 @@ import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.Map;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
+import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;
@@ -42,7 +46,9 @@ import org.robolectric.util.reflector.Static;
@SystemApi
public class ShadowImsMmTelManager {
- protected static final Map<Integer, ImsMmTelManager> existingInstances = new ArrayMap<>();
+ private static final Map<Integer, ImsMmTelManager> existingInstances = new ArrayMap<>();
+ private static final Map<Integer, Integer> subIdToRegistrationTransportTypeMap = new ArrayMap<>();
+ private static final Map<Integer, Integer> subIdToRegistrationStateMap = new ArrayMap<>();
private final Map<ImsMmTelManager.RegistrationCallback, Executor>
registrationCallbackExecutorMap = new ArrayMap<>();
@@ -53,12 +59,9 @@ public class ShadowImsMmTelManager {
private MmTelCapabilities mmTelCapabilitiesAvailable =
new MmTelCapabilities(); // start with empty
private int imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
- private int subId;
-
- @Implementation(maxSdk = VERSION_CODES.R)
- protected void __constructor__(int subId) {
- this.subId = subId;
- }
+ private Consumer<Integer> stateCallback;
+ private Consumer<Integer> transportTypeCallback;
+ @RealObject private ImsMmTelManager realImsMmTelManager;
/**
* Sets whether IMS is available on the device. Setting this to false will cause {@link
@@ -204,6 +207,47 @@ public class ShadowImsMmTelManager {
}
}
+ public static void setRegistrationState(int subId, int registrationState) {
+ subIdToRegistrationStateMap.put(subId, registrationState);
+ }
+
+ public Consumer<Integer> getRegistrationStateCallback() {
+ return stateCallback;
+ }
+
+ @HiddenApi
+ @Implementation(minSdk = VERSION_CODES.R)
+ public void getRegistrationState(Executor executor, Consumer<Integer> stateCallback) {
+ this.stateCallback = stateCallback;
+ int subId = getSubscriptionId();
+ if (subIdToRegistrationStateMap.containsKey(getSubscriptionId())) {
+ stateCallback.accept(subIdToRegistrationStateMap.get(subId));
+ }
+ }
+
+ public static void setRegistrationTransportType(int subId, int registrationTransportType) {
+ subIdToRegistrationTransportTypeMap.put(subId, registrationTransportType);
+ }
+
+ public Consumer<Integer> getRegistrationTransportTypeCallback() {
+ return transportTypeCallback;
+ }
+
+ @RequiresPermission(
+ anyOf = {
+ Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ Manifest.permission.READ_PRECISE_PHONE_STATE
+ })
+ @Implementation(minSdk = VERSION_CODES.R)
+ public void getRegistrationTransportType(
+ Executor executor, Consumer<Integer> transportTypeCallback) {
+ this.transportTypeCallback = transportTypeCallback;
+ int subId = getSubscriptionId();
+ if (subIdToRegistrationTransportTypeMap.containsKey(getSubscriptionId())) {
+ transportTypeCallback.accept(subIdToRegistrationTransportTypeMap.get(subId));
+ }
+ }
+
@RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@Implementation
protected void registerMmTelCapabilityCallback(
@@ -248,7 +292,7 @@ public class ShadowImsMmTelManager {
/** Get subscription id */
public int getSubscriptionId() {
- return subId;
+ return reflector(ImsMmTelManagerReflector.class, realImsMmTelManager).getSubId();
}
/** Returns only one instance per subscription id. */
@@ -268,13 +312,18 @@ public class ShadowImsMmTelManager {
}
@Resetter
- public static void clearExistingInstances() {
+ public static void clearExistingInstancesAndStates() {
existingInstances.clear();
+ subIdToRegistrationTransportTypeMap.clear();
+ subIdToRegistrationStateMap.clear();
}
@ForType(ImsMmTelManager.class)
interface ImsMmTelManagerReflector {
+ @Accessor("mSubId")
+ int getSubId();
+
@Static
@Direct
ImsMmTelManager createForSubscriptionId(int subId);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java
new file mode 100644
index 000000000..76fc9a172
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java
@@ -0,0 +1,40 @@
+package org.robolectric.shadows;
+
+import android.net.wifi.ScanResult;
+import android.os.Build.VERSION_CODES;
+import org.robolectric.annotation.Implements;
+
+/** Shadow for {@link android.net.wifi.ScanResult.InformationElement}. */
+@Implements(value = ScanResult.InformationElement.class, minSdk = VERSION_CODES.R)
+public class ShadowInformationElement {
+ /**
+ * A builder for creating ShadowInformationElement objects. Use build() to return the
+ * InformationElement object.
+ */
+ public static class Builder {
+ private final ScanResult.InformationElement informationElement;
+
+ public Builder() {
+ informationElement = new ScanResult.InformationElement();
+ }
+
+ public Builder setId(int id) {
+ informationElement.id = id;
+ return this;
+ }
+
+ public Builder setIdExt(int idExt) {
+ informationElement.idExt = idExt;
+ return this;
+ }
+
+ public Builder setBytes(byte[] bytes) {
+ informationElement.bytes = bytes;
+ return this;
+ }
+
+ public ScanResult.InformationElement build() {
+ return informationElement;
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
index 298fabec6..e1e664759 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
@@ -22,6 +22,7 @@ import org.robolectric.annotation.Resetter;
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 {@link InputManager} */
@Implements(value = InputManager.class, looseSignatures = true)
@@ -116,7 +117,7 @@ public class ShadowInputManager {
@Resetter
public static void reset() {
- if (SDK_INT < ShadowBuild.UPSIDE_DOWN_CAKE) {
+ if (SDK_INT < U.SDK_INT) {
ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
index 20a6d1364..dde69ced6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
@@ -34,8 +34,10 @@ import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;
+import org.robolectric.versioning.AndroidVersions.U;
-@Implements(value = InputMethodManager.class)
+/** Shadow for InputMethodManager. */
+@Implements(value = InputMethodManager.class, looseSignatures = true)
public class ShadowInputMethodManager {
/**
@@ -81,6 +83,13 @@ public class ShadowInputMethodManager {
return showSoftInput(view, flags, resultReceiver);
}
+ @Implementation(minSdk = U.SDK_INT)
+ protected boolean showSoftInput(
+ Object view, Object statsToken, Object flags, Object resultReceiver, Object reason) {
+ return showSoftInput(
+ (View) view, (Integer) flags, (ResultReceiver) resultReceiver, (Integer) reason);
+ }
+
@Implementation(minSdk = S)
protected boolean hideSoftInputFromWindow(
IBinder windowToken, int flags, ResultReceiver resultReceiver, int ignoredReason) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java
index da5070a7a..cc0722fbb 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java
@@ -63,6 +63,7 @@ import javax.annotation.concurrent.GuardedBy;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowActivity.IntentForResult;
@@ -1181,4 +1182,22 @@ public class ShadowInstrumentation {
}
return null;
}
+
+ /**
+ * Executes a runnable depending on the LooperMode.
+ *
+ * <p>For INSTRUMENTATION_TEST mode, will post the runnable to the instrumentation thread and
+ * block the caller's thread until that runnable is executed.
+ *
+ * <p>For other modes, simply executes the runnable.
+ *
+ * @param runnable a runnable to be executed
+ */
+ public static void runOnMainSyncNoIdle(Runnable runnable) {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) {
+ checkNotNull(getInstrumentation()).runOnMainSync(runnable);
+ } else {
+ runnable.run();
+ }
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
index bf87b3a5b..c527b5f5b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
@@ -7,6 +7,7 @@ import android.app.job.JobParameters;
import android.app.job.JobService;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.versioning.AndroidVersions.U;
@Implements(value = JobService.class, minSdk = LOLLIPOP)
public class ShadowJobService extends ShadowService {
@@ -21,13 +22,23 @@ public class ShadowJobService extends ShadowService {
}
/** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected void setNotification(
JobParameters params,
int notificationId,
Notification notification,
int jobEndNotificationPolicy) {}
+ /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */
+ @Implementation(minSdk = U.SDK_INT)
+ protected void updateEstimatedNetworkBytes(
+ JobParameters params, long downloadBytes, long uploadBytes) {}
+
+ /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */
+ @Implementation(minSdk = U.SDK_INT)
+ protected void updateTransferredNetworkBytes(
+ JobParameters params, long downloadBytes, long uploadBytes) {}
+
/**
* Returns whether the job has finished running. When using this shadow this returns true after
* {@link #jobFinished(JobParameters, boolean)} is called.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java
index 79b1cbc6b..d345491dc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java
@@ -10,6 +10,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Integer.max;
import static java.lang.Integer.min;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
@@ -19,6 +20,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Parcel;
+import android.os.Parcelable;
import android.util.DisplayMetrics;
import java.awt.Color;
import java.awt.Graphics2D;
@@ -40,6 +42,7 @@ import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.versioning.AndroidVersions.U;
@SuppressWarnings({"UnusedDeclaration"})
@Implements(value = Bitmap.class, isInAndroidSdk = false)
@@ -681,6 +684,16 @@ public class ShadowLegacyBitmap extends ShadowBitmap {
int[] pixels = new int[width * height];
getPixels(pixels, 0, width, 0, 0, width, height);
p.writeIntArray(pixels);
+
+ if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) {
+ Object gainmap = reflector(BitmapReflector.class, realBitmap).getGainmap();
+ if (gainmap != null) {
+ p.writeBoolean(true);
+ p.writeTypedObject((Parcelable) gainmap, flags);
+ } else {
+ p.writeBoolean(false);
+ }
+ }
}
@Implementation
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java
index a85af0c41..1401816df 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java
@@ -426,7 +426,7 @@ public class ShadowLegacyMatrix extends ShadowMatrix {
}
private SimpleMatrix(float[] values) {
- if (values.length != 9) {
+ if (values.length < 9) {
throw new ArrayIndexOutOfBoundsException();
}
mValues = Arrays.copyOf(values, 9);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java
index 938e41aac..5f4c2ebbb 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java
@@ -24,7 +24,8 @@ import org.robolectric.util.reflector.ForType;
* <p>In {@link LooperMode.Mode.LEGACY}, each Message is associated with a Runnable posted to the
* {@link Scheduler}.
*
- * @see ShadowLooper, ShadowLegacyMessageQueue
+ * @see ShadowLooper
+ * @see ShadowLegacyMessageQueue
*/
@Implements(value = Message.class, isInAndroidSdk = false)
public class ShadowLegacyMessage extends ShadowMessage {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java
index 8e38cc518..aa5082340 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java
@@ -18,15 +18,21 @@ public class ShadowLocaleList {
@Resetter
public static void reset() {
LocaleListReflector localeListReflector = reflector(LocaleListReflector.class);
- localeListReflector.setLastDefaultLocale(null);
- localeListReflector.setDefaultLocaleList(null);
- localeListReflector.setDefaultAdjustedLocaleList(null);
- localeListReflector.setLastExplicitlySetLocaleList(null);
+ synchronized (localeListReflector.getLock()) {
+ localeListReflector.setLastDefaultLocale(null);
+ localeListReflector.setDefaultLocaleList(null);
+ localeListReflector.setDefaultAdjustedLocaleList(null);
+ localeListReflector.setLastExplicitlySetLocaleList(null);
+ }
}
@ForType(LocaleList.class)
interface LocaleListReflector {
@Static
+ @Accessor("sLock")
+ Object getLock();
+
+ @Static
@Accessor("sLastDefaultLocale")
void setLastDefaultLocale(Locale lastDefaultLocal);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
index 9bef2d193..ed7eb8c85 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
@@ -37,6 +37,7 @@ import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.versioning.AndroidVersions.U;
/**
* Implementation of {@link android.media.MediaCodec} which supports both asynchronous and
@@ -408,7 +409,7 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected void invalidateByteBufferLocked(
@Nullable ByteBuffer[] buffers, int index, boolean input) {}
@@ -416,14 +417,14 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected void validateInputByteBufferLocked(@Nullable ByteBuffer[] buffers, int index) {}
/** Prevents calling Android-only methods on basic ByteBuffer objects. */
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {}
/**
@@ -441,7 +442,7 @@ public class ShadowMediaCodec {
}
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected void validateOutputByteBufferLocked(
@Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
validateOutputByteBuffer(buffers, index, info);
@@ -451,14 +452,14 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected void invalidateByteBuffersLocked(@Nullable ByteBuffer[] buffers) {}
/** Prevents attempting to free non-direct ByteBuffer objects. */
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void freeByteBuffer(@Nullable ByteBuffer buffer) {}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected void freeByteBufferLocked(@Nullable ByteBuffer buffer) {}
/** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */
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 31fc79634..cf349ebdd 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.content.ContentResolver;
@@ -7,6 +8,11 @@ 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;
+import java.util.List;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
@@ -19,10 +25,14 @@ import org.robolectric.util.reflector.ForType;
public class ShadowMediaStore {
private static Bitmap stubBitmap = null;
+ private static final List<CloudMediaChangedEvent> cloudMediaChangedEventList = new ArrayList<>();
+ @Nullable private static String currentCloudMediaProviderAuthority = null;
@Resetter
public static void reset() {
stubBitmap = null;
+ cloudMediaChangedEventList.clear();
+ currentCloudMediaProviderAuthority = null;
}
/** Shadow for {@link MediaStore.Images}. */
@@ -97,4 +107,47 @@ public class ShadowMediaStore {
@Direct
Bitmap getThumbnail(ContentResolver cr, long imageId, int kind, Options options);
}
+
+ @Implementation(minSdk = TIRAMISU)
+ protected static void notifyCloudMediaChangedEvent(
+ ContentResolver resolver, String authority, String currentMediaCollectionId) {
+ cloudMediaChangedEventList.add(
+ CloudMediaChangedEvent.create(authority, currentMediaCollectionId));
+ }
+
+ /**
+ * Returns an {@link ImmutableList} of all {@link CloudMediaChangedEvent} objects that {@link
+ * MediaStore} has been notified of.
+ */
+ public static ImmutableList<CloudMediaChangedEvent> getCloudMediaChangedEvents() {
+ return ImmutableList.copyOf(cloudMediaChangedEventList);
+ }
+
+ public static void clearCloudMediaChangedEventList() {
+ cloudMediaChangedEventList.clear();
+ }
+
+ /** Event info for {@link MediaStore#notifyCloudMediaChangedEvent} notify events. */
+ @AutoValue
+ public abstract static class CloudMediaChangedEvent {
+ public static CloudMediaChangedEvent create(String authority, String currentMediaCollectionId) {
+ return new AutoValue_ShadowMediaStore_CloudMediaChangedEvent(
+ authority, currentMediaCollectionId);
+ }
+
+ public abstract String authority();
+
+ public abstract String currentMediaCollectionId();
+ }
+
+ @Implementation(minSdk = TIRAMISU)
+ protected static boolean isCurrentCloudMediaProviderAuthority(
+ ContentResolver resolver, String authority) {
+ return currentCloudMediaProviderAuthority.equals(authority);
+ }
+
+ /** Mutator method to set the value of the current cloud media provider authority. */
+ public static void setCurrentCloudMediaProviderAuthority(@Nullable String authority) {
+ currentCloudMediaProviderAuthority = authority;
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java
index d1e9f3d6c..d0449af4a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java
@@ -52,7 +52,15 @@ public class ShadowMimeTypeMap {
return null;
}
+ /**
+ * @deprecated use addExtensionMimeTypeMapping
+ */
+ @Deprecated
public void addExtensionMimeTypMapping(String extension, String mimeType) {
+ addExtensionMimeTypeMapping(extension, mimeType);
+ }
+
+ public void addExtensionMimeTypeMapping(String extension, String mimeType) {
extensionToMimeTypeMap.put(extension, mimeType);
mimeTypeToExtensionMap.put(mimeType, extension);
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java
index 79b1eafaf..5286b0794 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java
@@ -3,19 +3,47 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
import static android.os.Build.VERSION_CODES.O;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.AnimatedVectorDrawable.VectorDrawableAnimatorRT;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
import org.robolectric.nativeruntime.AnimatedVectorDrawableNatives;
import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
import org.robolectric.shadows.ShadowNativeAnimatedVectorDrawable.Picker;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
/** Shadow for {@link AnimatedVectorDrawable} that is backed by native code */
@Implements(value = AnimatedVectorDrawable.class, minSdk = O, shadowPicker = Picker.class)
public class ShadowNativeAnimatedVectorDrawable extends ShadowDrawable {
+ @RealObject protected AnimatedVectorDrawable realAnimatedVectorDrawable;
+
+ private boolean startInitiated;
+
+ @Implementation
+ protected void start() {
+ reflector(AnimatedVectorDrawableReflector.class, realAnimatedVectorDrawable).start();
+ startInitiated = true;
+ }
+
+ @Implementation
+ protected void stop() {
+ reflector(AnimatedVectorDrawableReflector.class, realAnimatedVectorDrawable).stop();
+ startInitiated = false;
+ }
+
+ /**
+ * Returns true if {@link #start()} was called and false if {@link #start()} was not called or
+ * {@link #stop()} was called.
+ */
+ public final boolean isStartInitiated() {
+ return startInitiated;
+ }
+
@Implementation(minSdk = N)
protected static long nCreateAnimatorSet() {
DefaultNativeRuntimeLoader.injectAndLoad();
@@ -117,4 +145,14 @@ public class ShadowNativeAnimatedVectorDrawable extends ShadowDrawable {
super(null, ShadowNativeAnimatedVectorDrawable.class);
}
}
+
+ /** Accessor interface for {@link AnimatedVectorDrawable} internals. */
+ @ForType(AnimatedVectorDrawable.class)
+ private interface AnimatedVectorDrawableReflector {
+ @Direct
+ void start();
+
+ @Direct
+ void stop();
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java
index 42080522f..945575eb1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java
@@ -21,6 +21,7 @@ import org.robolectric.nativeruntime.BaseCanvasNatives;
import org.robolectric.shadows.ShadowNativeBaseCanvas.Picker;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
+import org.robolectric.versioning.AndroidVersions.U;
/** Shadow for {@link BaseCanvas} that is backed by native code */
@Implements(
@@ -685,7 +686,7 @@ public class ShadowNativeBaseCanvas extends ShadowCanvas {
BaseCanvasNatives.nPunchHole(renderer, left, top, right, bottom, rx, ry);
}
- @Implementation(minSdk = 10000)
+ @Implementation(minSdk = U.SDK_INT)
protected static void nPunchHole(
long renderer,
float left,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java
index 1f061b53e..c0a8b0101 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java
@@ -14,6 +14,7 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.nativeruntime.BaseRecordingCanvasNatives;
import org.robolectric.shadows.ShadowNativeBaseRecordingCanvas.Picker;
+import org.robolectric.versioning.AndroidVersions.U;
/** Shadow for {@link BaseRecordingCanvas} that is backed by native code */
@Implements(
@@ -575,7 +576,7 @@ public class ShadowNativeBaseRecordingCanvas extends ShadowNativeCanvas {
BaseRecordingCanvasNatives.nPunchHole(renderer, left, top, right, bottom, rx, ry);
}
- @Implementation(minSdk = 10000)
+ @Implementation(minSdk = U.SDK_INT)
protected static void nPunchHole(
long renderer,
float left,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
index 0359bc37f..a1ff96f25 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
@@ -18,12 +18,14 @@ import android.graphics.ColorSpace.Rgb.TransferParameters;
import android.graphics.Matrix;
import android.hardware.HardwareBuffer;
import android.os.Parcel;
+import android.os.Parcelable;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.Buffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
@@ -35,6 +37,7 @@ import org.robolectric.nativeruntime.NativeAllocationRegistryNatives;
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 {@link Bitmap} that is backed by native code */
@Implements(value = Bitmap.class, looseSignatures = true, minSdk = O, isInAndroidSdk = false)
@@ -374,6 +377,16 @@ public class ShadowNativeBitmap extends ShadowBitmap {
int[] pixels = new int[width * height];
realBitmap.getPixels(pixels, 0, width, 0, 0, width, height);
p.writeIntArray(pixels);
+
+ if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) {
+ Object gainmap = reflector(BitmapReflector.class, realBitmap).getGainmap();
+ if (gainmap != null) {
+ p.writeBoolean(true);
+ p.writeTypedObject((Parcelable) gainmap, flags);
+ } else {
+ p.writeBoolean(false);
+ }
+ }
}
@Implementation
@@ -413,11 +426,6 @@ public class ShadowNativeBitmap extends ShadowBitmap {
return bitmap;
}
- @ForType(Bitmap.class)
- interface BitmapReflector {
- void checkRecycled(String errorMessage);
- }
-
@Override
public Bitmap getCreatedFromBitmap() {
throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
index b2da82783..365b6a9a1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
@@ -11,6 +11,8 @@ import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
import org.robolectric.nativeruntime.FontFamilyBuilderNatives;
import org.robolectric.nativeruntime.FontsFontFamilyNatives;
import org.robolectric.shadows.ShadowNativeFontsFontFamily.Picker;
+import org.robolectric.versioning.AndroidVersions.U;
+import org.robolectric.versioning.AndroidVersions.V;
/** Shadow for {@link FontFamily} that is backed by native code */
@Implements(
@@ -63,7 +65,7 @@ public class ShadowNativeFontsFontFamily {
return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT)
protected static long nBuild(
long builderPtr,
String langTags,
@@ -73,6 +75,17 @@ public class ShadowNativeFontsFontFamily {
return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
}
+ @Implementation(minSdk = V.SDK_INT)
+ protected static long nBuild(
+ long builderPtr,
+ String langTags,
+ int variant,
+ boolean isCustomFallback,
+ boolean isDefaultFallback,
+ int variableFamilyType) {
+ return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
+ }
+
@Implementation
protected static long nGetReleaseNativeFamily() {
return FontFamilyBuilderNatives.nGetReleaseNativeFamily();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
index 2a1b2827b..1bfaa90e7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
@@ -19,6 +19,7 @@ import org.robolectric.annotation.Implements;
import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
import org.robolectric.nativeruntime.HardwareRendererNatives;
import org.robolectric.shadows.ShadowNativeHardwareRenderer.Picker;
+import org.robolectric.versioning.AndroidVersions.U;
/** Shadow for {@link HardwareRenderer} that is backed by native code */
@Implements(
@@ -370,7 +371,7 @@ public class ShadowNativeHardwareRenderer {
presentationDeadlineNanos);
}
- @Implementation(minSdk = 10000)
+ @Implementation(minSdk = U.SDK_INT)
protected static void nInitDisplayInfo(
int width,
int height,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java
index f5d029cce..7133f83a7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java
@@ -10,16 +10,28 @@ import org.robolectric.annotation.Implements;
import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
import org.robolectric.nativeruntime.LineBreakerNatives;
import org.robolectric.shadows.ShadowNativeLineBreaker.Picker;
+import org.robolectric.versioning.AndroidVersions.U;
+import org.robolectric.versioning.AndroidVersions.V;
/** Shadow for {@link LineBreaker} that is backed by native code */
@Implements(value = LineBreaker.class, minSdk = Q, shadowPicker = Picker.class)
public class ShadowNativeLineBreaker {
- @Implementation
+ @Implementation(maxSdk = U.SDK_INT)
protected static long nInit(
int breakStrategy, int hyphenationFrequency, boolean isJustified, int[] indents) {
return LineBreakerNatives.nInit(breakStrategy, hyphenationFrequency, isJustified, indents);
}
+ @Implementation(minSdk = V.SDK_INT)
+ protected static long nInit(
+ int breakStrategy,
+ int hyphenationFrequency,
+ boolean isJustified,
+ int[] indents,
+ boolean useBoundsForWidth) {
+ return nInit(breakStrategy, hyphenationFrequency, isJustified, indents);
+ }
+
@Implementation
protected static long nGetReleaseFunc() {
// Called first by the static initializer.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java
index 5b82a6cf5..8cf433558 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java
@@ -14,6 +14,8 @@ import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
import org.robolectric.nativeruntime.MeasuredTextBuilderNatives;
import org.robolectric.nativeruntime.MeasuredTextNatives;
import org.robolectric.shadows.ShadowNativeMeasuredText.Picker;
+import org.robolectric.versioning.AndroidVersions.U;
+import org.robolectric.versioning.AndroidVersions.V;
/** Shadow for {@link MeasuredText} that is backed by native code */
@Implements(value = MeasuredText.class, minSdk = Q, shadowPicker = Picker.class)
@@ -99,13 +101,27 @@ public class ShadowNativeMeasuredText {
nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout);
}
- @Implementation(minSdk = TIRAMISU)
+ @Implementation(minSdk = TIRAMISU, maxSdk = U.SDK_INT)
+ protected static long nBuildMeasuredText(
+ /* Non Zero */ long nativeBuilderPtr,
+ long hintMtPtr,
+ char[] text,
+ boolean computeHyphenation,
+ boolean computeLayout,
+ boolean fastHyphenationMode) {
+ return MeasuredTextBuilderNatives.nBuildMeasuredText(
+ nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout);
+ }
+
+ @Implementation(minSdk = V.SDK_INT)
protected static long nBuildMeasuredText(
/* Non Zero */ long nativeBuilderPtr,
long hintMtPtr,
char[] text,
boolean computeHyphenation,
boolean computeLayout,
+ boolean computeBounds,
+ /** ignored */
boolean fastHyphenationMode) {
return MeasuredTextBuilderNatives.nBuildMeasuredText(
nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout);
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 32d428088..4dbe0b03b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
@@ -10,6 +10,7 @@ import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Rect;
+import android.graphics.RectF;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorLong;
import org.robolectric.annotation.Implementation;
@@ -17,6 +18,8 @@ import org.robolectric.annotation.Implements;
import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
import org.robolectric.nativeruntime.PaintNatives;
import org.robolectric.shadows.ShadowNativePaint.Picker;
+import org.robolectric.versioning.AndroidVersions.U;
+import org.robolectric.versioning.AndroidVersions.V;
/** Shadow for {@link Paint} that is backed by native code */
@Implements(
@@ -813,7 +816,7 @@ public class ShadowNativePaint {
paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics);
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT)
protected static float nGetRunCharacterAdvance(
long paintPtr,
char[] text,
@@ -838,6 +841,32 @@ public class ShadowNativePaint {
advancesIndex);
}
+ @Implementation(minSdk = V.SDK_INT)
+ protected static float nGetRunCharacterAdvance(
+ long paintPtr,
+ char[] text,
+ int start,
+ int end,
+ int contextStart,
+ int contextEnd,
+ boolean isRtl,
+ int offset,
+ float[] advances,
+ int advancesIndex,
+ RectF drawingBounds) {
+ return nGetRunCharacterAdvance(
+ paintPtr,
+ text,
+ start,
+ end,
+ contextStart,
+ contextEnd,
+ isRtl,
+ offset,
+ advances,
+ advancesIndex);
+ }
+
/** Shadow picker for {@link Paint}. */
public static final class Picker extends GraphicsShadowPicker<Object> {
public Picker() {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java
index 0c7fcf630..a0b5d2760 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java
@@ -33,6 +33,7 @@ import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;
+import org.robolectric.versioning.AndroidVersions.U;
/** Shadow for {@link Typeface} that is backed by native code */
@Implements(value = Typeface.class, looseSignatures = true, minSdk = O, isInAndroidSdk = false)
@@ -202,7 +203,7 @@ public class ShadowNativeTypeface extends ShadowTypeface {
return TypefaceNatives.nativeWriteTypefaces(buffer, nativePtrs);
}
- @Implementation(minSdk = 10000)
+ @Implementation(minSdk = U.SDK_INT)
protected static int nativeWriteTypefaces(ByteBuffer buffer, int position, long[] nativePtrs) {
return nativeWriteTypefaces(buffer, nativePtrs);
}
@@ -212,7 +213,7 @@ public class ShadowNativeTypeface extends ShadowTypeface {
return TypefaceNatives.nativeReadTypefaces(buffer);
}
- @Implementation(minSdk = 10000)
+ @Implementation(minSdk = U.SDK_INT)
protected static long[] nativeReadTypefaces(ByteBuffer buffer, int position) {
return nativeReadTypefaces(buffer);
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
index 4cdfb4532..38137474f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.app.Activity;
@@ -11,7 +12,6 @@ import android.nfc.NdefMessage;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.Build;
-import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import java.util.Map;
import org.robolectric.RuntimeEnvironment;
@@ -19,6 +19,8 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
@@ -49,6 +51,27 @@ public class ShadowNfcAdapter {
return reflector(NfcAdapterReflector.class).getNfcAdapter(context);
}
+ /** Factory method for creating a mock NfcAdapter.Tag */
+ public static Tag createMockTag() {
+ if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+ return ReflectionHelpers.callStaticMethod(
+ Tag.class,
+ "createMockTag",
+ ClassParameter.from(byte[].class, new byte[0]),
+ ClassParameter.from(int[].class, new int[0]),
+ ClassParameter.from(Bundle[].class, new Bundle[0]));
+
+ } else {
+ return ReflectionHelpers.callStaticMethod(
+ Tag.class,
+ "createMockTag",
+ ClassParameter.from(byte[].class, new byte[0]),
+ ClassParameter.from(int[].class, new int[0]),
+ ClassParameter.from(Bundle[].class, new Bundle[0]),
+ ClassParameter.from(long.class, 0));
+ }
+ }
+
@Implementation
protected void enableForegroundDispatch(
Activity activity, PendingIntent intent, IntentFilter[] filters, String[][] techLists) {
@@ -221,7 +244,7 @@ public class ShadowNfcAdapter {
}
if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
nfcAdapterReflector.setHasNfcFeature(false);
- if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.TIRAMISU) {
+ if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
nfcAdapterReflector.setHasBeamFeature(false);
}
}
@@ -249,4 +272,9 @@ public class ShadowNfcAdapter {
@Static
NfcAdapter getNfcAdapter(Context context);
}
+
+ @ForType(Tag.class)
+ interface TagReflector {
+ Tag createMockTag(byte[] id, int[] techList, Bundle[] techListExtras, long cookie);
+ }
}
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 16a7398f7..b103034a2 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
@@ -342,6 +342,17 @@ public class ShadowPaint {
}
@Implementation
+ protected final boolean isFilterBitmap() {
+ return (flags & Paint.FILTER_BITMAP_FLAG) == Paint.FILTER_BITMAP_FLAG;
+ }
+
+ @Implementation
+ protected final void setFilterBitmap(boolean filterBitmap) {
+ this.flags =
+ (flags & ~Paint.FILTER_BITMAP_FLAG) | (filterBitmap ? Paint.FILTER_BITMAP_FLAG : 0);
+ }
+
+ @Implementation
protected PathEffect getPathEffect() {
return pathEffect;
}
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 d898b4309..dd66cc764 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java
@@ -2,23 +2,32 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE;
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.annotation.SuppressLint;
import android.os.Handler;
+import android.os.Parcel;
import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
import org.robolectric.RuntimeEnvironment;
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;
@@ -32,7 +41,13 @@ public class ShadowParcelFileDescriptor {
// level
private static final String PIPE_TMP_DIR = "ShadowParcelFileDescriptor";
private static final String PIPE_FILE_NAME = "pipe";
+ private static final Map<Integer, RandomAccessFile> filesInTransitById =
+ Collections.synchronizedMap(new HashMap<>());
+ private static final AtomicInteger NEXT_FILE_ID = new AtomicInteger();
+
private RandomAccessFile file;
+ private int fileIdPledgedOnClose; // != 0 if 'file' was written to a Parcel.
+ private int lazyFileId; // != 0 if we were created from a Parcel but don't own a 'file' yet.
private boolean closed;
private Handler handler;
private ParcelFileDescriptor.OnCloseListener onCloseListener;
@@ -41,6 +56,18 @@ public class ShadowParcelFileDescriptor {
@RealObject private ParcelFileDescriptor realObject;
@Implementation
+ protected static void __staticInitializer__() {
+ Shadow.directInitialize(ParcelFileDescriptor.class);
+ ReflectionHelpers.setStaticField(
+ ParcelFileDescriptor.class, "CREATOR", ShadowParcelFileDescriptor.CREATOR);
+ }
+
+ @Resetter
+ public static void reset() {
+ filesInTransitById.clear();
+ }
+
+ @Implementation
protected void __constructor__(ParcelFileDescriptor wrapped) {
invokeConstructor(
ParcelFileDescriptor.class, realObject, from(ParcelFileDescriptor.class, wrapped));
@@ -50,18 +77,53 @@ public class ShadowParcelFileDescriptor {
}
}
+ static final Parcelable.Creator<ParcelFileDescriptor> CREATOR =
+ new Parcelable.Creator<ParcelFileDescriptor>() {
+ @Override
+ public ParcelFileDescriptor createFromParcel(Parcel source) {
+ int fileId = source.readInt();
+ ParcelFileDescriptor result = newParcelFileDescriptor();
+ ShadowParcelFileDescriptor shadowResult = Shadow.extract(result);
+ shadowResult.lazyFileId = fileId;
+ return result;
+ }
+
+ @Override
+ public ParcelFileDescriptor[] newArray(int size) {
+ return new ParcelFileDescriptor[size];
+ }
+ };
+
@Implementation
- protected static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException {
- ParcelFileDescriptor pfd = null;
+ protected void writeToParcel(Parcel out, int flags) {
+ if (fileIdPledgedOnClose == 0) {
+ fileIdPledgedOnClose = (lazyFileId != 0) ? lazyFileId : NEXT_FILE_ID.incrementAndGet();
+ }
+ out.writeInt(fileIdPledgedOnClose);
+
+ if ((flags & PARCELABLE_WRITE_RETURN_VALUE) != 0) {
+ try {
+ close();
+ } catch (IOException e) {
+ // Close "quietly", just like Android does.
+ }
+ }
+ }
+
+ private static ParcelFileDescriptor newParcelFileDescriptor() {
if (RuntimeEnvironment.getApiLevel() > JELLY_BEAN) {
- pfd = new ParcelFileDescriptor(new FileDescriptor());
+ return new ParcelFileDescriptor(new FileDescriptor());
} else {
// In Jelly Bean, the ParcelFileDescriptor(FileDescriptor) constructor was non-public.
- pfd =
- ReflectionHelpers.callConstructor(
- ParcelFileDescriptor.class,
- ClassParameter.from(FileDescriptor.class, new FileDescriptor()));
+ return ReflectionHelpers.callConstructor(
+ ParcelFileDescriptor.class,
+ ClassParameter.from(FileDescriptor.class, new FileDescriptor()));
}
+ }
+
+ @Implementation
+ protected static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException {
+ ParcelFileDescriptor pfd = newParcelFileDescriptor();
ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(pfd);
shadowParcelFileDescriptor.file = new RandomAccessFile(file, getFileMode(mode));
if ((mode & ParcelFileDescriptor.MODE_TRUNCATE) != 0) {
@@ -136,10 +198,21 @@ public class ShadowParcelFileDescriptor {
return createPipe();
}
+ private RandomAccessFile getFile() {
+ if (file == null && lazyFileId != 0) {
+ file = filesInTransitById.remove(lazyFileId);
+ lazyFileId = 0;
+ if (file == null) {
+ throw new FileDescriptorFromParcelUnavailableException();
+ }
+ }
+ return file;
+ }
+
@Implementation
protected FileDescriptor getFileDescriptor() {
try {
- return file.getFD();
+ return getFile().getFD();
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -148,7 +221,7 @@ public class ShadowParcelFileDescriptor {
@Implementation
protected long getStatSize() {
try {
- return file.length();
+ return getFile().length();
} catch (IOException e) {
// This might occur when the file object has been closed.
return -1;
@@ -162,7 +235,7 @@ public class ShadowParcelFileDescriptor {
}
try {
- return ReflectionHelpers.getField(file.getFD(), "fd");
+ return ReflectionHelpers.getField(getFile().getFD(), "fd");
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -175,7 +248,21 @@ public class ShadowParcelFileDescriptor {
return;
}
- file.close();
+ if (file != null) {
+ if (fileIdPledgedOnClose != 0) {
+ // Don't actually close 'file'! Instead stash it where our Parcel reader(s) can find it.
+ filesInTransitById.put(fileIdPledgedOnClose, file);
+ fileIdPledgedOnClose = 0;
+
+ // Replace this.file with a dummy instance to be close()d below. This leaves instances that
+ // have been written to Parcels and never-parceled ones in exactly the same state.
+ File tempFile = Files.createTempFile(null, null).toFile();
+ file = new RandomAccessFile(tempFile, "rw");
+ tempFile.delete();
+ }
+ file.close();
+ }
+
reflector(ParcelFileDescriptorReflector.class, realParcelFd).close();
closed = true;
if (handler != null && onCloseListener != null) {
@@ -183,6 +270,17 @@ public class ShadowParcelFileDescriptor {
}
}
+ static class FileDescriptorFromParcelUnavailableException extends RuntimeException {
+ FileDescriptorFromParcelUnavailableException() {
+ super(
+ "ParcelFileDescriptors created from a Parcel refer to the same content as the"
+ + " ParcelFileDescriptor that originally wrote it. Robolectric has the unfortunate"
+ + " limitation that only one of these instances can be functional at a time. Try"
+ + " closing the original ParcelFileDescriptor before using any duplicates created via"
+ + " the Parcelable API.");
+ }
+ }
+
@ForType(ParcelFileDescriptor.class)
interface ParcelFileDescriptorReflector {
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 ee3bef016..6e85f733a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -6,6 +6,7 @@ import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.os.Build;
+import android.os.ConditionVariable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -24,25 +25,30 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.config.ConfigurationRegistry;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.Scheduler;
+import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;
/**
- * The shadow Looper for {@link LooperMode.Mode.PAUSED}.
+ * The shadow Looper for {@link LooperMode.Mode.PAUSED and @link
+ * LooperMode.Mode.INSTRUMENTATION_TEST}.
*
* <p>This shadow differs from the legacy {@link ShadowLegacyLooper} in the following ways:\ - Has
* no connection to {@link org.robolectric.util.Scheduler}. Its APIs are standalone - The main
- * looper is always paused. Posted messages are not executed unless {@link #idle()} is called. -
- * Just like in real Android, each looper has its own thread, and posted tasks get executed in that
+ * looper is always paused in PAUSED MODE but can be unpaused in INSTRUMENTATION_TEST mode. When a
+ * looper is paused, posted messages to it are not executed unless {@link #idle()} is called. - Just
+ * like in real Android, each looper has its own thread, and posted tasks get executed in that
* thread. - - There is only a single {@link SystemClock} value that all loopers read from. Unlike
* legacy behavior where each {@link org.robolectric.util.Scheduler} kept their own clock value.
*
@@ -122,7 +128,8 @@ public final class ShadowPausedLooper extends ShadowLooper {
@Override
public void unPause() {
- if (realLooper == Looper.getMainLooper()) {
+ if (realLooper == Looper.getMainLooper()
+ && looperMode() != LooperMode.Mode.INSTRUMENTATION_TEST) {
throw new UnsupportedOperationException("main looper cannot be unpaused");
}
executeOnLooper(new UnPauseRunnable());
@@ -164,7 +171,9 @@ public final class ShadowPausedLooper extends ShadowLooper {
@Override
public void idleIfPaused() {
- idle();
+ if (isPaused()) {
+ idle();
+ }
}
@Override
@@ -202,11 +211,12 @@ public final class ShadowPausedLooper extends ShadowLooper {
// compatibility for now
@Override
public void runPaused(Runnable runnable) {
- if (isPaused && Thread.currentThread() == realLooper.getThread()) {
+ if (Thread.currentThread() == realLooper.getThread()) {
// just run
runnable.run();
} else {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException(
+ "this method can only be called on " + realLooper.getThread().getName());
}
}
@@ -256,17 +266,76 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
@Resetter
+ @SuppressWarnings("deprecation") // This is Robolectric library code
public static synchronized void resetLoopers() {
- // do not use looperMode() here, because its cached value might already have been reset
- if (ConfigurationRegistry.get(LooperMode.Mode.class) != LooperMode.Mode.PAUSED) {
- // ignore if not realistic looper
+ // Do not use looperMode() here, because its cached value might already have been reset
+ LooperMode.Mode looperMode = ConfigurationRegistry.get(LooperMode.Mode.class);
+
+ if (looperMode == LooperMode.Mode.LEGACY) {
return;
}
- Collection<Looper> loopersCopy = new ArrayList(loopingLoopers);
- for (Looper looper : loopersCopy) {
- ShadowPausedMessageQueue shadowQueue = Shadow.extract(looper.getQueue());
- shadowQueue.reset();
+ createMainThreadAndLooperIfNotAlive();
+ for (Looper looper : getLoopers()) {
+ ShadowPausedLooper shadowPausedLooper = Shadow.extract(looper);
+ shadowPausedLooper.resetLooperToInitialState();
+ }
+ }
+
+ private static synchronized void createMainThreadAndLooperIfNotAlive() {
+ Looper mainLooper = Looper.getMainLooper();
+
+ switch (ConfigurationRegistry.get(LooperMode.Mode.class)) {
+ case INSTRUMENTATION_TEST:
+ if (mainLooper == null || !mainLooper.getThread().isAlive()) {
+ 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);
+ }
+ mainThreadPrepared.open();
+ Looper.loop();
+ }
+ };
+ mainThread.start();
+ mainThreadPrepared.block();
+ Thread.currentThread()
+ .setName(String.format("SDK %d Test Thread", RuntimeEnvironment.getApiLevel()));
+ }
+ break;
+ case PAUSED:
+ if (Looper.myLooper() == null) {
+ Looper.prepareMainLooper();
+ }
+ break;
+ default:
+ throw new UnsupportedOperationException(
+ "Only supports INSTRUMENTATION_TEST and PAUSED LooperMode.");
+ }
+ }
+
+ private synchronized void resetLooperToInitialState() {
+ // Do not use looperMode() here, because its cached value might already have been reset
+ LooperMode.Mode looperMode = ConfigurationRegistry.get(LooperMode.Mode.class);
+
+ ShadowPausedMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue());
+ shadowQueue.reset();
+
+ boolean canBeUnpaused =
+ !(realLooper == Looper.getMainLooper()
+ && looperMode != LooperMode.Mode.INSTRUMENTATION_TEST);
+ if (canBeUnpaused && realLooper.getThread().isAlive()) {
+ if (isPaused()) {
+ unPause();
+ }
}
}
@@ -274,7 +343,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
protected static void prepareMainLooper() {
reflector(LooperReflector.class).prepareMainLooper();
ShadowPausedLooper pausedLooper = Shadow.extract(Looper.getMainLooper());
- pausedLooper.isPaused = true;
+ pausedLooper.isPaused = looperMode() == Mode.PAUSED;
}
@Implementation
@@ -463,7 +532,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
runnable.run();
}
} else {
- if (realLooper.equals(Looper.getMainLooper())) {
+ if (looperMode() == LooperMode.Mode.PAUSED && realLooper.equals(Looper.getMainLooper())) {
throw new UnsupportedOperationException(
"main looper can only be controlled from main thread");
}
@@ -549,5 +618,11 @@ public final class ShadowPausedLooper extends ShadowLooper {
@Direct
void loop();
+
+ @Accessor("mThread")
+ void setThread(Thread thread);
+
+ @Accessor("sThreadLocal")
+ ThreadLocal<Looper> getThreadLocal();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java
index 2174acf26..100cc4364 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java
@@ -3,9 +3,9 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static org.robolectric.util.reflector.Reflector.reflector;
-import android.os.Build;
import android.os.Handler;
import android.os.Message;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
@@ -19,25 +19,25 @@ import org.robolectric.annotation.RealObject;
@Implements(value = Message.class, isInAndroidSdk = false)
public class ShadowPausedMessage extends ShadowMessage {
- @RealObject private Message realObject;
+ @RealObject private Message realMessage;
@Implementation
protected long getWhen() {
- return reflector(MessageReflector.class, realObject).getWhen();
+ return reflector(MessageReflector.class, realMessage).getWhen();
}
Message internalGetNext() {
- return reflector(MessageReflector.class, realObject).getNext();
+ return reflector(MessageReflector.class, realMessage).getNext();
}
- // TODO: reconsider this being exposed as a public method
+ // TODO: Reconsider this being exposed as a public method
@Override
@Implementation(minSdk = LOLLIPOP)
public void recycleUnchecked() {
- if (Build.VERSION.SDK_INT >= LOLLIPOP) {
- reflector(MessageReflector.class, realObject).recycleUnchecked();
+ if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) {
+ reflector(MessageReflector.class, realMessage).recycleUnchecked();
} else {
- reflector(MessageReflector.class, realObject).recycle();
+ reflector(MessageReflector.class, realMessage).recycle();
}
}
@@ -46,7 +46,7 @@ public class ShadowPausedMessage extends ShadowMessage {
throw new UnsupportedOperationException("Not supported in PAUSED LooperMode");
}
- // we could support these methods, but intentionally do not for now as its unclear what the
+ // We could support these methods, but intentionally do not for now as its unclear what the
// use case is.
@Override
@@ -61,6 +61,6 @@ public class ShadowPausedMessage extends ShadowMessage {
@Implementation
protected Handler getTarget() {
- return reflector(MessageReflector.class, realObject).getTarget();
+ return reflector(MessageReflector.class, realMessage).getTarget();
}
}
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 5caf01642..56af8a3bb 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
@@ -461,10 +461,11 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
Message msg = getMessages();
while (msg != null) {
boolean unused = msgProcessor.apply(msg.getCallback());
- ShadowMessage shadowMsg = Shadow.extract(msg);
- msg.recycle();
- msg = shadowMsg.getNext();
+ Message next = shadowOfMsg(msg).internalGetNext();
+ shadowOfMsg(msg).recycleUnchecked();
+ msg = next;
}
+ reflector(MessageQueueReflector.class, realQueue).setMessages(null);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java
index 61f3c3c0d..c7a86f7e9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java
@@ -230,7 +230,7 @@ public class ShadowPendingIntent {
// Copy the last intent before filling it in to avoid modifying this PendingIntent.
intentsToSend = Arrays.copyOf(savedIntents, savedIntents.length);
Intent lastIntentCopy = new Intent(intentsToSend[intentsToSend.length - 1]);
- lastIntentCopy.fillIn(intent, 0);
+ lastIntentCopy.fillIn(intent, flags);
intentsToSend[intentsToSend.length - 1] = lastIntentCopy;
} else {
intentsToSend = savedIntents;
@@ -691,7 +691,9 @@ public class ShadowPendingIntent {
public static void reset() {
synchronized (lock) {
createdIntents.clear();
+ parceledPendingIntents.clear();
}
+
}
@ForType(PendingIntent.class)
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 cbac3339e..82f616549 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java
@@ -1,10 +1,9 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.robolectric.util.reflector.Reflector.reflector;
-import android.app.Activity;
-import android.content.Context;
-import android.content.ContextWrapper;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -13,14 +12,17 @@ import android.os.Handler;
import android.os.Looper;
import android.view.PixelCopy;
import android.view.PixelCopy.OnPixelCopyFinishedListener;
+import android.view.Surface;
import android.view.SurfaceView;
import android.view.View;
-import android.view.ViewGroup;
import android.view.Window;
+import android.view.WindowManagerGlobal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowWindowManagerGlobal.WindowManagerGlobalReflector;
/**
* Shadow for PixelCopy that uses View.draw to create screenshots. The real PixelCopy performs a
@@ -29,8 +31,19 @@ import org.robolectric.annotation.Implements;
* <p>If listenerThread is backed by a paused looper, make sure to call ShadowLooper.idle() to
* ensure the screenshot finishes.
*/
-@Implements(value = PixelCopy.class, minSdk = P)
+@Implements(value = PixelCopy.class, minSdk = O)
public class ShadowPixelCopy {
+
+ @Implementation
+ protected static void request(
+ SurfaceView source,
+ @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ takeScreenshot(source, dest, null);
+ alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
+ }
+
@Implementation
protected static void request(
@NonNull SurfaceView source,
@@ -38,14 +51,10 @@ public class ShadowPixelCopy {
@NonNull Bitmap dest,
@NonNull OnPixelCopyFinishedListener listener,
@NonNull Handler listenerThread) {
- Activity activity = getActivity(source);
if (srcRect != null && srcRect.isEmpty()) {
throw new IllegalArgumentException("sourceRect is empty");
}
- if (activity == null) {
- throw new IllegalArgumentException("SourceView was not attached to an activity");
- }
- takeScreenshot(activity.getWindow(), dest, srcRect);
+ takeScreenshot(source, dest, srcRect);
alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
}
@@ -55,8 +64,7 @@ public class ShadowPixelCopy {
@NonNull Bitmap dest,
@NonNull OnPixelCopyFinishedListener listener,
@NonNull Handler listenerThread) {
- takeScreenshot(source, dest, null);
- alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
+ request(source, null, dest, listener, listenerThread);
}
@Implementation
@@ -69,19 +77,61 @@ public class ShadowPixelCopy {
if (srcRect != null && srcRect.isEmpty()) {
throw new IllegalArgumentException("sourceRect is empty");
}
- takeScreenshot(source, dest, srcRect);
+ View view = source.getDecorView();
+ Rect adjustedSrcRect = null;
+ if (srcRect != null) {
+ adjustedSrcRect = new Rect(srcRect);
+ int[] locationInWindow = new int[2];
+ view.getLocationInWindow(locationInWindow);
+ // offset the srcRect by the decor view's location in the window
+ adjustedSrcRect.offset(-locationInWindow[0], -locationInWindow[1]);
+ }
+ takeScreenshot(view, dest, adjustedSrcRect);
alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
}
- private static void takeScreenshot(Window window, Bitmap screenshot, @Nullable Rect srcRect) {
- validateBitmap(screenshot);
+ @Implementation
+ protected static void request(
+ @NonNull Surface source,
+ @Nullable Rect srcRect,
+ @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ if (srcRect != null && srcRect.isEmpty()) {
+ throw new IllegalArgumentException("sourceRect is empty");
+ }
+
+ View view = findViewForSurface(checkNotNull(source));
+ Rect adjustedSrcRect = null;
+ if (srcRect != null) {
+ adjustedSrcRect = new Rect(srcRect);
+ int[] locationInSurface = ShadowView.getLocationInSurfaceCompat(view);
+ // offset the srcRect by the decor view's location in the surface
+ adjustedSrcRect.offset(-locationInSurface[0], -locationInSurface[1]);
+ }
+ takeScreenshot(view, dest, adjustedSrcRect);
+ alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
+ }
+
+ private static View findViewForSurface(Surface source) {
+ for (View windowView :
+ reflector(WindowManagerGlobalReflector.class, WindowManagerGlobal.getInstance())
+ .getWindowViews()) {
+ ShadowViewRootImpl shadowViewRoot = Shadow.extract(windowView.getViewRootImpl());
+ if (source.equals(shadowViewRoot.getSurface())) {
+ return windowView;
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Could not find view for surface. Is it attached to a window?");
+ }
- // Draw the view to a bitmap in the canvas that is the size of the view itself.
- View decorView = window.getDecorView();
- Bitmap bitmap =
- Bitmap.createBitmap(decorView.getWidth(), decorView.getHeight(), Bitmap.Config.ARGB_8888);
+ private static void takeScreenshot(View view, Bitmap screenshot, @Nullable Rect srcRect) {
+ validateBitmap(screenshot);
+ Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas screenshotCanvas = new Canvas(bitmap);
- decorView.draw(screenshotCanvas);
+ view.draw(screenshotCanvas);
Rect dst = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight());
@@ -99,7 +149,7 @@ public class ShadowPixelCopy {
listenerThread.post(() -> listener.onPixelCopyFinished(result));
}
- private static void validateBitmap(Bitmap bitmap) {
+ private static Bitmap validateBitmap(Bitmap bitmap) {
if (bitmap == null) {
throw new IllegalArgumentException("Bitmap cannot be null");
}
@@ -109,33 +159,6 @@ public class ShadowPixelCopy {
if (!bitmap.isMutable()) {
throw new IllegalArgumentException("Bitmap is immutable");
}
- }
-
- private static Activity getActivity(Context context) {
- if (context instanceof Activity) {
- return (Activity) context;
- } else if (context instanceof ContextWrapper) {
- return getActivity(((ContextWrapper) context).getBaseContext());
- } else {
- return null;
- }
- }
-
- private static Activity getActivity(View view) {
- Activity activity = getActivity(view.getContext());
- if (activity != null) {
- return activity;
- }
-
- if (view instanceof ViewGroup) {
- ViewGroup viewGroup = (ViewGroup) view;
- if (viewGroup.getChildCount() > 0) {
- // getActivity is known to fail if View is a DecorView such as specified via espresso's
- // isRoot().
- // Make another attempt to find the activity from its first child view
- return getActivity(viewGroup.getChildAt(0).getContext());
- }
- }
- return null;
+ return 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 db47fc850..a5191fe7c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java
@@ -30,7 +30,6 @@ import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.os.WorkSource;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.time.Duration;
import java.util.ArrayList;
@@ -67,8 +66,8 @@ public class ShadowPowerManager {
@PowerManager.LocationPowerSaveMode
private int locationMode = PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF;
- private List<String> rebootReasons = new ArrayList<String>();
- private Map<String, Boolean> ignoringBatteryOptimizations = new HashMap<>();
+ private final List<String> rebootReasons = new ArrayList<>();
+ private final Map<String, Boolean> ignoringBatteryOptimizations = new HashMap<>();
private int thermalStatus = 0;
// Intentionally use Object instead of PowerManager.OnThermalStatusChangedListener to avoid
@@ -97,7 +96,7 @@ public class ShadowPowerManager {
}
/**
- * @deprecated Use {@link #setIsInteractive(boolean)} instead.
+ * @deprecated Use {@link #turnScreenOn(boolean)} instead.
*/
@Deprecated
public void setIsScreenOn(boolean screenOn) {
@@ -333,7 +332,7 @@ public class ShadowPowerManager {
}
@Implementation
- protected void reboot(String reason) {
+ protected void reboot(@Nullable String reason) {
if (RuntimeEnvironment.getApiLevel() >= R
&& "userspace".equals(reason)
&& !isRebootingUserspaceSupported()) {
@@ -348,9 +347,11 @@ public class ShadowPowerManager {
return rebootReasons.size();
}
- /** Returns the list of reasons for each reboot, in chronological order. */
- public ImmutableList<String> getRebootReasons() {
- return ImmutableList.copyOf(rebootReasons);
+ /**
+ * Returns the list of reasons for each reboot, in chronological order. May contain {@code null}.
+ */
+ public List<String> getRebootReasons() {
+ return new ArrayList<>(rebootReasons);
}
/** Sets the value returned by {@link #isAmbientDisplayAvailable()}. */
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java
index 0c42cefd0..2d0ccef53 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java
@@ -4,6 +4,7 @@ import static android.os.Build.VERSION_CODES.P;
import android.net.wifi.ScanResult;
import android.os.Build;
+import java.util.List;
import org.robolectric.shadow.api.Shadow;
public class ShadowScanResult {
@@ -45,7 +46,40 @@ public class ShadowScanResult {
} else {
scanResult.setFlag(0);
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ scanResult.informationElements = new ScanResult.InformationElement[0];
+ }
}
return scanResult;
}
+
+ public static ScanResult newInstance(
+ String ssid,
+ String bssid,
+ String caps,
+ int level,
+ int frequency,
+ boolean is80211McRttResponder,
+ List<ScanResult.InformationElement> informationElements) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ ScanResult scanResult = new ScanResult();
+ scanResult.SSID = ssid;
+ scanResult.BSSID = bssid;
+ scanResult.capabilities = caps;
+ scanResult.level = level;
+ scanResult.frequency = frequency;
+ scanResult.informationElements =
+ informationElements.toArray(new ScanResult.InformationElement[0]);
+ if (is80211McRttResponder) {
+ scanResult.setFlag(ScanResult.FLAG_80211mc_RESPONDER);
+ } else {
+ scanResult.setFlag(0);
+ }
+
+ return scanResult;
+ } else {
+ throw new UnsupportedOperationException(
+ "InformationElement not available on API " + Build.VERSION.SDK_INT);
+ }
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
index c40d24e96..89b9eb6e1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
@@ -22,6 +22,7 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.versioning.AndroidVersions.U;
@Implements(SoundPool.class)
public class ShadowSoundPool {
@@ -62,7 +63,7 @@ public class ShadowSoundPool {
return 1;
}
- @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ @Implementation(minSdk = U.SDK_INT)
protected int _play(
int soundID,
float leftVolume,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowStatsManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowStatsManager.java
new file mode 100644
index 000000000..61a595d76
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowStatsManager.java
@@ -0,0 +1,44 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import android.app.StatsManager;
+import java.util.HashMap;
+import java.util.Map;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+/** Shadow for {@link ShadowStatsManager} */
+@Implements(value = StatsManager.class, isInAndroidSdk = false, minSdk = P)
+public class ShadowStatsManager {
+
+ private static final Map<Long, byte[]> dataMap = new HashMap<>();
+ private static byte[] statsMetadata = new byte[] {};
+
+ @Resetter
+ public static void reset() {
+ dataMap.clear();
+ statsMetadata = new byte[] {};
+ }
+
+ public static void addReportData(long configKey, byte[] data) {
+ dataMap.put(configKey, data);
+ }
+
+ public static void setStatsMetadata(byte[] metadata) {
+ statsMetadata = metadata;
+ }
+
+ @Implementation
+ protected byte[] getReports(long configKey) {
+ byte[] data = dataMap.getOrDefault(configKey, new byte[] {});
+ dataMap.remove(configKey);
+ return data;
+ }
+
+ @Implementation
+ protected byte[] getStatsMetadata() {
+ return statsMetadata;
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
index 1b239d188..27b787d96 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
@@ -32,6 +32,7 @@ import org.robolectric.util.ReflectionHelpers;
public class ShadowSubscriptionManager {
private boolean readPhoneStatePermission = true;
+ private boolean readPhoneNumbersPermission = true;
public static final int INVALID_PHONE_INDEX =
ReflectionHelpers.getStaticField(SubscriptionManager.class, "INVALID_PHONE_INDEX");
@@ -178,9 +179,12 @@ public class ShadowSubscriptionManager {
/**
* Returns subscription that were set via {@link #setActiveSubscriptionInfoList} if it can find
* one with the specified id or null if none found.
+ *
+ * <p>An exception will be thrown if the READ_PHONE_STATE permission has not been granted.
*/
@Implementation(minSdk = LOLLIPOP_MR1)
protected SubscriptionInfo getActiveSubscriptionInfo(int subId) {
+ checkReadPhoneStatePermission();
if (subscriptionList == null) {
return null;
}
@@ -417,13 +421,31 @@ public class ShadowSubscriptionManager {
}
/**
+ * When set to false methods requiring {@link android.Manifest.permission.READ_PHONE_NUMBERS}
+ * permission will throw a {@link SecurityException}. By default it's set to true for backwards
+ * compatibility.
+ */
+ public void setReadPhoneNumbersPermission(boolean readPhoneNumbersPermission) {
+ this.readPhoneNumbersPermission = readPhoneNumbersPermission;
+ }
+
+ private void checkReadPhoneNumbersPermission() {
+ if (!readPhoneNumbersPermission) {
+ throw new SecurityException();
+ }
+ }
+
+ /**
* Returns the phone number for the given {@code subscriptionId}, or an empty string if not
* available.
*
* <p>The phone number can be set by {@link #setPhoneNumber(int, String)}
+ *
+ * <p>An exception will be thrown if the READ_PHONE_NUMBERS permission has not been granted.
*/
@Implementation(minSdk = TIRAMISU)
protected String getPhoneNumber(int subscriptionId) {
+ checkReadPhoneNumbersPermission();
return phoneNumberMap.getOrDefault(subscriptionId, "");
}
@@ -521,6 +543,11 @@ public class ShadowSubscriptionManager {
return this;
}
+ public SubscriptionInfoBuilder setIsOpportunistic(boolean isOpportunistic) {
+ ReflectionHelpers.setField(subscriptionInfo, "mIsOpportunistic", isOpportunistic);
+ return this;
+ }
+
public SubscriptionInfoBuilder setMnc(String mnc) {
if (VERSION.SDK_INT < Q) {
ReflectionHelpers.setField(subscriptionInfo, "mMnc", Integer.valueOf(mnc));
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
index 49a93bcfe..b279892c9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
@@ -19,6 +19,7 @@ import org.robolectric.annotation.Resetter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
+import org.robolectric.versioning.AndroidVersions.U;
/** Shadow for {@link android.view.SurfaceControl} */
@Implements(value = SurfaceControl.class, isInAndroidSdk = false, minSdk = JELLY_BEAN_MR2)
@@ -83,7 +84,7 @@ public class ShadowSurfaceControl {
void initializeNativeObject() {
surfaceControlReflector.setNativeObject(nativeObject.incrementAndGet());
- if (RuntimeEnvironment.getApiLevel() >= ShadowBuild.UPSIDE_DOWN_CAKE) {
+ if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) {
surfaceControlReflector.setFreeNativeResources(() -> {});
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java
index ef52021de..275266ad9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java
@@ -78,6 +78,7 @@ public class ShadowTelecomManager {
private final LinkedHashMap<PhoneAccountHandle, PhoneAccount> accounts = new LinkedHashMap<>();
private final LinkedHashMap<PhoneAccountHandle, String> voicemailNumbers = new LinkedHashMap<>();
+ private final LinkedHashMap<PhoneAccountHandle, String> line1Numbers = new LinkedHashMap<>();
private final List<IncomingCallRecord> incomingCalls = new ArrayList<>();
private final List<OutgoingCallRecord> outgoingCalls = new ArrayList<>();
@@ -91,6 +92,9 @@ public class ShadowTelecomManager {
private boolean isInCall;
private boolean ttySupported;
private PhoneAccountHandle userSelectedOutgoingPhoneAccount;
+ private boolean readPhoneStatePermission = true;
+ private boolean callPhonePermission = true;
+ private boolean handleMmiValue = false;
public CallRequestMode getCallRequestMode() {
return callRequestMode;
@@ -168,6 +172,7 @@ public class ShadowTelecomManager {
@Implementation(minSdk = M)
protected List<PhoneAccountHandle> getCallCapablePhoneAccounts() {
+ checkReadPhoneStatePermission();
return this.getCallCapablePhoneAccounts(false);
}
@@ -216,6 +221,7 @@ public class ShadowTelecomManager {
@Implementation
protected PhoneAccount getPhoneAccount(PhoneAccountHandle account) {
+ checkReadPhoneStatePermission();
return accounts.get(account);
}
@@ -325,7 +331,12 @@ public class ShadowTelecomManager {
@Implementation(minSdk = LOLLIPOP_MR1)
protected String getLine1Number(PhoneAccountHandle accountHandle) {
- return null;
+ checkReadPhoneStatePermission();
+ return line1Numbers.get(accountHandle);
+ }
+
+ public void setLine1Number(PhoneAccountHandle accountHandle, String number) {
+ line1Numbers.put(accountHandle, number);
}
/** Sets the return value for {@link TelecomManager#isInCall}. */
@@ -387,6 +398,7 @@ public class ShadowTelecomManager {
@Implementation
protected boolean isTtySupported() {
+ checkReadPhoneStatePermission();
return ttySupported;
}
@@ -481,6 +493,7 @@ public class ShadowTelecomManager {
@Implementation(minSdk = M)
protected void placeCall(Uri address, Bundle extras) {
+ checkCallPhonePermission();
OutgoingCallRecord call = new OutgoingCallRecord(address, extras);
outgoingCalls.add(call);
@@ -592,14 +605,18 @@ public class ShadowTelecomManager {
ServiceController.of(ReflectionHelpers.callConstructor(clazz), null).create().get());
}
+ public void setHandleMmiValue(boolean handleMmiValue) {
+ this.handleMmiValue = handleMmiValue;
+ }
+
@Implementation
protected boolean handleMmi(String dialString) {
- return false;
+ return handleMmiValue;
}
@Implementation(minSdk = M)
protected boolean handleMmi(String dialString, PhoneAccountHandle accountHandle) {
- return false;
+ return handleMmiValue;
}
@Implementation(minSdk = LOLLIPOP_MR1)
@@ -700,6 +717,36 @@ public class ShadowTelecomManager {
}
}
+ /**
+ * When set to false methods requiring {@link android.Manifest.permission.READ_PHONE_STATE}
+ * permission will throw a {@link SecurityException}. By default it's set to true for backwards
+ * compatibility.
+ */
+ public void setReadPhoneStatePermission(boolean readPhoneStatePermission) {
+ this.readPhoneStatePermission = readPhoneStatePermission;
+ }
+
+ private void checkReadPhoneStatePermission() {
+ if (!readPhoneStatePermission) {
+ throw new SecurityException();
+ }
+ }
+
+ /**
+ * When set to false methods requiring {@link android.Manifest.permission.CALL_PHONE} permission
+ * will throw a {@link SecurityException}. By default it's set to true for backwards
+ * compatibility.
+ */
+ public void setCallPhonePermission(boolean callPhonePermission) {
+ this.callPhonePermission = callPhonePermission;
+ }
+
+ private void checkCallPhonePermission() {
+ if (!callPhonePermission) {
+ throw new SecurityException();
+ }
+ }
+
/** Details about an incoming call request made via {@link TelecomManager#addNewIncomingCall}. */
public static class IncomingCallRecord extends CallRecord {
private boolean isHandled = false;
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 048abf03b..9ec00a58d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -55,6 +55,7 @@ import android.telephony.VisualVoicemailSmsFilterSettings;
import android.telephony.emergency.EmergencyNumber;
import android.text.TextUtils;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
@@ -95,20 +96,23 @@ public class ShadowTelephonyManager {
private final Map<PhoneAccountHandle, Uri> voicemailRingtoneUriMap = new HashMap<>();
private final Map<PhoneAccountHandle, TelephonyManager> phoneAccountToTelephonyManagers =
new HashMap<>();
+ private final Map<PhoneAccountHandle, Integer> phoneAccountHandleSubscriptionId = new HashMap<>();
private PhoneStateListener lastListener;
private /*TelephonyCallback*/ Object lastTelephonyCallback;
private int lastEventFlags;
private String deviceId;
+ private String deviceSoftwareVersion;
private String imei;
private String meid;
private String groupIdLevel1;
private String networkOperatorName = "";
private String networkCountryIso;
private String networkOperator = "";
+ private String networkSpecifier = "";
private Locale simLocale;
- private String simOperator;
+ private String simOperator = "";
private String simOperatorName;
private String simSerialNumber;
private boolean readPhoneStatePermission = true;
@@ -122,6 +126,7 @@ public class ShadowTelephonyManager {
private CellLocation cellLocation = null;
private int callState = CALL_STATE_IDLE;
private int dataState = TelephonyManager.DATA_DISCONNECTED;
+ private int dataActivity = TelephonyManager.DATA_ACTIVITY_NONE;
private String incomingPhoneNumber = null;
private boolean isSmsCapable = true;
private boolean voiceCapable = true;
@@ -141,12 +146,14 @@ public class ShadowTelephonyManager {
private int carrierIdFromSimMccMnc;
private String subscriberId;
private /*UiccSlotInfo[]*/ Object uiccSlotInfos;
- private /*UiccCardInfo[]*/ Object uiccCardsInfo;
+ private /*UiccCardInfo[]*/ Object uiccCardsInfo = new ArrayList<>();
private String visualVoicemailPackageName = null;
private SignalStrength signalStrength;
private boolean dataEnabled = false;
private final Set<Integer> dataDisabledReasons = new HashSet<>();
private boolean isRttSupported;
+ private boolean isTtyModeSupported;
+ private final SparseBooleanArray subIdToHasCarrierPrivileges = new SparseBooleanArray();
private final List<String> sentDialerSpecialCodes = new ArrayList<>();
private boolean hearingAidCompatibilitySupported = false;
private int requestCellInfoUpdateErrorCode = 0;
@@ -167,6 +174,8 @@ public class ShadowTelephonyManager {
*/
private Object callback;
+ private /*PhoneCapability*/ Object phoneCapability;
+
{
resetSimStates();
resetSimCountryIsos();
@@ -205,6 +214,16 @@ public class ShadowTelephonyManager {
this.callback = callback;
}
+ public void setPhoneCapability(/*PhoneCapability*/ Object phoneCapability) {
+ this.phoneCapability = phoneCapability;
+ }
+
+ @Implementation(minSdk = S)
+ @HiddenApi
+ public /*PhoneCapability*/ Object getPhoneCapability() {
+ return phoneCapability;
+ }
+
@Implementation
protected void listen(PhoneStateListener listener, int flags) {
lastListener = listener;
@@ -324,6 +343,24 @@ public class ShadowTelephonyManager {
this.dataState = dataState;
}
+ /**
+ * Data activity may be specified via {@link #setDataActivity(int)}. If no override is set, this
+ * defaults to {@link TelephonyManager#DATA_ACTIVITY_NONE}.
+ */
+ @Implementation
+ protected int getDataActivity() {
+ return dataActivity;
+ }
+
+ /**
+ * Sets the value to be returned by calls to {@link #getDataActivity()}. This <b>should</b>
+ * correspond to one of the {@code DATA_ACTIVITY_*} constants defined on {@link TelephonyManager},
+ * but this is not enforced.
+ */
+ public void setDataActivity(int dataActivity) {
+ this.dataActivity = dataActivity;
+ }
+
@Implementation
protected String getDeviceId() {
checkReadPhoneStatePermission();
@@ -334,6 +371,16 @@ public class ShadowTelephonyManager {
deviceId = newDeviceId;
}
+ @Implementation
+ protected String getDeviceSoftwareVersion() {
+ checkReadPhoneStatePermission();
+ return deviceSoftwareVersion;
+ }
+
+ public void setDeviceSoftwareVersion(String newDeviceSoftwareVersion) {
+ deviceSoftwareVersion = newDeviceSoftwareVersion;
+ }
+
@Implementation(minSdk = LOLLIPOP_MR1)
public void setNetworkOperatorName(String networkOperatorName) {
this.networkOperatorName = networkOperatorName;
@@ -422,6 +469,15 @@ public class ShadowTelephonyManager {
return networkOperator;
}
+ public void setNetworkSpecifier(String networkSpecifier) {
+ this.networkSpecifier = networkSpecifier;
+ }
+
+ @Implementation(minSdk = O)
+ protected String getNetworkSpecifier() {
+ return networkSpecifier;
+ }
+
@Implementation
protected String getSimOperator() {
return simOperator;
@@ -892,6 +948,7 @@ public class ShadowTelephonyManager {
*/
@Implementation(minSdk = M)
protected String getDeviceId(int slot) {
+ checkReadPhoneStatePermission();
return slotIndexToDeviceId.get(slot);
}
@@ -1095,6 +1152,16 @@ public class ShadowTelephonyManager {
this.subscriberId = subscriberId;
}
+ @Implementation(minSdk = R)
+ protected int getSubscriptionId(PhoneAccountHandle handle) {
+ checkReadPhoneStatePermission();
+ return phoneAccountHandleSubscriptionId.get(handle);
+ }
+
+ public void setPhoneAccountHandleSubscriptionId(PhoneAccountHandle handle, int subscriptionId) {
+ phoneAccountHandleSubscriptionId.put(handle, subscriptionId);
+ }
+
/** Returns the value set by {@link #setVisualVoicemailPackageName(String)}. */
@Implementation(minSdk = O)
protected String getVisualVoicemailPackageName() {
@@ -1280,6 +1347,41 @@ public class ShadowTelephonyManager {
}
/**
+ * Implementation for {@link TelephonyManager#isTtyModeSupported}.
+ *
+ * @return False by default, unless set with {@link #setTtyModeSupported(boolean)}.
+ */
+ @Implementation(minSdk = Build.VERSION_CODES.M)
+ protected boolean isTtyModeSupported() {
+ checkReadPhoneStatePermission();
+ return isTtyModeSupported;
+ }
+
+ /** Sets the value to be returned by {@link #isTtyModeSupported()} */
+ public void setTtyModeSupported(boolean isTtyModeSupported) {
+ this.isTtyModeSupported = isTtyModeSupported;
+ }
+
+ /**
+ * @return False by default, unless set with {@link #setHasCarrierPrivileges(int, boolean)}.
+ */
+ @Implementation(minSdk = Build.VERSION_CODES.N)
+ @HiddenApi
+ protected boolean hasCarrierPrivileges(int subId) {
+ return subIdToHasCarrierPrivileges.get(subId);
+ }
+
+ public void setHasCarrierPrivileges(boolean hasCarrierPrivileges) {
+ int subId = ReflectionHelpers.callInstanceMethod(realTelephonyManager, "getSubId");
+ setHasCarrierPrivileges(subId, hasCarrierPrivileges);
+ }
+
+ /** Sets the {@code hasCarrierPrivileges} for the given {@code subId}. */
+ public void setHasCarrierPrivileges(int subId, boolean hasCarrierPrivileges) {
+ subIdToHasCarrierPrivileges.put(subId, hasCarrierPrivileges);
+ }
+
+ /**
* Implementation for {@link TelephonyManager#sendDialerSpecialCode(String)}.
*
* @param inputCode special code to be sent.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java
index e442a2396..f2c06efd2 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java
@@ -6,9 +6,7 @@ import android.text.TextUtils.TruncateAt;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-/**
- * Implement {@lint TextUtils#ellipsize} by truncating the text.
- */
+/** Implement {@link TextUtils#ellipsize} by truncating the text. */
@SuppressWarnings({"UnusedDeclaration"})
@Implements(TextUtils.class)
public class ShadowTextUtils {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java
index 56b7eb08f..e1a5edc64 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java
@@ -1,25 +1,23 @@
package org.robolectric.shadows;
-import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
-import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
-
import android.annotation.SystemApi;
import android.app.time.Capabilities;
import android.app.time.Capabilities.CapabilityState;
import android.app.time.ExternalTimeSuggestion;
-import android.app.time.LocationTimeZoneAlgorithmStatus;
-import android.app.time.TelephonyTimeZoneAlgorithmStatus;
import android.app.time.TimeManager;
import android.app.time.TimeZoneCapabilities;
import android.app.time.TimeZoneCapabilitiesAndConfig;
import android.app.time.TimeZoneConfiguration;
-import android.app.time.TimeZoneDetectorStatus;
import android.os.Build.VERSION_CODES;
import android.os.UserHandle;
import java.util.Objects;
import java.util.concurrent.Executor;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.versioning.AndroidVersions.U;
/** Shadow for internal Android {@code TimeManager} class introduced in S. */
@Implements(value = TimeManager.class, minSdk = VERSION_CODES.S, isInAndroidSdk = false)
@@ -28,21 +26,7 @@ public class ShadowTimeManager {
public static final String CONFIGURE_GEO_DETECTION_CAPABILITY =
"configure_geo_detection_capability";
- private TimeZoneCapabilities timeZoneCapabilities =
- new TimeZoneCapabilities.Builder(UserHandle.CURRENT)
- .setConfigureAutoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED)
- .setUseLocationEnabled(true)
- .setConfigureGeoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED)
- .setSetManualTimeZoneCapability(Capabilities.CAPABILITY_POSSESSED)
- .build();
-
- private TimeZoneDetectorStatus detectorStatus =
- new TimeZoneDetectorStatus(
- DETECTOR_STATUS_RUNNING,
- new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING),
- new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
- LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null,
- LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null));
+ private TimeZoneCapabilities timeZoneCapabilities = getTimeZoneCapabilities();
private TimeZoneConfiguration timeZoneConfiguration;
@@ -66,11 +50,52 @@ public class ShadowTimeManager {
@Implementation
@SystemApi
- protected TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig() {
+ protected TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig()
+ throws ClassNotFoundException {
Objects.requireNonNull(timeZoneConfiguration, "timeZoneConfiguration was not set");
- return new TimeZoneCapabilitiesAndConfig(
- detectorStatus, timeZoneCapabilities, timeZoneConfiguration);
+ if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) {
+ Object telephonyAlgoStatus =
+ ReflectionHelpers.callConstructor(
+ Class.forName("android.app.time.TelephonyTimeZoneAlgorithmStatus"),
+ ClassParameter.from(int.class, 3));
+ Object locationAlgoStatus =
+ ReflectionHelpers.callConstructor(
+ Class.forName("android.app.time.LocationTimeZoneAlgorithmStatus"),
+ ClassParameter.from(int.class, 3),
+ ClassParameter.from(int.class, 3),
+ ClassParameter.from(
+ Class.forName("android.service.timezone.TimeZoneProviderStatus"), null),
+ ClassParameter.from(int.class, 3),
+ ClassParameter.from(
+ Class.forName("android.service.timezone.TimeZoneProviderStatus"), null));
+
+ Object timeZoneDetectorStatus =
+ ReflectionHelpers.callConstructor(
+ Class.forName("android.app.time.TimeZoneDetectorStatus"),
+ ClassParameter.from(int.class, 0),
+ ClassParameter.from(
+ Class.forName("android.app.time.TelephonyTimeZoneAlgorithmStatus"),
+ telephonyAlgoStatus),
+ ClassParameter.from(
+ Class.forName("android.app.time.LocationTimeZoneAlgorithmStatus"),
+ locationAlgoStatus));
+ return ReflectionHelpers.callConstructor(
+ TimeZoneCapabilitiesAndConfig.class,
+ ClassParameter.from(
+ Class.forName("android.app.time.TimeZoneDetectorStatus"), timeZoneDetectorStatus),
+ ClassParameter.from(
+ Class.forName("android.app.time.TimeZoneCapabilities"), timeZoneCapabilities),
+ ClassParameter.from(
+ Class.forName("android.app.time.TimeZoneConfiguration"), timeZoneConfiguration));
+ } else {
+ return ReflectionHelpers.callConstructor(
+ TimeZoneCapabilitiesAndConfig.class,
+ ClassParameter.from(
+ Class.forName("android.app.time.TimeZoneCapabilities"), timeZoneCapabilities),
+ ClassParameter.from(
+ Class.forName("android.app.time.TimeZoneConfiguration"), timeZoneConfiguration));
+ }
}
@Implementation
@@ -89,4 +114,29 @@ public class ShadowTimeManager {
@Implementation
protected void suggestExternalTime(ExternalTimeSuggestion timeSuggestion) {}
+
+ private TimeZoneCapabilities getTimeZoneCapabilities() {
+ TimeZoneCapabilities.Builder timeZoneCapabilitiesBuilder =
+ new TimeZoneCapabilities.Builder(UserHandle.CURRENT)
+ .setConfigureAutoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED)
+ .setConfigureGeoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED);
+
+ if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) {
+ ReflectionHelpers.callInstanceMethod(
+ timeZoneCapabilitiesBuilder,
+ "setUseLocationEnabled",
+ ClassParameter.from(boolean.class, true));
+ ReflectionHelpers.callInstanceMethod(
+ timeZoneCapabilitiesBuilder,
+ "setSetManualTimeZoneCapability",
+ ClassParameter.from(int.class, Capabilities.CAPABILITY_POSSESSED));
+ return timeZoneCapabilitiesBuilder.build();
+ } else {
+ ReflectionHelpers.callInstanceMethod(
+ timeZoneCapabilitiesBuilder,
+ "setSuggestManualTimeZoneCapability",
+ ClassParameter.from(int.class, Capabilities.CAPABILITY_POSSESSED));
+ return timeZoneCapabilitiesBuilder.build();
+ }
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java
index 73575c813..dd43e8165 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java
@@ -10,6 +10,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Sets.newConcurrentHashSet;
import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -26,7 +27,6 @@ import android.graphics.Paint;
import android.graphics.Point;
import android.os.Build;
import android.os.IBinder;
-import android.os.Looper;
import android.provider.Settings;
import android.view.Display;
import android.view.InputEvent;
@@ -41,11 +41,12 @@ import androidx.test.runner.lifecycle.ActivityLifecycleMonitor;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
@@ -88,51 +89,68 @@ public class ShadowUiAutomation {
@Implementation
protected boolean setRotation(int rotation) {
- if (rotation == UiAutomation.ROTATION_FREEZE_CURRENT
- || rotation == UiAutomation.ROTATION_UNFREEZE) {
- return true;
- }
- Display display = ShadowDisplay.getDefaultDisplay();
- int currentRotation = display.getRotation();
- boolean isRotated =
- (rotation == ROTATION_FREEZE_0 || rotation == ROTATION_FREEZE_180)
- != (currentRotation == ROTATION_FREEZE_0 || currentRotation == ROTATION_FREEZE_180);
- shadowOf(display).setRotation(rotation);
- if (isRotated) {
- int currentOrientation = Resources.getSystem().getConfiguration().orientation;
- String rotationQualifier =
- "+" + (currentOrientation == Configuration.ORIENTATION_PORTRAIT ? "land" : "port");
- ShadowDisplayManager.changeDisplay(display.getDisplayId(), rotationQualifier);
- RuntimeEnvironment.setQualifiers(rotationQualifier);
- }
- return true;
+ AtomicBoolean result = new AtomicBoolean(false);
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ if (rotation == UiAutomation.ROTATION_FREEZE_CURRENT
+ || rotation == UiAutomation.ROTATION_UNFREEZE) {
+ result.set(true);
+ return;
+ }
+ Display display = ShadowDisplay.getDefaultDisplay();
+ int currentRotation = display.getRotation();
+ boolean isRotated =
+ (rotation == ROTATION_FREEZE_0 || rotation == ROTATION_FREEZE_180)
+ != (currentRotation == ROTATION_FREEZE_0
+ || currentRotation == ROTATION_FREEZE_180);
+ shadowOf(display).setRotation(rotation);
+ if (isRotated) {
+ int currentOrientation = Resources.getSystem().getConfiguration().orientation;
+ String rotationQualifier =
+ "+" + (currentOrientation == Configuration.ORIENTATION_PORTRAIT ? "land" : "port");
+ ShadowDisplayManager.changeDisplay(display.getDisplayId(), rotationQualifier);
+ RuntimeEnvironment.setQualifiers(rotationQualifier);
+ }
+ result.set(true);
+ });
+ return result.get();
}
@Implementation
protected void throwIfNotConnectedLocked() {}
@Implementation
- protected Bitmap takeScreenshot() {
+ protected Bitmap takeScreenshot() throws Exception {
if (!ShadowView.useRealGraphics()) {
return null;
}
- Point displaySize = new Point();
- ShadowDisplay.getDefaultDisplay().getRealSize(displaySize);
- Bitmap screenshot = Bitmap.createBitmap(displaySize.x, displaySize.y, Bitmap.Config.ARGB_8888);
- Canvas screenshotCanvas = new Canvas(screenshot);
- Paint paint = new Paint();
- for (Root root : getViewRoots().reverse()) {
- View rootView = root.getRootView();
- if (rootView.getWidth() <= 0 || rootView.getHeight() <= 0) {
- continue;
- }
- Bitmap window =
- Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888);
- Canvas windowCanvas = new Canvas(window);
- rootView.draw(windowCanvas);
- screenshotCanvas.drawBitmap(window, root.params.x, root.params.y, paint);
- }
- return screenshot;
+
+ FutureTask<Bitmap> screenshotTask =
+ new FutureTask<>(
+ () -> {
+ Point displaySize = new Point();
+ ShadowDisplay.getDefaultDisplay().getRealSize(displaySize);
+ Bitmap screenshot =
+ Bitmap.createBitmap(displaySize.x, displaySize.y, Bitmap.Config.ARGB_8888);
+ Canvas screenshotCanvas = new Canvas(screenshot);
+ Paint paint = new Paint();
+ for (Root root : getViewRoots().reverse()) {
+ View rootView = root.getRootView();
+ if (rootView.getWidth() <= 0 || rootView.getHeight() <= 0) {
+ continue;
+ }
+ Bitmap window =
+ Bitmap.createBitmap(
+ rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888);
+ Canvas windowCanvas = new Canvas(window);
+ rootView.draw(windowCanvas);
+ screenshotCanvas.drawBitmap(window, root.params.x, root.params.y, paint);
+ }
+ return screenshot;
+ });
+
+ ShadowInstrumentation.runOnMainSyncNoIdle(screenshotTask);
+ return screenshotTask.get();
}
/**
@@ -141,14 +159,18 @@ public class ShadowUiAutomation {
* UiAutomation} API, this method is provided for backwards compatibility with SDK < 18.
*/
public static boolean injectInputEvent(InputEvent event) {
- checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
- if (event instanceof MotionEvent) {
- return injectMotionEvent((MotionEvent) event);
- } else if (event instanceof KeyEvent) {
- return injectKeyEvent((KeyEvent) event);
- } else {
- throw new IllegalArgumentException("Unrecognized event type: " + event);
- }
+ AtomicBoolean result = new AtomicBoolean(false);
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ if (event instanceof MotionEvent) {
+ result.set(injectMotionEvent((MotionEvent) event));
+ } else if (event instanceof KeyEvent) {
+ result.set(injectKeyEvent((KeyEvent) event));
+ } else {
+ throw new IllegalArgumentException("Unrecognized event type: " + event);
+ }
+ });
+ return result.get();
}
@Implementation
@@ -251,12 +273,15 @@ public class ShadowUiAutomation {
}
private static Set<IBinder> getStartedActivityTokens() {
- ActivityLifecycleMonitor monitor = ActivityLifecycleMonitorRegistry.getInstance();
- return ImmutableSet.<Activity>builder()
- .addAll(monitor.getActivitiesInStage(Stage.STARTED))
- .addAll(monitor.getActivitiesInStage(Stage.RESUMED))
- .build()
- .stream()
+ Set<Activity> startedActivities = newConcurrentHashSet();
+ ShadowInstrumentation.runOnMainSyncNoIdle(
+ () -> {
+ ActivityLifecycleMonitor monitor = ActivityLifecycleMonitorRegistry.getInstance();
+ startedActivities.addAll(monitor.getActivitiesInStage(Stage.STARTED));
+ startedActivities.addAll(monitor.getActivitiesInStage(Stage.RESUMED));
+ });
+
+ return startedActivities.stream()
.map(activity -> activity.getWindow().getDecorView().getApplicationWindowToken())
.collect(toSet());
}
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 4eb7f180c..a1bb4c1c9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
@@ -80,6 +80,8 @@ public class ShadowUserManager {
private static boolean isMultiUserSupported = false;
private static boolean isHeadlessSystemUserMode = false;
+ private final Object lock = new Object();
+
@RealObject private UserManager realObject;
private UserManagerState userManagerState;
private Boolean managedProfile;
@@ -484,8 +486,10 @@ public class ShadowUserManager {
@Implementation(minSdk = LOLLIPOP)
protected boolean hasUserRestriction(String restrictionKey, UserHandle userHandle) {
- Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier());
- return bundle != null && bundle.getBoolean(restrictionKey);
+ synchronized (lock) {
+ Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier());
+ return bundle != null && bundle.getBoolean(restrictionKey);
+ }
}
/**
@@ -496,7 +500,9 @@ public class ShadowUserManager {
@Implementation(minSdk = JELLY_BEAN_MR2)
protected void setUserRestriction(String key, boolean value, UserHandle userHandle) {
Bundle bundle = getUserRestrictionsForUser(userHandle);
- bundle.putBoolean(key, value);
+ synchronized (lock) {
+ bundle.putBoolean(key, value);
+ }
}
@Implementation(minSdk = JELLY_BEAN_MR2)
@@ -524,12 +530,14 @@ public class ShadowUserManager {
}
private Bundle getUserRestrictionsForUser(UserHandle userHandle) {
- Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier());
- if (bundle == null) {
- bundle = new Bundle();
- userManagerState.userRestrictions.put(userHandle.getIdentifier(), bundle);
+ synchronized (lock) {
+ Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier());
+ if (bundle == null) {
+ bundle = new Bundle();
+ userManagerState.userRestrictions.put(userHandle.getIdentifier(), bundle);
+ }
+ return bundle;
}
- return bundle;
}
/**
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java
index 4a9848dd5..30da1ba7d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java
@@ -5,6 +5,7 @@ import static android.os.Build.VERSION_CODES.Q;
import android.annotation.TargetApi;
import dalvik.system.VMRuntime;
+import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
@@ -15,7 +16,7 @@ import org.robolectric.res.android.NativeObjRegistry;
@Implements(value = VMRuntime.class, isInAndroidSdk = false)
public class ShadowVMRuntime {
- private final NativeObjRegistry<Object> nativeObjRegistry =
+ private final NativeObjRegistry<WeakReference<Object>> nativeObjRegistry =
new NativeObjRegistry<>("VRRuntime.nativeObjectRegistry");
// There actually isn't any android JNI code to call through to in Robolectric due to
// cross-platform compatibility issues. We default to a reasonable value that reflects the devices
@@ -42,7 +43,7 @@ public class ShadowVMRuntime {
*/
@Implementation
public long addressOf(Object obj) {
- return nativeObjRegistry.register(obj);
+ return nativeObjRegistry.register(new WeakReference<>(obj));
}
/**
@@ -50,7 +51,7 @@ public class ShadowVMRuntime {
*/
public @Nullable
Object getObjectForAddress(long address) {
- return nativeObjRegistry.getNativeObject(address);
+ return nativeObjRegistry.getNativeObject(address).get();
}
/**
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 5d4f329d1..f19492496 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
@@ -20,6 +20,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -55,6 +56,7 @@ import org.robolectric.annotation.ReflectorObject;
import org.robolectric.annotation.Resetter;
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;
@@ -149,6 +151,22 @@ public class ShadowView {
return shadowView.innerText();
}
+ static int[] getLocationInSurfaceCompat(View view) {
+ int[] locationInSurface = new int[2];
+ if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
+ view.getLocationInSurface(locationInSurface);
+ } else {
+ view.getLocationInWindow(locationInSurface);
+ Rect surfaceInsets =
+ reflector(ViewRootImplReflector.class, view.getViewRootImpl())
+ .getWindowAttributes()
+ .surfaceInsets;
+ locationInSurface[0] += surfaceInsets.left;
+ locationInSurface[1] += surfaceInsets.top;
+ }
+ return locationInSurface;
+ }
+
// Only override up to kitkat, while this version exists after kitkat it just calls through to the
// __constructor__(Context, AttributeSet, int, int) variant below.
@Implementation(maxSdk = KITKAT)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
index 2dd2e9a9e..9dd19321b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
@@ -18,6 +18,7 @@ import android.view.Display;
import android.view.HandlerActionQueue;
import android.view.IWindowSession;
import android.view.InsetsState;
+import android.view.Surface;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewRootImpl;
@@ -306,6 +307,10 @@ public class ShadowViewRootImpl {
}
}
+ Surface getSurface() {
+ return reflector(ViewRootImplReflector.class, realObject).getSurface();
+ }
+
/** Reflector interface for {@link ViewRootImpl}'s internals. */
@ForType(ViewRootImpl.class)
protected interface ViewRootImplReflector {
@@ -345,6 +350,12 @@ public class ShadowViewRootImpl {
@Accessor("mSurfaceControl")
SurfaceControl getSurfaceControl();
+ @Accessor("mSurface")
+ Surface getSurface();
+
+ @Accessor("mWindowAttributes")
+ WindowManager.LayoutParams getWindowAttributes();
+
// <= JELLY_BEAN
void dispatchResized(
int w,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java
index 2d4ac3b90..d612539a8 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java
@@ -1,17 +1,26 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static org.robolectric.util.ReflectionHelpers.callConstructor;
import android.app.Dialog;
+import android.app.VoiceInteractor;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.RemoteException;
import android.service.voice.VoiceInteractionSession;
+import android.service.voice.VoiceInteractionSession.CommandRequest;
+import android.service.voice.VoiceInteractionSession.Request;
+import com.android.internal.app.IVoiceInteractorCallback;
+import com.android.internal.app.IVoiceInteractorRequest;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.List;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
@@ -127,11 +136,79 @@ public class ShadowVoiceInteractionSession {
startVoiceActivityException = exception;
}
+ /**
+ * Simulates the creation of the {@link VoiceInteractionSession.CommandRequest} related to the
+ * provided {@link VoiceInteractor.CommandRequest}, as if it was being created by the framework.
+ * The method calls {@link VoiceInteractionSession#onRequestCommand(CommandRequest)} with newly
+ * created {@link VoiceInteractionSession.CommandRequest}.
+ *
+ * @param commandRequest: Command request sent by a third-party application.
+ * @param packageName: Package name of the application that initiated the request.
+ * @param uid: User ID of the application that initiated the request.
+ * @return newly created {@link VoiceInteractionSession.CommandRequest}
+ */
+ public CommandRequest sendCommandRequest(
+ @Nonnull VoiceInteractor.CommandRequest commandRequest,
+ @Nonnull String packageName,
+ int uid) {
+ String command = ReflectionHelpers.getField(commandRequest, "mCommand");
+ Bundle extras = ReflectionHelpers.getField(commandRequest, "mArgs");
+
+ IVoiceInteractorCallback callback = new ShadowVoiceInteractorCallback(commandRequest);
+
+ CommandRequest internalCommandRequest =
+ createCommandRequest(packageName, uid, callback, command, extras);
+ realSession.onRequestCommand(internalCommandRequest);
+ return internalCommandRequest;
+ }
+
+ /**
+ * Creates the {@link VoiceInteractionSession.CommandRequest}.
+ *
+ * @param packageName: Package name of the application that initiated the request.
+ * @param uid: User ID of the application that initiated the request.
+ * @param callback: IVoiceInteractorCallback.
+ * @param command: RequestCommand command.
+ * @param extras: Additional extra information that was supplied as part of the request.
+ * @return created {@link VoiceInteractionSession.CommandRequest}.
+ */
+ private CommandRequest createCommandRequest(
+ @Nonnull String packageName,
+ int uid,
+ @Nonnull IVoiceInteractorCallback callback,
+ @Nonnull String command,
+ @Nonnull Bundle extras) {
+ CommandRequest commandRequest =
+ callConstructor(
+ CommandRequest.class,
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(int.class, uid),
+ ClassParameter.from(IVoiceInteractorCallback.class, callback),
+ ClassParameter.from(VoiceInteractionSession.class, realSession),
+ ClassParameter.from(String.class, command),
+ ClassParameter.from(Bundle.class, extras));
+ ReflectionHelpers.callInstanceMethod(
+ realSession, "addRequest", ClassParameter.from(Request.class, commandRequest));
+ return commandRequest;
+ }
+
// Extends com.android.internal.app.IVoiceInteractionManagerService.Stub
private class FakeVoiceInteractionManagerService {
+ // Removed in Android U
// @Override
public boolean showSessionFromSession(IBinder token, Bundle args, int flags) {
+ return showSessionFromSessionImpl(args, flags);
+ }
+
+ // Added in Android U
+ // @Override
+ public boolean showSessionFromSession(
+ IBinder token, Bundle args, int flags, String attributionTag) {
+ return showSessionFromSessionImpl(args, flags);
+ }
+
+ private boolean showSessionFromSessionImpl(Bundle args, int flags) {
try {
Class<?> callbackClass =
Class.forName("com.android.internal.app.IVoiceInteractionSessionShowCallback");
@@ -177,4 +254,48 @@ public class ShadowVoiceInteractionSession {
isFinishing = true;
}
}
+
+ private static class ShadowVoiceInteractorCallback implements IVoiceInteractorCallback {
+ private final VoiceInteractor.CommandRequest commandRequest;
+
+ ShadowVoiceInteractorCallback(VoiceInteractor.CommandRequest commandRequest) {
+ this.commandRequest = commandRequest;
+ }
+
+ @Override
+ public void deliverConfirmationResult(
+ IVoiceInteractorRequest request, boolean confirmed, Bundle result) throws RemoteException {}
+
+ @Override
+ public void deliverPickOptionResult(
+ IVoiceInteractorRequest request, boolean finished, Option[] selections, Bundle result)
+ throws RemoteException {}
+
+ @Override
+ public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result)
+ throws RemoteException {}
+
+ @Override
+ public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result)
+ throws RemoteException {}
+
+ @Override
+ public void deliverCommandResult(
+ IVoiceInteractorRequest request, boolean finished, Bundle result) throws RemoteException {
+ commandRequest.onCommandResult(finished, result);
+ }
+
+ @Override
+ public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException {
+ commandRequest.onCancel();
+ }
+
+ @Override
+ public void destroy() throws RemoteException {}
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+ }
}
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 cc6cfccf9..a5c700067 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -73,6 +73,10 @@ public class ShadowWifiManager {
private boolean startScanSucceeds = true;
private boolean is5GHzBandSupported = false;
private boolean isStaApConcurrencySupported = false;
+ private boolean isWpa3SaeSupported = false;
+ private boolean isWpa3SaeH2eSupported = false;
+ private boolean isWpa3SaePublicKeySupported = false;
+ private boolean isWpa3SuiteBSupported = false;
private AtomicInteger activeLockCount = new AtomicInteger(0);
private final BitSet readOnlyNetworkIds = new BitSet();
private final ConcurrentHashMap<WifiManager.OnWifiUsabilityStatsListener, Executor>
@@ -128,7 +132,7 @@ public class ShadowWifiManager {
this.is5GHzBandSupported = is5GHzBandSupported;
}
- /** Returns last value provided to #setStaApConcurrencySupported. */
+ /** Returns last value provided to {@link #setStaApConcurrencySupported}. */
@Implementation(minSdk = R)
protected boolean isStaApConcurrencySupported() {
return isStaApConcurrencySupported;
@@ -139,6 +143,50 @@ public class ShadowWifiManager {
this.isStaApConcurrencySupported = isStaApConcurrencySupported;
}
+ /** Returns last value provided to {@link #setWpa3SaeSupported}. */
+ @Implementation(minSdk = Q)
+ protected boolean isWpa3SaeSupported() {
+ return isWpa3SaeSupported;
+ }
+
+ /** Sets whether WPA3-Personal SAE is supported. */
+ public void setWpa3SaeSupported(boolean isWpa3SaeSupported) {
+ this.isWpa3SaeSupported = isWpa3SaeSupported;
+ }
+
+ /** Returns last value provided to {@link #setWpa3SaePublicKeySupported}. */
+ @Implementation(minSdk = S)
+ protected boolean isWpa3SaePublicKeySupported() {
+ return isWpa3SaePublicKeySupported;
+ }
+
+ /** Sets whether WPA3 SAE Public Key is supported. */
+ public void setWpa3SaePublicKeySupported(boolean isWpa3SaePublicKeySupported) {
+ this.isWpa3SaePublicKeySupported = isWpa3SaePublicKeySupported;
+ }
+
+ /** Returns last value provided to {@link #setWpa3SaeH2eSupported}. */
+ @Implementation(minSdk = S)
+ protected boolean isWpa3SaeH2eSupported() {
+ return isWpa3SaeH2eSupported;
+ }
+
+ /** Sets whether WPA3 SAE Hash-to-Element is supported. */
+ public void setWpa3SaeH2eSupported(boolean isWpa3SaeH2eSupported) {
+ this.isWpa3SaeH2eSupported = isWpa3SaeH2eSupported;
+ }
+
+ /** Returns last value provided to {@link #setWpa3SuiteBSupported}. */
+ @Implementation(minSdk = Q)
+ protected boolean isWpa3SuiteBSupported() {
+ return isWpa3SuiteBSupported;
+ }
+
+ /** Sets whether WPA3-Enterprise Suite-B-192 is supported. */
+ public void setWpa3SuiteBSupported(boolean isWpa3SuiteBSupported) {
+ this.isWpa3SuiteBSupported = isWpa3SuiteBSupported;
+ }
+
/** Sets the connection info as the provided {@link WifiInfo}. */
public void setConnectionInfo(WifiInfo wifiInfo) {
this.wifiInfo = wifiInfo;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
index 75b0371eb..4317d26c0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
@@ -21,6 +21,7 @@ import android.view.View;
import android.view.WindowManagerGlobal;
import androidx.annotation.Nullable;
import java.lang.reflect.Proxy;
+import java.util.List;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -151,6 +152,9 @@ public class ShadowWindowManagerGlobal {
@Static
@Accessor("sUseBLASTAdapter")
void setUseBlastAdapter(boolean useBlastAdapter);
+
+ @Accessor("mViews")
+ List<View> getWindowViews();
}
private static class WindowSessionDelegate {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
index 4c38710f2..37b3f8d65 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
@@ -2,7 +2,6 @@ package org.robolectric.shadows;
import android.net.wifi.WifiUsabilityStatsEntry;
import android.net.wifi.WifiUsabilityStatsEntry.ContentionTimeStats;
-import android.net.wifi.WifiUsabilityStatsEntry.LinkStats;
import android.net.wifi.WifiUsabilityStatsEntry.RadioStats;
import android.net.wifi.WifiUsabilityStatsEntry.RateStats;
import android.os.Build.VERSION_CODES;
@@ -118,44 +117,44 @@ public class WifiUsabilityStatsEntryBuilder {
ClassParameter.from(boolean.class, isSameRegisteredCell),
ClassParameter.from(SparseArray.class, new SparseArray<>())); // new in >T
} else {
- return new WifiUsabilityStatsEntry(
- timeStampMillis,
- rssi,
- linkSpeedMbps,
- totalTxSuccess,
- totalTxRetries,
- totalTxBad,
- totalRxSuccess,
- totalRadioOnTimeMillis,
- totalRadioTxTimeMillis,
- totalRadioRxTimeMillis,
- totalScanTimeMillis,
- totalNanScanTimeMillis,
- totalBackgroundScanTimeMillis,
- totalRoamScanTimeMillis,
- totalPnoScanTimeMillis,
- totalHotspot2ScanTimeMillis,
- totalCcaBusyFreqTimeMillis,
- totalRadioOnFreqTimeMillis,
- totalBeaconRx,
- probeStatusSinceLastUpdate,
- probeElapsedTimeSinceLastUpdateMillis,
- probeMcsRateSinceLastUpdate,
- rxLinkSpeedMbps,
- timeSliceDutyCycleInPercent,
- new ContentionTimeStats[] {},
- new RateStats[] {},
- new RadioStats[] {},
- CHANNEL_UTILIZATION_RATIO,
- isThroughputSufficient,
- isWifiScoringEnabled,
- isCellularDataAvailable,
- cellularDataNetworkType,
- cellularSignalStrengthDbm,
- cellularSignalStrengthDb,
- isSameRegisteredCell,
- new SparseArray<LinkStats>());
-
+ return ReflectionHelpers.callConstructor(
+ WifiUsabilityStatsEntry.class,
+ ClassParameter.from(long.class, timeStampMillis),
+ ClassParameter.from(int.class, rssi),
+ ClassParameter.from(int.class, linkSpeedMbps),
+ ClassParameter.from(long.class, totalTxSuccess),
+ ClassParameter.from(long.class, totalTxRetries),
+ ClassParameter.from(long.class, totalTxBad),
+ ClassParameter.from(long.class, totalRxSuccess),
+ ClassParameter.from(long.class, totalRadioOnTimeMillis),
+ ClassParameter.from(long.class, totalRadioTxTimeMillis),
+ ClassParameter.from(long.class, totalRadioRxTimeMillis),
+ ClassParameter.from(long.class, totalScanTimeMillis),
+ ClassParameter.from(long.class, totalNanScanTimeMillis),
+ ClassParameter.from(long.class, totalBackgroundScanTimeMillis),
+ ClassParameter.from(long.class, totalRoamScanTimeMillis),
+ ClassParameter.from(long.class, totalPnoScanTimeMillis),
+ ClassParameter.from(long.class, totalHotspot2ScanTimeMillis),
+ ClassParameter.from(long.class, totalCcaBusyFreqTimeMillis),
+ ClassParameter.from(long.class, totalRadioOnFreqTimeMillis),
+ ClassParameter.from(long.class, totalBeaconRx),
+ ClassParameter.from(int.class, probeStatusSinceLastUpdate),
+ ClassParameter.from(int.class, probeElapsedTimeSinceLastUpdateMillis),
+ ClassParameter.from(int.class, probeMcsRateSinceLastUpdate),
+ ClassParameter.from(int.class, rxLinkSpeedMbps),
+ ClassParameter.from(int.class, timeSliceDutyCycleInPercent), // new in T
+ ClassParameter.from(
+ ContentionTimeStats[].class, new ContentionTimeStats[] {}), // new in T
+ ClassParameter.from(RateStats[].class, new RateStats[] {}), // new in T
+ ClassParameter.from(RadioStats[].class, new RadioStats[] {}), // new in T
+ ClassParameter.from(int.class, CHANNEL_UTILIZATION_RATIO), // new in T
+ ClassParameter.from(boolean.class, isThroughputSufficient), // new in T
+ ClassParameter.from(boolean.class, isWifiScoringEnabled), // new in T
+ ClassParameter.from(boolean.class, isCellularDataAvailable), // new in T
+ ClassParameter.from(int.class, cellularDataNetworkType),
+ ClassParameter.from(int.class, cellularSignalStrengthDbm),
+ ClassParameter.from(int.class, cellularSignalStrengthDb),
+ ClassParameter.from(boolean.class, isSameRegisteredCell));
}
}
diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java
index 631451883..e5431eb97 100644
--- a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java
+++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java
@@ -446,7 +446,7 @@ public final class AndroidVersions {
public static final String SHORT_CODE = "V";
- public static final String VERSION = "15.0";
+ public static final String VERSION = "15";
}
/** The current release this process is running on. */
@@ -738,6 +738,9 @@ public final class AndroidVersions {
try {
Field activeCodeFields = targetClass.getDeclaredField("ACTIVE_CODENAMES");
String[] activeCodeNames = (String[]) activeCodeFields.get(null);
+ if (activeCodeNames == null) {
+ return new ArrayList<>();
+ }
return asList(activeCodeNames);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException ex) {
return new ArrayList<>();
diff --git a/utils/build.gradle b/utils/build.gradle
index c31c9a0e0..54a2531e2 100644
--- a/utils/build.gradle
+++ b/utils/build.gradle
@@ -21,29 +21,11 @@ tasks.withType(GenerateModuleMetadata).configureEach {
}
compileKotlin {
- // Use java/main classes directory to replace default kotlin/main to
- // avoid d8 error when dexing & desugaring kotlin classes with non-exist
- // kotlin/main directory because utils module doesn't have kotlin code
- // in production. If utils module starts to add Kotlin code in main source
- // set, we can remove this destinationDirectory modification.
- destinationDirectory = file("${projectDir}/build/classes/java/main")
compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
-afterEvaluate {
- configurations {
- runtimeElements {
- attributes {
- // We should add artifactType with jar to ensure standard runtimeElements variant
- // has a max priority selection sequence than other variants that brought by
- // kotlin plugin.
- attribute(
- Attribute.of("artifactType", String.class),
- ArtifactTypeDefinition.JAR_TYPE
- )
- }
- }
- }
+compileTestKotlin {
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
dependencies {
@@ -52,8 +34,6 @@ dependencies {
api libs.javax.inject
api libs.javax.annotation.api
- // For @VisibleForTesting and ByteStreams
- implementation libs.guava
compileOnly libs.findbugs.jsr305
testCompileOnly libs.auto.service.annotations
diff --git a/utils/src/main/java/org/robolectric/util/Util.java b/utils/src/main/java/org/robolectric/util/Util.java
index b7292ad93..8f74dc28a 100644
--- a/utils/src/main/java/org/robolectric/util/Util.java
+++ b/utils/src/main/java/org/robolectric/util/Util.java
@@ -1,6 +1,5 @@
package org.robolectric.util;
-import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -37,9 +36,10 @@ public class Util {
version.substring(0, dotPos > -1 ? dotPos : dashPos > -1 ? dashPos : version.length()));
}
+ @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"}) // not relevant, always runs on JVM
public static void copy(InputStream in, OutputStream out) throws IOException {
try {
- ByteStreams.copy(in, out);
+ in.transferTo(out);
} finally {
in.close();
}
@@ -52,9 +52,10 @@ public class Util {
* @return The bytes read from the stream.
* @throws IOException Error reading from stream.
*/
+ @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"}) // not relevant, always runs on JVM
public static byte[] readBytes(InputStream is) throws IOException {
try {
- return ByteStreams.toByteArray(is);
+ return is.readAllBytes();
} finally {
is.close();
}