aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-14 16:28:52 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-14 16:28:52 +0000
commit58c0eef5f638ebe428f27ed9758c0e3f836ae5bf (patch)
tree6a83458e071f5958644cc26a398f5b1bddc5550c
parent4642d42e0d474f809fce807ac7a8b1be8e60c201 (diff)
parentac76e6685f68646cb4ecac667314159b4bccf8cd (diff)
downloadrobolectric-aml_tz4_332714010.tar.gz
Snap for 11219529 from ac76e6685f68646cb4ecac667314159b4bccf8cd to mainline-tzdata4-releaseaml_tz4_332714010
Change-Id: Id8a076dd1d4585e5ac562ffd246caab31a1f6802
-rw-r--r--.github/workflows/gradle_tasks_validation.yml23
-rw-r--r--.github/workflows/tests.yml24
-rw-r--r--Android.bp13
-rw-r--r--README.md17
-rw-r--r--annotations/build.gradle2
-rw-r--r--build.gradle19
-rw-r--r--buildSrc/build.gradle8
-rw-r--r--buildSrc/settings.gradle7
-rw-r--r--buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy1
-rw-r--r--buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy4
-rw-r--r--buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy4
-rw-r--r--dependencies.gradle47
-rw-r--r--errorprone/build.gradle22
-rw-r--r--gradle.properties2
-rw-r--r--gradle/libs.versions.toml237
-rw-r--r--integration_tests/agp/build.gradle9
-rw-r--r--integration_tests/agp/testsupport/build.gradle1
-rw-r--r--integration_tests/androidx/build.gradle23
-rw-r--r--integration_tests/androidx_test/build.gradle49
-rw-r--r--integration_tests/compat-target28/build.gradle7
-rw-r--r--integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt7
-rw-r--r--integration_tests/ctesque/build.gradle37
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java8
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java18
-rw-r--r--integration_tests/dependency-on-stubs/build.gradle8
-rw-r--r--integration_tests/jacoco-offline/build.gradle4
-rw-r--r--integration_tests/kotlin/build.gradle6
-rw-r--r--integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt28
-rw-r--r--integration_tests/libphonenumber/build.gradle8
-rw-r--r--integration_tests/memoryleaks/build.gradle7
-rw-r--r--integration_tests/mockito-experimental/build.gradle6
-rw-r--r--integration_tests/mockito-kotlin/build.gradle8
-rw-r--r--integration_tests/mockito/build.gradle8
-rw-r--r--integration_tests/mockk/build.gradle6
-rw-r--r--integration_tests/nativegraphics/build.gradle11
-rw-r--r--integration_tests/play_services/build.gradle6
-rw-r--r--integration_tests/powermock/build.gradle11
-rw-r--r--integration_tests/security-providers/build.gradle10
-rw-r--r--integration_tests/sparsearray/build.gradle7
-rw-r--r--junit/build.gradle4
-rw-r--r--nativeruntime/build.gradle16
-rw-r--r--nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java6
-rw-r--r--pluginapi/build.gradle10
-rw-r--r--plugins/maven-dependency-resolver/build.gradle19
-rw-r--r--plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java51
-rw-r--r--[-rwxr-xr-x]plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java0
-rw-r--r--preinstrumented/build.gradle15
-rw-r--r--preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java14
-rw-r--r--processor/build.gradle22
-rw-r--r--processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java5
-rw-r--r--processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java18
-rw-r--r--processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java26
-rw-r--r--processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java15
-rw-r--r--resources/build.gradle12
-rw-r--r--resources/src/main/java/org/robolectric/manifest/AndroidManifest.java8
-rw-r--r--resources/src/main/java/org/robolectric/res/android/LoadedArsc.java73
-rw-r--r--robolectric/Android.bp1
-rw-r--r--robolectric/build.gradle31
-rw-r--r--robolectric/src/main/java/org/robolectric/Robolectric.java5
-rw-r--r--[-rwxr-xr-x]robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java35
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java2
-rw-r--r--[-rwxr-xr-x]robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java0
-rw-r--r--[-rwxr-xr-x]robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java0
-rw-r--r--robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java39
-rw-r--r--robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java22
-rw-r--r--robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java32
-rw-r--r--robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java8
-rw-r--r--robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java4
-rw-r--r--robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java15
-rw-r--r--robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java4
-rw-r--r--robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java4
-rw-r--r--robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java2
-rw-r--r--robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java4
-rw-r--r--robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java14
-rw-r--r--[-rwxr-xr-x]robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java0
-rw-r--r--[-rwxr-xr-x]robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java0
-rw-r--r--robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java8
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java114
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java90
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java81
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java76
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java95
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java84
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java50
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java21
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java358
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java365
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java109
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java39
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java17
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java24
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java14
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java9
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java111
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java7
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java24
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java82
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java69
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java63
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java21
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java95
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java322
-rw-r--r--robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java4
-rw-r--r--robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml16
-rw-r--r--sandbox/build.gradle24
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java23
-rw-r--r--settings.gradle1
-rw-r--r--shadowapi/build.gradle10
-rw-r--r--shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java56
-rw-r--r--shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java38
-rw-r--r--shadows/framework/build.gradle26
-rw-r--r--[-rwxr-xr-x]shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java0
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java138
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java139
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java170
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java135
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java143
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java93
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java96
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java140
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java22
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java14
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java11
-rw-r--r--[-rwxr-xr-x]shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java0
-rw-r--r--[-rwxr-xr-x]shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java0
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java72
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java57
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java192
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java328
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java89
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java36
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java14
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java109
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java22
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java51
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java9
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java17
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java53
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java19
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java9
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java99
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java110
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java4
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java33
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java11
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java12
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java18
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java64
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java20
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java28
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java24
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java8
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java67
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java182
-rw-r--r--shadows/httpclient/build.gradle14
-rw-r--r--shadows/playservices/build.gradle19
-rw-r--r--shadows/versioning/Android.bp67
-rw-r--r--shadows/versioning/build.gradle21
-rw-r--r--shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java23
-rw-r--r--shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java779
-rw-r--r--shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java71
-rw-r--r--shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java205
-rw-r--r--shadows/versioning/src/test/resources/AndroidManifest.xml7
-rw-r--r--testapp/build.gradle1
-rw-r--r--utils/build.gradle28
-rw-r--r--utils/reflector/build.gradle10
-rw-r--r--utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java11
-rw-r--r--utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java14
-rw-r--r--utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java199
-rw-r--r--utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java41
-rw-r--r--[-rwxr-xr-x]utils/src/main/java/org/robolectric/util/Util.java0
184 files changed, 7471 insertions, 771 deletions
diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml
index 96b4b3307..9762b9e51 100644
--- a/.github/workflows/gradle_tasks_validation.yml
+++ b/.github/workflows/gradle_tasks_validation.yml
@@ -15,6 +15,23 @@ permissions:
contents: read
jobs:
+ run_checkForApiChanges:
+ runs-on: ubuntu-20.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 checkForApiChanges
+ run: ./gradlew checkForApiChanges
+
run_aggregateDocs:
runs-on: ubuntu-20.04
@@ -30,7 +47,7 @@ jobs:
- uses: gradle/gradle-build-action@v2
- name: Run aggregateDocs
- run: SKIP_NATIVERUNTIME_BUILD=true ./gradlew clean aggregateDocs # building the native runtime is not required for checking javadoc
+ run: ./gradlew clean aggregateDocs
run_instrumentAll:
runs-on: ubuntu-20.04
@@ -50,7 +67,7 @@ jobs:
- uses: gradle/gradle-build-action@v2
- name: Run :preinstrumented:instrumentAll
- run: SKIP_NATIVERUNTIME_BUILD=true ./gradlew :preinstrumented:instrumentAll
+ run: ./gradlew :preinstrumented:instrumentAll
- name: Run :preinstrumented:instrumentAll with SDK 33
- run: SKIP_NATIVERUNTIME_BUILD=true PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll
+ run: PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d83e804a7..b47f37522 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -101,7 +101,7 @@ jobs:
run: |
TARGET="google_apis"
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
-
+
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
@@ -147,3 +147,25 @@ jobs:
path: |
**/build/reports/*
**/build/outputs/*/connected/*
+
+ publish-to-snapshots:
+ runs-on: ubuntu-20.04
+ env:
+ SONATYPE_LOGIN: ${{ secrets.SONATYPE_LOGIN }}
+ SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+ needs: unit-tests
+ if: github.ref == 'refs/heads/master'
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: 11
+
+ - uses: gradle/gradle-build-action@v2
+
+ - name: Publish
+ run: |
+ ./gradlew publish --stacktrace --no-watch-fs
diff --git a/Android.bp b/Android.bp
index 319b5e3df..2487cafaf 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13,7 +13,10 @@
// limitations under the License.
package {
- default_visibility: [":__subpackages__"],
+ default_visibility: [
+ "//external/robolectric:__subpackages__",
+ "//test/robolectric-extensions:__subpackages__",
+ ],
default_applicable_licenses: ["external_robolectric_license"],
}
@@ -33,7 +36,10 @@ package {
// See: http://go/android-license-faq
license {
name: "external_robolectric_license",
- visibility: [":__subpackages__"],
+ visibility: [
+ ":__subpackages__",
+ "//test/robolectric-extensions:__subpackages__",
+ ],
license_kinds: [
"SPDX-license-identifier-Apache-2.0",
"SPDX-license-identifier-MIT",
@@ -111,6 +117,7 @@ java_library_host {
visibility: [
":__subpackages__",
"//prebuilts/misc/common/robolectric",
+ "//test/robolectric-extensions:__subpackages__",
],
}
@@ -133,10 +140,12 @@ 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",
+ "Robolectric_shadows_versioning_upstream",
"Robolectric_robolectric_upstream",
"Robolectric_annotations_upstream",
"Robolectric_resources_upstream",
diff --git a/README.md b/README.md
index 6b91ba31d..b183647e0 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ If you'd like to start a new project with Robolectric tests you can refer to `de
```groovy
testImplementation "junit:junit:4.13.2"
-testImplementation "org.robolectric:robolectric:4.10-alpha-1"
+testImplementation "org.robolectric:robolectric:4.10.3"
```
## Building And Contributing
@@ -79,18 +79,3 @@ 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.10-SNAPSHOT"
-}
-```
diff --git a/annotations/build.gradle b/annotations/build.gradle
index 65b4f06bb..d8bd113c5 100644
--- a/annotations/build.gradle
+++ b/annotations/build.gradle
@@ -5,6 +5,6 @@ apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
dependencies {
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+ compileOnly libs.findbugs.jsr305
compileOnly AndroidSdk.MAX_SDK.coordinates
}
diff --git a/build.gradle b/build.gradle
index 1eb662b1f..f3ab387aa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,18 +7,15 @@ buildscript {
google()
mavenCentral()
gradlePluginPortal()
- maven {
- url "https://plugins.gradle.org/m2/"
- }
}
dependencies {
gradle
- classpath 'com.android.tools.build:gradle:7.4.2'
- classpath 'net.ltgt.gradle:gradle-errorprone-plugin:3.0.1'
- classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.1'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
- classpath "com.diffplug.spotless:spotless-plugin-gradle:6.17.0"
+ classpath libs.android.gradle
+ classpath libs.error.prone.gradle
+ classpath libs.aggregate.javadocs.gradle
+ classpath libs.kotlin.gradle
+ classpath libs.spotless.gradle
}
}
@@ -120,7 +117,7 @@ task aggregateDocs {
dependsOn ':aggregateJsondocs'
}
-task prefetchSdks() {
+tasks.register('prefetchSdks') {
AndroidSdk.ALL_SDKS.each { androidSdk ->
doLast {
println("Prefetching ${androidSdk.coordinates}...")
@@ -139,7 +136,7 @@ task prefetchSdks() {
}
}
-task prefetchInstrumentedSdks() {
+tasks.register('prefetchInstrumentedSdks') {
AndroidSdk.ALL_SDKS.each { androidSdk ->
doLast {
println("Prefetching ${androidSdk.preinstrumentedCoordinates}...")
@@ -169,7 +166,7 @@ private void shellExec(String mvnCommand) {
if (process.exitValue() != 0) System.exit(1)
}
-task prefetchDependencies() {
+tasks.register('prefetchDependencies') {
doLast {
allprojects.each { p ->
p.configurations.each { config ->
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 612366308..67279efee 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -11,8 +11,8 @@ dependencies {
implementation gradleApi()
implementation localGroovy()
- api "com.google.guava:guava:31.1-jre"
- api 'org.jetbrains:annotations:24.0.1'
- implementation "org.ow2.asm:asm-tree:9.4"
- implementation 'com.android.tools.build:gradle:7.4.2'
+ api libs.guava
+ api libs.jetbrains.annotations
+ implementation libs.asm.tree
+ implementation libs.android.gradle
}
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
new file mode 100644
index 000000000..6f31e6ef7
--- /dev/null
+++ b/buildSrc/settings.gradle
@@ -0,0 +1,7 @@
+dependencyResolutionManagement {
+ versionCatalogs {
+ libs {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy b/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
index c0671c598..2f1476cf3 100644
--- a/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
+++ b/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
@@ -28,7 +28,6 @@ class CheckApiChangesPlugin implements Plugin<Project> {
project.checkApiChanges.from.each {
project.dependencies.checkApiChangesFrom(it) {
transitive = false
- force = true
}
}
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy
index 324d04d44..09e3b8d4a 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy
@@ -94,8 +94,8 @@ class DeployedRoboJavaModulePlugin implements Plugin<Project> {
url = project.version.endsWith("-SNAPSHOT") ? snapshotsRepoUrl : releasesRepoUrl
credentials {
- username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin']
- password = System.properties["sonatype-password"] ?: System.env['sonatypePassword']
+ username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN']
+ password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD']
}
}
}
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
index 6c0e05894..deb97c994 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
@@ -13,8 +13,8 @@ class RoboJavaModulePlugin implements Plugin<Project> {
if (!skipErrorprone) {
apply plugin: "net.ltgt.errorprone"
project.dependencies {
- errorprone("com.google.errorprone:error_prone_core:$errorproneVersion")
- errorproneJavac("com.google.errorprone:javac:$errorproneJavacVersion")
+ errorprone(libs.error.prone.core)
+ errorproneJavac(libs.error.prone.javac)
}
}
diff --git a/dependencies.gradle b/dependencies.gradle
index b890dc16d..204e3d354 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,40 +1,11 @@
ext {
- apiCompatVersion='4.9.2'
-
- errorproneVersion='2.18.0'
- errorproneJavacVersion='9+181-r4173-1'
-
- // AndroidX test versions
- axtMonitorVersion='1.6.1'
- axtRunnerVersion='1.5.2'
- axtRulesVersion='1.5.0'
- axtCoreVersion='1.5.0'
- axtTruthVersion='1.5.0'
- espressoVersion='3.5.1'
- axtJunitVersion='1.1.4'
- axtTestServicesVersion='1.4.2'
-
- // AndroidX versions
- coreVersion='1.9.0'
- appCompatVersion='1.6.1'
- constraintlayoutVersion='2.1.4'
- windowVersion='1.0.0'
- fragmentVersion='1.5.5'
-
- truthVersion='1.1.3'
-
- junitVersion='4.13.2'
-
- mockitoVersion='4.11.0'
-
- jacocoVersion='0.8.8'
-
- guavaJREVersion='31.1-jre'
-
- asmVersion='9.4'
-
- kotlinVersion='1.8.10'
- autoServiceVersion='1.0.1'
- multidexVersion='2.0.1'
- sqlite4javaVersion='1.0.392'
+ apiCompatVersion = libs.versions.robolectric.compat.get()
+
+ // https://github.com/gradle/gradle/issues/21267
+ axtCoreVersion = libs.versions.androidx.test.core.get()
+ axtJunitVersion = libs.versions.androidx.test.ext.junit.get()
+ axtMonitorVersion = libs.versions.androidx.test.monitor.get()
+ axtRunnerVersion = libs.versions.androidx.test.runner.get()
+ axtTruthVersion = libs.versions.androidx.test.ext.truth.get()
+ espressoVersion = libs.versions.androidx.test.espresso.get()
}
diff --git a/errorprone/build.gradle b/errorprone/build.gradle
index 1932066ae..5fc561605 100644
--- a/errorprone/build.gradle
+++ b/errorprone/build.gradle
@@ -20,14 +20,14 @@ dependencies {
implementation project(":shadowapi")
// Compile dependencies
- implementation "com.google.errorprone:error_prone_annotation:$errorproneVersion"
- implementation "com.google.errorprone:error_prone_refaster:$errorproneVersion"
- implementation "com.google.errorprone:error_prone_check_api:$errorproneVersion"
- compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
- compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
+ implementation libs.error.prone.annotations
+ implementation libs.error.prone.refaster
+ implementation libs.error.prone.check.api
+ compileOnly libs.auto.service.annotations
+ compileOnly(AndroidSdk.MAX_SDK.coordinates)
- annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
- annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
+ annotationProcessor libs.auto.service
+ annotationProcessor libs.error.prone.core
// in jdk 9, tools.jar disappears!
def toolsJar = Jvm.current().getToolsJar()
@@ -36,10 +36,10 @@ dependencies {
}
// Testing dependencies
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation("com.google.errorprone:error_prone_test_helpers:${errorproneVersion}") {
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation(libs.error.prone.test.helpers) {
exclude group: 'junit', module: 'junit' // because it depends on a snapshot!?
}
- testCompileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
+ testCompileOnly(AndroidSdk.MAX_SDK.coordinates)
}
diff --git a/gradle.properties b/gradle.properties
index dc9eb66c0..d97ed211a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
-thisVersion=4.10-SNAPSHOT
+thisVersion=4.11-SNAPSHOT
android.useAndroidX=true
kotlin.stdlib.default.dependency=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..7b43dfa1f
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,237 @@
+[versions]
+robolectric-compat = "4.10.2"
+robolectric-nativeruntime-dist-compat = "1.0.1"
+
+# https://developer.android.com/studio/releases
+android-gradle = "7.4.2"
+
+# https://github.com/google/conscrypt/tags
+conscrypt = "2.5.2"
+
+# https://github.com/bcgit/bc-java/tags
+bouncycastle = "1.73"
+
+# https://github.com/findbugsproject/findbugs/tags
+findbugs-jsr305 = "3.0.2"
+
+# https://github.com/hamcrest/JavaHamcrest/releases
+hamcrest = "2.0.0.0"
+
+# https://github.com/nebula-plugins/gradle-aggregate-javadocs-plugin/releases
+aggregate-javadocs-gradle = "3.0.1"
+
+# https://github.com/google/error-prone/releases
+error-prone = "2.19.1"
+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"
+
+# https://github.com/diffplug/spotless/blob/main/CHANGES.md
+spotless-gradle = "6.18.0"
+
+# https://hc.apache.org/news.html
+apache-http-core = "4.0.1"
+apache-http-client = "4.0.3"
+
+# https://asm.ow2.io/versions.html
+asm = "9.5"
+
+# https://github.com/google/auto/releases
+auto-common = "1.2.1"
+auto-service = "1.0.1"
+auto-value = "1.10.1"
+
+compile-testing = "0.21.0"
+
+# https://github.com/google/guava/releases
+guava-jre = "31.1-jre"
+
+# https://github.com/google/gson/releases
+gson = "2.10.1"
+
+# https://github.com/google/truth/releases
+truth = "1.1.3"
+
+# https://github.com/unicode-org/icu/releases
+icu4j = "73.1"
+
+jacoco = "0.8.10"
+
+# https://github.com/javaee/javax.annotation/tags
+javax-annotation-api = "1.3.2"
+javax-annotation-jsr250-api = "1.0"
+javax-inject = "1"
+
+# https://github.com/JetBrains/java-annotations/releases
+jetbrains-annotations = "24.0.1"
+
+# https://junit.org/junit4/
+junit4 = "4.13.2"
+
+# https://github.com/google/libphonenumber/releases
+libphonenumber = "8.13.11"
+
+# https://github.com/mockito/mockito/releases
+mockito = "4.11.0"
+
+# https://github.com/mockk/mockk/releases
+mockk = "1.13.5"
+
+# https://square.github.io/okhttp/changelogs/changelog/
+okhttp = "4.11.0"
+
+# https://github.com/powermock/powermock/releases
+powermock = "2.0.9"
+
+sqlite4java = "1.0.392"
+
+# https://developer.android.com/jetpack/androidx/versions
+androidx-annotation = "1.3.0"
+androidx-appcompat = "1.6.1"
+androidx-constraintlayout = "2.1.4"
+androidx-core = "1.10.1"
+androidx-fragment = "1.5.7"
+androidx-multidex = "2.0.1"
+androidx-window = "1.0.0"
+
+# https://github.com/android/android-test/tags
+androidx-test-annotation = "1.0.1"
+androidx-test-core = "1.5.0"
+androidx-test-espresso = "3.5.1"
+androidx-test-ext-junit = "1.1.5"
+androidx-test-ext-truth = "1.5.0"
+androidx-test-monitor="1.6.1"
+androidx-test-orchestrator="1.4.2"
+androidx-test-runner = "1.5.2"
+androidx-test-services = "1.4.2"
+
+# for shadows/playservices/build.gradle
+androidx-fragment-for-shadows = "1.2.0"
+play-services-base-for-shadows = "8.4.0"
+
+[libraries]
+android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
+kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless-gradle" }
+
+kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
+
+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" }
+auto-service = { module = "com.google.auto.service:auto-service", version.ref = "auto-service" }
+auto-value-annotations = { module = "com.google.auto.value:auto-value-annotations", version.ref = "auto-value" }
+auto-value = { module = "com.google.auto.value:auto-value", version.ref = "auto-value" }
+
+apache-http-core = { module = "org.apache.httpcomponents:httpcore", version.ref = "apache-http-core" }
+apache-http-client = { module = "org.apache.httpcomponents:httpclient", version.ref = "apache-http-client" }
+
+asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
+asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
+asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" }
+asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" }
+
+compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" }
+
+aggregate-javadocs-gradle = { module = "com.netflix.nebula:gradle-aggregate-javadocs-plugin", version.ref = "aggregate-javadocs-gradle" }
+
+error-prone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" }
+error-prone-annotations = { module = "com.google.errorprone:error_prone_annotation", version.ref = "error-prone" }
+error-prone-refaster= { module = "com.google.errorprone:error_prone_refaster", version.ref = "error-prone" }
+error-prone-check-api = { module = "com.google.errorprone:error_prone_check_api", version.ref = "error-prone" }
+error-prone-test-helpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "error-prone" }
+error-prone-javac = { module = "com.google.errorprone:javac", version.ref = "error-prone-javac" }
+
+error-prone-gradle = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "error-prone-gradle" }
+
+conscrypt-openjdk-uber = { module = "org.conscrypt:conscrypt-openjdk-uber", version.ref = "conscrypt" }
+bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
+findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs-jsr305" }
+
+guava = { module = "com.google.guava:guava", version.ref = "guava-jre" }
+guava-testlib = { module = "com.google.guava:guava-testlib", version.ref = "guava-jre" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+hamcrest-junit = { module = "org.hamcrest:hamcrest-junit", version.ref = "hamcrest" }
+
+icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" }
+
+jacoco-agent = { module = "org.jacoco:org.jacoco.agent", version.ref = "jacoco" }
+junit4 = { module = "junit:junit", version.ref = "junit4" }
+
+javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation-api" }
+javax-annotation-jsr250-api = { module = "javax.annotation:jsr250-api", version.ref = "javax-annotation-jsr250-api" }
+javax-inject = { module = "javax.inject:javax.inject", version.ref = "javax.inject" }
+
+jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
+
+libphonenumber = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "libphonenumber" }
+
+okhttp = { module = "com.squareup.okhttp3:okhttp" }
+okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" }
+
+powermock-module-junit4 = { module = "org.powermock:powermock-module-junit4", version.ref = "powermock" }
+powermock-module-junit4-rule = { module = "org.powermock:powermock-module-junit4-rule", version.ref = "powermock" }
+powermock-api-mockito2 = { module = "org.powermock:powermock-api-mockito2", version.ref = "powermock" }
+powermock-classloading-xstream = { module = "org.powermock:powermock-classloading-xstream", version.ref = "powermock" }
+
+robolectric-nativeruntime-dist-compat = { module = "org.robolectric:nativeruntime-dist-compat", version.ref = "robolectric-nativeruntime-dist-compat" }
+
+sqlite4java = { module = "com.almworks.sqlite4java:sqlite4java", version.ref = "sqlite4java" }
+sqlite4java-osx = { module = "com.almworks.sqlite4java:libsqlite4java-osx", version.ref = "sqlite4java" }
+sqlite4java-linux-amd64 = { module = "com.almworks.sqlite4java:libsqlite4java-linux-amd64", version.ref = "sqlite4java" }
+sqlite4java-win32-x64 = { module = "com.almworks.sqlite4java:sqlite4java-win32-x64", version.ref = "sqlite4java" }
+sqlite4java-linux-i386 = { module = "com.almworks.sqlite4java:libsqlite4java-linux-i386", version.ref = "sqlite4java" }
+sqlite4java-win32-x86 = { module = "com.almworks.sqlite4java:sqlite4java-win32-x86", version.ref = "sqlite4java" }
+
+truth = { module = "com.google.truth:truth", version.ref = "truth" }
+truth-java8-extension = { module = "com.google.truth.extensions:truth-java8-extension", version.ref = "truth" }
+
+mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
+mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" }
+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-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" }
+androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "androidx-fragment" }
+androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "androidx-multidex" }
+androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }
+
+androidx-test-annotation = { module = "androidx.test:annotation", version.ref = "androidx-test-annotation" }
+androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" }
+androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidx-test-monitor" }
+androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" }
+androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-core" }
+androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
+androidx-test-services = { module = "androidx.test.services:test-services", version.ref = "androidx-test-services" }
+androidx-test-services-storage = { module = "androidx.test.services:storage", version.ref = "androidx-test-services" }
+
+androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-accessibility = { module = "androidx.test.espresso:espresso-accessibility", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-remote = { module = "androidx.test.espresso:espresso-remote", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-web = { module = "androidx.test.espresso:espresso-web", version.ref = "androidx-test-espresso" }
+
+androidx-test-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-idling-concurrent = { module = "androidx.test.espresso.idling:idling-concurrent", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-idling-net = { module = "androidx.test.espresso.idling:idling-net", version.ref = "androidx-test-espresso" }
+
+androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" }
+androidx-test-ext-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-ext-truth" }
+
+androidx-fragment-for-shadows = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment-for-shadows" }
+play-services-base-for-shadows = { module = "com.google.android.gms:play-services-base", version.ref = "play-services-base-for-shadows" }
+play-services-basement-for-shadows = { module = "com.google.android.gms:play-services-basement", version.ref = "play-services-base-for-shadows" }
+
+[bundles]
+play-services-base-for-shadows = [ "androidx-fragment-for-shadows", "play-services-base-for-shadows", "play-services-basement-for-shadows" ]
+powermock = [ "powermock-module-junit4", "powermock-module-junit4-rule", "powermock-api-mockito2", "powermock-classloading-xstream" ]
+sqlite4java-native = [ "sqlite4java-osx", "sqlite4java-linux-amd64", "sqlite4java-win32-x64", "sqlite4java-linux-i386", "sqlite4java-win32-x86" ]
+
+[plugins]
diff --git a/integration_tests/agp/build.gradle b/integration_tests/agp/build.gradle
index a079d1ecf..55d90516f 100644
--- a/integration_tests/agp/build.gradle
+++ b/integration_tests/agp/build.gradle
@@ -5,6 +5,7 @@ apply plugin: AndroidProjectConfigPlugin
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.agp'
defaultConfig {
minSdk 16
@@ -25,8 +26,8 @@ dependencies {
testImplementation project(":robolectric")
testImplementation project(":integration_tests:agp:testsupport")
- testImplementation "junit:junit:${junitVersion}"
- testImplementation("androidx.test:core:$axtCoreVersion")
- testImplementation("androidx.test:runner:$axtRunnerVersion")
- testImplementation("androidx.test.ext:junit:$axtJunitVersion")
+ testImplementation libs.junit4
+ testImplementation libs.androidx.test.core
+ testImplementation libs.androidx.test.runner
+ testImplementation libs.androidx.test.ext.junit
}
diff --git a/integration_tests/agp/testsupport/build.gradle b/integration_tests/agp/testsupport/build.gradle
index dcec3d41d..e87274f2b 100644
--- a/integration_tests/agp/testsupport/build.gradle
+++ b/integration_tests/agp/testsupport/build.gradle
@@ -2,6 +2,7 @@ apply plugin: 'com.android.library'
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.agp.testsupport'
defaultConfig {
minSdk 16
diff --git a/integration_tests/androidx/build.gradle b/integration_tests/androidx/build.gradle
index 96535e1dd..10cc8c650 100644
--- a/integration_tests/androidx/build.gradle
+++ b/integration_tests/androidx/build.gradle
@@ -5,6 +5,7 @@ apply plugin: AndroidProjectConfigPlugin
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.androidx'
defaultConfig {
minSdk 16
@@ -25,19 +26,19 @@ android {
}
dependencies {
- implementation("androidx.appcompat:appcompat:$appCompatVersion")
- implementation("androidx.window:window:$windowVersion")
+ implementation libs.androidx.appcompat
+ implementation libs.androidx.window
// Testing dependencies
testImplementation project(path: ':testapp')
testImplementation project(":robolectric")
- testImplementation "junit:junit:$junitVersion"
- testImplementation("androidx.test:core:$axtCoreVersion")
- testImplementation("androidx.core:core:$coreVersion")
- testImplementation("androidx.test:runner:$axtRunnerVersion")
- testImplementation("androidx.test:rules:$axtRulesVersion")
- testImplementation("androidx.test.espresso:espresso-intents:$espressoVersion")
- testImplementation("androidx.test.ext:truth:$axtTruthVersion")
- testImplementation("androidx.test.ext:junit:$axtJunitVersion")
- testImplementation("com.google.truth:truth:$truthVersion")
+ testImplementation libs.junit4
+ testImplementation libs.androidx.test.core
+ testImplementation libs.androidx.core
+ testImplementation libs.androidx.test.runner
+ testImplementation libs.androidx.test.rules
+ testImplementation libs.androidx.test.espresso.intents
+ testImplementation libs.androidx.test.ext.truth
+ testImplementation libs.androidx.test.ext.junit
+ testImplementation libs.truth
}
diff --git a/integration_tests/androidx_test/build.gradle b/integration_tests/androidx_test/build.gradle
index 7f6f621b6..d07ef2ed6 100644
--- a/integration_tests/androidx_test/build.gradle
+++ b/integration_tests/androidx_test/build.gradle
@@ -7,6 +7,7 @@ apply plugin: GradleManagedDevicePlugin
android {
compileSdk 33
+ namespace 'org.robolectric.integration.axt'
defaultConfig {
minSdk 16
@@ -41,33 +42,33 @@ android {
}
dependencies {
- implementation "androidx.appcompat:appcompat:$appCompatVersion"
- implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion"
- implementation "androidx.multidex:multidex:$multidexVersion"
+ implementation libs.androidx.appcompat
+ implementation libs.androidx.constraintlayout
+ implementation libs.androidx.multidex
// Testing dependencies
testImplementation project(":robolectric")
- testImplementation "androidx.test:runner:$axtRunnerVersion"
- testImplementation "junit:junit:$junitVersion"
- testImplementation "androidx.test:rules:$axtRulesVersion"
- testImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
- testImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
- testImplementation "androidx.test.ext:truth:$axtTruthVersion"
- testImplementation "androidx.test:core:$axtCoreVersion"
- testImplementation "androidx.fragment:fragment:$fragmentVersion"
- testImplementation "androidx.fragment:fragment-testing:$fragmentVersion"
- testImplementation "androidx.test.ext:junit:$axtJunitVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
+ testImplementation libs.androidx.test.runner
+ testImplementation libs.junit4
+ testImplementation libs.androidx.test.rules
+ testImplementation libs.androidx.test.espresso.intents
+ testImplementation libs.androidx.test.espresso.core
+ testImplementation libs.androidx.test.ext.truth
+ testImplementation libs.androidx.test.core
+ testImplementation libs.androidx.fragment
+ testImplementation libs.androidx.fragment.testing
+ testImplementation libs.androidx.test.ext.junit
+ testImplementation libs.truth
androidTestImplementation project(':annotations')
- androidTestImplementation "androidx.test:runner:$axtRunnerVersion"
- androidTestImplementation "junit:junit:$junitVersion"
- androidTestImplementation "androidx.test:rules:$axtRulesVersion"
- androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
- androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
- androidTestImplementation "androidx.test.ext:truth:$axtTruthVersion"
- androidTestImplementation "androidx.test:core:$axtCoreVersion"
- androidTestImplementation "androidx.test.ext:junit:$axtJunitVersion"
- androidTestImplementation "com.google.truth:truth:$truthVersion"
- androidTestUtil "androidx.test.services:test-services:$axtTestServicesVersion"
+ androidTestImplementation libs.androidx.test.runner
+ androidTestImplementation libs.junit4
+ androidTestImplementation libs.androidx.test.rules
+ androidTestImplementation libs.androidx.test.espresso.intents
+ androidTestImplementation libs.androidx.test.espresso.core
+ androidTestImplementation libs.androidx.test.ext.truth
+ androidTestImplementation libs.androidx.test.core
+ androidTestImplementation libs.androidx.test.ext.junit
+ androidTestImplementation libs.truth
+ androidTestUtil libs.androidx.test.services
}
diff --git a/integration_tests/compat-target28/build.gradle b/integration_tests/compat-target28/build.gradle
index 37a856856..1fc7485ae 100644
--- a/integration_tests/compat-target28/build.gradle
+++ b/integration_tests/compat-target28/build.gradle
@@ -14,6 +14,7 @@ spotless {
android {
compileSdk 28
+ namespace 'org.robolectric.integrationtests.compattarget28'
defaultConfig {
minSdk 16
@@ -30,10 +31,10 @@ android {
}
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+ implementation libs.kotlin.stdlib
testImplementation project(path: ':testapp')
testImplementation project(":robolectric")
- testImplementation "junit:junit:$junitVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
+ testImplementation libs.junit4
+ testImplementation libs.truth
}
diff --git a/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt b/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt
index ee56fc6d2..69bbf73e0 100644
--- a/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt
+++ b/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt
@@ -1,7 +1,9 @@
package org.robolectric.integration.compat.target28
import android.content.Context
+import android.content.Context.VIBRATOR_SERVICE
import android.os.Build
+import android.os.Vibrator
import android.speech.SpeechRecognizer
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -47,4 +49,9 @@ class NormalCompatibilityTest {
fun `Create speech recognizer succeed`() {
assertThat(SpeechRecognizer.createSpeechRecognizer(application)).isNotNull()
}
+
+ @Test
+ fun `Get default Vibrator succeed`() {
+ assertThat(application.getSystemService(VIBRATOR_SERVICE) as Vibrator).isNotNull()
+ }
}
diff --git a/integration_tests/ctesque/build.gradle b/integration_tests/ctesque/build.gradle
index 11f27e1d6..3b40c88de 100644
--- a/integration_tests/ctesque/build.gradle
+++ b/integration_tests/ctesque/build.gradle
@@ -7,6 +7,7 @@ apply plugin: GradleManagedDevicePlugin
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.ctesque'
defaultConfig {
minSdk 16
@@ -48,24 +49,26 @@ dependencies {
implementation project(':testapp')
testImplementation project(':robolectric')
- testImplementation "junit:junit:${junitVersion}"
- testImplementation("androidx.test:monitor:$axtMonitorVersion")
- testImplementation("androidx.test:runner:$axtRunnerVersion")
- testImplementation("androidx.test:rules:$axtRulesVersion")
- testImplementation("androidx.test.ext:junit:$axtJunitVersion")
- testImplementation("androidx.test.ext:truth:$axtTruthVersion")
- testImplementation("androidx.test:core:$axtCoreVersion")
- testImplementation("com.google.truth:truth:${truthVersion}")
- testImplementation("com.google.guava:guava:$guavaJREVersion")
+ testImplementation libs.junit4
+ testImplementation libs.androidx.test.monitor
+ testImplementation libs.androidx.test.runner
+ testImplementation libs.androidx.test.rules
+ testImplementation libs.androidx.test.ext.junit
+ testImplementation libs.androidx.test.ext.truth
+ testImplementation libs.androidx.test.core
+ testImplementation libs.androidx.test.espresso.core
+ testImplementation libs.truth
+ testImplementation libs.guava
// Testing dependencies
androidTestImplementation project(':shadowapi')
- androidTestImplementation("androidx.test:monitor:$axtMonitorVersion")
- androidTestImplementation("androidx.test:runner:$axtRunnerVersion")
- androidTestImplementation("androidx.test:rules:$axtRulesVersion")
- androidTestImplementation("androidx.test.ext:junit:$axtJunitVersion")
- androidTestImplementation("androidx.test.ext:truth:$axtTruthVersion")
- androidTestImplementation("com.google.truth:truth:${truthVersion}")
- androidTestImplementation("com.google.guava:guava:$guavaJREVersion")
- androidTestUtil "androidx.test.services:test-services:$axtTestServicesVersion"
+ androidTestImplementation libs.androidx.test.monitor
+ androidTestImplementation libs.androidx.test.runner
+ androidTestImplementation libs.androidx.test.rules
+ androidTestImplementation libs.androidx.test.ext.junit
+ androidTestImplementation libs.androidx.test.ext.truth
+ androidTestImplementation libs.androidx.test.espresso.core
+ androidTestImplementation libs.truth
+ androidTestImplementation libs.guava
+ androidTestUtil libs.androidx.test.services
}
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java b/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java
index c1922b60d..5e4de013c 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java
@@ -503,6 +503,14 @@ public class ResourcesTest {
assertThat(id).isEqualTo(0);
}
+ @Test
+ @SdkSuppress(minSdkVersion = LOLLIPOP)
+ @Config(minSdk = LOLLIPOP)
+ public void getIdentifier_material() {
+ int id = Resources.getSystem().getIdentifier("btn_check_material_anim", "drawable", "android");
+ assertThat(id).isGreaterThan(0);
+ }
+
/**
* Public framework symbols are defined here:
* https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
index fa11ce7e2..eea5deaee 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
@@ -12,6 +12,7 @@ import android.database.sqlite.SQLiteException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.Suppress;
import com.google.common.base.Ascii;
import com.google.common.base.Throwables;
import com.google.common.io.ByteStreams;
@@ -174,7 +175,8 @@ public class SQLiteDatabaseTest {
}
// TODO(hoisie): This test crashes in emulators, enable when it is fixed in Android.
- @SdkSuppress(minSdkVersion = 34)
+ // Use Suppress here to stop it from running on emulators, but not on Robolectric
+ @Suppress
@Test
public void cursorWindow_finalize_concurrentStressTest() throws Throwable {
final PrintStream originalErr = System.err;
@@ -223,4 +225,18 @@ public class SQLiteDatabaseTest {
c.close();
assertThat(sorted).containsExactly("aaa", "abc", "ABC", "bbb").inOrder();
}
+
+ @Test
+ @Config(minSdk = LOLLIPOP)
+ @SdkSuppress(minSdkVersion = LOLLIPOP)
+ public void regex_selection() {
+ ContentValues values = new ContentValues();
+ values.put("first_column", "test");
+ database.insert("table_name", null, values);
+ String select = "first_column regexp ?";
+ String[] selectArgs = {
+ "test",
+ };
+ assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1);
+ }
}
diff --git a/integration_tests/dependency-on-stubs/build.gradle b/integration_tests/dependency-on-stubs/build.gradle
index 6efe51373..683de182d 100644
--- a/integration_tests/dependency-on-stubs/build.gradle
+++ b/integration_tests/dependency-on-stubs/build.gradle
@@ -6,13 +6,13 @@ apply plugin: RoboJavaModulePlugin
dependencies {
api project(":robolectric")
- api "junit:junit:${junitVersion}"
+ api libs.junit4
testImplementation files("${System.getenv("ANDROID_HOME")}/platforms/android-29/android.jar")
testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
- testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0"
+ testImplementation libs.truth
+ testImplementation libs.mockito
+ testImplementation libs.hamcrest.junit
}
diff --git a/integration_tests/jacoco-offline/build.gradle b/integration_tests/jacoco-offline/build.gradle
index e5d3bb5eb..3db34c00a 100644
--- a/integration_tests/jacoco-offline/build.gradle
+++ b/integration_tests/jacoco-offline/build.gradle
@@ -3,6 +3,8 @@ import org.robolectric.gradle.RoboJavaModulePlugin
apply plugin: RoboJavaModulePlugin
apply plugin: "jacoco"
+def jacocoVersion = libs.versions.jacoco.get()
+
jacoco {
toolVersion = jacocoVersion
}
@@ -18,7 +20,7 @@ dependencies {
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation project(":robolectric")
- testImplementation "junit:junit:$junitVersion"
+ testImplementation libs.junit4
testImplementation "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime"
}
diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle
index 68c5c677f..fd52d973d 100644
--- a/integration_tests/kotlin/build.gradle
+++ b/integration_tests/kotlin/build.gradle
@@ -21,8 +21,8 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
- testImplementation "junit:junit:$junitVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
+ testImplementation libs.kotlin.stdlib
+ testImplementation libs.junit4
+ testImplementation libs.truth
testImplementation "androidx.test:core:$axtCoreVersion@aar"
}
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt
new file mode 100644
index 000000000..6d77aa91f
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt
@@ -0,0 +1,28 @@
+package org.robolectric.integrationtests.kotlin
+
+import android.net.Uri
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
+import org.robolectric.annotation.Config
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+class ParameterizedRobolectricTestRunnerTest(private var uri: Uri) {
+ @Test
+ @Config(manifest = Config.NONE)
+ fun parse() {
+ val currentUri = Uri.parse("http://host/")
+ assertThat(currentUri).isEqualTo(uri)
+ }
+
+ companion object {
+ @Parameters
+ @JvmStatic
+ fun getTestData(): Collection<*> {
+ val data = arrayOf<Any>(Uri.parse("http://host/"))
+ return listOf(data)
+ }
+ }
+}
diff --git a/integration_tests/libphonenumber/build.gradle b/integration_tests/libphonenumber/build.gradle
index 2c27a7968..61120f227 100644
--- a/integration_tests/libphonenumber/build.gradle
+++ b/integration_tests/libphonenumber/build.gradle
@@ -4,10 +4,10 @@ apply plugin: RoboJavaModulePlugin
dependencies {
api project(":robolectric")
- api "junit:junit:${junitVersion}"
+ api libs.junit4
compileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation 'com.googlecode.libphonenumber:libphonenumber:8.13.8'
-} \ No newline at end of file
+ testImplementation libs.truth
+ testImplementation libs.libphonenumber
+}
diff --git a/integration_tests/memoryleaks/build.gradle b/integration_tests/memoryleaks/build.gradle
index 2cc51247c..91c5eb01e 100644
--- a/integration_tests/memoryleaks/build.gradle
+++ b/integration_tests/memoryleaks/build.gradle
@@ -5,6 +5,7 @@ apply plugin: AndroidProjectConfigPlugin
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.memoryleaks'
defaultConfig {
minSdk 16
@@ -28,7 +29,7 @@ dependencies {
// Testing dependencies
testImplementation project(path: ':testapp')
testImplementation project(":robolectric")
- testImplementation "junit:junit:$junitVersion"
- testImplementation "com.google.guava:guava-testlib:$guavaJREVersion"
- testImplementation "androidx.fragment:fragment:$fragmentVersion"
+ testImplementation libs.junit4
+ testImplementation libs.guava.testlib
+ testImplementation libs.androidx.fragment
}
diff --git a/integration_tests/mockito-experimental/build.gradle b/integration_tests/mockito-experimental/build.gradle
index 4aafcbc08..f5172d6af 100644
--- a/integration_tests/mockito-experimental/build.gradle
+++ b/integration_tests/mockito-experimental/build.gradle
@@ -8,7 +8,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockito.inline
}
diff --git a/integration_tests/mockito-kotlin/build.gradle b/integration_tests/mockito-kotlin/build.gradle
index ae97f1b7a..776f33bd2 100644
--- a/integration_tests/mockito-kotlin/build.gradle
+++ b/integration_tests/mockito-kotlin/build.gradle
@@ -18,8 +18,8 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
- testImplementation "junit:junit:$junitVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
- testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
- testImplementation "org.mockito:mockito-core:$mockitoVersion"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.kotlin.stdlib
+ testImplementation libs.mockito
}
diff --git a/integration_tests/mockito/build.gradle b/integration_tests/mockito/build.gradle
index e199cd74d..31e6ce675 100644
--- a/integration_tests/mockito/build.gradle
+++ b/integration_tests/mockito/build.gradle
@@ -8,7 +8,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
-} \ No newline at end of file
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockito
+}
diff --git a/integration_tests/mockk/build.gradle b/integration_tests/mockk/build.gradle
index 78344a9aa..1d590714e 100644
--- a/integration_tests/mockk/build.gradle
+++ b/integration_tests/mockk/build.gradle
@@ -21,7 +21,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation 'io.mockk:mockk:1.13.4'
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockk
}
diff --git a/integration_tests/nativegraphics/build.gradle b/integration_tests/nativegraphics/build.gradle
index 10e8f6138..f88f53a3f 100644
--- a/integration_tests/nativegraphics/build.gradle
+++ b/integration_tests/nativegraphics/build.gradle
@@ -7,6 +7,7 @@ apply plugin: GradleManagedDevicePlugin
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.nativegraphics'
defaultConfig {
minSdk 26
@@ -32,9 +33,9 @@ dependencies {
testImplementation AndroidSdk.MAX_SDK.coordinates
testImplementation project(':robolectric')
- testImplementation "androidx.core:core:$coreVersion"
- testImplementation "androidx.test.ext:junit:$axtJunitVersion"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+ testImplementation libs.androidx.core
+ testImplementation libs.androidx.test.ext.junit
+ testImplementation libs.truth
+ testImplementation libs.junit4
+ testImplementation libs.mockito
}
diff --git a/integration_tests/play_services/build.gradle b/integration_tests/play_services/build.gradle
index f7499dd73..0e05dd6b3 100644
--- a/integration_tests/play_services/build.gradle
+++ b/integration_tests/play_services/build.gradle
@@ -9,7 +9,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "junit:junit:$junitVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
+ testImplementation libs.junit4
+ testImplementation libs.truth
testImplementation "com.google.android.gms:play-services-basement:18.0.1"
-} \ No newline at end of file
+}
diff --git a/integration_tests/powermock/build.gradle b/integration_tests/powermock/build.gradle
index be4180cf5..6d5cf689d 100644
--- a/integration_tests/powermock/build.gradle
+++ b/integration_tests/powermock/build.gradle
@@ -7,11 +7,8 @@ dependencies {
compileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
- testImplementation "org.powermock:powermock-module-junit4:2.0.9"
- testImplementation "org.powermock:powermock-module-junit4-rule:2.0.9"
- testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
- testImplementation "org.powermock:powermock-classloading-xstream:2.0.9"
-} \ No newline at end of file
+ testImplementation libs.bundles.powermock
+}
diff --git a/integration_tests/security-providers/build.gradle b/integration_tests/security-providers/build.gradle
index f96df56b9..8ac0264a8 100644
--- a/integration_tests/security-providers/build.gradle
+++ b/integration_tests/security-providers/build.gradle
@@ -4,12 +4,12 @@ apply plugin: RoboJavaModulePlugin
dependencies {
api project(":robolectric")
- api "junit:junit:${junitVersion}"
+ api libs.junit4
compileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.4.0"
- testImplementation "com.squareup.okhttp3:okhttp"
- testImplementation platform("com.squareup.okhttp3:okhttp-bom:4.10.0")
+ testImplementation libs.truth
+ testImplementation libs.conscrypt.openjdk.uber
+ testImplementation libs.okhttp
+ testImplementation platform(libs.okhttp.bom)
}
diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle
index 762717794..1e4ba1ddf 100644
--- a/integration_tests/sparsearray/build.gradle
+++ b/integration_tests/sparsearray/build.gradle
@@ -14,6 +14,7 @@ spotless {
android {
compileSdk 33
+ namespace 'org.robolectric.sparsearray'
defaultConfig {
minSdk 16
@@ -41,7 +42,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation project(":robolectric")
- testImplementation "junit:junit:$junitVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
- testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.kotlin.stdlib
}
diff --git a/junit/build.gradle b/junit/build.gradle
index 9a1197871..5d5855254 100644
--- a/junit/build.gradle
+++ b/junit/build.gradle
@@ -11,6 +11,6 @@ dependencies {
api project(":shadowapi")
api project(":utils:reflector")
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
- compileOnly "junit:junit:${junitVersion}"
+ compileOnly libs.findbugs.jsr305
+ compileOnly libs.junit4
}
diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle
index 1ef93175a..e784984ab 100644
--- a/nativeruntime/build.gradle
+++ b/nativeruntime/build.gradle
@@ -49,8 +49,8 @@ if (System.getenv('PUBLISH_NATIVERUNTIME_DIST_COMPAT') == "true") {
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
- username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin']
- password = System.properties["sonatype-password"] ?: System.env['sonatypePassword']
+ username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN']
+ password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD']
}
}
}
@@ -64,17 +64,17 @@ if (System.getenv('PUBLISH_NATIVERUNTIME_DIST_COMPAT') == "true") {
dependencies {
api project(":utils")
api project(":utils:reflector")
- api "com.google.guava:guava:$guavaJREVersion"
+ api libs.guava
- implementation "org.robolectric:nativeruntime-dist-compat:1.0.0"
+ implementation libs.robolectric.nativeruntime.dist.compat
- annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
- compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
+ annotationProcessor libs.auto.service
+ compileOnly libs.auto.service.annotations
compileOnly AndroidSdk.MAX_SDK.coordinates
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation project(":robolectric")
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
}
diff --git a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
index 03911593e..e5d395f2e 100644
--- a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
+++ b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
@@ -2,7 +2,7 @@ package org.robolectric.nativeruntime;
import static android.os.Build.VERSION_CODES.O;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
import android.database.CursorWindow;
import android.database.sqlite.SQLiteDatabase;
@@ -33,8 +33,8 @@ public final class DefaultNativeRuntimeLoaderTest {
@Test
public void extracts_fontsAndIcuData() {
- assumeTrue(hasResource("fonts"));
- assumeTrue(hasResource("icu/icudt68l.dat"));
+ assume().that(hasResource("fonts")).isTrue();
+ assume().that(hasResource("icu/icudt68l.dat")).isTrue();
DefaultNativeRuntimeLoader defaultNativeRuntimeLoader = new DefaultNativeRuntimeLoader();
defaultNativeRuntimeLoader.ensureLoaded();
// Check that extraction of some key files worked.
diff --git a/pluginapi/build.gradle b/pluginapi/build.gradle
index 375cd10e4..9d7885291 100644
--- a/pluginapi/build.gradle
+++ b/pluginapi/build.gradle
@@ -5,11 +5,11 @@ apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
dependencies {
- compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
+ compileOnly libs.findbugs.jsr305
api project(":annotations")
- api "com.google.guava:guava:$guavaJREVersion"
+ api libs.guava
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockito
}
diff --git a/plugins/maven-dependency-resolver/build.gradle b/plugins/maven-dependency-resolver/build.gradle
index de20b2b8f..2aa33d9f5 100644
--- a/plugins/maven-dependency-resolver/build.gradle
+++ b/plugins/maven-dependency-resolver/build.gradle
@@ -1,3 +1,4 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.robolectric.gradle.DeployedRoboJavaModulePlugin
import org.robolectric.gradle.RoboJavaModulePlugin
@@ -13,7 +14,7 @@ spotless {
}
}
-tasks.withType(GenerateModuleMetadata) {
+tasks.withType(GenerateModuleMetadata).configureEach {
// We don't want to release gradle module metadata now to avoid
// potential compatibility problems.
enabled = false
@@ -22,11 +23,11 @@ tasks.withType(GenerateModuleMetadata) {
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
+ // 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 = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
afterEvaluate {
@@ -48,10 +49,12 @@ afterEvaluate {
dependencies {
api project(":pluginapi")
api project(":utils")
- api "com.google.guava:guava:$guavaJREVersion"
+ api libs.auto.value.annotations
+ api libs.guava
+ annotationProcessor libs.auto.value
- testImplementation "junit:junit:$junitVersion"
- testImplementation "org.mockito:mockito-core:$mockitoVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
- testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+ testImplementation libs.junit4
+ testImplementation libs.mockito
+ testImplementation libs.truth
+ testImplementation libs.kotlin.stdlib
}
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
index 60f852dbc..adeda9e78 100644
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
@@ -2,6 +2,7 @@ package org.robolectric.internal.dependency;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
@@ -24,6 +25,7 @@ import java.net.URLConnection;
import java.util.Base64;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import javax.annotation.Nonnull;
import org.robolectric.util.Logger;
/**
@@ -82,15 +84,27 @@ public class MavenArtifactFetcher {
return Futures.immediateFuture(null);
}
createArtifactSubdirectory(artifact, localRepositoryDir);
- boolean pomValid =
+ ValidationResult pomResult =
validateStagedFiles(artifact.pomPath(), artifact.pomSha512Path());
- if (!pomValid) {
- throw new AssertionError("SHA512 mismatch for POM file fetched in " + artifact);
+ if (!pomResult.isSuccess()) {
+ throw new AssertionError(
+ "SHA-512 mismatch for POM file for "
+ + artifact
+ + ", expected SHA-512="
+ + pomResult.expectedHashCode()
+ + ", actual SHA-512="
+ + pomResult.calculatedHashCode());
}
- boolean jarValid =
+ ValidationResult jarResult =
validateStagedFiles(artifact.jarPath(), artifact.jarSha512Path());
- if (!jarValid) {
- throw new AssertionError("SHA512 mismatch for JAR file fetched in " + artifact);
+ if (!jarResult.isSuccess()) {
+ throw new AssertionError(
+ "SHA-512 mismatch for POM file for "
+ + artifact
+ + ", expected SHA-512="
+ + jarResult.expectedHashCode()
+ + ", actual SHA-512="
+ + jarResult.calculatedHashCode());
}
Logger.info(
String.format(
@@ -123,7 +137,8 @@ public class MavenArtifactFetcher {
new File(repositoryDir, artifact.pomSha512Path()).delete();
}
- private boolean validateStagedFiles(String filePath, String sha512Path) throws IOException {
+ private ValidationResult validateStagedFiles(String filePath, String sha512Path)
+ throws IOException {
File tempFile = new File(this.stagingRepositoryDir, filePath);
File sha512File = new File(this.stagingRepositoryDir, sha512Path);
@@ -131,7 +146,24 @@ public class MavenArtifactFetcher {
HashCode.fromString(new String(Files.asByteSource(sha512File).read(), UTF_8));
HashCode actual = Files.asByteSource(tempFile).hash(Hashing.sha512());
- return expected.equals(actual);
+ return ValidationResult.create(expected.equals(actual), expected.toString(), actual.toString());
+ }
+
+ @AutoValue
+ abstract static class ValidationResult {
+ abstract boolean isSuccess();
+
+ @Nonnull
+ abstract String expectedHashCode();
+
+ @Nonnull
+ abstract String calculatedHashCode();
+
+ static ValidationResult create(
+ boolean isSuccess, String expectedHashCode, String calculatedHashCode) {
+ return new AutoValue_MavenArtifactFetcher_ValidationResult(
+ isSuccess, expectedHashCode, calculatedHashCode);
+ }
}
private void createArtifactSubdirectory(MavenJarArtifact artifact, File repositoryDir)
@@ -218,6 +250,9 @@ public class MavenArtifactFetcher {
try (InputStream inputStream = connection.getInputStream();
FileOutputStream outputStream = new FileOutputStream(localFile)) {
ByteStreams.copy(inputStream, outputStream);
+ // Ensure all contents are written to disk.
+ outputStream.flush();
+ outputStream.getFD().sync();
}
return Futures.immediateFuture(null);
}
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..bb5604d80 100755..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
diff --git a/preinstrumented/build.gradle b/preinstrumented/build.gradle
index 95d533e4d..8ffb5bbf2 100644
--- a/preinstrumented/build.gradle
+++ b/preinstrumented/build.gradle
@@ -17,11 +17,12 @@ java {
}
dependencies {
- implementation "com.google.guava:guava:$guavaJREVersion"
+ implementation libs.guava
implementation project(":sandbox")
+ implementation project(":shadows:versioning")
}
-task instrumentAll {
+tasks.register('instrumentAll') {
dependsOn ':prefetchSdks'
dependsOn 'build'
@@ -42,11 +43,11 @@ task instrumentAll {
}
}
-task('sourcesJar', type: Jar) {
+tasks.register('sourcesJar', Jar) {
archiveClassifier = "sources"
}
-task('javadocJar', type: Jar) {
+tasks.register('javadocJar', Jar) {
archiveClassifier = "javadoc"
}
@@ -102,8 +103,8 @@ if (System.getenv('PUBLISH_PREINSTRUMENTED_JARS') == "true") {
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
- username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin']
- password = System.properties["sonatype-password"] ?: System.env['sonatypePassword']
+ username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN']
+ password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD']
}
}
}
@@ -132,4 +133,4 @@ clean.doFirst {
AndroidSdk.ALL_SDKS.each { androidSdk ->
delete "${buildDir}/${androidSdk.preinstrumentedJarFileName}"
}
-} \ No newline at end of file
+}
diff --git a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
index a9c5aceab..753c1670d 100644
--- a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
+++ b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
@@ -8,7 +8,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Locale;
-import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
@@ -21,6 +20,8 @@ import org.robolectric.internal.bytecode.ClassNodeProvider;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
import org.robolectric.internal.bytecode.Interceptors;
import org.robolectric.util.inject.Injector;
+import org.robolectric.versioning.AndroidVersionInitTools;
+import org.robolectric.versioning.AndroidVersions.AndroidRelease;
/** Runs Robolectric invokedynamic instrumentation on an android-all jar. */
public class JarInstrumentor {
@@ -146,14 +147,7 @@ public class JarInstrumentor {
}
private int getJarAndroidSDKVersion(JarFile jarFile) throws IOException {
- ZipEntry buildProp = jarFile.getEntry("build.prop");
- Properties buildProps = new Properties();
- buildProps.load(jarFile.getInputStream(buildProp));
- String codename = buildProps.getProperty("ro.build.version.codename");
- // Check for a prerelease SDK.
- if (!"REL".equals(codename)) {
- return 10000;
- }
- return Integer.parseInt(buildProps.getProperty("ro.build.version.sdk"));
+ AndroidRelease release = AndroidVersionInitTools.computeReleaseVersion(jarFile);
+ return release.getSdkInt();
}
}
diff --git a/processor/build.gradle b/processor/build.gradle
index ac14e4282..9185d088a 100644
--- a/processor/build.gradle
+++ b/processor/build.gradle
@@ -35,21 +35,21 @@ dependencies {
api project(":annotations")
api project(":shadowapi")
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
- api "org.ow2.asm:asm:${asmVersion}"
- api "org.ow2.asm:asm-commons:${asmVersion}"
- api "com.google.guava:guava:$guavaJREVersion"
- api "com.google.code.gson:gson:2.10.1"
- implementation 'com.google.auto:auto-common:1.1.2'
+ compileOnly libs.findbugs.jsr305
+ api libs.asm
+ api libs.asm.commons
+ api libs.guava
+ api libs.gson
+ implementation libs.auto.common
def toolsJar = Jvm.current().getToolsJar()
if (toolsJar != null) {
implementation files(toolsJar)
}
- testImplementation "javax.annotation:jsr250-api:1.0"
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
- testImplementation "com.google.testing.compile:compile-testing:0.21.0"
- testImplementation "com.google.truth:truth:${truthVersion}"
+ testImplementation libs.javax.annotation.jsr250.api
+ testImplementation libs.junit4
+ testImplementation libs.mockito
+ testImplementation libs.compile.testing
+ testImplementation libs.truth
}
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
index fd5e77dd2..d9905f6cf 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
@@ -1,5 +1,6 @@
package org.robolectric.annotation.processing;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.newTreeMap;
import static com.google.common.collect.Sets.newTreeSet;
@@ -127,6 +128,10 @@ public class RobolectricModel {
}
public void addResetter(TypeElement shadowTypeElement, ExecutableElement elem) {
+ checkState(
+ !resetterMap.containsKey(shadowTypeElement.getQualifiedName().toString()),
+ "Trying to register a duplicate resetter on %s",
+ shadowTypeElement.getQualifiedName());
registerType(shadowTypeElement);
resetterMap.put(shadowTypeElement.getQualifiedName().toString(),
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
index d409f8396..39df257f9 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
@@ -1,6 +1,9 @@
package org.robolectric.annotation.processing.validator;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
@@ -13,6 +16,9 @@ import org.robolectric.annotation.processing.RobolectricModel;
* Validator that checks usages of {@link org.robolectric.annotation.Resetter}.
*/
public class ResetterValidator extends FoundOnImplementsValidator {
+
+ private final Map<TypeElement, ExecutableElement> resetterMethodsByClass = new HashMap<>();
+
public ResetterValidator(RobolectricModel.Builder modelBuilder, ProcessingEnvironment env) {
super(modelBuilder, env, "org.robolectric.annotation.Resetter");
}
@@ -35,7 +41,19 @@ public class ResetterValidator extends FoundOnImplementsValidator {
error("@Resetter methods must not have parameters");
error = true;
}
+ if (resetterMethodsByClass.containsKey(parent)) {
+ error(
+ String.format(
+ Locale.US,
+ "Duplicate @Resetter methods found on %s: %s() and %s(). Only one @Resetter method"
+ + " is permitted on each shadow.",
+ parent.getQualifiedName(),
+ resetterMethodsByClass.get(parent).getSimpleName(),
+ elem.getSimpleName()));
+ error = true;
+ }
if (!error) {
+ resetterMethodsByClass.put(parent, elem);
modelBuilder.addResetter(parent, elem);
}
}
diff --git a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
index 68900409b..c7924e8b0 100644
--- a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
+++ b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
@@ -12,7 +12,8 @@ import org.junit.runners.JUnit4;
public class ResetterValidatorTest {
@Test
public void resetterWithoutImplements_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -22,7 +23,8 @@ public class ResetterValidatorTest {
@Test
public void nonStaticResetter_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -32,7 +34,8 @@ public class ResetterValidatorTest {
@Test
public void nonPublicResetter_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -42,7 +45,8 @@ public class ResetterValidatorTest {
@Test
public void resetterWithParameters_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -51,6 +55,20 @@ public class ResetterValidatorTest {
}
@Test
+ public void twoValidResetters_shouldNotCompile() {
+ final String testClass = "org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters";
+
+ assertAbout(singleClass())
+ .that(testClass)
+ .failsToCompile()
+ .withErrorContaining(
+ "Duplicate @Resetter methods found on"
+ + " org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters:"
+ + " resetter_method_one() and resetter_method_two().")
+ .onLine(13);
+ }
+
+ @Test
public void goodResetter_shouldCompile() {
final String testClass = "org.robolectric.annotation.processing.shadows.ShadowDummy";
assertAbout(singleClass())
diff --git a/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java
new file mode 100644
index 000000000..8183073b6
--- /dev/null
+++ b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java
@@ -0,0 +1,15 @@
+package org.robolectric.annotation.processing.shadows;
+
+import com.example.objects.Dummy;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+@Implements(Dummy.class)
+public class ShadowWithTwoResetters {
+
+ @Resetter
+ public static void resetter_method_one() {}
+
+ @Resetter
+ public static void resetter_method_two() {}
+}
diff --git a/resources/build.gradle b/resources/build.gradle
index 129dc20ad..077cdd3b3 100644
--- a/resources/build.gradle
+++ b/resources/build.gradle
@@ -9,11 +9,11 @@ dependencies {
api project(":annotations")
api project(":pluginapi")
- api "com.google.guava:guava:$guavaJREVersion"
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+ api libs.guava
+ compileOnly libs.findbugs.jsr305
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "com.google.testing.compile:compile-testing:0.21.0"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.compile.testing
+ testImplementation libs.mockito
}
diff --git a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java
index cfabcbb2b..7402b4982 100644
--- a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java
+++ b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java
@@ -53,6 +53,7 @@ public class AndroidManifest implements UsesSdk {
private String processName;
private String themeRef;
private String labelRef;
+ private String appComponentFactory; // Added from SDK 28
private Integer minSdkVersion;
private Integer targetSdkVersion;
private Integer maxSdkVersion;
@@ -191,6 +192,7 @@ public class AndroidManifest implements UsesSdk {
rClassName = packageName + ".R";
Node applicationNode = findApplicationNode(manifestDocument);
+ // Parse application node of the AndroidManifest.xml
if (applicationNode != null) {
NamedNodeMap attributes = applicationNode.getAttributes();
int attrCount = attributes.getLength();
@@ -204,6 +206,7 @@ public class AndroidManifest implements UsesSdk {
processName = applicationAttributes.get("android:process");
themeRef = applicationAttributes.get("android:theme");
labelRef = applicationAttributes.get("android:label");
+ appComponentFactory = applicationAttributes.get("android:appComponentFactory");
parseReceivers(applicationNode);
parseServices(applicationNode);
@@ -605,6 +608,11 @@ public class AndroidManifest implements UsesSdk {
return labelRef;
}
+ public String getAppComponentFactory() {
+ parseAndroidManifest();
+ return appComponentFactory;
+ }
+
/**
* Returns the minimum Android SDK version that this package expects to be runnable on, as
* specified in the manifest.
diff --git a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
index 0de3390e7..8b8165bd1 100644
--- a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
+++ b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
@@ -526,32 +526,79 @@ public class LoadedArsc {
return 0;
}
+ // for (const auto& type_entry : type_spec->type_entries) {
for (ResTable_type iter : type_spec.types) {
+ // const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type;
ResTable_type type = iter;
+ // const size_t entry_count = dtohl(type->entryCount);
int entry_count = type.entryCount;
+ // const auto entry_offsets = type.offset(dtohs(type->header.headerSize));
+ // for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) {
for (int entry_idx = 0; entry_idx < entry_count; entry_idx++) {
- // const uint32_t* entry_offsets = reinterpret_cast<const uint32_t*>(
- // reinterpret_cast<const uint8_t*>(type.type) + dtohs(type.type.header.headerSize));
- // ResTable_type entry_offsets = new ResTable_type(type.myBuf(),
- // type.myOffset() + type.header.headerSize);
- // int offset = dtohl(entry_offsets[entry_idx]);
- int offset = dtohl(type.entryOffset(entry_idx));
+ // uint32_t offset;
+ int offset;
+ // uint16_t res_idx;
+ short res_idx;
+ // if (type->flags & ResTable_type::FLAG_SPARSE) {
+ if (isTruthy(type.flags & ResTable_type.FLAG_SPARSE)) {
+ // auto sparse_entry = entry_offsets.convert<ResTable_sparseTypeEntry>() + entry_idx;
+
+ ResTable_sparseTypeEntry sparse_entry =
+ new ResTable_sparseTypeEntry(
+ type.myBuf(), type.myOffset() + entry_idx * ResTable_sparseTypeEntry.SIZEOF);
+ // if (!sparse_entry) {
+ // return base::unexpected(IOError::PAGES_MISSING);
+ // }
+ // TODO: implement above
+ // offset = dtohs(sparse_entry->offset) * 4u;
+ offset = dtohs(sparse_entry.offset) * 4;
+ // res_idx = dtohs(sparse_entry->idx);
+ res_idx = dtohs(sparse_entry.idx);
+ // } else if (type->flags & ResTable_type::FLAG_OFFSET16) {
+ } else if (isTruthy(type.flags & ResTable_type.FLAG_OFFSET16)) {
+ // auto entry = entry_offsets.convert<uint16_t>() + entry_idx;
+ int entry = type.entryOffset(entry_idx);
+ // if (!entry) {
+ // return base::unexpected(IOError::PAGES_MISSING);
+ // }
+ // offset = offset_from16(entry.value());
+ offset = entry;
+ // res_idx = entry_idx;
+ res_idx = (short) entry_idx;
+ } else {
+ // auto entry = entry_offsets.convert<uint32_t>() + entry_idx;
+ int entry = type.entryOffset(entry_idx);
+ // if (!entry) {
+ // return base::unexpected(IOError::PAGES_MISSING);
+ // }
+ // offset = dtohl(entry.value());
+ offset = dtohl(entry);
+ res_idx = (short) entry_idx;
+ }
+
if (offset != ResTable_type.NO_ENTRY) {
- // const ResTable_entry* entry =
- // reinterpret_cast<const ResTable_entry*>(reinterpret_cast<const uint8_t*>(type.type) +
- // dtohl(type.type.entriesStart) + offset);
+ // auto entry = type.offset(dtohl(type->entriesStart) +
+ // offset).convert<ResTable_entry>();
ResTable_entry entry =
- new ResTable_entry(type.myBuf(), type.myOffset() +
- dtohl(type.entriesStart) + offset);
+ new ResTable_entry(
+ type.myBuf(), type.myOffset() + dtohl(type.entriesStart) + offset);
+ // if (!entry) {
+ // return base::unexpected(IOError::PAGES_MISSING);
+ // }
+ // TODO implement above
+ // if (entry->key() == static_cast<uint32_t>(*key_idx)) {
if (dtohl(entry.getKeyIndex()) == key_idx) {
- // The package ID will be overridden by the caller (due to runtime assignment of package
+ // The package ID will be overridden by the caller (due to runtime assignment of
+ // package
// IDs for shared libraries).
- return make_resid((byte) 0x00, (byte) (type_idx + type_id_offset_ + 1), (short) entry_idx);
+ // return make_resid(0x00, *type_idx + type_id_offset_ + 1, res_idx);
+ return make_resid((byte) 0x00, (byte) (type_idx + type_id_offset_ + 1), res_idx);
}
}
}
}
+ // return base::unexpected(std::nullopt);
return 0;
}
diff --git a/robolectric/Android.bp b/robolectric/Android.bp
index 139062ca6..e690895a3 100644
--- a/robolectric/Android.bp
+++ b/robolectric/Android.bp
@@ -15,6 +15,7 @@ java_library_host {
name: "Robolectric_robolectric_upstream",
libs: [
"Robolectric_shadows_framework_upstream",
+ "Robolectric_shadows_versioning_upstream",
"Robolectric_annotations_upstream",
"Robolectric_shadowapi_upstream",
"Robolectric_resources_upstream",
diff --git a/robolectric/build.gradle b/robolectric/build.gradle
index faaa8b3a0..b826e9232 100644
--- a/robolectric/build.gradle
+++ b/robolectric/build.gradle
@@ -5,8 +5,8 @@ apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
dependencies {
- annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
- annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
+ annotationProcessor libs.auto.service
+ annotationProcessor libs.error.prone.core
api project(":annotations")
api project(":junit")
@@ -16,33 +16,34 @@ dependencies {
api project(":utils")
api project(":utils:reflector")
api project(":plugins:maven-dependency-resolver")
- api "javax.inject:javax.inject:1"
- compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
- api "javax.annotation:javax.annotation-api:1.3.2"
+ api libs.javax.inject
+ compileOnly libs.auto.service.annotations
+ api libs.javax.annotation.api
// We need to have shadows-framework.jar on the runtime system classpath so ServiceLoader
// can find its META-INF/services/org.robolectric.shadows.ShadowAdapter.
api project(":shadows:framework")
- implementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2'
- api "org.bouncycastle:bcprov-jdk18on:1.72"
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+ implementation libs.conscrypt.openjdk.uber
+ api libs.bcprov.jdk18on
+ compileOnly libs.findbugs.jsr305
compileOnly AndroidSdk.MAX_SDK.coordinates
- compileOnly "junit:junit:${junitVersion}"
+ compileOnly libs.junit4
+
api "androidx.test:monitor:$axtMonitorVersion@aar"
implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion@aar"
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
- testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.truth.java8.extension
+ testImplementation libs.mockito
+ testImplementation libs.hamcrest.junit
testImplementation "androidx.test:core:$axtCoreVersion@aar"
testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
testImplementation "androidx.test.ext:truth:$axtTruthVersion@aar"
testImplementation "androidx.test:runner:$axtRunnerVersion@aar"
- testImplementation("com.google.guava:guava:$guavaJREVersion")
+ testImplementation libs.guava
testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports
}
diff --git a/robolectric/src/main/java/org/robolectric/Robolectric.java b/robolectric/src/main/java/org/robolectric/Robolectric.java
index 47a52c54e..3ce637e76 100644
--- a/robolectric/src/main/java/org/robolectric/Robolectric.java
+++ b/robolectric/src/main/java/org/robolectric/Robolectric.java
@@ -1,5 +1,6 @@
package org.robolectric;
+import static com.google.common.base.Preconditions.checkState;
import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
import android.annotation.IdRes;
@@ -11,6 +12,7 @@ import android.app.backup.BackupAgent;
import android.content.ContentProvider;
import android.content.Intent;
import android.os.Bundle;
+import android.os.Looper;
import android.util.AttributeSet;
import android.view.View;
import javax.annotation.Nullable;
@@ -104,6 +106,9 @@ public class Robolectric {
*/
public static <T extends Activity> ActivityController<T> buildActivity(
Class<T> activityClass, Intent intent, @Nullable Bundle activityOptions) {
+ checkState(
+ Thread.currentThread() == Looper.getMainLooper().getThread(),
+ "buildActivity must be called on main Looper thread");
return ActivityController.of(
ReflectionHelpers.callConstructor(activityClass), intent, activityOptions);
}
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 a807d4e68..ed859dd23 100755..100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
@@ -361,7 +361,7 @@ public class AndroidTestEnvironment implements TestEnvironment {
// Preload fonts resources
FontsContract.setApplicationContextForResources(application);
}
- registerBroadcastReceivers(application, appManifest);
+ registerBroadcastReceivers(application, appManifest, loadedApk);
appResources.updateConfiguration(androidConfiguration, displayMetrics);
// propagate any updates to configuration via RuntimeEnvironment.setQualifiers
@@ -412,6 +412,11 @@ public class AndroidTestEnvironment implements TestEnvironment {
Path packageFile = appManifest.getApkFile();
parsedPackage = ShadowPackageParser.callParsePackage(packageFile);
}
+ if (parsedPackage != null
+ && parsedPackage.applicationInfo != null
+ && RuntimeEnvironment.getApiLevel() >= P) {
+ parsedPackage.applicationInfo.appComponentFactory = appManifest.getAppComponentFactory();
+ }
return parsedPackage;
}
@@ -691,15 +696,39 @@ public class AndroidTestEnvironment implements TestEnvironment {
.toString();
}
+ private static BroadcastReceiver newBroadcastReceiverFromP(
+ String receiverClassName, LoadedApk loadedApk) {
+ ClassLoader classLoader = Shadow.class.getClassLoader();
+ if (loadedApk == null || loadedApk.getAppFactory() == null) {
+ return (BroadcastReceiver) newInstanceOf(receiverClassName);
+ } else {
+ try {
+ return loadedApk.getAppFactory().instantiateReceiver(classLoader, receiverClassName, null);
+ } catch (ReflectiveOperationException e) {
+ Logger.warn(
+ "Failed to initialize receiver %s with AppComponentFactory %s: %s",
+ receiverClassName, loadedApk.getAppFactory(), e);
+ }
+ }
+ return null;
+ }
+
// TODO move/replace this with packageManager
@VisibleForTesting
- static void registerBroadcastReceivers(Application application, AndroidManifest androidManifest) {
+ static void registerBroadcastReceivers(
+ Application application, AndroidManifest androidManifest, LoadedApk loadedApk) {
for (BroadcastReceiverData receiver : androidManifest.getBroadcastReceivers()) {
IntentFilter filter = new IntentFilter();
for (String action : receiver.getActions()) {
filter.addAction(action);
}
- application.registerReceiver((BroadcastReceiver) newInstanceOf(receiver.getName()), filter);
+ String receiverClassName = receiver.getName();
+ if (loadedApk != null && RuntimeEnvironment.getApiLevel() >= P) {
+ application.registerReceiver(
+ newBroadcastReceiverFromP(receiverClassName, loadedApk), filter);
+ } else {
+ application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter);
+ }
}
}
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
index a6908af09..dd1bc5cca 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
@@ -157,7 +157,7 @@ public class LocalUiController implements UiController {
@Override
public void loopMainThreadUntilIdle() {
- if (!ShadowLooper.looperMode().equals(LooperMode.Mode.PAUSED)) {
+ if (ShadowLooper.looperMode().equals(LooperMode.Mode.LEGACY)) {
shadowMainLooper().idle();
} else {
ImmutableSet<IdlingResourceProxy> idlingResources = syncIdlingResources();
diff --git a/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java b/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java
index 69adb67e6..69adb67e6 100755..100644
--- a/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java
+++ b/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java
diff --git a/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java b/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java
index 837e966e5..837e966e5 100755..100644
--- a/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java
+++ b/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java
diff --git a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
index c8b2693ea..cf3af7d55 100644
--- a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
+++ b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
@@ -1,5 +1,23 @@
package org.robolectric.plugins;
+import com.google.auto.service.AutoService;
+import com.google.common.base.Preconditions;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import javax.annotation.Priority;
+import javax.inject.Inject;
+import org.robolectric.internal.dependency.DependencyJar;
+import org.robolectric.internal.dependency.DependencyResolver;
+import org.robolectric.pluginapi.Sdk;
+import org.robolectric.pluginapi.SdkProvider;
+import org.robolectric.shadows.ShadowBuild;
+import org.robolectric.util.Util;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
@@ -17,28 +35,9 @@ import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.S_V2;
import static android.os.Build.VERSION_CODES.TIRAMISU;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import android.os.Build;
-import com.google.auto.service.AutoService;
-import com.google.common.base.Preconditions;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Locale;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import javax.annotation.Priority;
-import javax.inject.Inject;
-import org.robolectric.internal.dependency.DependencyJar;
-import org.robolectric.internal.dependency.DependencyResolver;
-import org.robolectric.pluginapi.Sdk;
-import org.robolectric.pluginapi.SdkProvider;
-import org.robolectric.util.Util;
-
/**
* Robolectric's default {@link SdkProvider}.
*
@@ -85,8 +84,6 @@ public class DefaultSdkProvider implements SdkProvider {
knownSdks.put(S, new DefaultSdk(S, "12", "7732740", "REL", 9));
knownSdks.put(S_V2, new DefaultSdk(S_V2, "12.1", "8229987", "REL", 9));
knownSdks.put(TIRAMISU, new DefaultSdk(TIRAMISU, "13", "9030017", "Tiramisu", 9));
- // TODO(rexhoffman): should this have a dedicated mechanism? Should we maintain a known good version?
- knownSdks.put(CUR_DEVELOPMENT, new DefaultSdk(CUR_DEVELOPMENT, "current", "r0", "UpsideDownCake", 9));
}
@Override
diff --git a/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java b/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java
new file mode 100644
index 000000000..22df750a9
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java
@@ -0,0 +1,22 @@
+package org.robolectric;
+
+import android.app.AppComponentFactory;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver;
+
+public final class CustomAppComponentFactory extends AppComponentFactory {
+ @Override
+ public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ if (className != null) {
+ if (className.contains(CustomConstructorWithOneActionReceiver.class.getName())) {
+ return new CustomConstructorWithOneActionReceiver(100);
+ } else if (className.contains(CustomConstructorWithEmptyActionReceiver.class.getName())) {
+ return new CustomConstructorWithEmptyActionReceiver(100);
+ }
+ }
+ return super.instantiateReceiver(cl, className, intent);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java b/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java
new file mode 100644
index 000000000..1132f6eb7
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java
@@ -0,0 +1,32 @@
+package org.robolectric;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class CustomConstructorReceiverWrapper {
+ private static class CustomConstructorReceiver extends BroadcastReceiver {
+ private final int intValue;
+
+ public CustomConstructorReceiver(int intValue) {
+ // We don't use intValue actually, and we only want to use this class to test the
+ // initialization of BroadcastReceiver with a custom constructor.
+ this.intValue = intValue;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {}
+ }
+
+ public static class CustomConstructorWithOneActionReceiver extends CustomConstructorReceiver {
+ public CustomConstructorWithOneActionReceiver(int intValue) {
+ super(intValue);
+ }
+ }
+
+ public static class CustomConstructorWithEmptyActionReceiver extends CustomConstructorReceiver {
+ public CustomConstructorWithEmptyActionReceiver(int intValue) {
+ super(intValue);
+ }
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
index 0ba55d893..c0108de97 100644
--- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
+++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
@@ -50,6 +50,7 @@ import org.robolectric.annotation.Config;
import org.robolectric.annotation.Config.Implementation;
import org.robolectric.annotation.experimental.LazyApplication;
import org.robolectric.annotation.experimental.LazyApplication.LazyLoad;
+import org.robolectric.config.ConfigurationRegistry;
import org.robolectric.internal.AndroidSandbox.TestEnvironmentSpec;
import org.robolectric.internal.ResourcesMode;
import org.robolectric.internal.ShadowProvider;
@@ -163,10 +164,10 @@ public class RobolectricTestRunnerTest {
assertThat(events)
.containsExactly(
"started: first",
- "failure: ShadowActivityThread.reset: ActivityThread not set",
+ "failure: fake error in setUpApplicationState",
"finished: first",
"started: second",
- "failure: ShadowActivityThread.reset: ActivityThread not set",
+ "failure: fake error in setUpApplicationState",
"finished: second")
.inOrder();
}
@@ -319,6 +320,9 @@ public class RobolectricTestRunnerTest {
@Override
public void setUpApplicationState(Method method,
Configuration configuration, AndroidManifest appManifest) {
+ // ConfigurationRegistry.instance is required for resetters.
+ Config config = configuration.get(Config.class);
+ ConfigurationRegistry.instance = new ConfigurationRegistry(configuration.map());
throw new RuntimeException("fake error in setUpApplicationState");
}
}
diff --git a/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java
index 87ddb071b..0428c44e1 100644
--- a/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java
@@ -3,9 +3,9 @@ package org.robolectric.android;
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assume.assumeTrue;
import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
import android.animation.Animator;
@@ -31,7 +31,7 @@ public class DrawableResourceLoaderTest {
@Before
public void setup() throws Exception {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
resources = ApplicationProvider.getApplicationContext().getResources();
}
diff --git a/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java
index b895d6503..15ca52c68 100644
--- a/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java
@@ -2,7 +2,7 @@ package org.robolectric.android;
import static android.os.Build.VERSION_CODES.O;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
import android.content.res.Configuration;
@@ -32,7 +32,7 @@ public class ResourceLoaderTest {
@Before
public void setUp() {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
optsForO = RuntimeEnvironment.getApiLevel() >= O
? "nowidecg-lowdr-"
@@ -71,7 +71,11 @@ public class ResourceLoaderTest {
private void checkForPollutionHelper() {
assertThat(RuntimeEnvironment.getQualifiers())
- .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav-v" + Build.VERSION.RESOURCES_SDK_INT);
+ .isEqualTo(
+ "en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-"
+ + optsForO
+ + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav-v"
+ + Build.VERSION.RESOURCES_SDK_INT);
View view =
LayoutInflater.from(ApplicationProvider.getApplicationContext())
@@ -97,7 +101,10 @@ public class ResourceLoaderTest {
assertThat(resId).isNotNull();
assertThat(resourceProvider.getResName(resId)).isEqualTo(internalResource);
- Class<?> internalRIdClass = Robolectric.class.getClassLoader().loadClass("com.android.internal.R$" + internalResource.type);
+ Class<?> internalRIdClass =
+ Robolectric.class
+ .getClassLoader()
+ .loadClass("com.android.internal.R$" + internalResource.type);
int internalResourceId;
internalResourceId = (Integer) internalRIdClass.getDeclaredField(internalResource.name).get(null);
assertThat(resId).isEqualTo(internalResourceId);
diff --git a/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java b/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java
index 0ae467557..d4395c5a7 100644
--- a/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java
@@ -1,7 +1,7 @@
package org.robolectric.android;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
import android.os.Build;
@@ -17,7 +17,7 @@ import org.robolectric.res.ResName;
public class ResourceTableFactoryIntegrationTest {
@Test
public void shouldIncludeStyleableAttributesThatDoNotHaveACorrespondingEntryInAttrClass() throws Exception {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
// This covers a corner case in Framework resources where an attribute is mentioned in a styleable array, e.g: R.styleable.Toolbar_buttonGravity but there is no corresponding R.attr.buttonGravity
assertThat(RuntimeEnvironment.getSystemResourceTable()
.getResourceId(new ResName("android", "attr", "buttonGravity"))).isGreaterThan(0);
diff --git a/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java b/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java
index b57f606ee..0c8d977d0 100644
--- a/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java
@@ -2,11 +2,11 @@ package org.robolectric.android;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
import android.app.Application;
import android.content.res.XmlResourceParser;
@@ -276,7 +276,7 @@ public class XmlResourceParserImplTest {
@Test
public void testIsWhitespace() throws Exception {
- assumeTrue(RuntimeEnvironment.useLegacyResources());
+ assume().that(RuntimeEnvironment.useLegacyResources()).isTrue();
XmlResourceParserImpl parserImpl = (XmlResourceParserImpl) parser;
assertThat(parserImpl.isWhitespace("bar")).isFalse();
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 6edc42886..dc6ac3138 100644
--- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
@@ -88,7 +88,7 @@ public class AndroidTestEnvironmentCreateApplicationTest {
Application application = AndroidTestEnvironment.createApplication(appManifest, null,
new ApplicationInfo());
shadowOf(application).callAttach(RuntimeEnvironment.systemContext);
- registerBroadcastReceivers(application, appManifest);
+ registerBroadcastReceivers(application, appManifest, null);
List<ShadowApplication.Wrapper> receivers = shadowOf(application).getRegisteredReceivers();
assertThat(receivers).hasSize(1);
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 051188139..70dd385b4 100644
--- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
@@ -2,8 +2,8 @@ package org.robolectric.android.internal;
import static android.os.Build.VERSION_CODES.O;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
import static org.robolectric.annotation.ConscryptMode.Mode.OFF;
import static org.robolectric.annotation.ConscryptMode.Mode.ON;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
@@ -241,7 +241,7 @@ public class AndroidTestEnvironmentTest {
@Test
public void testResourceNotFound() {
// not relevant for binary resources mode
- assumeTrue(bootstrapWrapper.isLegacyResources());
+ assume().that(bootstrapWrapper.isLegacyResources()).isTrue();
try {
bootstrapWrapper.changeAppManifest(new ThrowingManifest(bootstrapWrapper.getAppManifest()));
diff --git a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
index e3163ccdc..af1d8ba86 100644
--- a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
+++ b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
@@ -66,10 +66,10 @@ public class AndroidInterceptorsIntegrationTest {
@Test
public void systemNanoTime_shouldReturnShadowClockTime() throws Throwable {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- SystemClock.setCurrentTimeMillis(200);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos());
+ } else {
+ SystemClock.setCurrentTimeMillis(200);
}
long nanoTime = invokeDynamic(System.class, "nanoTime", long.class);
@@ -78,10 +78,10 @@ public class AndroidInterceptorsIntegrationTest {
@Test
public void systemCurrentTimeMillis_shouldReturnShadowClockTime() throws Throwable {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- SystemClock.setCurrentTimeMillis(200);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos());
+ } else {
+ SystemClock.setCurrentTimeMillis(200);
}
long currentTimeMillis = invokeDynamic(System.class, "currentTimeMillis", long.class);
@@ -187,6 +187,6 @@ public class AndroidInterceptorsIntegrationTest {
callsite
.dynamicInvoker()
.invokeWithArguments(
- Arrays.stream(params).map(param -> param.val).collect(Collectors.toList()));
+ Arrays.stream(params).map(param -> param.value).collect(Collectors.toList()));
}
}
diff --git a/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java b/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java
index 414fdb09d..414fdb09d 100755..100644
--- a/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java
+++ b/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java
diff --git a/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java b/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java
index f4562f5d2..f4562f5d2 100755..100644
--- a/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java
+++ b/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java
diff --git a/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java
index 8b493ba71..dc2026601 100644
--- a/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java
@@ -2,7 +2,7 @@ package org.robolectric.res;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.robolectric.util.TestUtil.sdkResources;
import org.junit.Before;
@@ -18,14 +18,16 @@ public class StyleResourceLoaderTest {
@Before
public void setUp() throws Exception {
- assumeTrue(RuntimeEnvironment.useLegacyResources());
+ assume().that(RuntimeEnvironment.useLegacyResources()).isTrue();
ResourcePath resourcePath = sdkResources(JELLY_BEAN);
resourceTable = new ResourceTableFactory().newResourceTable("android", resourcePath);
}
@Test
public void testStyleDataIsLoadedCorrectly() throws Exception {
- TypedResource typedResource = resourceTable.getValue(new ResName("android", "style", "Theme_Holo"), new ResTable_config());
+ TypedResource typedResource =
+ resourceTable.getValue(
+ new ResName("android", "style", "Theme_Holo"), new ResTable_config());
StyleData styleData = (StyleData) typedResource.getData();
assertThat(styleData.getName()).isEqualTo("Theme_Holo");
assertThat(styleData.getParent()).isEqualTo("Theme");
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java
new file mode 100644
index 000000000..72887ddb5
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java
@@ -0,0 +1,114 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+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;
+
+/** Test for {@link CellIdentityLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellIdentityLteBuilderTest {
+
+ private static final String MCC = "310";
+ private static final String MNC = "260";
+ private static final int CI = 0;
+ private static final int PCI = 1;
+ private static final int TAC = 2;
+ private static final int EARFCN = 4;
+ private static final int[] BANDS = new int[] {2, 4};
+ private static final int BANDWIDTH = 5;
+ private static final String SHORT_OPERATOR_NAME = "short operator name";
+ private static final String LONG_OPERATOR_NAME = "long operator name";
+ private static final ImmutableList<String> ADDITIONAL_PLMNS = ImmutableList.of("310240");
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellIdentityLte cellIdentity = CellIdentityLteBuilder.newBuilder().build();
+
+ assertThat(cellIdentity.getCi()).isEqualTo(CellInfo.UNAVAILABLE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.M)
+ public void build_sdkJtoM() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.N, maxSdk = Build.VERSION_CODES.O_MR1)
+ public void build_sdkNtoO() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+ public void build_sdkPtoQ() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+ assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+ assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+ assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH);
+ assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+ assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.S)
+ public void build_fromSdkS() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+ assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+ assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+ assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH);
+ assertThat(cellIdentity.getBands()).isEqualTo(BANDS);
+ assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+ assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+ assertThat(cellIdentity.getAdditionalPlmns()).containsExactlyElementsIn(ADDITIONAL_PLMNS);
+ }
+
+ /**
+ * Assertions on {@link android.telephony.CellIdentityLte} values that are common across all
+ * tested SDKs.
+ */
+ private void assertCellIdentityFieldsForAllSdks(CellIdentityLte cellIdentity) {
+ assertThat(cellIdentity.getMcc()).isEqualTo(Integer.parseInt(MCC));
+ assertThat(cellIdentity.getMnc()).isEqualTo(Integer.parseInt(MNC));
+ assertThat(cellIdentity.getCi()).isEqualTo(CI);
+ assertThat(cellIdentity.getPci()).isEqualTo(PCI);
+ assertThat(cellIdentity.getTac()).isEqualTo(TAC);
+ }
+
+ private CellIdentityLte getCellIdentityLte() {
+ return CellIdentityLteBuilder.newBuilder()
+ .setMcc(MCC)
+ .setMnc(MNC)
+ .setCi(CI)
+ .setPci(PCI)
+ .setTac(TAC)
+ .setEarfcn(EARFCN)
+ .setBands(BANDS)
+ .setBandwidth(BANDWIDTH)
+ .setLongOperatorName(LONG_OPERATOR_NAME)
+ .setShortOperatorName(SHORT_OPERATOR_NAME)
+ .setAdditionalPlmns(ADDITIONAL_PLMNS)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java
new file mode 100644
index 000000000..8474b2136
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java
@@ -0,0 +1,90 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfo;
+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;
+
+/** Test for {@link CellIdentityNrBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.Q)
+public class CellIdentityNrBuilderTest {
+
+ private static final int PCI = 1;
+ private static final int TAC = 2;
+ private static final int NRARFCN = 4;
+ private static final int[] BANDS =
+ new int[] {
+ AccessNetworkConstants.NgranBands.BAND_1, AccessNetworkConstants.NgranBands.BAND_2
+ };
+ private static final String MCC = "310";
+ private static final String MNC = "260";
+ private static final int NCI = 0;
+ private static final String LONG_OPERATOR_NAME = "long operator name";
+ private static final String SHORT_OPERATOR_NAME = "short operator name";
+ private static final ImmutableList<String> ADDITIONAL_PLMNS = ImmutableList.of("310240");
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellIdentityNr cellIdentity = CellIdentityNrBuilder.newBuilder().build();
+
+ assertThat(cellIdentity.getPci()).isEqualTo(CellInfo.UNAVAILABLE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+ public void build_sdkQtoR() {
+ CellIdentityNr cellIdentity = getCellIdentityNr();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.S)
+ public void build_fromSdkS() {
+ CellIdentityNr cellIdentity = getCellIdentityNr();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ assertThat(cellIdentity.getBands()).isEqualTo(BANDS);
+ assertThat(cellIdentity.getAdditionalPlmns()).containsExactlyElementsIn(ADDITIONAL_PLMNS);
+ }
+
+ /**
+ * Assertions on {@link android.telephony.CellIdentityNr} values that are common across all tested
+ * SDKs.
+ */
+ private void assertCellIdentityFieldsForAllSdks(CellIdentityNr cellIdentity) {
+ assertThat(cellIdentity.getPci()).isEqualTo(PCI);
+ assertThat(cellIdentity.getTac()).isEqualTo(TAC);
+ assertThat(cellIdentity.getNrarfcn()).isEqualTo(NRARFCN);
+ assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+ assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+ assertThat(cellIdentity.getNci()).isEqualTo(NCI);
+ assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+ assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+ }
+
+ private CellIdentityNr getCellIdentityNr() {
+ return CellIdentityNrBuilder.newBuilder()
+ .setPci(PCI)
+ .setTac(TAC)
+ .setNrarfcn(NRARFCN)
+ .setBands(BANDS)
+ .setMcc(MCC)
+ .setMnc(MNC)
+ .setNci(NCI)
+ .setLongOperatorName(LONG_OPERATOR_NAME)
+ .setShortOperatorName(SHORT_OPERATOR_NAME)
+ .setAdditionalPlmns(ADDITIONAL_PLMNS)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java
new file mode 100644
index 000000000..f61abad90
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java
@@ -0,0 +1,81 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfoLte;
+import android.telephony.CellSignalStrengthLte;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.time.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellInfoLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellInfoLteBuilderTest {
+
+ private static final boolean REGISTERED = false;
+ private static final long TIMESTAMP_NANOS = 123L;
+ private static final long TIMESTAMP_MILLIS = Duration.ofNanos(TIMESTAMP_NANOS).toMillis();
+ private static final int CELL_CONNECTION_STATUS = 1;
+
+ private static final CellIdentityLte cellIdentity =
+ CellIdentityLteBuilder.newBuilder().setMcc("310").build();
+ private static final CellSignalStrengthLte cellSignalStrength =
+ CellSignalStrengthLteBuilder.newBuilder().setRsrp(-120).build();
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellInfoLte cellInfo = CellInfoLteBuilder.newBuilder().build();
+
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(0);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1)
+ public void build_sdkJtoN() {
+ CellInfoLte cellInfo = getCellInfoLte();
+
+ assertThat(cellInfo.isRegistered()).isFalse();
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+ assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.P, maxSdk = Build.VERSION_CODES.Q)
+ public void build_fromSdkPtoQ() {
+ CellInfoLte cellInfo = getCellInfoLte();
+
+ assertThat(cellInfo.isRegistered()).isFalse();
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+ assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+ assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.R, maxSdk = Config.NEWEST_SDK)
+ public void build_fromSdkR() {
+ CellInfoLte cellInfo = getCellInfoLte();
+
+ assertThat(cellInfo.isRegistered()).isFalse();
+ assertThat(cellInfo.getTimestampMillis()).isEqualTo(TIMESTAMP_MILLIS);
+ assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+ assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+ assertThat(cellInfo.getCellIdentity()).isEqualTo(cellIdentity);
+ }
+
+ private CellInfoLte getCellInfoLte() {
+ return CellInfoLteBuilder.newBuilder()
+ .setRegistered(REGISTERED)
+ .setTimeStampNanos(TIMESTAMP_NANOS)
+ .setCellConnectionStatus(CELL_CONNECTION_STATUS)
+ .setCellIdentity(cellIdentity)
+ .setCellSignalStrength(cellSignalStrength)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java
new file mode 100644
index 000000000..80e7dcc6e
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java
@@ -0,0 +1,76 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfoNr;
+import android.telephony.CellSignalStrengthNr;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.time.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellInfoNrBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.Q)
+public class CellInfoNrBuilderTest {
+
+ private static final boolean REGISTERED = false;
+ private static final long TIMESTAMP_NANOS = 123L;
+ private static final long TIMESTAMP_MILLIS = Duration.ofNanos(TIMESTAMP_NANOS).toMillis();
+ private static final int CELL_CONNECTION_STATUS = 1;
+
+ private static final CellIdentityNr cellIdentity =
+ CellIdentityNrBuilder.newBuilder().setMcc("310").build();
+ private static final CellSignalStrengthNr cellSignalStrength =
+ CellSignalStrengthNrBuilder.newBuilder().setCsiRsrp(-100).build();
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellInfoNr cellInfo = CellInfoNrBuilder.newBuilder().build();
+
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(0);
+ }
+
+ @Test
+ @Config(sdk = Build.VERSION_CODES.Q)
+ public void build_sdkQ() {
+ CellInfoNr cellInfo = getCellInfoNr();
+
+ assertCellInfoFieldsForAllSdks(cellInfo);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.R)
+ public void build_fromSdkR() {
+ CellInfoNr cellInfo = getCellInfoNr();
+
+ assertCellInfoFieldsForAllSdks(cellInfo);
+ assertThat(cellInfo.getTimestampMillis()).isEqualTo(TIMESTAMP_MILLIS);
+ }
+
+ /**
+ * Assertions on {@link android.telephony.CellInfo} values that are common across all tested SDKs.
+ */
+ private void assertCellInfoFieldsForAllSdks(CellInfoNr cellInfo) {
+ assertThat(cellInfo.isRegistered()).isFalse();
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+ assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+ assertThat(cellInfo.getCellIdentity()).isEqualTo(cellIdentity);
+ assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+ }
+
+ private CellInfoNr getCellInfoNr() {
+ return CellInfoNrBuilder.newBuilder()
+ .setRegistered(REGISTERED)
+ .setTimeStampNanos(TIMESTAMP_NANOS)
+ .setCellConnectionStatus(CELL_CONNECTION_STATUS)
+ .setCellIdentity(cellIdentity)
+ .setCellSignalStrength(cellSignalStrength)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java
new file mode 100644
index 000000000..cfd3bbeeb
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java
@@ -0,0 +1,95 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthLte;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellSignalStrengthLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellSignalStrengthLteBuilderTest {
+
+ // The platform enforces that some of these values are within a certain range - otherwise, it will
+ // default to {@link android.telephony.CellInfo.UNAVAILABLE}.
+ private static final int RSSI = -100;
+ private static final int RSRP = -120;
+ private static final int RSRQ = -10;
+ private static final int RSSNR = 30;
+ private static final int CQI_TABLE_INDEX = 4;
+ private static final int CQI = 5;
+ private static final int TIMING_ADVANCE = 6;
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellSignalStrengthLte cellSignalStrength = CellSignalStrengthLteBuilder.newBuilder().build();
+
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(CellInfo.UNAVAILABLE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1)
+ public void build_sdkJtoN() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.O, maxSdk = Build.VERSION_CODES.P)
+ public void build_sdkOToP() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+ assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+ assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+ assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+ public void build_sdkQtoR() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI);
+ assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+ assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+ assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+ assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.S)
+ public void build_fromSdkS() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI);
+ assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+ assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+ assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+ assertThat(cellSignalStrength.getCqiTableIndex()).isEqualTo(CQI_TABLE_INDEX);
+ assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ private CellSignalStrengthLte getCellSignalStrength() {
+ return CellSignalStrengthLteBuilder.newBuilder()
+ .setRssi(RSSI)
+ .setRsrp(RSRP)
+ .setRsrq(RSRQ)
+ .setRssnr(RSSNR)
+ .setCqi(CQI)
+ .setCqiTableIndex(CQI_TABLE_INDEX)
+ .setTimingAdvance(TIMING_ADVANCE)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java
new file mode 100644
index 000000000..1f18ee8dc
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java
@@ -0,0 +1,84 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthNr;
+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;
+
+/** Test for {@link CellSignalStrengthNrBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.Q)
+public class CellSignalStrengthNrBuilderTest {
+
+ // The platform enforces that some of these values are within a certain range - otherwise, it will
+ // default to {@link android.telephony.CellInfo.UNAVAILABLE}.
+ private static final int CSI_RSRP = -100;
+ private static final int CSI_RSRQ = -10;
+ private static final int CSI_SINR = -20;
+ private static final int CSI_CQI_TABLE_INDEX = 1;
+ private static final ImmutableList<Byte> CSI_CQI_REPORT = ImmutableList.of((byte) 7);
+ private static final int SS_RSRP = -140;
+ private static final int SS_RSRQ = -15;
+ private static final int SS_SINR = -20;
+ private static final int TIMING_ADVANCE = 10;
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellSignalStrengthNr cellSignalStrength = CellSignalStrengthNrBuilder.newBuilder().build();
+
+ assertThat(cellSignalStrength.getCsiRsrp()).isEqualTo(CellInfo.UNAVAILABLE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.S_V2)
+ public void build_sdkQtoS() {
+ CellSignalStrengthNr cellSignalStrength = getCellSignalStrength();
+
+ assertCellSignalStrengthFieldsForAllSdks(cellSignalStrength);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+ public void build_fromSdkT() {
+ CellSignalStrengthNr cellSignalStrength = getCellSignalStrength();
+
+ assertCellSignalStrengthFieldsForAllSdks(cellSignalStrength);
+ assertThat(cellSignalStrength.getCsiCqiTableIndex()).isEqualTo(CSI_CQI_TABLE_INDEX);
+ assertThat(cellSignalStrength.getCsiCqiReport()).containsExactly(7);
+ }
+
+ /**
+ * Assertions on {@link android.telephony.CellSignalStrengthNr} values that are common across all
+ * tested SDKs.
+ */
+ private void assertCellSignalStrengthFieldsForAllSdks(CellSignalStrengthNr cellSignalStrength) {
+ assertThat(cellSignalStrength.getCsiRsrp()).isEqualTo(CSI_RSRP);
+ assertThat(cellSignalStrength.getCsiRsrq()).isEqualTo(CSI_RSRQ);
+ assertThat(cellSignalStrength.getCsiSinr()).isEqualTo(CSI_SINR);
+ assertThat(cellSignalStrength.getSsRsrp()).isEqualTo(SS_RSRP);
+ assertThat(cellSignalStrength.getSsRsrq()).isEqualTo(SS_RSRQ);
+ assertThat(cellSignalStrength.getSsSinr()).isEqualTo(SS_SINR);
+ }
+
+ private CellSignalStrengthNr getCellSignalStrength() {
+ return CellSignalStrengthNrBuilder.newBuilder()
+ .setCsiRsrp(CSI_RSRP)
+ .setCsiRsrq(CSI_RSRQ)
+ .setCsiSinr(CSI_SINR)
+ .setCsiCqiTableIndex(CSI_CQI_TABLE_INDEX)
+ .setCsiCqiReport(CSI_CQI_REPORT)
+ .setSsRsrp(SS_RSRP)
+ .setSsRsrq(SS_RSRQ)
+ .setSsSinr(SS_SINR)
+ .setTimingAdvance(TIMING_ADVANCE)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
index 27e635c8f..06769c66e 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
@@ -14,6 +14,7 @@ import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList;
import android.media.MediaFormat;
+import android.util.Range;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -29,6 +30,10 @@ public class MediaCodecInfoBuilderTest {
private static final String VP9_DECODER_NAME = "test.decoder.vp9";
private static final String MULTIFORMAT_ENCODER_NAME = "test.encoder.multiformat";
+ private static final int WIDTH = 1920;
+ private static final int HEIGHT = 1080;
+ private static final Range<Integer> DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE = new Range<>(2, 896);
+
private static final MediaFormat AAC_MEDIA_FORMAT =
createMediaFormat(
MIMETYPE_AUDIO_AAC, new String[] {CodecCapabilities.FEATURE_DynamicTimestamp});
@@ -37,6 +42,9 @@ public class MediaCodecInfoBuilderTest {
MIMETYPE_AUDIO_OPUS, new String[] {CodecCapabilities.FEATURE_AdaptivePlayback});
private static final MediaFormat AVC_MEDIA_FORMAT =
createMediaFormat(MIMETYPE_VIDEO_AVC, new String[] {CodecCapabilities.FEATURE_IntraRefresh});
+ private static final MediaFormat AVC_MEDIA_FORMAT_WITH_RESOLUTION =
+ createMediaFormat(
+ MIMETYPE_VIDEO_AVC, WIDTH, HEIGHT, new String[] {CodecCapabilities.FEATURE_IntraRefresh});
private static final MediaFormat VP9_MEDIA_FORMAT =
createMediaFormat(
MIMETYPE_VIDEO_VP9,
@@ -123,6 +131,10 @@ public class MediaCodecInfoBuilderTest {
assertThat(codecCapabilities.getMimeType()).isEqualTo(MIMETYPE_VIDEO_AVC);
assertThat(codecCapabilities.getAudioCapabilities()).isNull();
assertThat(codecCapabilities.getVideoCapabilities()).isNotNull();
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths())
+ .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE);
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights())
+ .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE);
assertThat(codecCapabilities.getEncoderCapabilities()).isNotNull();
assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_IntraRefresh))
.isTrue();
@@ -136,6 +148,24 @@ public class MediaCodecInfoBuilderTest {
@Test
@Config(minSdk = Q)
+ public void canCreateVideoEncoderCapabilities_supportedFormatResolutionIsSet() {
+ CodecCapabilities codecCapabilities =
+ MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
+ .setMediaFormat(AVC_MEDIA_FORMAT_WITH_RESOLUTION)
+ .setIsEncoder(true)
+ .setProfileLevels(AVC_PROFILE_LEVELS)
+ .setColorFormats(AVC_COLOR_FORMATS)
+ .build();
+
+ assertThat(codecCapabilities.getVideoCapabilities()).isNotNull();
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths())
+ .isEqualTo(new Range<>(1, WIDTH));
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights())
+ .isEqualTo(new Range<>(1, HEIGHT));
+ }
+
+ @Test
+ @Config(minSdk = Q)
public void canCreateVideoDecoderCapabilities() {
CodecCapabilities codecCapabilities =
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
@@ -353,4 +383,24 @@ public class MediaCodecInfoBuilderTest {
}
return mediaFormat;
}
+
+ /**
+ * Create a sample {@link MediaFormat}.
+ *
+ * @param mime one of MIMETYPE_* from {@link MediaFormat}.
+ * @param width The width of the content (in pixels).
+ * @param height The height of the content (in pixels).
+ * @param features an array of CodecCapabilities.FEATURE_ features to be enabled.
+ */
+ private static MediaFormat createMediaFormat(
+ String mime, int width, int height, String[] features) {
+ MediaFormat mediaFormat = new MediaFormat();
+ mediaFormat.setString(MediaFormat.KEY_MIME, mime);
+ mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
+ mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
+ for (String feature : features) {
+ mediaFormat.setFeatureEnabled(feature, true);
+ }
+ return mediaFormat;
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java
index 9ecb63d06..5de84d80d 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java
@@ -1,8 +1,8 @@
package org.robolectric.shadows;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.robolectric.shadows.ShadowAssetManager.legacyShadowOf;
@@ -46,7 +46,7 @@ public class ShadowAssetManagerTest {
@Test
public void openFd_shouldProvideFileDescriptorForDeflatedAsset() throws Exception {
- assumeTrue(!useLegacy());
+ assume().that(useLegacy()).isFalse();
expectedException.expect(FileNotFoundException.class);
expectedException.expectMessage(
"This file can not be opened as a file descriptor; it is probably compressed");
@@ -79,7 +79,7 @@ public class ShadowAssetManagerTest {
@Test
public void openNonAssetShouldThrowExceptionWhenFileDoesNotExist() throws IOException {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
expectedException.expect(IOException.class);
expectedException.expectMessage(
@@ -90,7 +90,7 @@ public class ShadowAssetManagerTest {
@Test
public void unknownResourceIdsShouldReportPackagesSearched() throws IOException {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
expectedException.expect(Resources.NotFoundException.class);
expectedException.expectMessage("Resource ID #0xffffffff");
@@ -102,7 +102,7 @@ public class ShadowAssetManagerTest {
@Test
public void forSystemResources_unknownResourceIdsShouldReportPackagesSearched()
throws IOException {
- if (!useLegacy()) return;
+ assume().that(useLegacy()).isTrue();
expectedException.expect(Resources.NotFoundException.class);
expectedException.expectMessage("Resource ID #0xffffffff");
@@ -113,8 +113,7 @@ public class ShadowAssetManagerTest {
@Test
@Config(qualifiers = "mdpi")
public void openNonAssetShouldOpenCorrectAssetBasedOnQualifierMdpi() throws IOException {
- if (!useLegacy()) return;
-
+ assume().that(useLegacy()).isTrue();
InputStream inputStream = assetManager.openNonAsset(0, "res/drawable/robolectric.png", 0);
assertThat(countBytes(inputStream)).isEqualTo(8141);
}
@@ -122,8 +121,7 @@ public class ShadowAssetManagerTest {
@Test
@Config(qualifiers = "hdpi")
public void openNonAssetShouldOpenCorrectAssetBasedOnQualifierHdpi() throws IOException {
- if (!useLegacy()) return;
-
+ assume().that(useLegacy()).isTrue();
InputStream inputStream = assetManager.openNonAsset(0, "res/drawable/robolectric.png", 0);
assertThat(countBytes(inputStream)).isEqualTo(23447);
}
@@ -178,8 +176,7 @@ public class ShadowAssetManagerTest {
@Test
public void attrsToTypedArray_shouldAllowMockedAttributeSets() {
- if (!useLegacy()) return;
-
+ assume().that(useLegacy()).isTrue();
AttributeSet mockAttributeSet = mock(AttributeSet.class);
when(mockAttributeSet.getAttributeCount()).thenReturn(1);
when(mockAttributeSet.getAttributeNameResource(0)).thenReturn(android.R.attr.windowBackground);
@@ -191,7 +188,7 @@ public class ShadowAssetManagerTest {
@Test
public void whenStyleAttrResolutionFails_attrsToTypedArray_returnsNiceErrorMessage() {
- if (!useLegacy()) return;
+ assume().that(useLegacy()).isTrue();
expectedException.expect(RuntimeException.class);
expectedException.expectMessage(
"no value for org.robolectric:attr/styleNotSpecifiedInAnyTheme in theme with applied"
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
index b798a74a3..149856254 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
@@ -8,6 +8,7 @@ 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.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -24,6 +25,7 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioRecordingConfiguration;
+import android.media.AudioSystem;
import android.media.MediaRecorder.AudioSource;
import android.media.audiopolicy.AudioPolicy;
import androidx.test.core.app.ApplicationProvider;
@@ -48,6 +50,17 @@ public class ShadowAudioManagerTest {
private Context appContext;
private AudioManager audioManager;
+ // When creating Audio Device Info, we need to pass external device type instead of internal input
+ // device(e.g. AudioDeviceInfo.TYPE_BLUETOOTH_SCO)
+ // The mapping between external device type and internal input device is:
+ // http://shortn/_7pV0nML4Cr
+ // Copied from
+ // http://cs/android-internal/frameworks/base/media/java/android/media/AudioSystem.java;l=989
+ private static final int DEVICE_OUT_BLUETOOTH_SCO = 0x10;
+ // Copied from
+ // http://cs/android-internal/frameworks/base/media/java/android/media/AudioSystem.java;l=1000
+ private static final int DEVICE_OUT_BLUETOOTH_A2DP = 0x80;
+
@Before
public void setUp() {
appContext = ApplicationProvider.getApplicationContext();
@@ -403,7 +416,7 @@ public class ShadowAudioManagerTest {
@Config(minSdk = M)
public void registerAudioDeviceCallback_availableDevices_onAudioDevicesAddedCallback()
throws Exception {
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
@@ -419,7 +432,7 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
verifyNoMoreInteractions(callback);
@@ -434,7 +447,7 @@ public class ShadowAudioManagerTest {
audioManager.unregisterAudioDeviceCallback(callback);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
verifyNoMoreInteractions(callback);
@@ -448,7 +461,7 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
@@ -463,7 +476,7 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
verifyNoMoreInteractions(callback);
@@ -476,7 +489,7 @@ public class ShadowAudioManagerTest {
AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -492,7 +505,7 @@ public class ShadowAudioManagerTest {
AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -508,7 +521,7 @@ public class ShadowAudioManagerTest {
AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
@@ -524,7 +537,7 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
verifyNoMoreInteractions(callback);
@@ -537,7 +550,7 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
verifyNoMoreInteractions(callback);
@@ -551,7 +564,7 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
@@ -566,7 +579,7 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
verifyNoMoreInteractions(callback);
@@ -579,7 +592,7 @@ public class ShadowAudioManagerTest {
AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -595,7 +608,7 @@ public class ShadowAudioManagerTest {
AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -611,7 +624,7 @@ public class ShadowAudioManagerTest {
AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
@@ -627,17 +640,130 @@ public class ShadowAudioManagerTest {
audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
- AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
verifyNoMoreInteractions(callback);
}
@Test
+ @Config(minSdk = S)
+ public void setAvailableCommunicationDevices_withCallbackRegistered_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ addAvailableCommunicationDevice_withCallbackRegisteredAndNoDevice_deviceAddedAndNotifiesCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ shadowOf(audioManager)
+ .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ addAvailableCommunicationDeviceNoCallbackNotification_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ shadowOf(audioManager)
+ .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ addAvailableCommunicationDevice_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager)
+ .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ removeAvailableCommunicationDevice_withCallbackRegisteredAndDevicePresent_deviceRemovedAndNotifiesCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager)
+ .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verify(callback).onAudioDevicesRemoved(new AudioDeviceInfo[] {device});
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ removeAvailableCommunicationDeviceNoCallbackNotification_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager)
+ .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ removeAvailableCommunicationDevice_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ shadowOf(audioManager)
+ .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
@Config(minSdk = M)
public void getDevices_criteriaInputs_getsAllInputDevices() throws Exception {
- AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
- AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+ AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP);
shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice));
shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice));
@@ -648,8 +774,8 @@ public class ShadowAudioManagerTest {
@Test
@Config(minSdk = M)
public void getDevices_criteriaOutputs_getsAllOutputDevices() throws Exception {
- AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
- AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+ AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP);
shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice));
shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice));
@@ -660,8 +786,8 @@ public class ShadowAudioManagerTest {
@Test
@Config(minSdk = M)
public void getDevices_criteriaInputsAndOutputs_getsAllDevices() throws Exception {
- AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
- AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+ AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+ AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP);
shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice));
shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice));
@@ -672,7 +798,7 @@ public class ShadowAudioManagerTest {
@Test
@Config(minSdk = S)
public void setCommunicationDevice_updatesCommunicationDevice() throws Exception {
- AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setCommunicationDevice(scoDevice);
assertThat(audioManager.getCommunicationDevice()).isEqualTo(scoDevice);
@@ -681,7 +807,7 @@ public class ShadowAudioManagerTest {
@Test
@Config(minSdk = S)
public void clearCommunicationDevice_clearsCommunicationDevice() throws Exception {
- AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
shadowOf(audioManager).setCommunicationDevice(scoDevice);
assertThat(audioManager.getCommunicationDevice()).isEqualTo(scoDevice);
@@ -991,6 +1117,190 @@ public class ShadowAudioManagerTest {
assertThat(audioSessionId).isNotEqualTo(audioSessionId2);
}
+ @Test
+ @Config(minSdk = Q)
+ public void isOffloadSupported_withoutSupport() {
+ assertThat(
+ AudioManager.isOffloadedPlaybackSupported(
+ new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build(),
+ new AudioAttributes.Builder().build()))
+ .isFalse();
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void isOffloadSupported_withSetOffloadSupported() {
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+
+ ShadowAudioSystem.setOffloadSupported(format, attributes, true);
+
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void isOffloadSupported_withSetOffloadSupportedAddedAndRemoved() {
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadSupported(format, attributes, true);
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+
+ ShadowAudioSystem.setOffloadSupported(format, attributes, false);
+
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void isOffloadSupported_withSetOffloadPlaybackSupport() {
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+
+ ShadowAudioSystem.setOffloadPlaybackSupport(format, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void getPlaybackOffloadSupport_withSetOffloadSupport_returnsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setOffloadPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ int playbackOffloadSupport =
+ AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ getPlaybackOffloadSupport_withoutSetDirectPlaybackSupport_returnsOffloadNotSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+
+ int playbackOffloadSupport =
+ AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_NOT_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void getPlaybackOffloadSupport_withSameAudioAttrUsage_returnsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setOffloadPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ AudioAttributes audioAttributes2 =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ int playbackOffloadSupport =
+ AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes2);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getDirectPlaybackSupport_withSetDirectPlaybackSupport_returnsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setDirectPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+
+ int playbackOffloadSupport =
+ AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getDirectPlaybackSupport_withShadowAudioSystemReset_returnsOffloadNotSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setDirectPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+ ShadowAudioSystem.reset();
+
+ int playbackOffloadSupport =
+ AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_NOT_SUPPORTED);
+ }
+
private static AudioDeviceInfo createAudioDevice(int type) throws ReflectiveOperationException {
AudioDeviceInfo info = Shadow.newInstanceOf(AudioDeviceInfo.class);
Field portField = AudioDeviceInfo.class.getDeclaredField("mPort");
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
index adffa0111..e8869bd49 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
@@ -4,13 +4,20 @@ import static android.media.AudioTrack.ERROR_BAD_VALUE;
import static android.media.AudioTrack.WRITE_BLOCKING;
import static android.media.AudioTrack.WRITE_NON_BLOCKING;
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 android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
+import android.media.AudioSystem;
import android.media.AudioTrack;
+import android.media.PlaybackParams;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.nio.ByteBuffer;
import org.junit.Test;
@@ -172,6 +179,360 @@ public class ShadowAudioTrackTest implements ShadowAudioTrack.OnAudioDataWritten
assertThat(written).isEqualTo(ERROR_BAD_VALUE);
}
+ @Test
+ @Config(minSdk = M)
+ public void getPlaybackParams_withSetPlaybackParams_returnsSetPlaybackParams() {
+ PlaybackParams playbackParams =
+ new PlaybackParams()
+ .allowDefaults()
+ .setSpeed(1.0f)
+ .setPitch(1.0f)
+ .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL);
+ AudioTrack audioTrack = getSampleAudioTrack();
+ audioTrack.setPlaybackParams(playbackParams);
+
+ assertThat(audioTrack.getPlaybackParams()).isEqualTo(playbackParams);
+ }
+
+ @Test
+ public void addDirectPlaybackSupport_forPcmEncoding_throws() {
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_8BIT), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_16BIT), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_24BIT_PACKED), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_32BIT), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_FLOAT), attributes));
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void isDirectPlaybackSupported() {
+ AudioFormat ac3Format = getAudioFormat(AudioFormat.ENCODING_AC3);
+ AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse();
+
+ ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void isDirectPlaybackSupported_differentFormatOrAttributeFields() {
+ AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build();
+ AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+
+ ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+
+ assertThat(
+ AudioTrack.isDirectPlaybackSupported(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(65000)
+ .build(),
+ audioAttributes))
+ .isFalse();
+ assertThat(
+ AudioTrack.isDirectPlaybackSupported(
+ ac3Format,
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .build()))
+ .isFalse();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void clearDirectPlaybackSupportedEncodings() {
+ AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build();
+ AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+ ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue();
+
+ ShadowAudioTrack.clearDirectPlaybackSupportedFormats();
+
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse();
+ }
+
+ @Test
+ public void addAllowedNonPcmEncoding_forPcmEncoding_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_8BIT));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_16BIT));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_24BIT_PACKED));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_32BIT));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_FLOAT));
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void createInstance_withNonPcmEncodingNotAllowed_throws() {
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setBufferSizeInBytes(65536)
+ .build());
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void createInstance_withNonPcmEncodingAllowed() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setBufferSizeInBytes(65536)
+ .build();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void createInstance_withOffloadAndEncodingNotOffloaded_throws() {
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build());
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void createInstance_withOffloadAndEncodingIsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadSupported(audioFormat, attributes, /* supported= */ true);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(audioFormat)
+ .setAudioAttributes(attributes)
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+ }
+
+ @Test
+ @Config(sdk = S)
+ public void createInstance_withOffloadAndGetOffloadSupport() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadPlaybackSupport(
+ audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(audioFormat)
+ .setAudioAttributes(attributes)
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void createInstance_withOffloadAndGetDirectPlaybackSupport() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setDirectPlaybackSupport(
+ audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(audioFormat)
+ .setAudioAttributes(attributes)
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void clearAllowedNonPcmEncodings() {
+ AudioFormat surroundAudioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+ new AudioTrack.Builder()
+ .setAudioFormat(surroundAudioFormat)
+ .setBufferSizeInBytes(65536)
+ .build();
+
+ ShadowAudioTrack.clearAllowedNonPcmEncodings();
+
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ new AudioTrack.Builder()
+ .setAudioFormat(surroundAudioFormat)
+ .setBufferSizeInBytes(65536)
+ .build());
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void write_withNonPcmEncodingSupported_succeeds() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(32 * 1024)
+ .build();
+
+ assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128);
+ assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void write_withOffloadUntilApi30_succeeds() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+ AudioFormat ac3Format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadSupported(ac3Format, attributes, /* supported= */ true);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(ac3Format)
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(32 * 1024)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128);
+ assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void write_withNonPcmEncodingNoLongerSupported_returnsErrorDeadObject() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(32 * 1024)
+ .build();
+
+ ShadowAudioTrack.clearAllowedNonPcmEncodings();
+
+ assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ }
+
@Override
@Config(minSdk = Q)
public void onAudioDataWritten(
@@ -195,4 +556,8 @@ public class ShadowAudioTrackTest implements ShadowAudioTrack.OnAudioDataWritten
.build())
.build();
}
+
+ private AudioFormat getAudioFormat(int encoding) {
+ return new AudioFormat.Builder().setEncoding(encoding).build();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
index caed17812..78b9edbd9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
@@ -31,6 +31,7 @@ public class ShadowBluetoothGattTest {
private static final String ACTION_DISCOVER = "DISCOVER";
private static final String ACTION_READ = "READ";
private static final String ACTION_WRITE = "WRITE";
+ private static final String REMOTE_ADDRESS = "R-A";
private int resultStatus = INITIAL_VALUE;
private int resultState = INITIAL_VALUE;
@@ -274,6 +275,15 @@ public class ShadowBluetoothGattTest {
@Test
@Config(minSdk = O)
+ public void getService_afterAddService() {
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ assertThat(bluetoothGatt.discoverServices()).isFalse();
+ assertThat(bluetoothGatt.getService(service1.getUuid())).isEqualTo(service1);
+ assertThat(bluetoothGatt.getService(service2.getUuid())).isNull();
+ }
+
+ @Test
+ @Config(minSdk = O)
public void discoverServices_clearsService() {
shadowOf(bluetoothGatt).setGattCallback(callback);
shadowOf(bluetoothGatt).addDiscoverableService(service1);
@@ -471,4 +481,103 @@ public class ShadowBluetoothGattTest {
assertThat(resultCharacteristic).isEqualTo(characteristic);
assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
}
+
+ @Test
+ public void test_getBluetoothConnectionManager() {
+ assertThat(shadowOf(bluetoothGatt).getBluetoothConnectionManager()).isNotNull();
+ }
+
+ @Test
+ public void test_notifyConnection_connects() {
+ shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyConnection_connectsWithCallbackSet() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ public void test_notifyDisconnection_disconnects() {
+ shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyDisconnection_disconnectsWithCallbackSet() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyDisconnection_disconnectsWithCallbackSet_connectedInitially() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+ shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isFalse();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void allowCharacteristicNotification_canSetNotification() {
+ service1.addCharacteristic(characteristicWithReadProperty);
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ shadowOf(bluetoothGatt).allowCharacteristicNotification(characteristicWithReadProperty);
+ assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true))
+ .isTrue();
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void disallowCharacteristicNotification_cannotSetNotification() {
+ service1.addCharacteristic(characteristicWithReadProperty);
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ shadowOf(bluetoothGatt).disallowCharacteristicNotification(characteristicWithReadProperty);
+ assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true))
+ .isFalse();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
index 9482ba8d7..cae11d9fa 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
@@ -22,9 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
-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.shadow.api.Shadow;
@@ -37,8 +35,6 @@ public class ShadowBluetoothHeadsetTest {
private BluetoothHeadset bluetoothHeadset;
private Application context;
- @Rule public ExpectedException thrown = ExpectedException.none();
-
@Before
public void setUp() throws Exception {
device1 = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:AA:BB");
@@ -61,6 +57,41 @@ public class ShadowBluetoothHeadsetTest {
}
@Test
+ public void getConnectedDevices_doesNotReturnDevicesInNonConnectedStates() {
+ shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING);
+ shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING);
+
+ assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+ }
+
+ @Test
+ public void getConnectionState_returnsStoredConnectionState() {
+ shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING);
+ shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING);
+
+ assertThat(bluetoothHeadset.getConnectionState(device1))
+ .isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ assertThat(bluetoothHeadset.getConnectionState(device2))
+ .isEqualTo(BluetoothProfile.STATE_DISCONNECTING);
+ }
+
+ @Test
+ public void removeDevice_getConnectionStateReturnsDisconnected() {
+ shadowOf(bluetoothHeadset).addConnectedDevice(device1);
+ shadowOf(bluetoothHeadset).removeDevice(device1);
+
+ assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+ }
+
+ @Test
+ public void removeDevice_getConnectedDevicesReturnsEmpty() {
+ shadowOf(bluetoothHeadset).addConnectedDevice(device1);
+ shadowOf(bluetoothHeadset).removeDevice(device1);
+
+ assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+ }
+
+ @Test
public void getConnectionState_defaultsToDisconnected() {
shadowOf(bluetoothHeadset).addConnectedDevice(device1);
shadowOf(bluetoothHeadset).addConnectedDevice(device2);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java
index 02b149da7..cf3eba8a5 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java
@@ -4,6 +4,7 @@ import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.P;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
@@ -46,6 +47,8 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ConfigTestReceiver;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver;
import org.robolectric.R;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
@@ -95,6 +98,20 @@ public class ShadowContextWrapperTest {
}
@Test
+ @Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P)
+ public void registerReceiver_shouldGetReceiverWithCustomConstructorEmptyAction() {
+ BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithEmptyActionReceiver.class);
+ assertThat(receiver).isInstanceOf(CustomConstructorWithEmptyActionReceiver.class);
+ }
+
+ @Test
+ @Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P)
+ public void registerReceiver_shouldGetReceiverWithCustomConstructorAndOneAction() {
+ BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithOneActionReceiver.class);
+ assertThat(receiver).isInstanceOf(CustomConstructorWithOneActionReceiver.class);
+ }
+
+ @Test
public void registerReceiver_shouldRegisterForAllIntentFilterActions() throws Exception {
BroadcastReceiver receiver = broadcastReceiver("Larry");
contextWrapper.registerReceiver(receiver, intentFilter("foo", "baz"));
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
index 8ee669ae1..8000cc656 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
@@ -1,10 +1,14 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.hardware.input.InputManager;
+import android.hardware.input.InputManager.InputDeviceListener;
+import android.os.Handler;
+import android.os.Looper;
import android.view.MotionEvent;
import android.view.VerifiedMotionEvent;
import androidx.test.core.app.ApplicationProvider;
@@ -16,7 +20,7 @@ import org.robolectric.annotation.Config;
/** Unit tests for {@link ShadowInputManager}. */
@RunWith(AndroidJUnit4.class)
-@Config(minSdk = R)
+@Config(minSdk = R, maxSdk = TIRAMISU)
public class ShadowInputManagerTest {
private InputManager inputManager;
@@ -38,4 +42,22 @@ public class ShadowInputManagerTest {
assertThat(verifiedMotionEvent.getEventTimeNanos()).isEqualTo(23456000000L);
assertThat(verifiedMotionEvent.getDownTimeNanos()).isEqualTo(12345000000L);
}
+
+ static class InputDeviceListenerNoOp implements InputDeviceListener {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {}
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {}
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {}
+ }
+
+ @Test
+ public void testRegisterInputDeviceListener_doesNotCrash() {
+ InputDeviceListenerNoOp listener = new InputDeviceListenerNoOp();
+ inputManager.registerInputDeviceListener(listener, new Handler(Looper.getMainLooper()));
+ inputManager.unregisterInputDeviceListener(listener);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
index edff632ed..323cb57fc 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
@@ -264,6 +264,20 @@ public class ShadowLauncherAppsTest {
}
@Test
+ @Config(minSdk = L)
+ public void testIsActivityEnabled() {
+ ComponentName c1 = new ComponentName(ApplicationProvider.getApplicationContext(), "Activity1");
+ ComponentName c2 = new ComponentName(ApplicationProvider.getApplicationContext(), "Activity2");
+ ComponentName c3 = new ComponentName("other", "Activity1");
+ assertThat(launcherApps.isActivityEnabled(c1, USER_HANDLE)).isFalse();
+
+ shadowOf(launcherApps).setActivityEnabled(USER_HANDLE, c1);
+ assertThat(launcherApps.isActivityEnabled(c1, USER_HANDLE)).isTrue();
+ assertThat(launcherApps.isActivityEnabled(c2, USER_HANDLE)).isFalse();
+ assertThat(launcherApps.isActivityEnabled(c3, USER_HANDLE)).isFalse();
+ }
+
+ @Test
@Config(minSdk = O)
public void testGetApplicationInfo_packageNotFound() throws Exception {
Throwable throwable =
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
index 6ae57f951..7744b9507 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
@@ -71,6 +71,15 @@ public class ShadowPaintTest {
}
@Test
+ public void shouldSetStrikeThruText() {
+ Paint paint = new Paint();
+ paint.setStrikeThruText(true);
+ assertThat(paint.isStrikeThruText()).isTrue();
+ paint.setStrikeThruText(false);
+ assertThat(paint.isStrikeThruText()).isFalse();
+ }
+
+ @Test
public void measureTextActuallyMeasuresLength() {
Paint paint = new Paint();
assertThat(paint.measureText("Hello")).isEqualTo(5.0f);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
index cadc3e969..71e85c9b6 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
@@ -4,6 +4,7 @@ import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
@@ -13,6 +14,7 @@ import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -32,6 +34,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.res.android.Ref;
import org.robolectric.shadow.api.Shadow;
@@ -526,10 +529,8 @@ public class ShadowPausedLooperTest {
}
@Test
- public void testIdleNotStuck_whenThreadCrashes() throws Exception {
- HandlerThread thread = new HandlerThread("WillCrash");
- thread.start();
- Looper looper = thread.getLooper();
+ public void idle_looperPaused_idleHandlerThrowsException() throws Exception {
+ Looper looper = handlerThread.getLooper();
shadowOf(looper).pause();
new Handler(looper)
.post(
@@ -537,12 +538,69 @@ public class ShadowPausedLooperTest {
Looper.myQueue()
.addIdleHandler(
() -> {
- throw new RuntimeException();
+ throw new IllegalStateException();
});
});
- shadowOf(looper).idle();
- thread.join(5_000);
- assertThat(thread.getState()).isEqualTo(Thread.State.TERMINATED);
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+ }
+
+ @Test
+ public void idle_looperPaused_runnableThrowsException() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ shadowOf(looper).pause();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+ }
+
+ @Test
+ public void idle_looperRunning_runnableThrowsException() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+ }
+
+ @Test
+ public void post_throws_if_looper_died() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+
+ assertThrows(IllegalStateException.class, () -> new Handler(looper).post(() -> {}));
+ }
+
+ @Test
+ public void idle_throws_if_looper_died() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
}
@Test
@@ -565,6 +623,43 @@ public class ShadowPausedLooperTest {
assertThat(foregroundThreadReceived.get()).isTrue();
}
+ @Test
+ @Config(minSdk = VERSION_CODES.M)
+ public void runOneTask_ignoreSyncBarrier() {
+ int barrier = Looper.getMainLooper().getQueue().postSyncBarrier();
+
+ final AtomicBoolean wasRun = new AtomicBoolean(false);
+ new Handler(Looper.getMainLooper()).post(() -> wasRun.set(true));
+
+ ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper());
+ shadowPausedLooper.runOneTask();
+
+ // tasks should not be executed when blocked by a sync barrier
+ assertThat(wasRun.get()).isFalse();
+ // sync barrier will throw if the barrier was not found.
+ Looper.getMainLooper().getQueue().removeSyncBarrier(barrier);
+
+ shadowPausedLooper.runOneTask();
+ assertThat(wasRun.get()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.P)
+ public void runOneTask_ignoreSyncBarrier_with_async() {
+ int barrier = Looper.getMainLooper().getQueue().postSyncBarrier();
+
+ final AtomicBoolean wasRun = new AtomicBoolean(false);
+ Handler.createAsync(Looper.getMainLooper()).post(() -> wasRun.set(true));
+
+ ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper());
+ shadowPausedLooper.runOneTask();
+
+ // tasks should be executed as the handler is async
+ assertThat(wasRun.get()).isTrue();
+ // sync barrier will throw if the barrier was not found.
+ Looper.getMainLooper().getQueue().removeSyncBarrier(barrier);
+ }
+
private static class BlockingRunnable implements Runnable {
CountDownLatch latch = new CountDownLatch(1);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java
index da3440139..a4732b920 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java
@@ -2,8 +2,7 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.N_MR1;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
@@ -85,7 +84,7 @@ public class ShadowResourcesTest {
@Test
public void openRawResourceFd_shouldReturnsNullForLegacyResource() throws Exception {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
try (AssetFileDescriptor afd = resources.openRawResourceFd(R.raw.raw_resource)) {
assertThat(afd).isNull();
}
@@ -93,7 +92,7 @@ public class ShadowResourcesTest {
@Test
public void openRawResourceFd_shouldReturnsValidFdForUnCompressFile() throws Exception {
- assumeFalse(useLegacy());
+ assume().that(useLegacy()).isFalse();
try (AssetFileDescriptor afd = resources.openRawResourceFd(R.raw.raw_resource)) {
assertThat(afd).isNotNull();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
index 323511fc3..878777c10 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
@@ -3,8 +3,8 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
import static org.robolectric.annotation.SQLiteMode.Mode.LEGACY;
import static org.robolectric.shadows.ShadowLegacySQLiteConnection.convertSQLWithLocalizedUnicodeCollator;
@@ -64,7 +64,7 @@ public class ShadowSQLiteConnectionTest {
@Test
public void testSqlConversion() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
assertThat(convertSQLWithLocalizedUnicodeCollator("select * from `routine`"))
.isEqualTo("select * from `routine`");
@@ -88,7 +88,7 @@ public class ShadowSQLiteConnectionTest {
@Test
public void testSQLWithLocalizedOrUnicodeCollatorShouldBeSortedAsNoCase() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
database.execSQL("insert into routine(name) values ('الصحافة اليدوية')");
database.execSQL("insert into routine(name) values ('Hand press 1')");
database.execSQL("insert into routine(name) values ('hand press 2')");
@@ -116,28 +116,28 @@ public class ShadowSQLiteConnectionTest {
@Test
public void nativeOpen_addsConnectionToPool() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
assertThat(conn).isNotNull();
assertWithMessage("open").that(conn.isOpen()).isTrue();
}
@Test
public void nativeClose_closesConnection() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
ShadowLegacySQLiteConnection.nativeClose(ptr);
assertWithMessage("open").that(conn.isOpen()).isFalse();
}
@Test
public void reset_closesConnection() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
ShadowLegacySQLiteConnection.reset();
assertWithMessage("open").that(conn.isOpen()).isFalse();
}
@Test
public void reset_clearsConnectionCache() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
final Map<Long, SQLiteConnection> connectionsMap =
ReflectionHelpers.getField(connections, "connectionsMap");
@@ -149,7 +149,7 @@ public class ShadowSQLiteConnectionTest {
@Test
public void reset_clearsStatementCache() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
final Map<Long, SQLiteStatement> statementsMap =
ReflectionHelpers.getField(connections, "statementsMap");
@@ -161,7 +161,7 @@ public class ShadowSQLiteConnectionTest {
@Test
public void error_resultsInSpecificExceptionWithCause() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
try {
database.execSQL("insert into routine(name) values ('Hand press 1')");
ContentValues values = new ContentValues(1);
@@ -178,7 +178,7 @@ public class ShadowSQLiteConnectionTest {
@Test
public void interruption_doesNotConcurrentlyModifyDatabase() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
Thread.currentThread().interrupt();
try {
database.execSQL("insert into routine(name) values ('الصحافة اليدوية')");
@@ -190,7 +190,7 @@ public class ShadowSQLiteConnectionTest {
@Test
public void test_setUseInMemoryDatabase() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
assertThat(conn.isMemoryDatabase()).isFalse();
ShadowSQLiteConnection.setUseInMemoryDatabase(true);
SQLiteDatabase inMemoryDb = createDatabase("in_memory.db");
@@ -201,7 +201,7 @@ public class ShadowSQLiteConnectionTest {
@Test
public void cancel_shouldCancelAllStatements() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
SQLiteStatement statement1 =
database.compileStatement("insert into routine(name) values ('Hand press 1')");
SQLiteStatement statement2 =
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
index 4e7fb16ce..a527ec98f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
@@ -10,6 +10,8 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.robolectric.Shadows.shadowOf;
+import android.os.Handler;
+import android.os.Looper;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -90,6 +92,29 @@ public class ShadowSubscriptionManagerTest {
}
@Test
+ public void
+ addOnSubscriptionsChangedListener_whenHasExecutorParameter_shouldCallbackImmediately() {
+ DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+ shadowOf(subscriptionManager)
+ .addOnSubscriptionsChangedListener(new Handler(Looper.getMainLooper())::post, listener);
+
+ assertThat(listener.subscriptionChangedCount).isEqualTo(1);
+ }
+
+ @Test
+ public void addOnSubscriptionsChangedListener_whenHasExecutorParameter_shouldAddListener() {
+ DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+ shadowOf(subscriptionManager)
+ .addOnSubscriptionsChangedListener(new Handler(Looper.getMainLooper())::post, listener);
+
+ shadowOf(subscriptionManager)
+ .setActiveSubscriptionInfos(
+ SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo());
+
+ assertThat(listener.subscriptionChangedCount).isEqualTo(2);
+ }
+
+ @Test
public void removeOnSubscriptionsChangedListener_shouldRemoveListener() {
DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
DummySubscriptionsChangedListener listener2 = new DummySubscriptionsChangedListener();
@@ -106,6 +131,21 @@ public class ShadowSubscriptionManagerTest {
}
@Test
+ public void hasOnSubscriptionsChangedListener_whenListenerNotExist_shouldReturnFalse() {
+ DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+
+ assertThat(shadowOf(subscriptionManager).hasOnSubscriptionsChangedListener(listener)).isFalse();
+ }
+
+ @Test
+ public void hasOnSubscriptionsChangedListener_whenListenerExist_shouldReturnTrue() {
+ DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+ shadowOf(subscriptionManager).addOnSubscriptionsChangedListener(listener);
+
+ assertThat(shadowOf(subscriptionManager).hasOnSubscriptionsChangedListener(listener)).isTrue();
+ }
+
+ @Test
public void getActiveSubscriptionInfo_shouldReturnInfoWithSubId() {
SubscriptionInfo expectedSubscriptionInfo =
SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo();
@@ -331,6 +371,48 @@ public class ShadowSubscriptionManagerTest {
.isEqualTo("123");
}
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPhoneNumberWithSource_phoneNumberNotSet_returnsEmptyString() {
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_UICC))
+ .isEqualTo("");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER))
+ .isEqualTo("");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_IMS))
+ .isEqualTo("");
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPhoneNumberWithSource_setPhoneNumber_returnsPhoneNumber() {
+ shadowOf(subscriptionManager)
+ .setPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "123");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_UICC))
+ .isEqualTo("123");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER))
+ .isEqualTo("123");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_IMS))
+ .isEqualTo("123");
+ }
+
private static class DummySubscriptionsChangedListener
extends SubscriptionManager.OnSubscriptionsChangedListener {
private int subscriptionChangedCount;
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
index 8571627a4..ad3adebcf 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -27,6 +27,8 @@ import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK;
import static android.telephony.TelephonyManager.CALL_STATE_RINGING;
import static android.telephony.TelephonyManager.NETWORK_TYPE_EVDO_0;
import static android.telephony.TelephonyManager.NETWORK_TYPE_LTE;
+import static android.telephony.emergency.EmergencyNumber.EMERGENCY_NUMBER_SOURCE_DATABASE;
+import static android.telephony.emergency.EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertEquals;
@@ -74,10 +76,12 @@ import android.telephony.TelephonyManager.BootstrapAuthenticationCallback;
import android.telephony.TelephonyManager.CellInfoCallback;
import android.telephony.UiccSlotInfo;
import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.telephony.emergency.EmergencyNumber;
import android.telephony.gba.UaSecurityProtocolIdentifier;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -374,6 +378,21 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = S)
+ public void shouldGiveCallStateForSubscription() {
+ PhoneStateListener listener = mock(PhoneStateListener.class);
+ telephonyManager.listen(listener, LISTEN_CALL_STATE);
+
+ shadowOf(telephonyManager).setCallState(CALL_STATE_RINGING, "911");
+ assertEquals(CALL_STATE_RINGING, telephonyManager.getCallStateForSubscription());
+ verify(listener).onCallStateChanged(CALL_STATE_RINGING, "911");
+
+ shadowOf(telephonyManager).setCallState(CALL_STATE_OFFHOOK, "911");
+ assertEquals(CALL_STATE_OFFHOOK, telephonyManager.getCallStateForSubscription());
+ verify(listener).onCallStateChanged(CALL_STATE_OFFHOOK, null);
+ }
+
+ @Test
public void shouldGiveCallState() {
PhoneStateListener listener = mock(PhoneStateListener.class);
telephonyManager.listen(listener, LISTEN_CALL_STATE);
@@ -803,6 +822,24 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = S)
+ public void setDataEnabledForReasonChangesIsDataEnabledForReason() {
+ int correctReason = TelephonyManager.DATA_ENABLED_REASON_POLICY;
+ int incorrectReason = TelephonyManager.DATA_ENABLED_REASON_USER;
+
+ assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue();
+ assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+
+ telephonyManager.setDataEnabledForReason(correctReason, false);
+ assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isFalse();
+ assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+
+ telephonyManager.setDataEnabledForReason(correctReason, true);
+ assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue();
+ assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+ }
+
+ @Test
public void setDataStateChangesDataState() {
assertThat(telephonyManager.getDataState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED);
shadowOf(telephonyManager).setDataState(TelephonyManager.DATA_CONNECTING);
@@ -1068,4 +1105,36 @@ public class ShadowTelephonyManagerTest {
public void getEmergencyCallback_notSet_returnsFalse() {
assertThat(telephonyManager.getEmergencyCallbackMode()).isFalse();
}
+
+ @Test
+ @Config(minSdk = R)
+ public void getEmergencyNumbersList_notSet_returnsEmptyList() {
+ assertThat(telephonyManager.getEmergencyNumberList()).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void getEmergencyNumbersList_wasSet_returnsCorrectList() throws Exception {
+ EmergencyNumber emergencyNumber =
+ EmergencyNumber.class
+ .getConstructor(
+ String.class,
+ String.class,
+ String.class,
+ int.class,
+ List.class,
+ int.class,
+ int.class)
+ .newInstance(
+ "911",
+ "us",
+ "30",
+ EMERGENCY_NUMBER_SOURCE_DATABASE,
+ ImmutableList.of(),
+ EMERGENCY_SERVICE_CATEGORY_POLICE,
+ EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL);
+ ShadowTelephonyManager.setEmergencyNumberList(
+ ImmutableMap.of(0, ImmutableList.of(emergencyNumber)));
+ assertThat(telephonyManager.getEmergencyNumberList().get(0)).containsExactly(emergencyNumber);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
index 75df1101a..7c9bfafca 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
@@ -9,6 +9,7 @@ import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
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 com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
@@ -68,7 +69,8 @@ public class ShadowUserManagerTest {
UserHandle anotherProfile = newUserHandle(2);
shadowOf(userManager).addUserProfile(anotherProfile);
- assertThat(userManager.getUserProfiles()).containsExactly(Process.myUserHandle(), anotherProfile);
+ assertThat(userManager.getUserProfiles())
+ .containsExactly(Process.myUserHandle(), anotherProfile);
}
@Test
@@ -243,7 +245,8 @@ public class ShadowUserManagerTest {
try {
userManager.isManagedProfile();
fail("Expected exception");
- } catch (SecurityException expected) {}
+ } catch (SecurityException expected) {
+ }
setPermissions(permission.MANAGE_USERS);
@@ -317,6 +320,19 @@ public class ShadowUserManagerTest {
}
@Test
+ @Config(minSdk = R)
+ public void getUserHandles() {
+ assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(1);
+ shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(0);
+ assertThat(UserHandle.myUserId()).isEqualTo(UserHandle.USER_SYSTEM);
+
+ UserHandle expectedUserHandle = shadowOf(userManager).addUser(10, "secondary_user", 0);
+ assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(2);
+ assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(1))
+ .isEqualTo(expectedUserHandle);
+ }
+
+ @Test
@Config(minSdk = N_MR1, maxSdk = Q)
public void isDemoUser() {
// All methods are based on the current user, so no need to pass a UserHandle.
@@ -565,6 +581,34 @@ public class ShadowUserManagerTest {
}
@Test
+ @Config(minSdk = Q)
+ public void removeSecondaryUser_noExistingUser_doesNotRemove() {
+ assertThat(shadowOf(userManager).removeUser(UserHandle.of(10))).isFalse();
+ assertThat(userManager.getUserCount()).isEqualTo(1);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void removeUserWhenPossible_twoUsersRemoveOne_hasOneUserLeft() {
+ shadowOf(userManager).addUser(10, "secondary_user", 0);
+ assertThat(
+ userManager.removeUserWhenPossible(
+ UserHandle.of(10), /* overrideDevicePolicy= */ false))
+ .isEqualTo(UserManager.REMOVE_RESULT_REMOVED);
+ assertThat(userManager.getUserCount()).isEqualTo(1);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void removeUserWhenPossible_nonExistingUser_fails() {
+ assertThat(
+ userManager.removeUserWhenPossible(
+ UserHandle.of(10), /* overrideDevicePolicy= */ false))
+ .isEqualTo(UserManager.REMOVE_RESULT_ERROR_UNKNOWN);
+ assertThat(userManager.getUserCount()).isEqualTo(1);
+ }
+
+ @Test
@Config(minSdk = JELLY_BEAN_MR1)
public void switchToSecondaryUser() {
shadowOf(userManager).addUser(10, "secondary_user", 0);
@@ -653,8 +697,8 @@ public class ShadowUserManagerTest {
@Config(minSdk = LOLLIPOP)
public void getProfiles_addedProfile_containsProfile() {
shadowOf(userManager).addUser(TEST_USER_HANDLE, "", 0);
- shadowOf(userManager).addProfile(
- TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS);
+ shadowOf(userManager)
+ .addProfile(TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS);
// getProfiles(userId) include user itself and asssociated profiles.
assertThat(userManager.getProfiles(TEST_USER_HANDLE).get(0).id).isEqualTo(TEST_USER_HANDLE);
@@ -850,7 +894,6 @@ public class ShadowUserManagerTest {
assertThat(UserManager.supportsMultipleUsers()).isTrue();
}
-
@Test
@Config(minSdk = Q)
public void getUserSwitchability_shouldReturnLastSetSwitchability() {
@@ -859,8 +902,7 @@ public class ShadowUserManagerTest {
.setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
assertThat(userManager.getUserSwitchability())
.isEqualTo(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
- shadowOf(userManager)
- .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
+ shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
assertThat(userManager.getUserSwitchability()).isEqualTo(UserManager.SWITCHABILITY_STATUS_OK);
}
@@ -880,8 +922,7 @@ public class ShadowUserManagerTest {
shadowOf(userManager)
.setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
assertThat(userManager.canSwitchUsers()).isFalse();
- shadowOf(userManager)
- .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
+ shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
assertThat(userManager.canSwitchUsers()).isTrue();
}
@@ -889,7 +930,7 @@ public class ShadowUserManagerTest {
@Config(minSdk = Q)
public void getUserName_shouldReturnSetUserName() {
shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
- shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0);
+ shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0);
shadowOf(userManager).switchUser(10);
assertThat(userManager.getUserName()).isEqualTo(PROFILE_USER_NAME);
}
@@ -900,7 +941,7 @@ public class ShadowUserManagerTest {
userManager.setUserIcon(TEST_USER_ICON);
assertThat(userManager.getUserIcon()).isEqualTo(TEST_USER_ICON);
- shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0);
+ shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0);
shadowOf(userManager).switchUser(10);
assertThat(userManager.getUserIcon()).isNull();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java
index b8b57851c..ef5527abf 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java
@@ -16,7 +16,6 @@ import android.content.Context;
import android.media.AudioAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
-import android.os.vibrator.PrimitiveSegment;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
@@ -132,7 +131,7 @@ public class ShadowVibratorTest {
@Config(minSdk = S)
@Test
- public void getVibrationEffectSegments_composeOnce_shouldReturnSameFragment() {
+ public void getPrimitiveSegmentsInPrimitiveEffects_composeOnce_shouldReturnSameFragment() {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20)
@@ -140,17 +139,17 @@ public class ShadowVibratorTest {
.addPrimitive(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150)
.compose());
- assertThat(shadowOf(vibrator).getVibrationEffectSegments())
+ assertThat(shadowOf(vibrator).getPrimitiveSegmentsInPrimitiveEffects())
.isEqualTo(
ImmutableList.of(
- new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20),
- new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.7f, /* delay= */ 50),
- new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150)));
+ new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20),
+ new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.7f, /* delay= */ 50),
+ new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150)));
}
@Config(minSdk = S)
@Test
- public void getVibrationEffectSegments_composeTwice_shouldReturnTheLastComposition() {
+ public void getPrimitiveSegmentsInPrimitiveEffects_composeTwice_shouldReturnTheLastComposition() {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20)
@@ -164,12 +163,12 @@ public class ShadowVibratorTest {
.addPrimitive(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150)
.compose());
- assertThat(shadowOf(vibrator).getVibrationEffectSegments())
+ assertThat(shadowOf(vibrator).getPrimitiveSegmentsInPrimitiveEffects())
.isEqualTo(
ImmutableList.of(
- new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.4f, /* delay= */ 120),
- new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150),
- new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150)));
+ new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.4f, /* delay= */ 120),
+ new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150),
+ new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150)));
}
@Config(minSdk = R)
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java
new file mode 100644
index 000000000..dbf7250ef
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java
@@ -0,0 +1,95 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Intent;
+import android.net.Ikev2VpnProfile;
+import android.net.VpnManager;
+import android.net.VpnProfileState;
+import android.os.Build.VERSION_CODES;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = VERSION_CODES.R)
+public class ShadowVpnManagerTest {
+ private VpnManager vpnManager;
+ private ShadowVpnManager shadowVpnManager;
+
+ @Before
+ public void setUp() throws Exception {
+ vpnManager = ApplicationProvider.getApplicationContext().getSystemService(VpnManager.class);
+ shadowVpnManager = shadowOf(vpnManager);
+ }
+
+ @Test
+ public void provisionVpnProfile() {
+ Intent intent = new Intent("foo");
+ shadowVpnManager.setProvisionVpnProfileResult(intent);
+
+ assertThat(
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity")
+ .setAuthPsk(new byte[0])
+ .build()))
+ .isSameInstanceAs(intent);
+
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ VpnProfileState state = vpnManager.getProvisionedVpnProfileState();
+ assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED);
+ assertThat(state.getSessionId()).isNull();
+ }
+ }
+
+ @Test
+ public void deleteVpnProfile() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ vpnManager.deleteProvisionedVpnProfile();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void deleteVpnProfile_tiramisu() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ assertThat(vpnManager.getProvisionedVpnProfileState()).isNotNull();
+
+ vpnManager.deleteProvisionedVpnProfile();
+ assertThat(vpnManager.getProvisionedVpnProfileState()).isNull();
+ }
+
+ @Test
+ public void startAndStopVpnProfile() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ vpnManager.startProvisionedVpnProfile();
+ vpnManager.stopProvisionedVpnProfile();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void startAndStopVpnProfile_tiramisu() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ String sessionKey = vpnManager.startProvisionedVpnProfileSession();
+ VpnProfileState state = vpnManager.getProvisionedVpnProfileState();
+ assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_CONNECTED);
+ assertThat(state.getSessionId()).isEqualTo(sessionKey);
+ assertThat(state.isAlwaysOn()).isFalse();
+ assertThat(state.isLockdownEnabled()).isFalse();
+
+ vpnManager.stopProvisionedVpnProfile();
+ state = vpnManager.getProvisionedVpnProfileState();
+ assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED);
+ assertThat(state.getSessionId()).isNull();
+ assertThat(state.isAlwaysOn()).isFalse();
+ assertThat(state.isLockdownEnabled()).isFalse();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index 7a1bee691..15dd9ec96 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -1,12 +1,17 @@
package org.robolectric.shadows;
+import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -14,10 +19,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
+import android.app.Application;
import android.app.admin.DeviceAdminService;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.DhcpInfo;
import android.net.NetworkInfo;
@@ -27,13 +34,17 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
+import android.net.wifi.WifiManager.PnoScanResultsCallback;
+import android.net.wifi.WifiSsid;
import android.net.wifi.WifiUsabilityStatsEntry;
import android.os.Build;
import android.util.Pair;
-import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -47,9 +58,7 @@ public class ShadowWifiManagerTest {
@Before
public void setUp() throws Exception {
- wifiManager =
- (WifiManager)
- ApplicationProvider.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
}
@Test
@@ -494,8 +503,7 @@ public class ShadowWifiManagerTest {
// THEN
NetworkInfo networkInfo =
((ConnectivityManager)
- ApplicationProvider.getApplicationContext()
- .getSystemService(Context.CONNECTIVITY_SERVICE))
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE))
.getActiveNetworkInfo();
assertThat(networkInfo.getType()).isEqualTo(ConnectivityManager.TYPE_WIFI);
assertThat(networkInfo.isConnected()).isTrue();
@@ -784,13 +792,305 @@ public class ShadowWifiManagerTest {
assertThat(shadowOf(wifiManager).getSoftApConfiguration().getSsid()).isEqualTo("foo");
}
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_nullCallback_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ /* callback= */ null));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_nullExecutor_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})),
+ /* frequencies= */ null,
+ /* executor= */ null,
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_nullSsidList_throwsIllegalStateException() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ /* ssids= */ null,
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_emptySsidList_throwsIllegalStateException() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ /* ssids= */ List.of(),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_moreThan2Ssids_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(
+ WifiSsid.fromBytes(new byte[] {1, 2, 3}),
+ WifiSsid.fromBytes(new byte[] {9, 8, 7, 6}),
+ WifiSsid.fromBytes(new byte[] {90, 81, 72, 63, 54})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_moreThan10Frequencies_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(
+ WifiSsid.fromBytes(new byte[] {1, 2, 3}),
+ WifiSsid.fromBytes(new byte[] {9, 8, 7, 6})),
+ new int[] {5160, 5180, 5200, 5220, 5240, 5260, 5280, 5300, 5320, 5340, 5360},
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_validRequest_successCallbackInvoked() throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ assertThat(callback.successfulRegistrations.take()).isNotNull();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void
+ setExternalPnoScanRequest_outstandingRequest_failureCallbackInvokedWithAlreadyRegisteredStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {9, 2, 5})),
+ new int[] {5280},
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ assertThat(callback.failedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_differentUid_failureCallbackInvokedWithBusyStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ int firstAppUid = ShadowProcess.myUid();
+ int secondAppUid;
+ do {
+ secondAppUid = ShadowProcess.getRandomApplicationUid();
+ } while (firstAppUid == secondAppUid);
+ ShadowProcess.setUid(secondAppUid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ assertThat(callback.failedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void clearExternalPnoScanRequest_outstandingRequest_callbackInvokedWithUnregisteredStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+ wifiManager.clearExternalPnoScanRequest();
+
+ assertThat(callback.removedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void clearExternalPnoScanRequest_wrongUid_callbackNotInvoked() throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ executor,
+ callback);
+
+ int firstAppUid = ShadowProcess.myUid();
+ int secondAppUid;
+ do {
+ secondAppUid = ShadowProcess.getRandomApplicationUid();
+ } while (firstAppUid == secondAppUid);
+ ShadowProcess.setUid(secondAppUid);
+
+ wifiManager.clearExternalPnoScanRequest();
+
+ executor.shutdown();
+
+ assertThat(executor.awaitTermination(5, MINUTES)).isTrue();
+ assertThat(callback.removedRegistrations).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_matchingSsid_availableCallbackInvoked() throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(wifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ assertThat(callback.incomingScanResults.take()).containsExactly(scanResult);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_matchingSsid_removedCallbackInvokedWithDeliveredStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(wifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ assertThat(callback.removedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_matchingSsid_scanResultsAvailableBroadcastSent() {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(wifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ Intent expectedIntent = new Intent(SCAN_RESULTS_AVAILABLE_ACTION);
+ expectedIntent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
+ expectedIntent.setPackage(getApplicationContext().getPackageName());
+
+ assertThat(
+ shadowOf((Application) getApplicationContext()).getBroadcastIntents().stream()
+ .anyMatch(expectedIntent::filterEquals))
+ .isTrue();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_noMatchingSsid_availableCallbackNotInvoked()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ WifiSsid otherWifiSsid = WifiSsid.fromBytes(new byte[] {9, 8, 7, 6});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(otherWifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, executor, callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ executor.shutdown();
+
+ assertThat(executor.awaitTermination(5, MINUTES)).isTrue();
+ assertThat(callback.incomingScanResults).isEmpty();
+ }
+
+ private class TestPnoScanResultsCallback implements PnoScanResultsCallback {
+ LinkedBlockingQueue<List<ScanResult>> incomingScanResults = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<Object> successfulRegistrations = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<Integer> failedRegistrations = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<Integer> removedRegistrations = new LinkedBlockingQueue<>();
+
+ @Override
+ public void onScanResultsAvailable(List<ScanResult> scanResults) {
+ incomingScanResults.add(scanResults);
+ }
+
+ @Override
+ public void onRegisterSuccess() {
+ successfulRegistrations.add(new Object());
+ }
+
+ @Override
+ public void onRegisterFailed(int reason) {
+ failedRegistrations.add(reason);
+ }
+
+ @Override
+ public void onRemoved(int reason) {
+ removedRegistrations.add(reason);
+ }
+ }
+
private void setDeviceOwner() {
shadowOf(
(DevicePolicyManager)
- ApplicationProvider.getApplicationContext()
- .getSystemService(Context.DEVICE_POLICY_SERVICE))
- .setDeviceOwner(
- new ComponentName(
- ApplicationProvider.getApplicationContext(), DeviceAdminService.class));
+ getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE))
+ .setDeviceOwner(new ComponentName(getApplicationContext(), DeviceAdminService.class));
}
}
diff --git a/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java b/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java
index 3b06a4e32..612fd3bfa 100644
--- a/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java
@@ -1,8 +1,8 @@
package org.robolectric.util;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
@@ -56,7 +56,7 @@ public class SQLiteLibraryLoaderTest {
@Test
public void shouldExtractNativeLibrary() {
- assumeTrue(SQLiteLibraryLoader.isOsSupported());
+ assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
assertThat(loader.isLoaded()).isFalse();
loader.doLoad();
assertThat(loader.isLoaded()).isTrue();
diff --git a/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml
new file mode 100644
index 000000000..cbda17e65
--- /dev/null
+++ b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.robolectric">
+ <uses-sdk android:targetSdkVersion="18"/>
+
+ <application
+ android:appComponentFactory="org.robolectric.CustomAppComponentFactory">
+ <receiver
+ android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithOneActionReceiver">
+ <intent-filter>
+ <action android:name="org.robolectric.ACTION_CUSTOM_CONSTRUCTOR"/>
+ </intent-filter>
+ </receiver>
+ <receiver
+ android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithEmptyActionReceiver" />
+ </application>
+</manifest>
diff --git a/sandbox/build.gradle b/sandbox/build.gradle
index 64accd737..358b027c2 100644
--- a/sandbox/build.gradle
+++ b/sandbox/build.gradle
@@ -5,24 +5,24 @@ apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
dependencies {
- annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
- annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
+ annotationProcessor libs.auto.service
+ annotationProcessor libs.error.prone.core
api project(":annotations")
api project(":utils")
api project(":shadowapi")
api project(":utils:reflector")
- compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
- api "javax.annotation:javax.annotation-api:1.3.2"
- api "javax.inject:javax.inject:1"
+ compileOnly libs.auto.service.annotations
+ api libs.javax.annotation.api
+ api libs.javax.inject
- api "org.ow2.asm:asm:${asmVersion}"
- api "org.ow2.asm:asm-commons:${asmVersion}"
- api "com.google.guava:guava:$guavaJREVersion"
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+ api libs.asm
+ api libs.asm.commons
+ api libs.guava
+ compileOnly libs.findbugs.jsr305
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockito
testImplementation project(":junit")
}
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 fac00226d..e1463a17b 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -10,6 +10,7 @@ import java.lang.invoke.MethodType;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.ListIterator;
+import java.util.Objects;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
@@ -212,23 +213,25 @@ public class ClassInstrumentor {
}
/**
- * Checks if the first instruction is a Jacoco load instructions. Robolectric is not capable at
- * the moment of re-instrumenting Jacoco-instrumented constructors.
+ * Checks if the first or second instruction is a Jacoco load instruction. Robolectric is not
+ * capable at the moment of re-instrumenting Jacoco-instrumented constructors, so these are
+ * currently skipped.
*
* @param ctor constructor method node
* @return whether or not the constructor can be instrumented
*/
private boolean isJacocoInstrumented(MethodNode ctor) {
AbstractInsnNode[] insns = ctor.instructions.toArray();
- if (insns.length > 0) {
- if (insns[0] instanceof LdcInsnNode
- && ((LdcInsnNode) insns[0]).cst instanceof ConstantDynamic) {
- ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) insns[0]).cst;
+ if (insns.length > 1) {
+ AbstractInsnNode node = insns[0];
+ if (node instanceof LabelNode) {
+ node = insns[1];
+ }
+ if ((node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof ConstantDynamic)) {
+ ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) node).cst;
return cst.getName().equals("$jacocoData");
- } else if (insns.length > 1
- && insns[0] instanceof LabelNode
- && insns[1] instanceof MethodInsnNode) {
- return "$jacocoInit".equals(((MethodInsnNode) insns[1]).name);
+ } else if (node instanceof MethodInsnNode) {
+ return Objects.equals(((MethodInsnNode) node).name, "$jacocoInit");
}
}
return false;
diff --git a/settings.gradle b/settings.gradle
index 20f8fae14..1894b8c98 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -16,6 +16,7 @@ include ":shadows:framework"
include ":shadows:httpclient"
include ":shadows:multidex"
include ":shadows:playservices"
+include ":shadows:versioning"
include ":shadowapi"
include ":errorprone"
include ":nativeruntime"
diff --git a/shadowapi/build.gradle b/shadowapi/build.gradle
index f63d048e1..3f0064fb7 100644
--- a/shadowapi/build.gradle
+++ b/shadowapi/build.gradle
@@ -5,11 +5,11 @@ apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
dependencies {
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+ compileOnly libs.findbugs.jsr305
api project(":annotations")
api project(":utils")
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
-} \ No newline at end of file
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockito
+}
diff --git a/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java b/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java
index eaaee1a3d..8ae639971 100644
--- a/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java
+++ b/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java
@@ -10,7 +10,6 @@ import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import javax.annotation.Nullable;
/** Collection of helper methods for calling methods and accessing fields reflectively. */
@SuppressWarnings(value = {"unchecked", "TypeParameterUnusedInFormals", "NewApi"})
@@ -45,9 +44,10 @@ public class ReflectionHelpers {
* <p>The returned object will be an instance of the given class, but all methods will return
* either the "default" value for primitives, or another deep proxy for non-primitive types.
*
- * <p>This should be used rarely, for cases where we need to create deep proxies in order not
- * to crash. The inner proxies are impossible to configure, so there is no way to create
- * meaningful behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions.
+ * <p>This should be used rarely, for cases where we need to create deep proxies in order not to
+ * crash. The inner proxies are impossible to configure, so there is no way to create meaningful
+ * behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions.
+ *
* @param clazz the class to provide a proxy instance of.
* @return a new "Deep Proxy" instance of the given class.
*/
@@ -127,7 +127,8 @@ public class ReflectionHelpers {
* @param fieldName The field name.
* @param fieldNewValue New value.
*/
- public static void setField(final Object object, final String fieldName, final Object fieldNewValue) {
+ public static void setField(
+ final Object object, final String fieldName, final Object fieldNewValue) {
try {
traverseClassHierarchy(
object.getClass(),
@@ -152,7 +153,8 @@ public class ReflectionHelpers {
* @param fieldName The field name.
* @param fieldNewValue New value.
*/
- public static void setField(Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) {
+ public static void setField(
+ Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) {
try {
Field field = type.getDeclaredField(fieldName);
field.setAccessible(true);
@@ -163,6 +165,22 @@ public class ReflectionHelpers {
}
/**
+ * Reflectively check if a class has a given field (static or non static).
+ *
+ * @param clazz Target class.
+ * @param fieldName The field name.
+ * @return boolean to indicate whether the field exists or not in clazz.
+ */
+ public static boolean hasField(Class<?> clazz, String fieldName) {
+ try {
+ Field field = clazz.getDeclaredField(fieldName);
+ return (field != null);
+ } catch (NoSuchFieldException e) {
+ return false;
+ }
+ }
+
+ /**
* Reflectively get the value of a static field.
*
* @param field Field object.
@@ -392,7 +410,9 @@ public class ReflectionHelpers {
public static <T> T newInstance(Class<T> cl) {
try {
return cl.getDeclaredConstructor().newInstance();
- } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
+ } catch (InstantiationException
+ | IllegalAccessException
+ | NoSuchMethodException
| InvocationTargetException e) {
throw new RuntimeException(e);
}
@@ -465,15 +485,15 @@ public class ReflectionHelpers {
*/
public static class ClassParameter<V> {
public final Class<? extends V> clazz;
- public final V val;
+ public final V value;
- public ClassParameter(Class<? extends V> clazz, V val) {
+ public ClassParameter(Class<? extends V> clazz, V value) {
this.clazz = clazz;
- this.val = val;
+ this.value = value;
}
- public static <V> ClassParameter<V> from(Class<? extends V> clazz, V val) {
- return new ClassParameter<>(clazz, val);
+ public static <V> ClassParameter<V> from(Class<? extends V> clazz, V value) {
+ return new ClassParameter<>(clazz, value);
}
public static ClassParameter<?>[] fromComponentLists(Class<?>[] classes, Object[] values) {
@@ -496,7 +516,7 @@ public class ReflectionHelpers {
public static Object[] getValues(ClassParameter<?>... classParameters) {
Object[] values = new Object[classParameters.length];
for (int i = 0; i < classParameters.length; i++) {
- Object paramValue = classParameters[i].val;
+ Object paramValue = classParameters[i].value;
values[i] = paramValue;
}
return values;
@@ -510,15 +530,15 @@ public class ReflectionHelpers {
*/
public static class StringParameter<V> {
public final String className;
- public final V val;
+ public final V value;
- public StringParameter(String className, V val) {
+ public StringParameter(String className, V value) {
this.className = className;
- this.val = val;
+ this.value = value;
}
- public static <V> StringParameter<V> from(String className, V val) {
- return new StringParameter<>(className, val);
+ public static <V> StringParameter<V> from(String className, V value) {
+ return new StringParameter<>(className, value);
}
}
}
diff --git a/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java b/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java
index 56c489df1..e5f281ba6 100644
--- a/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java
+++ b/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java
@@ -141,7 +141,8 @@ public class ReflectionHelpersTest {
}
@Test
- public void callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() {
+ public void
+ callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() {
ExampleDescendant example = new ExampleDescendant();
int returnNumber =
ReflectionHelpers.callInstanceMethod(
@@ -282,23 +283,35 @@ public class ReflectionHelpersTest {
}
@Test
- public void callConstructorReflectively_whenMultipleSignaturesExistForTheConstructor_callsConstructorWithCorrectSignature() {
- ExampleClass ec = ReflectionHelpers.callConstructor(ExampleClass.class, ClassParameter.from(int.class, 16));
+ public void
+ callConstructorReflectively_whenMultipleSignaturesExistForTheConstructor_callsConstructorWithCorrectSignature() {
+ ExampleClass ec =
+ ReflectionHelpers.callConstructor(ExampleClass.class, ClassParameter.from(int.class, 16));
assertWithMessage("index").that(ec.index).isEqualTo(16);
assertWithMessage("name").that(ec.name).isNull();
}
- @SuppressWarnings("serial")
- private static class TestError extends Error {
+ @Test
+ public void callHasField_withstaticandregularmember() {
+ assertWithMessage("has field failed for member: unusedName")
+ .that(ReflectionHelpers.hasField(FieldTestClass.class, "unusedName"))
+ .isTrue();
+ assertWithMessage("has field failed for member: unusedStaticName")
+ .that(ReflectionHelpers.hasField(FieldTestClass.class, "unusedStaticName"))
+ .isTrue();
+ assertWithMessage("has field failed for non existant member: noname")
+ .that(ReflectionHelpers.hasField(FieldTestClass.class, "noname"))
+ .isFalse();
}
@SuppressWarnings("serial")
- private static class TestException extends Exception {
- }
+ private static class TestError extends Error {}
@SuppressWarnings("serial")
- private static class TestRuntimeException extends RuntimeException {
- }
+ private static class TestException extends Exception {}
+
+ @SuppressWarnings("serial")
+ private static class TestRuntimeException extends RuntimeException {}
@SuppressWarnings("unused")
private static class ExampleBase {
@@ -406,4 +419,11 @@ public class ReflectionHelpersTest {
this.index = index;
}
}
+
+ private static class FieldTestClass {
+ public String unusedName;
+ public static String unusedStaticName = "unusedStaticNameValue";
+
+ private FieldTestClass() {}
+ }
}
diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle
index a273d5a6f..a2230b0fe 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -15,6 +15,8 @@ configurations {
sqlite4java
}
+def sqlite4javaVersion = libs.versions.sqlite4java.get()
+
task copySqliteNatives(type: Copy) {
from project.configurations.sqlite4java {
include '**/*.dll'
@@ -45,22 +47,20 @@ dependencies {
api project(":pluginapi")
api project(":sandbox")
api project(":shadowapi")
+ api project(":shadows:versioning")
api project(":utils")
api project(":utils:reflector")
+
api "androidx.test:monitor:$axtMonitorVersion@aar"
- implementation "com.google.errorprone:error_prone_annotations:$errorproneVersion"
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
- api "com.almworks.sqlite4java:sqlite4java:$sqlite4javaVersion"
- compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
- api "com.ibm.icu:icu4j:72.1"
- api "androidx.annotation:annotation:1.1.0"
- api "com.google.auto.value:auto-value-annotations:1.10.1"
- annotationProcessor "com.google.auto.value:auto-value:1.10.1"
+ implementation libs.error.prone.annotations
+ compileOnly libs.findbugs.jsr305
+ api libs.sqlite4java
+ compileOnly(AndroidSdk.MAX_SDK.coordinates)
+ api libs.icu4j
+ api libs.androidx.annotation
+ api libs.auto.value.annotations
+ annotationProcessor libs.auto.value
- sqlite4java "com.almworks.sqlite4java:libsqlite4java-osx:$sqlite4javaVersion"
- sqlite4java "com.almworks.sqlite4java:libsqlite4java-linux-amd64:$sqlite4javaVersion"
- sqlite4java "com.almworks.sqlite4java:sqlite4java-win32-x64:$sqlite4javaVersion"
- sqlite4java "com.almworks.sqlite4java:libsqlite4java-linux-i386:$sqlite4javaVersion"
- sqlite4java "com.almworks.sqlite4java:sqlite4java-win32-x86:$sqlite4javaVersion"
+ sqlite4java libs.bundles.sqlite4java.native
}
diff --git a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
index 33276d916..33276d916 100755..100644
--- a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
+++ b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
new file mode 100644
index 000000000..e2b8f0df3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
@@ -0,0 +1,138 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.companion.AssociationInfo;
+import android.net.MacAddress;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Builder for {@link AssociationInfo}. */
+public class AssociationInfoBuilder {
+ private int id;
+ private int userId;
+ private String packageName;
+ private String deviceMacAddress;
+ private CharSequence displayName;
+ private String deviceProfile;
+ private boolean selfManaged;
+ private boolean notifyOnDeviceNearby;
+ private long approvedMs;
+ private long lastTimeConnectedMs;
+
+ private AssociationInfoBuilder() {}
+
+ public static AssociationInfoBuilder newBuilder() {
+ return new AssociationInfoBuilder();
+ }
+
+ public AssociationInfoBuilder setId(int id) {
+ this.id = id;
+ return this;
+ }
+
+ public AssociationInfoBuilder setUserId(int userId) {
+ this.userId = userId;
+ return this;
+ }
+
+ public AssociationInfoBuilder setPackageName(String packageName) {
+ this.packageName = packageName;
+ return this;
+ }
+
+ public AssociationInfoBuilder setDeviceMacAddress(String deviceMacAddress) {
+ this.deviceMacAddress = deviceMacAddress;
+ return this;
+ }
+
+ public AssociationInfoBuilder setDisplayName(CharSequence displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+
+ public AssociationInfoBuilder setDeviceProfile(String deviceProfile) {
+ this.deviceProfile = deviceProfile;
+ return this;
+ }
+
+ public AssociationInfoBuilder setSelfManaged(boolean selfManaged) {
+ this.selfManaged = selfManaged;
+ return this;
+ }
+
+ public AssociationInfoBuilder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby) {
+ this.notifyOnDeviceNearby = notifyOnDeviceNearby;
+ return this;
+ }
+
+ public AssociationInfoBuilder setApprovedMs(long approvedMs) {
+ this.approvedMs = approvedMs;
+ return this;
+ }
+
+ public AssociationInfoBuilder setLastTimeConnectedMs(long lastTimeConnectedMs) {
+ this.lastTimeConnectedMs = lastTimeConnectedMs;
+ return this;
+ }
+
+ public AssociationInfo build() {
+ try {
+ if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+ // We have two different constructors for AssociationInfo across
+ // T branches. aosp has the constructor that takes a new "revoked" parameter.
+ // Since there is not deterministic way to know which branch we are running in,
+ // we will reflect on the class to see if it has the mRevoked member.
+ // Based on the result we will either invoke the constructor with "revoked" or the
+ // one without this parameter.
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) {
+ 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.fromString(deviceMacAddress)),
+ 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 only supported in aosp*/),
+ ClassParameter.from(long.class, approvedMs),
+ ClassParameter.from(long.class, lastTimeConnectedMs));
+ } 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.fromString(deviceMacAddress)),
+ 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 {
+ 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.fromString(deviceMacAddress)),
+ ClassParameter.from(CharSequence.class, displayName),
+ ClassParameter.from(String.class, deviceProfile),
+ ClassParameter.from(Class.forName("android.companion.AssociatedDevice"), null),
+ 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*/));
+ }
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
new file mode 100644
index 000000000..70e54b190
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
@@ -0,0 +1,139 @@
+package org.robolectric.shadows;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Manages remote address connections for {@link ShadowBluetoothGatt} and {@link
+ * ShadowBluetoothGattServer}.
+ */
+final class BluetoothConnectionManager {
+
+ private static volatile BluetoothConnectionManager instance;
+
+ /** Connection metadata for Gatt Server and Client connections. */
+ private static class BluetoothConnectionMetadata {
+ boolean hasGattClientConnection = false;
+ boolean hasGattServerConnection = false;
+
+ void setHasGattClientConnection(boolean hasGattClientConnection) {
+ this.hasGattClientConnection = hasGattClientConnection;
+ }
+
+ void setHasGattServerConnection(boolean hasGattServerConnection) {
+ this.hasGattServerConnection = hasGattServerConnection;
+ }
+
+ boolean hasGattClientConnection() {
+ return hasGattClientConnection;
+ }
+
+ boolean hasGattServerConnection() {
+ return hasGattServerConnection;
+ }
+
+ boolean isConnected() {
+ return hasGattClientConnection || hasGattServerConnection;
+ }
+ }
+
+ private BluetoothConnectionManager() {}
+
+ static BluetoothConnectionManager getInstance() {
+ if (instance == null) {
+ synchronized (BluetoothConnectionManager.class) {
+ if (instance == null) {
+ instance = new BluetoothConnectionManager();
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Map representing remote address connections, mapping a remote address to a {@link
+ * BluetoothConnectionMetadata}.
+ */
+ private final Map<String, BluetoothConnectionMetadata> remoteAddressConnectionMap =
+ new HashMap<>();
+
+ /**
+ * Register a Gatt Client Connection. Intended for use by {@link
+ * ShadowBluetoothGatt#notifyConnection} when simulating a successful Gatt Client Connection.
+ */
+ void registerGattClientConnection(String remoteAddress) {
+ if (!remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata());
+ }
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(true);
+ }
+
+ /**
+ * Unregister a Gatt Client Connection. Intended for use by {@link
+ * ShadowBluetoothGatt#notifyDisconnection} when simulating a successful Gatt client
+ * disconnection.
+ */
+ void unregisterGattClientConnection(String remoteAddress) {
+ if (remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(false);
+ }
+ }
+
+ /**
+ * Register a Gatt Server Connection. Intended for use by {@link
+ * ShadowBluetoothGattServer#notifyConnection} when simulating a successful Gatt server
+ * connection.
+ */
+ void registerGattServerConnection(String remoteAddress) {
+ if (!remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata());
+ }
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(true);
+ }
+
+ /**
+ * Unregister a Gatt Server Connection. Intended for use by {@link
+ * ShadowBluetoothGattServer#notifyDisconnection} when simulating a successful Gatt server
+ * disconnection.
+ */
+ void unregisterGattServerConnection(String remoteAddress) {
+ if (remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(false);
+ }
+ }
+
+ /**
+ * Returns true if remote address has an active gatt client connection.
+ *
+ * @param remoteAddress remote address
+ */
+ boolean hasGattClientConnection(String remoteAddress) {
+ return remoteAddressConnectionMap.containsKey(remoteAddress)
+ && remoteAddressConnectionMap.get(remoteAddress).hasGattClientConnection();
+ }
+
+ /**
+ * Returns true if remote address has an active gatt server connection.
+ *
+ * @param remoteAddress remote address
+ */
+ boolean hasGattServerConnection(String remoteAddress) {
+ return remoteAddressConnectionMap.containsKey(remoteAddress)
+ && remoteAddressConnectionMap.get(remoteAddress).hasGattServerConnection();
+ }
+
+ /**
+ * Returns true if remote address has an active connection.
+ *
+ * @param remoteAddress remote address
+ */
+ boolean isConnected(String remoteAddress) {
+ return remoteAddressConnectionMap.containsKey(remoteAddress)
+ && remoteAddressConnectionMap.get(remoteAddress).isConnected();
+ }
+
+ /** Clears all connection information */
+ void resetConnections() {
+ this.remoteAddressConnectionMap.clear();
+ }
+} \ No newline at end of file
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java
new file mode 100644
index 000000000..597aef246
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java
@@ -0,0 +1,170 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import android.telephony.ClosedSubscriberGroupInfo;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellIdentityLte}. */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellIdentityLteBuilder {
+
+ @Nullable private String mcc = null;
+ @Nullable private String mnc = null;
+ private int ci = CellInfo.UNAVAILABLE;
+ private int pci = CellInfo.UNAVAILABLE;
+ private int tac = CellInfo.UNAVAILABLE;
+ private int earfcn = CellInfo.UNAVAILABLE;
+ private int[] bands = new int[0];
+ private int bandwidth = CellInfo.UNAVAILABLE;
+ @Nullable private String alphal = null;
+ @Nullable private String alphas = null;
+ private List<String> additionalPlmns = new ArrayList<>();
+
+ private CellIdentityLteBuilder() {}
+
+ public static CellIdentityLteBuilder newBuilder() {
+ return new CellIdentityLteBuilder();
+ }
+
+ protected static CellIdentityLte getDefaultInstance() {
+ return reflector(CellIdentityLteReflector.class).newCellIdentityLte();
+ }
+
+ public CellIdentityLteBuilder setMcc(String mcc) {
+ this.mcc = mcc;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setMnc(String mnc) {
+ this.mnc = mnc;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setCi(int ci) {
+ this.ci = ci;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setPci(int pci) {
+ this.pci = pci;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setTac(int tac) {
+ this.tac = tac;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setEarfcn(int earfcn) {
+ this.earfcn = earfcn;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setBands(int[] bands) {
+ this.bands = bands;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setBandwidth(int bandwidth) {
+ this.bandwidth = bandwidth;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setLongOperatorName(String longOperatorName) {
+ this.alphal = longOperatorName;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setShortOperatorName(String shortOperatorName) {
+ this.alphas = shortOperatorName;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setAdditionalPlmns(List<String> additionalPlmns) {
+ this.additionalPlmns = additionalPlmns;
+ return this;
+ }
+
+ public CellIdentityLte build() {
+ CellIdentityLteReflector cellIdentityLteReflector = reflector(CellIdentityLteReflector.class);
+ int apiLevel = RuntimeEnvironment.getApiLevel();
+ if (apiLevel < Build.VERSION_CODES.N) {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac);
+ } else if (apiLevel < Build.VERSION_CODES.P) {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac, earfcn);
+ } else if (apiLevel < Build.VERSION_CODES.R) {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ ci, pci, tac, earfcn, bandwidth, mcc, mnc, alphal, alphas);
+ } else {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ ci,
+ pci,
+ tac,
+ earfcn,
+ bands,
+ bandwidth,
+ mcc,
+ mnc,
+ alphal,
+ alphas,
+ additionalPlmns,
+ /* csgInfo= */ null);
+ }
+ }
+
+ private static int mccOrMncToInt(@Nullable String mccOrMnc) {
+ return mccOrMnc == null ? CellInfo.UNAVAILABLE : Integer.parseInt(mccOrMnc);
+ }
+
+ @ForType(CellIdentityLte.class)
+ private interface CellIdentityLteReflector {
+ @Constructor
+ CellIdentityLte newCellIdentityLte();
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac);
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac, int earfcn);
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(
+ int ci,
+ int pci,
+ int tac,
+ int earfcn,
+ int bandwidth,
+ String mcc,
+ String mnc,
+ String alphal,
+ String alphas);
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(
+ int ci,
+ int pci,
+ int tac,
+ int earfcn,
+ int[] bands,
+ int bandwidth,
+ String mcc,
+ String mnc,
+ String alphal,
+ String alphas,
+ Collection<String> additionalPlmns,
+ ClosedSubscriberGroupInfo csgInfo);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java
new file mode 100644
index 000000000..22a0e75c0
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java
@@ -0,0 +1,135 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfo;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellIdentityNr}. */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellIdentityNrBuilder {
+
+ private int pci = CellInfo.UNAVAILABLE;
+ private int tac = CellInfo.UNAVAILABLE;
+ private int nrarfcn = CellInfo.UNAVAILABLE;
+ private int[] bands = new int[0];
+ @Nullable private String mcc = null;
+ @Nullable private String mnc = null;
+ private long nci = CellInfo.UNAVAILABLE;
+ @Nullable private String alphal = null;
+ @Nullable private String alphas = null;
+ private List<String> additionalPlmns = new ArrayList<>();
+
+ private CellIdentityNrBuilder() {}
+
+ public static CellIdentityNrBuilder newBuilder() {
+ return new CellIdentityNrBuilder();
+ }
+
+ // An empty constructor is not available on Q.
+ @RequiresApi(Build.VERSION_CODES.R)
+ protected static CellIdentityNr getDefaultInstance() {
+ return reflector(CellIdentityNrReflector.class).newCellIdentityNr();
+ }
+
+ public CellIdentityNrBuilder setNci(long nci) {
+ this.nci = nci;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setPci(int pci) {
+ this.pci = pci;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setTac(int tac) {
+ this.tac = tac;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setNrarfcn(int nrarfcn) {
+ this.nrarfcn = nrarfcn;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setMcc(String mcc) {
+ this.mcc = mcc;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setMnc(String mnc) {
+ this.mnc = mnc;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setBands(int[] bands) {
+ this.bands = bands;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setLongOperatorName(String longOperatorName) {
+ this.alphal = longOperatorName;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setShortOperatorName(String shortOperatorName) {
+ this.alphas = shortOperatorName;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setAdditionalPlmns(List<String> additionalPlmns) {
+ this.additionalPlmns = additionalPlmns;
+ return this;
+ }
+
+ public CellIdentityNr build() {
+ CellIdentityNrReflector cellIdentityReflector = reflector(CellIdentityNrReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.R) {
+ return cellIdentityReflector.newCellIdentityNr(
+ pci, tac, nrarfcn, mcc, mnc, nci, alphal, alphas);
+ } else {
+ return cellIdentityReflector.newCellIdentityNr(
+ pci, tac, nrarfcn, bands, mcc, mnc, nci, alphal, alphas, additionalPlmns);
+ }
+ }
+
+ @ForType(CellIdentityNr.class)
+ private interface CellIdentityNrReflector {
+
+ @Constructor
+ CellIdentityNr newCellIdentityNr();
+
+ @Constructor
+ CellIdentityNr newCellIdentityNr(
+ int pci,
+ int tac,
+ int nrarfcn,
+ String mcc,
+ String mnc,
+ long nci,
+ String alphal,
+ String alphas);
+
+ @Constructor
+ CellIdentityNr newCellIdentityNr(
+ int pci,
+ int tac,
+ int nrarfcn,
+ int[] bands,
+ String mcc,
+ String mnc,
+ long nci,
+ String alphal,
+ String alphas,
+ Collection<String> additionalPlmns);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
new file mode 100644
index 000000000..6f3f93420
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
@@ -0,0 +1,143 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoLte;
+import android.telephony.CellSignalStrengthLte;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.WithType;
+
+/** Builder for {@link android.telephony.CellInfoLte}. */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellInfoLteBuilder {
+
+ private boolean isRegistered = false;
+ private long timeStamp = 0L;
+ private int cellConnectionStatus = 0;
+ private CellIdentityLte cellIdentity;
+ private CellSignalStrengthLte cellSignalStrength;
+
+ private CellInfoLteBuilder() {}
+
+ public static CellInfoLteBuilder newBuilder() {
+ return new CellInfoLteBuilder();
+ }
+
+ public CellInfoLteBuilder setRegistered(boolean isRegistered) {
+ this.isRegistered = isRegistered;
+ return this;
+ }
+
+ public CellInfoLteBuilder setTimeStampNanos(long timeStamp) {
+ this.timeStamp = timeStamp;
+ return this;
+ }
+
+ public CellInfoLteBuilder setCellConnectionStatus(int cellConnectionStatus) {
+ this.cellConnectionStatus = cellConnectionStatus;
+ return this;
+ }
+
+ public CellInfoLteBuilder setCellIdentity(CellIdentityLte cellIdentity) {
+ this.cellIdentity = cellIdentity;
+ return this;
+ }
+
+ public CellInfoLteBuilder setCellSignalStrength(CellSignalStrengthLte cellSignalStrength) {
+ this.cellSignalStrength = cellSignalStrength;
+ return this;
+ }
+
+ public CellInfoLte build() {
+ int apiLevel = RuntimeEnvironment.getApiLevel();
+ if (cellIdentity == null) {
+ if (apiLevel > Build.VERSION_CODES.Q) {
+ cellIdentity = CellIdentityLteBuilder.getDefaultInstance();
+ } else {
+ cellIdentity = CellIdentityLteBuilder.newBuilder().build();
+ }
+ }
+ if (cellSignalStrength == null) {
+ cellSignalStrength = CellSignalStrengthLteBuilder.getDefaultInstance();
+ }
+ CellInfoLteReflector cellInfoLteReflector = reflector(CellInfoLteReflector.class);
+ if (apiLevel < Build.VERSION_CODES.TIRAMISU) {
+ CellInfoLte cellInfo = cellInfoLteReflector.newCellInfoLte();
+ cellInfoLteReflector = reflector(CellInfoLteReflector.class, cellInfo);
+ cellInfoLteReflector.setCellIdentity(cellIdentity);
+ cellInfoLteReflector.setCellSignalStrength(cellSignalStrength);
+ CellInfoReflector cellInfoReflector = reflector(CellInfoReflector.class, cellInfo);
+ cellInfoReflector.setTimeStamp(timeStamp);
+ if (apiLevel <= Build.VERSION_CODES.KITKAT) {
+ cellInfoReflector.setRegisterd(isRegistered);
+ } else {
+ cellInfoReflector.setRegistered(isRegistered);
+ }
+ if (apiLevel > Build.VERSION_CODES.O_MR1) {
+ cellInfoReflector.setCellConnectionStatus(cellConnectionStatus);
+ }
+ return cellInfo;
+ } else {
+ try {
+ // This reflection is highly brittle but there is currently no choice as CellConfigLte is
+ // entirely @hide.
+ Class cellConfigLteClass = Class.forName("android.telephony.CellConfigLte");
+ return cellInfoLteReflector.newCellInfoLte(
+ cellConnectionStatus,
+ isRegistered,
+ timeStamp,
+ cellIdentity,
+ cellSignalStrength,
+ ReflectionHelpers.callConstructor(cellConfigLteClass));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @ForType(CellInfoLte.class)
+ private interface CellInfoLteReflector {
+ @Constructor
+ CellInfoLte newCellInfoLte();
+
+ @Constructor
+ CellInfoLte newCellInfoLte(
+ int cellConnectionStatus,
+ boolean isRegistered,
+ long timeStamp,
+ CellIdentityLte cellIdentity,
+ CellSignalStrengthLte cellSignalStrength,
+ @WithType("android.telephony.CellConfigLte") Object cellConfigLte);
+
+ @Accessor("mCellIdentityLte")
+ void setCellIdentity(CellIdentityLte cellIdentity);
+
+ @Accessor("mCellSignalStrengthLte")
+ void setCellSignalStrength(CellSignalStrengthLte cellSignalStrength);
+ }
+
+ @ForType(CellInfo.class)
+ private interface CellInfoReflector {
+
+ // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/kitkat-release/telephony/java/android/telephony/CellInfo.java#79
+ @Accessor("mRegistered")
+ void setRegisterd(boolean registered); // NOTYPO
+
+ @Accessor("mRegistered")
+ void setRegistered(boolean registered);
+
+ @Accessor("mTimeStamp")
+ void setTimeStamp(long registered);
+
+ @Accessor("mCellConnectionStatus")
+ void setCellConnectionStatus(int cellConnectionStatus);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java
new file mode 100644
index 000000000..78195246e
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java
@@ -0,0 +1,93 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfoNr;
+import android.telephony.CellSignalStrengthNr;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellInfoNr}. */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellInfoNrBuilder {
+
+ private boolean isRegistered = false;
+ private long timeStamp = 0L;
+ private int cellConnectionStatus = 0;
+ private CellIdentityNr cellIdentity;
+ private CellSignalStrengthNr cellSignalStrength;
+
+ private CellInfoNrBuilder() {}
+
+ public static CellInfoNrBuilder newBuilder() {
+ return new CellInfoNrBuilder();
+ }
+
+ public CellInfoNrBuilder setRegistered(boolean isRegistered) {
+ this.isRegistered = isRegistered;
+ return this;
+ }
+
+ public CellInfoNrBuilder setTimeStampNanos(long timeStamp) {
+ this.timeStamp = timeStamp;
+ return this;
+ }
+
+ public CellInfoNrBuilder setCellConnectionStatus(int cellConnectionStatus) {
+ this.cellConnectionStatus = cellConnectionStatus;
+ return this;
+ }
+
+ public CellInfoNrBuilder setCellIdentity(CellIdentityNr cellIdentity) {
+ this.cellIdentity = cellIdentity;
+ return this;
+ }
+
+ public CellInfoNrBuilder setCellSignalStrength(CellSignalStrengthNr cellSignalStrength) {
+ this.cellSignalStrength = cellSignalStrength;
+ return this;
+ }
+
+ public CellInfoNr build() {
+ if (cellIdentity == null) {
+ cellIdentity = CellIdentityNrBuilder.getDefaultInstance();
+ }
+ if (cellSignalStrength == null) {
+ cellSignalStrength = CellSignalStrengthNrBuilder.getDefaultInstance();
+ }
+ // CellInfoNr has no default constructor below T so we write it to a Parcel.
+ if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.TIRAMISU) {
+ Parcel p = Parcel.obtain();
+ p.writeInt(/* CellInfo#TYPE_NR */ 6);
+ p.writeInt(isRegistered ? 1 : 0);
+ p.writeLong(timeStamp);
+ p.writeInt(cellConnectionStatus);
+ cellIdentity.writeToParcel(p, 0);
+ cellSignalStrength.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ CellInfoNr cellInfoNr = CellInfoNr.CREATOR.createFromParcel(p);
+ p.recycle();
+ return cellInfoNr;
+ } else {
+ return reflector(CellInfoNrReflector.class)
+ .newCellInfoNr(
+ cellConnectionStatus, isRegistered, timeStamp, cellIdentity, cellSignalStrength);
+ }
+ }
+
+ @ForType(CellInfoNr.class)
+ private interface CellInfoNrReflector {
+ @Constructor
+ CellInfoNr newCellInfoNr(
+ int cellConnectionStatus,
+ boolean isRegistered,
+ long timeStamp,
+ CellIdentityNr cellIdentity,
+ CellSignalStrengthNr cellSignalStrength);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java
new file mode 100644
index 000000000..9b5d1a1ac
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java
@@ -0,0 +1,96 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthLte;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellSignalStrengthLte} */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellSignalStrengthLteBuilder {
+
+ private int rssi = CellInfo.UNAVAILABLE;
+ private int rsrp = CellInfo.UNAVAILABLE;
+ private int rsrq = CellInfo.UNAVAILABLE;
+ private int rssnr = CellInfo.UNAVAILABLE;
+ private int cqiTableIndex = CellInfo.UNAVAILABLE;
+ private int cqi = CellInfo.UNAVAILABLE;
+ private int timingAdvance = CellInfo.UNAVAILABLE;
+
+ private CellSignalStrengthLteBuilder() {}
+
+ public static CellSignalStrengthLteBuilder newBuilder() {
+ return new CellSignalStrengthLteBuilder();
+ }
+
+ protected static CellSignalStrengthLte getDefaultInstance() {
+ return reflector(CellSignalStrengthLteReflector.class).newCellSignalStrength();
+ }
+
+ /** This is equivalent to {@code signalStrength} pre SDK Q. */
+ public CellSignalStrengthLteBuilder setRssi(int rssi) {
+ this.rssi = rssi;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setRsrp(int rsrp) {
+ this.rsrp = rsrp;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setRsrq(int rsrq) {
+ this.rsrq = rsrq;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setRssnr(int rssnr) {
+ this.rssnr = rssnr;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setCqiTableIndex(int cqiTableIndex) {
+ this.cqiTableIndex = cqiTableIndex;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setCqi(int cqi) {
+ this.cqi = cqi;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setTimingAdvance(int timingAdvance) {
+ this.timingAdvance = timingAdvance;
+ return this;
+ }
+
+ public CellSignalStrengthLte build() {
+ CellSignalStrengthLteReflector cellSignalStrengthReflector =
+ reflector(CellSignalStrengthLteReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.S) {
+ return cellSignalStrengthReflector.newCellSignalStrength(
+ rssi, rsrp, rsrq, rssnr, cqi, timingAdvance);
+ } else {
+ return cellSignalStrengthReflector.newCellSignalStrength(
+ rssi, rsrp, rsrq, rssnr, cqiTableIndex, cqi, timingAdvance);
+ }
+ }
+
+ @ForType(CellSignalStrengthLte.class)
+ private interface CellSignalStrengthLteReflector {
+ @Constructor
+ CellSignalStrengthLte newCellSignalStrength();
+
+ @Constructor
+ CellSignalStrengthLte newCellSignalStrength(
+ int rssi, int rsrp, int rsrq, int rssnr, int cqi, int timingAdvance);
+
+ @Constructor
+ CellSignalStrengthLte newCellSignalStrength(
+ int rssi, int rsrp, int rsrq, int rssnr, int cqiTableIndex, int cqi, int timingAdvance);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java
new file mode 100644
index 000000000..4f3f859bc
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java
@@ -0,0 +1,140 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthNr;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellSignalStrengthNr} */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellSignalStrengthNrBuilder {
+
+ private int csiRrsp = CellInfo.UNAVAILABLE;
+ private int csiRsrq = CellInfo.UNAVAILABLE;
+ private int csiSinr = CellInfo.UNAVAILABLE;
+ private int csiCqiTableIndex = CellInfo.UNAVAILABLE;
+ private List<Byte> csiCqiReport = new ArrayList<>();
+ private int ssRsrp = CellInfo.UNAVAILABLE;
+ private int ssRsrq = CellInfo.UNAVAILABLE;
+ private int ssSinr = CellInfo.UNAVAILABLE;
+ private int timingAdvance = CellInfo.UNAVAILABLE;
+
+ private CellSignalStrengthNrBuilder() {}
+
+ public static CellSignalStrengthNrBuilder newBuilder() {
+ return new CellSignalStrengthNrBuilder();
+ }
+
+ protected static CellSignalStrengthNr getDefaultInstance() {
+ return reflector(CellSignalStrengthNrReflector.class).newCellSignalStrengthNr();
+ }
+
+ public CellSignalStrengthNrBuilder setCsiRsrp(int csiRrsp) {
+ this.csiRrsp = csiRrsp;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiRsrq(int csiRsrq) {
+ this.csiRsrq = csiRsrq;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiSinr(int csiSinr) {
+ this.csiSinr = csiSinr;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiCqiTableIndex(int csiCqiTableIndex) {
+ this.csiCqiTableIndex = csiCqiTableIndex;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiCqiReport(List<Byte> csiCqiReport) {
+ this.csiCqiReport = csiCqiReport;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setSsRsrp(int ssRsrp) {
+ this.ssRsrp = ssRsrp;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setSsRsrq(int ssRsrq) {
+ this.ssRsrq = ssRsrq;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setSsSinr(int ssSinr) {
+ this.ssSinr = ssSinr;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setTimingAdvance(int timingAdvance) {
+ this.timingAdvance = timingAdvance;
+ return this;
+ }
+
+ public CellSignalStrengthNr build() {
+ CellSignalStrengthNrReflector cellSignalStrengthReflector =
+ reflector(CellSignalStrengthNrReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.TIRAMISU) {
+ return cellSignalStrengthReflector.newCellSignalStrengthNr(
+ csiRrsp, csiRsrq, csiSinr, ssRsrp, ssRsrq, ssSinr);
+ } else if (RuntimeEnvironment.getApiLevel() == Build.VERSION_CODES.TIRAMISU) {
+ return cellSignalStrengthReflector.newCellSignalStrengthNr(
+ csiRrsp, csiRsrq, csiSinr, csiCqiTableIndex, csiCqiReport, ssRsrp, ssRsrq, ssSinr);
+ } else {
+ return cellSignalStrengthReflector.newCellSignalStrengthNr(
+ csiRrsp,
+ csiRsrq,
+ csiSinr,
+ csiCqiTableIndex,
+ csiCqiReport,
+ ssRsrp,
+ ssRsrq,
+ ssSinr,
+ timingAdvance);
+ }
+ }
+
+ @ForType(CellSignalStrengthNr.class)
+ private interface CellSignalStrengthNrReflector {
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr();
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr(
+ int csRsrp, int csiRsrq, int csiSinr, int ssRsrp, int ssRsrq, int ssSinr);
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr(
+ int csRsrp,
+ int csiRsrq,
+ int csiSinr,
+ int csiCqiTableIndex,
+ List<Byte> csiCqiReport,
+ int ssRsrp,
+ int ssRsrq,
+ int ssSinr);
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr(
+ int csRsrp,
+ int csiRsrq,
+ int csiSinr,
+ int csiCqiTableIndex,
+ List<Byte> csiCqiReport,
+ int ssRsrp,
+ int ssRsrq,
+ int ssSinr,
+ int timingAdvance);
+ }
+}
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 841f7d5f8..459340273 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
@@ -10,6 +10,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecInfo.EncoderCapabilities;
import android.media.MediaCodecInfo.VideoCapabilities;
import android.media.MediaFormat;
+import android.util.Range;
import com.google.common.base.Preconditions;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
@@ -266,6 +267,17 @@ public class MediaCodecInfoBuilder {
void setFlagsSupported(int flagsSupported);
}
+ /** Accessor interface for {@link VideoCapabilities}'s internals. */
+ @ForType(VideoCapabilities.class)
+ interface VideoCapabilitiesReflector {
+
+ @Accessor("mWidthRange")
+ void setWidthRange(Range<Integer> range);
+
+ @Accessor("mHeightRange")
+ void setHeightRange(Range<Integer> range);
+ }
+
public CodecCapabilities build() {
Preconditions.checkNotNull(mediaFormat, "mediaFormat is not set.");
Preconditions.checkNotNull(profileLevels, "profileLevels is not set.");
@@ -298,6 +310,16 @@ public class MediaCodecInfoBuilder {
if (isVideoCodec) {
VideoCapabilities videoCaps = createDefaultVideoCapabilities(caps, mediaFormat);
+ VideoCapabilitiesReflector videoCapsReflector =
+ Reflector.reflector(VideoCapabilitiesReflector.class, videoCaps);
+ if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) {
+ videoCapsReflector.setWidthRange(
+ new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_WIDTH)));
+ }
+ 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);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
index 5da1409f3..d23045b24 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import android.os.Build;
+import android.os.Build.VERSION_CODES;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadow.api.ShadowPicker;
@@ -10,6 +11,7 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> {
private Class<? extends T> binaryShadowClass;
private Class<? extends T> binary9ShadowClass;
private Class<? extends T> binary10ShadowClass;
+ private Class<? extends T> binary14ShadowClass;
public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass,
Class<? extends T> binaryShadowClass,
@@ -18,16 +20,19 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> {
this.binaryShadowClass = binaryShadowClass;
this.binary9ShadowClass = binary9ShadowClass;
this.binary10ShadowClass = binary9ShadowClass;
+ this.binary14ShadowClass = binary9ShadowClass;
}
public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass,
Class<? extends T> binaryShadowClass,
Class<? extends T> binary9ShadowClass,
- Class<? extends T> binary10ShadowClass) {
+ Class<? extends T> binary10ShadowClass,
+ Class<? extends T> binary14ShadowClass) {
this.legacyShadowClass = legacyShadowClass;
this.binaryShadowClass = binaryShadowClass;
this.binary9ShadowClass = binary9ShadowClass;
this.binary10ShadowClass = binary10ShadowClass;
+ this.binary14ShadowClass = binary14ShadowClass;
}
@Override
@@ -35,10 +40,11 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> {
if (ShadowAssetManager.useLegacy()) {
return legacyShadowClass;
} else {
- if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
+ if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) {
+ return binary14ShadowClass;
+ } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
return binary10ShadowClass;
- } else
- if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) {
+ } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) {
return binary9ShadowClass;
} else {
return binaryShadowClass;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
index 47f7306a4..1d575a4b9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
@@ -426,10 +426,10 @@ public class ShadowActivity extends ShadowContextThemeWrapper {
@Implementation
protected void runOnUiThread(Runnable action) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
+ } else {
+ reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action);
}
}
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 883dd2cad..70464bf9c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
@@ -26,7 +26,6 @@ import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import javax.annotation.Nonnull;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
@@ -34,6 +33,7 @@ import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.annotation.Resetter;
+import org.robolectric.util.Logger;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
@@ -275,7 +275,12 @@ public class ShadowActivityThread {
@Resetter
public static void reset() {
Object activityThread = RuntimeEnvironment.getActivityThread();
- Objects.requireNonNull(activityThread, "ShadowActivityThread.reset: ActivityThread not set");
- reflector(_ActivityThread_.class, activityThread).getActivities().clear();
+ if (activityThread == null) {
+ Logger.warn(
+ "RuntimeEnvironment.getActivityThread() is null, an error likely occurred during test"
+ + " initialization.");
+ } else {
+ reflector(_ActivityThread_.class, activityThread).getActivities().clear();
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
index eb2276c9e..eb2276c9e 100755..100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
index c0c9c7a8e..c0c9c7a8e 100755..100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
new file mode 100644
index 000000000..8771d6adb
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
@@ -0,0 +1,72 @@
+package org.robolectric.shadows;
+
+
+import android.annotation.Nullable;
+import android.content.res.AssetManager;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+// 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,
+ shadowPicker = ShadowAssetManager.Picker.class)
+@SuppressWarnings("NewApi")
+public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 {
+
+ // static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint
+ // mnc,
+ // jstring locale, jint orientation, jint touchscreen, jint
+ // density,
+ // jint keyboard, jint keyboard_hidden, jint navigation,
+ // jint screen_width, jint screen_height,
+ // 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)
+ protected static void nativeSetConfiguration(
+ long ptr,
+ int mcc,
+ int mnc,
+ @Nullable String locale,
+ int orientation,
+ int touchscreen,
+ int density,
+ int keyboard,
+ int keyboard_hidden,
+ int navigation,
+ int screen_width,
+ int screen_height,
+ int smallest_screen_width_dp,
+ int screen_width_dp,
+ int screen_height_dp,
+ int screen_layout,
+ int ui_mode,
+ int color_mode,
+ int grammaticalGender, // ignore for now?
+ int major_version) {
+ ShadowArscAssetManager10.nativeSetConfiguration(
+ ptr,
+ mcc,
+ mnc,
+ locale,
+ orientation,
+ touchscreen,
+ density,
+ keyboard,
+ keyboard_hidden,
+ navigation,
+ screen_width,
+ screen_height,
+ smallest_screen_width_dp,
+ screen_width_dp,
+ screen_height_dp,
+ screen_layout,
+ ui_mode,
+ color_mode,
+ major_version);
+ }
+}
+// namespace android
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
index 1f6e40ddf..19c5196f0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
@@ -25,7 +25,8 @@ abstract public class ShadowAssetManager {
ShadowLegacyAssetManager.class,
ShadowArscAssetManager.class,
ShadowArscAssetManager9.class,
- ShadowArscAssetManager10.class);
+ ShadowArscAssetManager10.class,
+ ShadowArscAssetManager14.class);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
index 223435110..5f2d4fd3c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
@@ -93,6 +93,7 @@ public class ShadowAudioManager {
private ImmutableList<Object> defaultDevicesForAttributes = ImmutableList.of();
private List<AudioDeviceInfo> inputDevices = new ArrayList<>();
private List<AudioDeviceInfo> outputDevices = new ArrayList<>();
+ private List<AudioDeviceInfo> availableCommunicationDevices = new ArrayList<>();
private AudioDeviceInfo communicationDevice = null;
public ShadowAudioManager() {
@@ -451,6 +452,23 @@ public class ShadowAudioManager {
}
/**
+ * Sets the list of available communication devices represented by {@link AudioDeviceInfo}.
+ *
+ * <p>The previous list of communication devices is replaced and no notifications of the list of
+ * {@link AudioDeviceCallback} is done.
+ *
+ * <p>To add/remove devices one by one and trigger notifications for the list of {@link
+ * AudioDeviceCallback} please use one of the following methods {@link
+ * #addOutputDevice(AudioDeviceInfo, boolean)}, {@link #removeOutputDevice(AudioDeviceInfo,
+ * boolean)}.
+ */
+ @TargetApi(VERSION_CODES.S)
+ public void setAvailableCommunicationDevices(
+ List<AudioDeviceInfo> availableCommunicationDevices) {
+ this.availableCommunicationDevices = new ArrayList<>(availableCommunicationDevices);
+ }
+
+ /**
* Adds an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if
* the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}.
*/
@@ -497,6 +515,36 @@ public class ShadowAudioManager {
}
/**
+ * Adds an available communication {@link AudioDeviceInfo} and notifies the list of {@link
+ * AudioDeviceCallback} if the device was not present before and indicated by {@code
+ * notifyAudioDeviceCallbacks}.
+ */
+ @TargetApi(VERSION_CODES.S)
+ public void addAvailableCommunicationDevice(
+ AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed =
+ !this.availableCommunicationDevices.contains(communicationDevice)
+ && this.availableCommunicationDevices.add(communicationDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ true);
+ }
+ }
+
+ /**
+ * Removes an available communication {@link AudioDeviceInfo} and notifies the list of {@link
+ * AudioDeviceCallback} if the device was present before and indicated by {@code
+ * notifyAudioDeviceCallbacks}.
+ */
+ @TargetApi(VERSION_CODES.S)
+ public void removeAvailableCommunicationDevice(
+ AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed = this.availableCommunicationDevices.remove(communicationDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ false);
+ }
+ }
+
+ /**
* Registers an {@link AudioDeviceCallback} object to receive notifications of changes to the set
* of connected audio devices.
*
@@ -504,8 +552,10 @@ public class ShadowAudioManager {
*
* @see #addInputDevice(AudioDeviceInfo, boolean)
* @see #addOutputDevice(AudioDeviceInfo, boolean)
+ * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean)
* @see #removeInputDevice(AudioDeviceInfo, boolean)
* @see #removeOutputDevice(AudioDeviceInfo, boolean)
+ * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean)
*/
@Implementation(minSdk = M)
protected void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) {
@@ -520,8 +570,10 @@ public class ShadowAudioManager {
*
* @see #addInputDevice(AudioDeviceInfo, boolean)
* @see #addOutputDevice(AudioDeviceInfo, boolean)
+ * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean)
* @see #removeInputDevice(AudioDeviceInfo, boolean)
* @see #removeOutputDevice(AudioDeviceInfo, boolean)
+ * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean)
*/
@Implementation(minSdk = M)
protected void unregisterAudioDeviceCallback(AudioDeviceCallback callback) {
@@ -563,6 +615,11 @@ public class ShadowAudioManager {
this.communicationDevice = null;
}
+ @Implementation(minSdk = S)
+ protected List<AudioDeviceInfo> getAvailableCommunicationDevices() {
+ return availableCommunicationDevices;
+ }
+
@Implementation(minSdk = M)
public AudioDeviceInfo[] getDevices(int flags) {
List<AudioDeviceInfo> result = new ArrayList<>();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
index 5b051405f..c1e78be00 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
@@ -3,10 +3,23 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static com.google.common.base.Preconditions.checkNotNull;
+import android.annotation.NonNull;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
import android.media.AudioSystem;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Table;
+import com.google.common.collect.Tables;
+import java.util.Optional;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
/** Shadow for {@link AudioSystem}. */
@Implements(value = AudioSystem.class, isInAndroidSdk = false)
@@ -17,6 +30,33 @@ public class ShadowAudioSystem {
private static final int MAX_SAMPLE_RATE = 192000;
private static final int MIN_SAMPLE_RATE = 4000;
+ /**
+ * Table to store key-pair of {@link AudioFormat} and {@link AudioAttributes#getUsage()} with
+ * value of support for Direct Playback. Used with {@link #setDirectPlaybackSupport(AudioFormat,
+ * AudioAttributes, int)}, and {@link #getDirectPlaybackSupport(AudioFormat, AudioAttributes)}.
+ */
+ private static final Table<AudioFormat, Integer, Integer> directPlaybackSupportTable =
+ Tables.synchronizedTable(HashBasedTable.create());
+ /**
+ * Table to store pair of {@link OffloadSupportFormat} and {@link
+ * AudioAttributes#getVolumeControlStream()} with a value of Offload Playback support. Used with
+ * {@link #native_get_offload_support}. The table uses {@link OffloadSupportFormat} rather than
+ * {@link AudioFormat} because {@link #native_get_offload_support} does not pass all the fields
+ * needed to reliably reconstruct {@link AudioFormat} instances.
+ */
+ private static final Table<OffloadSupportFormat, Integer, Integer> offloadPlaybackSupportTable =
+ Tables.synchronizedTable(HashBasedTable.create());
+
+ /**
+ * Multimap to store whether a pair of {@link OffloadSupportFormat} and {@link
+ * AudioAttributes#getVolumeControlStream()} ()} support offloaded playback. Used with {@link
+ * #native_is_offload_supported}. The map uses {@link OffloadSupportFormat} keys rather than
+ * {@link AudioFormat} because {@link #native_is_offload_supported} does not pass all the fields
+ * needed to reliably reconstruct {@link AudioFormat} instances.
+ */
+ private static final Multimap<OffloadSupportFormat, Integer> offloadSupportedMap =
+ Multimaps.synchronizedMultimap(HashMultimap.create());
+
@Implementation(minSdk = S)
protected static int native_getMaxChannelCount() {
return MAX_CHANNEL_COUNT;
@@ -38,4 +78,156 @@ public class ShadowAudioSystem {
// https://cs.android.com/android/platform/superproject/+/master:system/media/audio/include/system/audio-base.h;l=197;drc=c84ca89fa5d660046364897482b202c797c8595e
return 8;
}
+
+ /**
+ * Sets direct playback support for a key-pair of {@link AudioFormat} and {@link AudioAttributes}.
+ * As a result, calling {@link #getDirectPlaybackSupport} with the same pair of {@link
+ * AudioFormat} and {@link AudioAttributes} values will return the cached support value.
+ *
+ * @param format the audio format (codec, sample rate, channels)
+ * @param attr the {@link AudioAttributes} to be used for playback
+ * @param directPlaybackSupport the level of direct playback support to save for the format and
+ * attribute pair. Must be one of {@link AudioSystem#DIRECT_NOT_SUPPORTED}, {@link
+ * AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link AudioSystem#OFFLOAD_SUPPORTED}, {@link
+ * AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}, or a combination of {@link
+ * AudioSystem#DIRECT_OFFLOAD_SUPPORTED}, {@link AudioSystem#DIRECT_OFFLOAD_GAPLESS_SUPPORTED}
+ * and {@link AudioSystem#DIRECT_BITSTREAM_SUPPORTED}
+ */
+ public static void setDirectPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr, int directPlaybackSupport) {
+ checkNotNull(format, "Illegal null AudioFormat");
+ checkNotNull(attr, "Illegal null AudioAttributes");
+ directPlaybackSupportTable.put(format, attr.getUsage(), directPlaybackSupport);
+ }
+
+ /**
+ * Retrieves the stored direct playback support for the {@link AudioFormat} and {@link
+ * AudioAttributes}. If no value was stored for the key-pair then {@link
+ * AudioSystem#DIRECT_NOT_SUPPORTED} is returned.
+ *
+ * @param format the audio format (codec, sample rate, channels) to be used for playback
+ * @param attr the {@link AudioAttributes} to be used for playback
+ * @return the level of direct playback playback support for the format and attributes.
+ */
+ @Implementation(minSdk = TIRAMISU)
+ protected static int getDirectPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr) {
+ return Optional.ofNullable(directPlaybackSupportTable.get(format, attr.getUsage()))
+ .orElse(AudioSystem.DIRECT_NOT_SUPPORTED);
+ }
+
+ /**
+ * Sets offload playback support for a key-pair of {@link AudioFormat} and {@link
+ * AudioAttributes}. As a result, calling {@link AudioSystem#getOffloadSupport} with the same pair
+ * of {@link AudioFormat} and {@link AudioAttributes} values will return the cached support value.
+ *
+ * @param format the audio format (codec, sample rate, channels)
+ * @param attr the {@link AudioAttributes} to be used for playback
+ * @param offloadSupport the level of offload playback support to save for the format and
+ * attribute pair. Must be one of {@link AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link
+ * AudioSystem#OFFLOAD_SUPPORTED} or {@link AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}.
+ */
+ public static void setOffloadPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr, int offloadSupport) {
+ checkNotNull(format, "Illegal null AudioFormat");
+ checkNotNull(attr, "Illegal null AudioAttributes");
+ offloadPlaybackSupportTable.put(
+ new OffloadSupportFormat(
+ format.getEncoding(),
+ format.getSampleRate(),
+ format.getChannelMask(),
+ format.getChannelIndexMask()),
+ attr.getVolumeControlStream(),
+ offloadSupport);
+ }
+
+ /**
+ * Sets whether offload playback is supported for a key-pair of {@link AudioFormat} and {@link
+ * AudioAttributes}. As a result, calling {@link AudioSystem#isOffloadSupported} with the same
+ * pair of {@link AudioFormat} and {@link AudioAttributes} values will return {@code supported}.
+ *
+ * @param format the audio format (codec, sample rate, channels)
+ * @param attr the {@link AudioAttributes} to be used for playback
+ */
+ public static void setOffloadSupported(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr, boolean supported) {
+ OffloadSupportFormat offloadSupportFormat =
+ new OffloadSupportFormat(
+ format.getEncoding(),
+ format.getSampleRate(),
+ format.getChannelMask(),
+ format.getChannelIndexMask());
+ if (supported) {
+ offloadSupportedMap.put(offloadSupportFormat, attr.getVolumeControlStream());
+ } else {
+ offloadSupportedMap.remove(offloadSupportFormat, attr.getVolumeControlStream());
+ }
+ }
+
+ @Implementation(minSdk = Q, maxSdk = R)
+ protected static boolean native_is_offload_supported(
+ int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) {
+ return offloadSupportedMap.containsEntry(
+ new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask), streamType);
+ }
+
+ @Implementation(minSdk = S)
+ protected static int native_get_offload_support(
+ int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) {
+ return Optional.ofNullable(
+ offloadPlaybackSupportTable.get(
+ new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask),
+ streamType))
+ .orElse(AudioSystem.OFFLOAD_NOT_SUPPORTED);
+ }
+
+ @Resetter
+ public static void reset() {
+ directPlaybackSupportTable.clear();
+ offloadPlaybackSupportTable.clear();
+ offloadSupportedMap.clear();
+ }
+
+ /**
+ * Struct to hold specific values from {@link AudioFormat} which are used in {@link
+ * #native_get_offload_support} and {@link #native_is_offload_supported}.
+ */
+ private static class OffloadSupportFormat {
+ public final int encoding;
+ public final int sampleRate;
+ public final int channelMask;
+ public final int channelIndexMask;
+
+ public OffloadSupportFormat(
+ int encoding, int sampleRate, int channelMask, int channelIndexMask) {
+ this.encoding = encoding;
+ this.sampleRate = sampleRate;
+ this.channelMask = channelMask;
+ this.channelIndexMask = channelIndexMask;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OffloadSupportFormat)) {
+ return false;
+ }
+ OffloadSupportFormat that = (OffloadSupportFormat) o;
+ return encoding == that.encoding
+ && sampleRate == that.sampleRate
+ && channelMask == that.channelMask
+ && channelIndexMask == that.channelIndexMask;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = encoding;
+ result = 31 * result + sampleRate;
+ result = 31 * result + channelMask;
+ result = 31 * result + channelIndexMask;
+ return result;
+ }
+ }
}
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 45a5557a6..6408ce87c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
@@ -1,20 +1,37 @@
package org.robolectric.shadows;
import static android.media.AudioTrack.ERROR_BAD_VALUE;
+import static android.media.AudioTrack.ERROR_DEAD_OBJECT;
import static android.media.AudioTrack.WRITE_BLOCKING;
import static android.media.AudioTrack.WRITE_NON_BLOCKING;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
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.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
import android.annotation.NonNull;
+import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.media.AudioTrack.WriteMode;
+import android.media.PlaybackParams;
+import android.os.Build.VERSION;
+import android.os.Parcel;
import android.util.Log;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -50,11 +67,24 @@ public class ShadowAudioTrack {
protected static final int DEFAULT_MIN_BUFFER_SIZE = 1024;
+ // Copied from native code
+ // https://cs.android.com/android/platform/superproject/+/android13-release:frameworks/base/core/jni/android_media_AudioTrack.cpp?q=AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED
+ private static final int AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED = -20;
+
private static final String TAG = "ShadowAudioTrack";
- private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE;
+ /** Direct playback support checked from {@link #native_is_direct_output_supported}. */
+ private static final Multimap<AudioFormatInfo, AudioAttributesInfo> directSupportedFormats =
+ Multimaps.synchronizedMultimap(HashMultimap.create());
+ /** Non-PCM encodings allowed for creating an AudioTrack instance. */
+ private static final Set<Integer> allowedNonPcmEncodings =
+ Collections.synchronizedSet(new HashSet<>());
+
private static final List<OnAudioDataWrittenListener> audioDataWrittenListeners =
new CopyOnWriteArrayList<>();
+ private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE;
+
private int numBytesReceived;
+ private PlaybackParams playbackParams;
@RealObject AudioTrack audioTrack;
/**
@@ -67,6 +97,61 @@ public class ShadowAudioTrack {
minBufferSize = bufferSize;
}
+ /**
+ * Adds support for direct playback for the pair of {@link AudioFormat} and {@link
+ * AudioAttributes} where the format encoding must be non-PCM. Calling {@link
+ * AudioTrack#isDirectPlaybackSupported(AudioFormat, AudioAttributes)} will return {@code true}
+ * for matching {@link AudioFormat} and {@link AudioAttributes}. The matching is performed against
+ * the format's {@linkplain AudioFormat#getEncoding() encoding}, {@linkplain
+ * AudioFormat#getSampleRate() sample rate}, {@linkplain AudioFormat#getChannelMask() channel
+ * mask} and {@linkplain AudioFormat#getChannelIndexMask() channel index mask}, and the
+ * attribute's {@linkplain AudioAttributes#getContentType() content type}, {@linkplain
+ * AudioAttributes#getUsage() usage} and {@linkplain AudioAttributes#getFlags() flags}.
+ *
+ * @param format The {@link AudioFormat}, which must be of a non-PCM encoding. If the encoding is
+ * PCM, the method will throw an {@link IllegalArgumentException}.
+ * @param attr The {@link AudioAttributes}.
+ */
+ public static void addDirectPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr) {
+ checkNotNull(format);
+ checkNotNull(attr);
+ checkArgument(!isPcm(format.getEncoding()));
+
+ directSupportedFormats.put(
+ new AudioFormatInfo(
+ format.getEncoding(),
+ format.getSampleRate(),
+ format.getChannelMask(),
+ format.getChannelIndexMask()),
+ new AudioAttributesInfo(attr.getContentType(), attr.getUsage(), attr.getFlags()));
+ }
+
+ /**
+ * Clears all encodings that have been added for direct playback support with {@link
+ * #addDirectPlaybackSupport}.
+ */
+ public static void clearDirectPlaybackSupportedFormats() {
+ directSupportedFormats.clear();
+ }
+
+ /**
+ * Add a non-PCM encoding for which {@link AudioTrack} instances are allowed to be created.
+ *
+ * @param encoding One of {@link AudioFormat} {@code ENCODING_} constants that represents a
+ * non-PCM encoding. If {@code encoding} is PCM, this method throws an {@link
+ * IllegalArgumentException}.
+ */
+ public static void addAllowedNonPcmEncoding(int encoding) {
+ checkArgument(!isPcm(encoding));
+ allowedNonPcmEncodings.add(encoding);
+ }
+
+ /** Clears all encodings that have been added with {@link #addAllowedNonPcmEncoding(int)}. */
+ public static void clearAllowedNonPcmEncodings() {
+ allowedNonPcmEncodings.clear();
+ }
+
@Implementation(minSdk = N, maxSdk = P)
protected static int native_get_FCC_8() {
// Return the value hard-coded in native code:
@@ -74,6 +159,20 @@ public class ShadowAudioTrack {
return 8;
}
+ @Implementation(minSdk = Q)
+ protected static boolean native_is_direct_output_supported(
+ int encoding,
+ int sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int contentType,
+ int usage,
+ int flags) {
+ return directSupportedFormats.containsEntry(
+ new AudioFormatInfo(encoding, sampleRate, channelMask, channelIndexMask),
+ new AudioAttributesInfo(contentType, usage, flags));
+ }
+
/** Returns a predefined or default minimum buffer size. Audio format and config are neglected. */
@Implementation
protected static int native_get_min_buff_size(
@@ -81,24 +180,141 @@ public class ShadowAudioTrack {
return minBufferSize;
}
+ @Implementation(minSdk = P, maxSdk = Q)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ long nativeAudioTrack,
+ boolean offload) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
+ @Implementation(minSdk = R, maxSdk = R)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ long nativeAudioTrack,
+ boolean offload,
+ int encapsulationMode,
+ Object tunerConfiguration) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
+ @Implementation(minSdk = S, maxSdk = TIRAMISU)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ long nativeAudioTrack,
+ boolean offload,
+ int encapsulationMode,
+ Object tunerConfiguration,
+ String opPackageName) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ @NonNull Parcel attributionSource,
+ long nativeAudioTrack,
+ boolean offload,
+ int encapsulationMode,
+ Object tunerConfiguration,
+ @NonNull String opPackageName) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
/**
- * Always return the number of bytes to write. This method returns immedidately even with {@link
- * AudioTrack#WRITE_BLOCKING}
+ * Returns the number of bytes to write. This method returns immediately even with {@link
+ * AudioTrack#WRITE_BLOCKING}. If the {@link AudioTrack} instance was created with a non-PCM
+ * encoding and the encoding can no longer be played directly, the method will return {@link
+ * AudioTrack#ERROR_DEAD_OBJECT};
*/
@Implementation(minSdk = M)
protected final int native_write_byte(
byte[] audioData, int offsetInBytes, int sizeInBytes, int format, boolean isBlocking) {
+ int encoding = audioTrack.getAudioFormat();
+ // Assume that offload support does not change during the lifetime of the instance.
+ if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback())
+ && !isPcm(encoding)
+ && !allowedNonPcmEncodings.contains(encoding)) {
+ return ERROR_DEAD_OBJECT;
+ }
return sizeInBytes;
}
+ @Implementation(minSdk = M)
+ public void setPlaybackParams(@NonNull PlaybackParams params) {
+ playbackParams = checkNotNull(params, "Illegal null params");
+ }
+
+ @Implementation(minSdk = M)
+ @NonNull
+ protected final PlaybackParams getPlaybackParams() {
+ return playbackParams;
+ }
+
/**
- * Always return the number of bytes to write except with invalid parameters. Assumes AudioTrack
- * is already initialized (object properly created). Do not block even if AudioTrack in offload
- * mode is in STOPPING play state. This method returns immediately even with {@link
- * AudioTrack#WRITE_BLOCKING}
+ * Returns the number of bytes to write, except with invalid parameters. If the {@link AudioTrack}
+ * was created for a non-PCM encoding that can no longer be played directly, it returns {@link
+ * AudioTrack#ERROR_DEAD_OBJECT}. Assumes {@link AudioTrack} is already initialized (object
+ * properly created). Do not block even if {@link AudioTrack} in offload mode is in STOPPING play
+ * state. This method returns immediately even with {@link AudioTrack#WRITE_BLOCKING}
*/
@Implementation(minSdk = LOLLIPOP)
protected int write(@NonNull ByteBuffer audioData, int sizeInBytes, @WriteMode int writeMode) {
+ int encoding = audioTrack.getAudioFormat();
+ // Assume that offload support does not change during the lifetime of the instance.
+ if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback())
+ && !isPcm(encoding)
+ && !allowedNonPcmEncodings.contains(encoding)) {
+ return ERROR_DEAD_OBJECT;
+ }
if (writeMode != WRITE_BLOCKING && writeMode != WRITE_NON_BLOCKING) {
Log.e(TAG, "ShadowAudioTrack.write() called with invalid blocking mode");
return ERROR_BAD_VALUE;
@@ -150,5 +366,103 @@ public class ShadowAudioTrack {
@Resetter
public static void resetTest() {
audioDataWrittenListeners.clear();
+ clearDirectPlaybackSupportedFormats();
+ clearAllowedNonPcmEncodings();
+ }
+
+ private static boolean isPcm(int encoding) {
+ switch (encoding) {
+ case AudioFormat.ENCODING_PCM_8BIT:
+ case AudioFormat.ENCODING_PCM_16BIT:
+ case AudioFormat.ENCODING_PCM_24BIT_PACKED:
+ case AudioFormat.ENCODING_PCM_32BIT:
+ case AudioFormat.ENCODING_PCM_FLOAT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Specific fields from {@link AudioFormat} that are used for detection of direct playback
+ * support.
+ *
+ * @see #native_is_direct_output_supported
+ */
+ private static class AudioFormatInfo {
+ private final int encoding;
+ private final int sampleRate;
+ private final int channelMask;
+ private final int channelIndexMask;
+
+ public AudioFormatInfo(int encoding, int sampleRate, int channelMask, int channelIndexMask) {
+ this.encoding = encoding;
+ this.sampleRate = sampleRate;
+ this.channelMask = channelMask;
+ this.channelIndexMask = channelIndexMask;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AudioFormatInfo)) {
+ return false;
+ }
+
+ AudioFormatInfo other = (AudioFormatInfo) o;
+ return encoding == other.encoding
+ && sampleRate == other.sampleRate
+ && channelMask == other.channelMask
+ && channelIndexMask == other.channelIndexMask;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = encoding;
+ result = 31 * result + sampleRate;
+ result = 31 * result + channelMask;
+ result = 31 * result + channelIndexMask;
+ return result;
+ }
+ }
+
+ /**
+ * Specific fields from {@link AudioAttributes} used for detection of direct playback support.
+ *
+ * @see #native_is_direct_output_supported
+ */
+ private static class AudioAttributesInfo {
+ private final int contentType;
+ private final int usage;
+ private final int flags;
+
+ public AudioAttributesInfo(int contentType, int usage, int flags) {
+ this.contentType = contentType;
+ this.usage = usage;
+ this.flags = flags;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AudioAttributesInfo)) {
+ return false;
+ }
+
+ AudioAttributesInfo other = (AudioAttributesInfo) o;
+ return contentType == other.contentType && usage == other.usage && flags == other.flags;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = contentType;
+ result = 31 * result + usage;
+ result = 31 * result + flags;
+ return result;
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
index 737df1fb8..7b6ff7e37 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
@@ -18,6 +18,8 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.UUID;
+import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -40,8 +42,12 @@ public class ShadowBluetoothGatt {
private boolean isClosed = false;
private byte[] writtenBytes;
private byte[] readBytes;
+ // TODO: ShadowBluetoothGatt.services should be removed in favor of just using the real
+ // BluetoothGatt.mServices.
private final Set<BluetoothGattService> discoverableServices = new HashSet<>();
private final ArrayList<BluetoothGattService> services = new ArrayList<>();
+ private final Set<BluetoothGattCharacteristic> characteristicNotificationEnableSet =
+ new HashSet<>();
@RealObject private BluetoothGatt realBluetoothGatt;
@ReflectorObject protected BluetoothGattReflector bluetoothGattReflector;
@@ -185,6 +191,7 @@ public class ShadowBluetoothGatt {
protected boolean discoverServices() {
this.services.clear();
if (!this.discoverableServices.isEmpty()) {
+ // TODO: Don't store the services in the shadow.
this.services.addAll(this.discoverableServices);
if (this.getGattCallback() != null) {
@@ -204,10 +211,39 @@ public class ShadowBluetoothGatt {
*/
@Implementation(minSdk = O)
protected List<BluetoothGattService> getServices() {
+ // TODO: Remove this method when real BluetoothGatt#getServices() works.
return new ArrayList<>(this.services);
}
/**
+ * Overrides {@link BluetoothGatt#getService} to return a service with given UUID.
+ *
+ * @return a service with given UUID that have been discovered through {@link
+ * ShadowBluetoothGatt#discoverServices}.
+ */
+ @Implementation(minSdk = O)
+ @Nullable
+ protected BluetoothGattService getService(UUID uuid) {
+ // TODO: Remove this method when real BluetoothGatt#getService() works.
+ for (BluetoothGattService service : this.services) {
+ if (service.getUuid().equals(uuid)) {
+ return service;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Overrides {@link BluetoothGatt#setCharacteristicNotification} so it returns true (false) if
+ * allowCharacteristicNotification (disallowCharacteristicNotification) is called.
+ */
+ @Implementation(minSdk = O)
+ protected boolean setCharacteristicNotification(
+ BluetoothGattCharacteristic characteristic, boolean enable) {
+ return characteristicNotificationEnableSet.contains(characteristic) == enable;
+ }
+
+ /**
* Reads bytes from incoming characteristic if properties are valid and callback is set. Callback
* responds with {@link BluetoothGattCallback#onCharacteristicWrite} and returns true when
* successful.
@@ -258,6 +294,16 @@ public class ShadowBluetoothGatt {
return true;
}
+ /** Allows the incoming characteristic to be set to enable notification. */
+ public void allowCharacteristicNotification(BluetoothGattCharacteristic characteristic) {
+ characteristicNotificationEnableSet.add(characteristic);
+ }
+
+ /** Disallows the incoming characteristic to be set to enable notification. */
+ public void disallowCharacteristicNotification(BluetoothGattCharacteristic characteristic) {
+ characteristicNotificationEnableSet.remove(characteristic);
+ }
+
public void addDiscoverableService(BluetoothGattService service) {
this.discoverableServices.add(service);
}
@@ -294,6 +340,49 @@ public class ShadowBluetoothGatt {
return this.readBytes;
}
+ public BluetoothConnectionManager getBluetoothConnectionManager() {
+ return BluetoothConnectionManager.getInstance();
+ }
+
+ /**
+ * Simulate a successful Gatt Client Conection with {@link BluetoothConnectionManager}. Performs a
+ * {@link BluetoothGattCallback#onConnectionStateChange} if available.
+ *
+ * @param remoteAddress address of Gatt client
+ */
+ public void notifyConnection(String remoteAddress) {
+ BluetoothConnectionManager.getInstance().registerGattClientConnection(remoteAddress);
+ this.isConnected = true;
+ if (this.isCallbackAppropriate()) {
+ this.getGattCallback()
+ .onConnectionStateChange(
+ this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+ }
+ }
+
+ /**
+ * Simulate a successful Gatt Client Disconnection with {@link BluetoothConnectionManager}.
+ * Performs a {@link BluetoothGattCallback#onConnectionStateChange} if available.
+ *
+ * @param remoteAddress address of Gatt client
+ */
+ public void notifyDisconnection(String remoteAddress) {
+ BluetoothConnectionManager.getInstance().unregisterGattClientConnection(remoteAddress);
+ if (this.isCallbackAppropriate()) {
+ this.getGattCallback()
+ .onConnectionStateChange(
+ this.realBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS,
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+ this.isConnected = false;
+ }
+
+ private boolean isCallbackAppropriate() {
+ return this.getGattCallback() != null && this.isConnected;
+ }
+
+
@ForType(BluetoothGatt.class)
private interface BluetoothGattReflector {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
index 54b96265b..f13928859 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
@@ -3,13 +3,17 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.S;
+import static java.util.stream.Collectors.toCollection;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@@ -21,7 +25,8 @@ import org.robolectric.annotation.Implements;
@NotThreadSafe
@Implements(value = BluetoothHeadset.class)
public class ShadowBluetoothHeadset {
- private final List<BluetoothDevice> connectedDevices = new ArrayList<>();
+
+ private final Map<BluetoothDevice, Integer> bluetoothDevices = new HashMap<>();
private boolean allowsSendVendorSpecificResultCode = true;
private BluetoothDevice activeBluetoothDevice;
private boolean isVoiceRecognitionSupported = true;
@@ -32,12 +37,29 @@ public class ShadowBluetoothHeadset {
*/
@Implementation
protected List<BluetoothDevice> getConnectedDevices() {
- return connectedDevices;
+ return bluetoothDevices.entrySet().stream()
+ .filter(entry -> entry.getValue() == BluetoothProfile.STATE_CONNECTED)
+ .map(Entry::getKey)
+ .collect(toCollection(ArrayList::new));
}
/** Adds the given BluetoothDevice to the shadow's list of "connected devices" */
public void addConnectedDevice(BluetoothDevice device) {
- connectedDevices.add(device);
+ addDevice(device, BluetoothProfile.STATE_CONNECTED);
+ }
+
+ /**
+ * Adds the provided BluetoothDevice to the shadow profile's device list with an associated
+ * connectionState. The provided connection state will be returned by {@link
+ * ShadowBluetoothHeadset#getConnectionState}.
+ */
+ public void addDevice(BluetoothDevice bluetoothDevice, int connectionState) {
+ bluetoothDevices.put(bluetoothDevice, connectionState);
+ }
+
+ /** Remove the given BluetoothDevice from the shadow profile's device list */
+ public void removeDevice(BluetoothDevice bluetoothDevice) {
+ bluetoothDevices.remove(bluetoothDevice);
}
/**
@@ -49,9 +71,7 @@ public class ShadowBluetoothHeadset {
*/
@Implementation
protected int getConnectionState(BluetoothDevice device) {
- return connectedDevices.contains(device)
- ? BluetoothProfile.STATE_CONNECTED
- : BluetoothProfile.STATE_DISCONNECTED;
+ return bluetoothDevices.getOrDefault(device, BluetoothProfile.STATE_DISCONNECTED);
}
/**
@@ -63,7 +83,7 @@ public class ShadowBluetoothHeadset {
*/
@Implementation
protected boolean startVoiceRecognition(BluetoothDevice bluetoothDevice) {
- if (bluetoothDevice == null || !connectedDevices.contains(bluetoothDevice)) {
+ if (bluetoothDevice == null || !getConnectedDevices().contains(bluetoothDevice)) {
return false;
}
if (activeBluetoothDevice != null) {
@@ -113,7 +133,7 @@ public class ShadowBluetoothHeadset {
if (command == null) {
throw new IllegalArgumentException("Command cannot be null");
}
- return allowsSendVendorSpecificResultCode && connectedDevices.contains(device);
+ return allowsSendVendorSpecificResultCode && getConnectedDevices().contains(device);
}
@Nullable
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 b0cc137fe..c1c887acc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
@@ -23,6 +23,12 @@ 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.
+ */
+ public static final int UPSIDE_DOWN_CAKE = 34;
+
+ /**
* Sets the value of the {@link Build#DEVICE} field.
*
* <p>It will be reset for the next test.
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 55b9b68c6..19be93acc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
@@ -77,7 +77,18 @@ public class ShadowCameraManager {
cameraTorches.put(cameraId, enabled);
}
- @Implementation(minSdk = Build.VERSION_CODES.S)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected CameraDevice openCameraDeviceUserAsync(
+ String cameraId,
+ CameraDevice.StateCallback callback,
+ Executor executor,
+ final int uid,
+ final int oomScoreOffset,
+ boolean overrideToPortrait) {
+ return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset);
+ }
+
+ @Implementation(minSdk = Build.VERSION_CODES.S, maxSdk = Build.VERSION_CODES.TIRAMISU)
protected CameraDevice openCameraDeviceUserAsync(
String cameraId,
CameraDevice.StateCallback callback,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
index cf1aac20e..a4b1fb79f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
@@ -54,13 +54,13 @@ public abstract class ShadowChoreographer {
* <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
*/
public static void setFrameDelay(Duration delay) {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
frameDelay = delay;
}
/** See {@link #setFrameDelay(Duration)}. */
public static Duration getFrameDelay() {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
return frameDelay;
}
@@ -72,13 +72,13 @@ public abstract class ShadowChoreographer {
* <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
*/
public static void setPaused(boolean paused) {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
isPaused = paused;
}
/** See {@link #setPaused(boolean)}. */
public static boolean isPaused() {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
return isPaused;
}
@@ -109,11 +109,11 @@ public abstract class ShadowChoreographer {
*/
@Deprecated
public static void setPostFrameCallbackDelay(int delayMillis) {
- if (looperMode() == Mode.PAUSED) {
+ if (looperMode() == Mode.LEGACY) {
+ ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
+ } else {
setPaused(delayMillis != 0);
setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis));
- } else {
- ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
}
}
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 933024646..75566ce68 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
@@ -12,6 +12,7 @@ import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.os.MessageQueue;
import android.os.SystemClock;
@@ -29,9 +30,8 @@ import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.res.android.NativeObjRegistry;
import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Constructor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;
@@ -86,7 +86,7 @@ public class ShadowDisplayEventReceiver {
new NativeDisplayEventReceiver(new WeakReference<>((DisplayEventReceiver) receiver)));
}
- @Implementation(minSdk = R)
+ @Implementation(minSdk = R, maxSdk = TIRAMISU)
protected static long nativeInit(
WeakReference<DisplayEventReceiver> receiver,
MessageQueue msgQueue,
@@ -95,11 +95,21 @@ public class ShadowDisplayEventReceiver {
return nativeInit(receiver, msgQueue);
}
- @Implementation(minSdk = KITKAT_WATCH)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected static long nativeInit(
+ WeakReference<DisplayEventReceiver> receiver,
+ WeakReference<Object> vsyncEventData,
+ MessageQueue msgQueue,
+ int vsyncSource,
+ int eventRegistration,
+ long layerHandle) {
+ return nativeInit(receiver, msgQueue);
+ }
+
+ @Implementation(minSdk = KITKAT_WATCH, maxSdk = TIRAMISU)
protected static void nativeDispose(long receiverPtr) {
NativeDisplayEventReceiver receiver = nativeObjRegistry.unregister(receiverPtr);
if (receiver != null) {
- receiver.dispose();
}
}
@@ -141,24 +151,11 @@ public class ShadowDisplayEventReceiver {
displayEventReceiverReflector.onVsync(
ShadowSystem.nanoTime(), 0L, /* SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN */ 1);
} else if (RuntimeEnvironment.getApiLevel() < TIRAMISU) {
- try {
- // onVsync takes a package-private VSyncData class as a parameter, thus reflection
- // needs to be used
- Object vsyncData =
- ReflectionHelpers.callConstructor(
- Class.forName("android.view.DisplayEventReceiver$VsyncEventData"),
- ClassParameter.from(long.class, 1), /* id */
- ClassParameter.from(long.class, 10), /* frameDeadline */
- ClassParameter.from(long.class, 1)); /* frameInterval */
-
- displayEventReceiverReflector.onVsync(
- ShadowSystem.nanoTime(),
- 0L, /* physicalDisplayId currently ignored */
- /* frame= */ 1,
- vsyncData /* VsyncEventData */);
- } catch (ClassNotFoundException e) {
- throw new LinkageError("Unable to construct VsyncEventData", e);
- }
+ displayEventReceiverReflector.onVsync(
+ ShadowSystem.nanoTime(),
+ 0L, /* physicalDisplayId currently ignored */
+ /* frame= */ 1,
+ newVsyncEventData() /* VsyncEventData */);
} else {
displayEventReceiverReflector.onVsync(
ShadowSystem.nanoTime(),
@@ -240,6 +237,11 @@ public class ShadowDisplayEventReceiver {
}
private static Object /* VsyncEventData */ newVsyncEventData() {
+ VsyncEventDataReflector vsyncEventDataReflector = reflector(VsyncEventDataReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < TIRAMISU) {
+ return vsyncEventDataReflector.newVsyncEventData(
+ /* id= */ 1, /* frameDeadline= */ 10, /* frameInterval= */ 1);
+ }
try {
// onVsync on T takes a package-private VsyncEventData class, which is itself composed of a
// package private VsyncEventData.FrameTimeline class. So use reflection to build these up
@@ -247,33 +249,26 @@ public class ShadowDisplayEventReceiver {
Class.forName("android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline");
int timelineArrayLength = RuntimeEnvironment.getApiLevel() == TIRAMISU ? 1 : 7;
-
+ FrameTimelineReflector frameTimelineReflector = reflector(FrameTimelineReflector.class);
Object timelineArray = Array.newInstance(frameTimelineClass, timelineArrayLength);
for (int i = 0; i < timelineArrayLength; i++) {
- Array.set(timelineArray, i, newFrameTimeline(frameTimelineClass));
+ Array.set(timelineArray, i, frameTimelineReflector.newFrameTimeline(1, 1, 10));
+ }
+ if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+ return vsyncEventDataReflector.newVsyncEventData(
+ timelineArray, /* preferredFrameTimelineIndex= */ 0, /* frameInterval= */ 1);
+ } else {
+ return vsyncEventDataReflector.newVsyncEventData(
+ timelineArray,
+ /* preferredFrameTimelineIndex= */ 0,
+ timelineArrayLength,
+ /* frameInterval= */ 1);
}
-
- // get FrameTimeline[].class
- Class<?> frameTimeLineArrayClass =
- Class.forName("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;");
- return ReflectionHelpers.callConstructor(
- Class.forName("android.view.DisplayEventReceiver$VsyncEventData"),
- ClassParameter.from(frameTimeLineArrayClass, timelineArray),
- ClassParameter.from(int.class, 0), /* frameDeadline */
- ClassParameter.from(long.class, 1)); /* frameInterval */
} catch (ClassNotFoundException e) {
throw new LinkageError("Unable to construct VsyncEventData", e);
}
}
- private static Object newFrameTimeline(Class<?> frameTimelineClass) {
- return ReflectionHelpers.callConstructor(
- frameTimelineClass,
- ClassParameter.from(long.class, 1) /* vsync id */,
- ClassParameter.from(long.class, 1) /* expectedPresentTime */,
- ClassParameter.from(long.class, 10) /* deadline */);
- }
-
/** Reflector interface for {@link DisplayEventReceiver}'s internals. */
@ForType(DisplayEventReceiver.class)
protected interface DisplayEventReceiverReflector {
@@ -295,5 +290,35 @@ public class ShadowDisplayEventReceiver {
@Accessor("mCloseGuard")
CloseGuard getCloseGuard();
+
+ @Accessor("mReceiverPtr")
+ long getReceiverPtr();
+ }
+
+ @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData")
+ interface VsyncEventDataReflector {
+ @Constructor
+ Object newVsyncEventData(long id, long frameDeadline, long frameInterval);
+
+ @Constructor
+ Object newVsyncEventData(
+ @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;")
+ Object frameTimelineArray,
+ int preferredFrameTimelineIndex,
+ long frameInterval);
+
+ @Constructor
+ Object newVsyncEventData(
+ @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;")
+ Object frameTimelineArray,
+ int preferredFrameTimelineIndex,
+ int timelineArrayLength,
+ long frameInterval);
+ }
+
+ @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline")
+ interface FrameTimelineReflector {
+ @Constructor
+ Object newFrameTimeline(long id, long expectedPresentTime, long deadline);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
index 9f7957303..1aa08b09a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.P;
@@ -28,7 +27,6 @@ import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.annotation.Nullable;
-import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.Bootstrap;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
@@ -88,20 +86,26 @@ public class ShadowDisplayManagerGlobal {
reflector(DisplayManagerGlobalReflector.class, instance);
displayManagerGlobal.setDm(displayManager);
displayManagerGlobal.setLock(new Object());
+ List<Handler> displayListeners = createDisplayListeners();
+ displayManagerGlobal.setDisplayListeners(displayListeners);
+ displayManagerGlobal.setDisplayInfoCache(new SparseArray<>());
+ return instance;
+ }
- List displayListeners = new CopyOnWriteArrayList();
+ private static List<Handler> createDisplayListeners() {
try {
- // TODO: rexhoffman when we have sufficient detection in android dev replace
- // this with a version check.
+ // The type for mDisplayListeners was changed from ArrayList to CopyOnWriteArrayList
+ // in some branches of T and U, so we need to reflect on DisplayManagerGlobal class
+ // to check the type of mDisplayListeners member before initializing appropriately.
Field f = DisplayManagerGlobal.class.getDeclaredField("mDisplayListeners");
if (f.getType().isAssignableFrom(ArrayList.class)) {
- displayListeners = new ArrayList();
+ return new ArrayList<>();
+ } else {
+ return new CopyOnWriteArrayList<>();
}
} catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
}
- displayManagerGlobal.setDisplayListeners(displayListeners);
- displayManagerGlobal.setDisplayInfoCache(new SparseArray<>());
- return instance;
}
@VisibleForTesting
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 791ece2ec..58cd55818 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
@@ -10,6 +10,7 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ColorSpace;
+import android.graphics.ColorSpace.Named;
import android.graphics.ImageDecoder;
import android.graphics.ImageDecoder.DecodeException;
import android.graphics.ImageDecoder.Source;
@@ -247,14 +248,16 @@ public class ShadowImageDecoder {
static String ImageDecoder_nGetMimeType(long nativePtr) {
CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
// return encodedFormatToString(decoder.mCodec.getEncodedFormat());
- throw new UnsupportedOperationException();
+ // TODO: fix this properly. Just hardcode to png for now or just remove GraphicsMode.LEGACY
+ return "image/png";
}
static ColorSpace ImageDecoder_nGetColorSpace(long nativePtr) {
// auto colorType = codec.computeOutputColorType(codec.getInfo().colorType());
// sk_sp<SkColorSpace> colorSpace = codec.computeOutputColorSpace(colorType);
// return GraphicsJNI.getColorSpace(colorSpace, colorType);
- throw new UnsupportedOperationException();
+ // TODO: fix this properly. Just hardcode to SRGB for now or just remove GraphicsMode.LEGACY
+ return ColorSpace.get(Named.SRGB);
}
// native method implementations...
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 0654fbc4f..b8564ca40 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.S_V2;
import static android.os.Build.VERSION_CODES.TIRAMISU;
@@ -70,7 +69,7 @@ public class ShadowImageReader {
return nativeImageSetup(image);
}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected int nativeImageSetup(Object /* Image */ image) {
return nativeImageSetup((Image) image);
}
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 50635c8b2..298fabec6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
@@ -1,10 +1,14 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.hardware.input.InputManager;
+import android.util.SparseArray;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
@@ -13,13 +17,18 @@ import android.view.VerifiedKeyEvent;
import android.view.VerifiedMotionEvent;
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.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
/** Shadow for {@link InputManager} */
@Implements(value = InputManager.class, looseSignatures = true)
public class ShadowInputManager {
+ @RealObject InputManager realInputManager;
+
@Implementation
protected boolean injectInputEvent(InputEvent event, int mode) {
// ignore
@@ -37,6 +46,35 @@ public class ShadowInputManager {
return new int[0];
}
+ @Implementation(maxSdk = TIRAMISU)
+ protected void populateInputDevicesLocked() throws ClassNotFoundException {
+ if (ReflectionHelpers.getField(realInputManager, "mInputDevicesChangedListener") == null) {
+ ReflectionHelpers.setField(
+ realInputManager,
+ "mInputDevicesChangedListener",
+ ReflectionHelpers.callConstructor(
+ Class.forName("android.hardware.input.InputManager$InputDevicesChangedListener")));
+ }
+
+ if (getInputDevices() == null) {
+ final int[] ids = realInputManager.getInputDeviceIds();
+
+ SparseArray<InputDevice> inputDevices = new SparseArray<>();
+ for (int i = 0; i < ids.length; i++) {
+ inputDevices.put(ids[i], null);
+ }
+ setInputDevices(inputDevices);
+ }
+ }
+
+ private SparseArray<InputDevice> getInputDevices() {
+ return reflector(InputManagerReflector.class, realInputManager).getInputDevices();
+ }
+
+ private void setInputDevices(SparseArray<InputDevice> devices) {
+ reflector(InputManagerReflector.class, realInputManager).setInputDevices(devices);
+ }
+
/**
* Provides a local java implementation, since the real implementation is in system server +
* native code.
@@ -78,6 +116,17 @@ public class ShadowInputManager {
@Resetter
public static void reset() {
- ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null);
+ if (SDK_INT < ShadowBuild.UPSIDE_DOWN_CAKE) {
+ ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null);
+ }
+ }
+
+ @ForType(InputManager.class)
+ interface InputManagerReflector {
+ @Accessor("mInputDevices")
+ SparseArray<InputDevice> getInputDevices();
+
+ @Accessor("mInputDevices")
+ void setInputDevices(SparseArray<InputDevice> devices);
}
}
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 42baa4c1f..bf87b3a5b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
@@ -2,6 +2,7 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import android.app.Notification;
import android.app.job.JobParameters;
import android.app.job.JobService;
import org.robolectric.annotation.Implementation;
@@ -19,6 +20,14 @@ public class ShadowJobService extends ShadowService {
this.isRescheduleNeeded = needsReschedule;
}
+ /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected void setNotification(
+ JobParameters params,
+ int notificationId,
+ Notification notification,
+ int jobEndNotificationPolicy) {}
+
/**
* 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/ShadowLauncherApps.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
index 4b28bafa2..e06f348bc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
@@ -49,6 +49,7 @@ import org.robolectric.util.reflector.ForType;
public class ShadowLauncherApps {
private List<ShortcutInfo> shortcuts = new ArrayList<>();
private final Multimap<UserHandle, String> enabledPackages = HashMultimap.create();
+ private final Multimap<UserHandle, ComponentName> enabledActivities = HashMultimap.create();
private final Multimap<UserHandle, LauncherActivityInfo> shortcutActivityList =
HashMultimap.create();
private final Multimap<UserHandle, LauncherActivityInfo> activityList = HashMultimap.create();
@@ -100,6 +101,17 @@ public class ShadowLauncherApps {
}
/**
+ * Sets an activity referenced by ComponentName as enabled, to be checked by {@link
+ * #isActivityEnabled(ComponentName, UserHandle)}.
+ *
+ * @param userHandle the user handle to be set.
+ * @param componentName the component name of the activity to be enabled.
+ */
+ public void setActivityEnabled(UserHandle userHandle, ComponentName componentName) {
+ enabledActivities.put(userHandle, componentName);
+ }
+
+ /**
* Adds a {@link LauncherActivityInfo} to be retrieved by {@link
* #getShortcutConfigActivityList(String, UserHandle)}.
*
@@ -219,10 +231,9 @@ public class ShadowLauncherApps {
"This method is not currently supported in Robolectric.");
}
- @Implementation
+ @Implementation(minSdk = L)
protected boolean isActivityEnabled(ComponentName component, UserHandle user) {
- throw new UnsupportedOperationException(
- "This method is not currently supported in Robolectric.");
+ return enabledActivities.containsEntry(user, component);
}
/**
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
index f624b60d5..2fb348ebd 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
@@ -58,7 +58,7 @@ public class ShadowLegacyLooper extends ShadowLooper {
@Resetter
public static synchronized void resetThreadLoopers() {
// do not use looperMode() here, because its cached value might already have been reset
- if (ConfigurationRegistry.get(LooperMode.Mode.class) == LooperMode.Mode.PAUSED) {
+ if (ConfigurationRegistry.get(LooperMode.Mode.class) != LooperMode.Mode.LEGACY) {
// ignore if realistic looper
return;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
index 2a3a61b10..f954ac234 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
@@ -1,20 +1,38 @@
package org.robolectric.shadows;
+import static org.robolectric.shadow.api.Shadow.newInstanceOf;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
import android.app.Application;
import android.app.LoadedApk;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Build.VERSION_CODES;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
@Implements(value = LoadedApk.class, isInAndroidSdk = false)
public class ShadowLoadedApk {
+ @RealObject private LoadedApk realLoadedApk;
+ private boolean isClassLoaderInitialized = false;
+ private final Object classLoaderLock = new Object();
@Implementation
public ClassLoader getClassLoader() {
+ // The AppComponentFactory was introduced from SDK 28.
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
+ synchronized (classLoaderLock) {
+ if (!isClassLoaderInitialized) {
+ isClassLoaderInitialized = true;
+ tryInitAppComponentFactory(realLoadedApk);
+ }
+ }
+ }
return this.getClass().getClassLoader();
}
@@ -23,6 +41,35 @@ public class ShadowLoadedApk {
return this.getClass().getClassLoader();
}
+ private void tryInitAppComponentFactory(LoadedApk realLoadedApk) {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
+ ApplicationInfo applicationInfo = realLoadedApk.getApplicationInfo();
+ if (applicationInfo == null || applicationInfo.appComponentFactory == null) {
+ return;
+ }
+ _LoadedApk_ loadedApkReflector = reflector(_LoadedApk_.class, realLoadedApk);
+ if (!loadedApkReflector.getIncludeCode()) {
+ return;
+ }
+ String fullQualifiedClassName =
+ calculateFullQualifiedClassName(
+ applicationInfo.appComponentFactory, applicationInfo.packageName);
+ android.app.AppComponentFactory factory =
+ (android.app.AppComponentFactory) newInstanceOf(fullQualifiedClassName);
+ if (factory == null) {
+ factory = new android.app.AppComponentFactory();
+ }
+ loadedApkReflector.setAppFactory(factory);
+ }
+ }
+
+ private String calculateFullQualifiedClassName(String className, String packageName) {
+ if (packageName == null) {
+ return className;
+ }
+ return className.startsWith(".") ? packageName + className : className;
+ }
+
/** Accessor interface for {@link LoadedApk}'s internals. */
@ForType(LoadedApk.class)
public interface _LoadedApk_ {
@@ -32,5 +79,11 @@ public class ShadowLoadedApk {
@Accessor("mResources")
void setResources(Resources resources);
+
+ @Accessor("mIncludeCode")
+ boolean getIncludeCode();
+
+ @Accessor("mAppComponentFactory")
+ void setAppFactory(Object appFactory);
}
}
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 103907b93..9bef2d193 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.N_MR1;
@@ -409,7 +408,7 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void invalidateByteBufferLocked(
@Nullable ByteBuffer[] buffers, int index, boolean input) {}
@@ -417,14 +416,14 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
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 = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {}
/**
@@ -442,7 +441,7 @@ public class ShadowMediaCodec {
}
}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void validateOutputByteBufferLocked(
@Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
validateOutputByteBuffer(buffers, index, info);
@@ -452,14 +451,14 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
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 = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
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/ShadowMediaPlayer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
index 587356009..f08a53c3f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
@@ -37,6 +37,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import org.robolectric.annotation.Implementation;
@@ -112,8 +113,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
private static final Map<DataSource, Exception> exceptions = new HashMap<>();
private static final Map<DataSource, MediaInfo> mediaInfoMap = new HashMap<>();
- private static final MediaInfoProvider DEFAULT_MEDIA_INFO_PROVIDER = mediaInfoMap::get;
- private static MediaInfoProvider mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
+ private static Optional<MediaInfoProvider> mediaInfoProvider = Optional.empty();
@RealObject private MediaPlayer player;
@@ -650,7 +650,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
* @see #setDataSource(DataSource)
*/
public void doSetDataSource(DataSource dataSource) {
- MediaInfo mediaInfo = mediaInfoProvider.get(dataSource);
+ MediaInfo mediaInfo = getMediaInfo(dataSource);
if (mediaInfo == null) {
throw new IllegalArgumentException(
"Don't know what to do with dataSource "
@@ -663,17 +663,16 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
}
public static MediaInfo getMediaInfo(DataSource dataSource) {
- return mediaInfoProvider.get(dataSource);
+ if (mediaInfoMap.containsKey(dataSource)) {
+ return mediaInfoMap.get(dataSource);
+ }
+ return mediaInfoProvider.map(provider -> provider.get(dataSource)).orElse(null);
}
/**
* Adds a {@link MediaInfo} for a {@link DataSource}.
- *
- * <p>This overrides any {@link MediaInfoProvider} previously set by calling {@link
- * #setMediaInfoProvider}, i.e., the provider will not be used for any {@link DataSource}.
*/
public static void addMediaInfo(DataSource dataSource, MediaInfo info) {
- ShadowMediaPlayer.mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
mediaInfoMap.put(dataSource, info);
}
@@ -685,7 +684,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
* {@link MediaInfo} provided by this {@link MediaInfoProvider} will be used instead.
*/
public static void setMediaInfoProvider(MediaInfoProvider mediaInfoProvider) {
- ShadowMediaPlayer.mediaInfoProvider = mediaInfoProvider;
+ ShadowMediaPlayer.mediaInfoProvider = Optional.of(mediaInfoProvider);
}
public static void addException(DataSource dataSource, RuntimeException e) {
@@ -1536,7 +1535,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
@Resetter
public static void resetStaticState() {
createListener = null;
- mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
+ mediaInfoProvider = Optional.empty();
exceptions.clear();
mediaInfoMap.clear();
DataSource.reset();
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 8b7b8fa86..7b045e836 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
@@ -64,6 +63,12 @@ public class ShadowNativeFontsFontFamily {
return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
}
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected static long nBuild(
+ long builderPtr, String langTags, int variant, boolean isCustomFallback, boolean isDefaultFallback) {
+ return nBuild(builderPtr, langTags, variant, isCustomFallback);
+ }
+
@Implementation
protected static long nGetReleaseNativeFamily() {
return FontFamilyBuilderNatives.nGetReleaseNativeFamily();
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 94fadb5ab..32d428088 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
@@ -813,7 +813,7 @@ public class ShadowNativePaint {
paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics);
}
- @Implementation(minSdk = 10000)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected static float nGetRunCharacterAdvance(
long paintPtr,
char[] text,
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 0f9e44d17..4cdfb4532 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
@@ -221,7 +221,7 @@ public class ShadowNfcAdapter {
}
if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
nfcAdapterReflector.setHasNfcFeature(false);
- if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.CUR_DEVELOPMENT) {
+ if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.TIRAMISU) {
nfcAdapterReflector.setHasBeamFeature(false);
}
}
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 8fec6464a..16a7398f7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
@@ -112,6 +112,15 @@ public class ShadowPaint {
}
@Implementation
+ protected void setStrikeThruText(boolean strikeThruText) {
+ if (strikeThruText) {
+ setFlags(flags | Paint.STRIKE_THRU_TEXT_FLAG);
+ } else {
+ setFlags(flags & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ }
+ }
+
+ @Implementation
protected Shader setShader(Shader shader) {
this.shader = shader;
return shader;
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 50f8adf62..ee3bef016 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -12,6 +12,7 @@ import android.os.Message;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import android.util.Log;
+import com.google.common.base.Preconditions;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
@@ -58,6 +59,8 @@ public final class ShadowPausedLooper extends ShadowLooper {
private static Set<Looper> loopingLoopers =
Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<Looper, Boolean>()));
+ private static boolean ignoreUncaughtExceptions = false;
+
@RealObject private Looper realLooper;
private boolean isPaused = false;
// the Executor that executes looper messages. Must be written to on looper thread
@@ -317,6 +320,51 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
/**
+ * By default Robolectric will put Loopers that throw uncaught exceptions in their loop method
+ * into an error state, where any future posting to the looper's queue will throw an error.
+ *
+ * <p>This API allows you to disable this behavior. Note this is a permanent setting - it is not
+ * reset between tests.
+ *
+ * @deprecated this method only exists to accommodate legacy tests with preexisting issues.
+ * Silently discarding exceptions is not recommended, and can lead to deadlocks.
+ */
+ @Deprecated
+ public static void setIgnoreUncaughtExceptions(boolean shouldIgnore) {
+ ignoreUncaughtExceptions = shouldIgnore;
+ }
+
+ /**
+ * Shadow loop to handle uncaught exceptions. Without this logic an uncaught exception on a looper
+ * thread will cause idle() to deadlock.
+ */
+ @Implementation
+ protected static void loop() {
+ try {
+ reflector(LooperReflector.class).loop();
+ } catch (Exception e) {
+ Looper realLooper = Preconditions.checkNotNull(Looper.myLooper());
+ ShadowPausedMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue());
+
+ if (ignoreUncaughtExceptions) {
+ // ignore
+ } else {
+ shadowQueue.setUncaughtException(e);
+ // release any ControlRunnables currently in queue to prevent deadlocks
+ shadowQueue.drainQueue(
+ input -> {
+ if (input instanceof ControlRunnable) {
+ ((ControlRunnable) input).runLatch.countDown();
+ return true;
+ }
+ return false;
+ });
+ }
+ throw e;
+ }
+ }
+
+ /**
* If the given {@code lastMessageRead} is not null and the queue is now idle, get the idle
* handlers and run them. This synchronization mirrors what happens in the real message queue
* next() method, but does not block after running the idle handlers.
@@ -345,21 +393,40 @@ public final class ShadowPausedLooper extends ShadowLooper {
private abstract static class ControlRunnable implements Runnable {
protected final CountDownLatch runLatch = new CountDownLatch(1);
+ private volatile RuntimeException exception;
- public void waitTillComplete() {
+ @Override
+ public void run() {
+ try {
+ doRun();
+ } catch (RuntimeException e) {
+ if (!ignoreUncaughtExceptions) {
+ exception = e;
+ }
+ throw e;
+ } finally {
+ runLatch.countDown();
+ }
+ }
+
+ protected abstract void doRun() throws RuntimeException;
+
+ public void waitTillComplete() throws RuntimeException {
try {
runLatch.await();
} catch (InterruptedException e) {
Log.w("ShadowPausedLooper", "wait till idle interrupted");
}
+ if (exception != null) {
+ throw exception;
+ }
}
}
private class IdlingRunnable extends ControlRunnable {
@Override
- public void run() {
- try {
+ public void doRun() {
while (true) {
Message msg = getNextExecutableMessage();
if (msg == null) {
@@ -369,26 +436,20 @@ public final class ShadowPausedLooper extends ShadowLooper {
shadowMsg(msg).recycleUnchecked();
triggerIdleHandlersIfNeeded(msg);
}
- } finally {
- runLatch.countDown();
- }
}
}
private class RunOneRunnable extends ControlRunnable {
@Override
- public void run() {
- try {
+ public void doRun() {
+
Message msg = shadowQueue().getNextIgnoringWhen();
if (msg != null) {
SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen());
msg.getTarget().dispatchMessage(msg);
triggerIdleHandlersIfNeeded(msg);
}
- } finally {
- runLatch.countDown();
- }
}
}
@@ -408,6 +469,8 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
looperExecutor.execute(runnable);
runnable.waitTillComplete();
+ // throw immediately if looper died while executing tasks
+ shadowQueue().checkQueueState();
}
}
@@ -422,6 +485,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
@Override
public void execute(Runnable runnable) {
+ shadowQueue().checkQueueState();
executionQueue.add(runnable);
}
@@ -435,18 +499,22 @@ public final class ShadowPausedLooper extends ShadowLooper {
Runnable runnable = executionQueue.take();
runnable.run();
} catch (InterruptedException e) {
- // ignore
+ // ignored
}
}
}
+
+ @Override
+ protected void doRun() throws RuntimeException {
+ throw new UnsupportedOperationException();
+ }
}
private class UnPauseRunnable extends ControlRunnable {
@Override
- public void run() {
+ public void doRun() {
setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
isPaused = false;
- runLatch.countDown();
}
}
@@ -478,5 +546,8 @@ public final class ShadowPausedLooper extends ShadowLooper {
@Direct
void quitSafely();
+
+ @Direct
+ void loop();
}
}
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 162330aad..5caf01642 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
@@ -16,6 +16,9 @@ import android.os.Message;
import android.os.MessageQueue;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import com.google.common.base.Predicate;
import java.time.Duration;
import java.util.ArrayList;
import org.robolectric.RuntimeEnvironment;
@@ -47,6 +50,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
new NativeObjRegistry<ShadowPausedMessageQueue>(ShadowPausedMessageQueue.class);
private boolean isPolling = false;
private ShadowPausedSystemClock.Listener clockListener;
+ private Exception uncaughtException = null;
// shadow constructor instead of nativeInit because nativeInit signature has changed across SDK
// versions
@@ -55,7 +59,16 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
invokeConstructor(MessageQueue.class, realQueue, from(boolean.class, quitAllowed));
int ptr = (int) nativeQueueRegistry.register(this);
reflector(MessageQueueReflector.class, realQueue).setPtr(ptr);
- clockListener = () -> nativeWake(ptr);
+ clockListener =
+ () -> {
+ synchronized (realQueue) {
+ // only wake up the Looper thread if queue is non empty to reduce contention if many
+ // Looper threads are active
+ if (getMessages() != null) {
+ nativeWake(ptr);
+ }
+ }
+ };
ShadowPausedSystemClock.addStaticListener(clockListener);
}
@@ -210,8 +223,28 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
return reflector(MessageQueueReflector.class, realQueue).getQuitAllowed();
}
+ @VisibleForTesting
void doEnqueueMessage(Message msg, long when) {
- reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when);
+ enqueueMessage(msg, when);
+ }
+
+ @Implementation
+ protected boolean enqueueMessage(Message msg, long when) {
+ synchronized (realQueue) {
+ if (uncaughtException != null) {
+ // looper thread has died
+ IllegalStateException e =
+ new IllegalStateException(
+ msg.getTarget()
+ + " sending message to a Looper thread that has died due to an uncaught"
+ + " exception",
+ uncaughtException);
+ Log.w("ShadowPausedMessageQueue", e);
+ msg.recycle();
+ throw e;
+ }
+ return reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when);
+ }
}
Message getMessages() {
@@ -283,7 +316,9 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
return Duration.ZERO;
}
while (next != null) {
- when = shadowOfMsg(next).getWhen();
+ if (next.getTarget() != null) {
+ when = shadowOfMsg(next).getWhen();
+ }
next = shadowOfMsg(next).internalGetNext();
}
}
@@ -309,7 +344,9 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
synchronized (realQueue) {
Message next = getMessages();
while (next != null) {
- count++;
+ if (next.getTarget() != null) {
+ count++;
+ }
next = shadowOfMsg(next).internalGetNext();
}
}
@@ -323,12 +360,24 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
*/
Message getNextIgnoringWhen() {
synchronized (realQueue) {
- Message head = getMessages();
- if (head != null) {
- Message next = shadowOfMsg(head).internalGetNext();
- reflector(MessageQueueReflector.class, realQueue).setMessages(next);
+ Message prev = null;
+ Message msg = getMessages();
+ // Head is blocked on synchronization barrier, find next asynchronous message.
+ if (msg != null && msg.getTarget() == null) {
+ do {
+ prev = msg;
+ msg = shadowOfMsg(msg).internalGetNext();
+ } while (msg != null && !msg.isAsynchronous());
}
- return head;
+ if (msg != null) {
+ Message next = shadowOfMsg(msg).internalGetNext();
+ if (prev == null) {
+ reflector(MessageQueueReflector.class, realQueue).setMessages(next);
+ } else {
+ ReflectionHelpers.setField(prev, "next", next);
+ }
+ }
+ return msg;
}
}
@@ -340,6 +389,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
msgQueue.setMessages(null);
msgQueue.setIdleHandlers(new ArrayList<>());
msgQueue.setNextBarrierToken(0);
+ setUncaughtException(null);
}
private static ShadowPausedMessage shadowOfMsg(Message head) {
@@ -378,10 +428,50 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
}
}
+ /**
+ * Called when an uncaught exception occurred in this message queue's Looper thread.
+ *
+ * <p>In real android, by default an exception handler is installed which kills the entire process
+ * when an uncaught exception occurs. We don't want to do this in robolectric to isolate tests, so
+ * instead an uncaught exception puts the message queue into an error state, where any future
+ * interaction will rethrow the exception.
+ */
+ void setUncaughtException(Exception e) {
+ synchronized (realQueue) {
+ this.uncaughtException = e;
+ }
+ }
+
+ void checkQueueState() {
+ synchronized (realQueue) {
+ if (uncaughtException != null) {
+ throw new IllegalStateException(
+ "Looper thread that has died due to an uncaught exception", uncaughtException);
+ }
+ }
+ }
+
+ /**
+ * Remove all messages from queue
+ *
+ * @param msgProcessor a callback to apply to each mesg
+ */
+ void drainQueue(Predicate<Runnable> msgProcessor) {
+ synchronized (realQueue) {
+ Message msg = getMessages();
+ while (msg != null) {
+ boolean unused = msgProcessor.apply(msg.getCallback());
+ ShadowMessage shadowMsg = Shadow.extract(msg);
+ msg.recycle();
+ msg = shadowMsg.getNext();
+ }
+ }
+ }
+
/** Accessor interface for {@link MessageQueue}'s internals. */
@ForType(MessageQueue.class)
private interface MessageQueueReflector {
-
+ @Direct
boolean enqueueMessage(Message msg, long when);
Message next();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
index 839e28595..cbf52ae39 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
@@ -41,6 +41,7 @@ import android.content.integrity.IAppIntegrityManager;
import android.content.pm.ICrossProfileApps;
import android.content.pm.IShortcutService;
import android.content.rollback.IRollbackManager;
+import android.hardware.ISensorPrivacyManager;
import android.hardware.biometrics.IAuthService;
import android.hardware.biometrics.IBiometricService;
import android.hardware.fingerprint.IFingerprintService;
@@ -57,6 +58,7 @@ import android.net.IIpSecService;
import android.net.INetworkPolicyManager;
import android.net.INetworkScoreService;
import android.net.ITetheringConnector;
+import android.net.IVpnManager;
import android.net.nsd.INsdManager;
import android.net.vcn.IVcnManagementService;
import android.net.wifi.IWifiManager;
@@ -205,6 +207,8 @@ public class ShadowServiceManager {
addBinderService(Context.UWB_SERVICE, IUwbAdapter.class);
addBinderService(Context.VCN_MANAGEMENT_SERVICE, IVcnManagementService.class);
addBinderService(Context.TRANSLATION_MANAGER_SERVICE, ITranslationManager.class);
+ addBinderService(Context.SENSOR_PRIVACY_SERVICE, ISensorPrivacyManager.class);
+ addBinderService(Context.VPN_MANAGEMENT_SERVICE, IVpnManager.class);
}
if (RuntimeEnvironment.getApiLevel() >= TIRAMISU) {
addBinderService(Context.AMBIENT_CONTEXT_SERVICE, IAmbientContextManager.class);
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 a1895ff6b..c40d24e96 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
@@ -63,7 +62,7 @@ public class ShadowSoundPool {
return 1;
}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected int _play(
int soundID,
float leftVolume,
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 529aaa405..1b239d188 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
@@ -21,6 +21,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Executor;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -282,6 +283,17 @@ public class ShadowSubscriptionManager {
}
/**
+ * Adds a listener to a local list of listeners. Will be triggered by {@link
+ * #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated.
+ */
+ @Implementation(minSdk = R)
+ protected void addOnSubscriptionsChangedListener(
+ Executor executor, OnSubscriptionsChangedListener listener) {
+ listeners.add(listener);
+ listener.onSubscriptionsChanged();
+ }
+
+ /**
* Removes a listener from a local list of listeners. Will be triggered by {@link
* #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated.
*/
@@ -290,6 +302,16 @@ public class ShadowSubscriptionManager {
listeners.remove(listener);
}
+ /**
+ * Check if a listener exists in the {@link ShadowSubscriptionManager.listeners}.
+ *
+ * @param listener The listener to check.
+ * @return boolean True if the listener already added, otherwise false.
+ */
+ public boolean hasOnSubscriptionsChangedListener(OnSubscriptionsChangedListener listener) {
+ return listeners.contains(listener);
+ }
+
/** Returns subscription Ids that were set via {@link #setActiveSubscriptionInfoList}. */
@Implementation(minSdk = LOLLIPOP_MR1)
@HiddenApi
@@ -405,6 +427,17 @@ public class ShadowSubscriptionManager {
return phoneNumberMap.getOrDefault(subscriptionId, "");
}
+ /**
+ * Returns the phone number for the given {@code subscriptionId}, or an empty string if not
+ * available. {@code source} is ignored and will return the same as {@link #getPhoneNumber(int)}.
+ *
+ * <p>The phone number can be set by {@link #setPhoneNumber(int, String)}
+ */
+ @Implementation(minSdk = TIRAMISU)
+ protected String getPhoneNumber(int subscriptionId, int source) {
+ return getPhoneNumber(subscriptionId);
+ }
+
/** Sets the phone number returned by {@link #getPhoneNumber(int)}. */
public void setPhoneNumber(int subscriptionId, String phoneNumber) {
phoneNumberMap.put(subscriptionId, phoneNumber);
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 bc8528781..63d12e6a1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
@@ -11,6 +11,7 @@ import android.view.SurfaceControl;
import android.view.SurfaceSession;
import dalvik.system.CloseGuard;
import java.util.concurrent.atomic.AtomicInteger;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.ReflectorObject;
@@ -82,6 +83,13 @@ public class ShadowSurfaceControl {
void initializeNativeObject() {
surfaceControlReflector.setNativeObject(nativeObject.incrementAndGet());
+ if (RuntimeEnvironment.getApiLevel() >= ShadowBuild.UPSIDE_DOWN_CAKE) {
+ try {
+ surfaceControlReflector.setFreeNativeResources(() -> {});
+ } catch(Exception e) {
+ // tm branches not yet have mFreeNativeResources added while in partial U state
+ }
+ }
}
@ForType(SurfaceControl.class)
@@ -94,5 +102,8 @@ public class ShadowSurfaceControl {
@Direct
void finalize();
+
+ @Accessor("mFreeNativeResources")
+ void setFreeNativeResources(Runnable runnable);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
index a2bb38aba..b9013704a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
@@ -13,10 +13,10 @@ public class ShadowSystem {
*/
@SuppressWarnings("unused")
public static long nanoTime() {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis());
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
return ShadowLegacySystemClock.nanoTime();
+ } else {
+ return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis());
}
}
@@ -27,10 +27,10 @@ public class ShadowSystem {
*/
@SuppressWarnings("unused")
public static long currentTimeMillis() {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return SystemClock.uptimeMillis();
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
return ShadowLegacySystemClock.currentTimeMillis();
+ } else {
+ return SystemClock.uptimeMillis();
}
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
index cce1990a0..41fbf4e7d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
@@ -15,9 +15,9 @@ import android.media.AudioAttributes;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemVibrator;
-import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.vibrator.VibrationEffectSegment;
+import com.google.common.base.Preconditions;
import java.util.List;
import java.util.Optional;
import org.robolectric.RuntimeEnvironment;
@@ -25,7 +25,8 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
-@Implements(value = SystemVibrator.class, isInAndroidSdk = false)
+/** Shadow for {@link SystemVibrator}. */
+@Implements(value = SystemVibrator.class, isInAndroidSdk = false, looseSignatures = true)
public class ShadowSystemVibrator extends ShadowVibrator {
private final Handler handler = new Handler(Looper.getMainLooper());
@@ -133,11 +134,14 @@ public class ShadowSystemVibrator extends ShadowVibrator {
@Implementation(minSdk = S)
protected void vibrate(
- int uid,
- String opPkg,
- VibrationEffect effect,
- String reason,
- VibrationAttributes attributes) {
+ Object uid, Object opPkg, Object effect, Object reason, Object attributes) {
+ Preconditions.checkArgument(uid instanceof Integer);
+ Preconditions.checkArgument(opPkg == null || opPkg instanceof String);
+ // The SystemVibrator#vibrate needs effect NonNull.
+ Preconditions.checkArgument(effect instanceof VibrationEffect);
+ Preconditions.checkArgument(reason == null || reason instanceof String);
+ // The SystemVibrator#vibrate needs attributes NonNull.
+ Preconditions.checkArgument(attributes instanceof android.os.VibrationAttributes);
if (effect instanceof VibrationEffect.Composed) {
VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect;
vibrationAttributesFromLastVibration = attributes;
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 0864354d9..048abf03b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -52,6 +52,7 @@ import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyManager.CellInfoCallback;
import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.telephony.emergency.EmergencyNumber;
import android.text.TextUtils;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -59,13 +60,16 @@ import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Executor;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
@@ -141,6 +145,7 @@ public class ShadowTelephonyManager {
private String visualVoicemailPackageName = null;
private SignalStrength signalStrength;
private boolean dataEnabled = false;
+ private final Set<Integer> dataDisabledReasons = new HashSet<>();
private boolean isRttSupported;
private final List<String> sentDialerSpecialCodes = new ArrayList<>();
private boolean hearingAidCompatibilitySupported = false;
@@ -152,6 +157,7 @@ public class ShadowTelephonyManager {
private VisualVoicemailSmsParams lastVisualVoicemailSmsParams;
private VisualVoicemailSmsFilterSettings visualVoicemailSmsFilterSettings;
private boolean emergencyCallbackMode;
+ private static Map<Integer, List<EmergencyNumber>> emergencyNumbersList;
/**
* Should be {@link TelephonyManager.BootstrapAuthenticationCallback} but this object was
@@ -169,6 +175,7 @@ public class ShadowTelephonyManager {
@Resetter
public static void reset() {
callComposerStatus = 0;
+ emergencyNumbersList = null;
}
@Implementation(minSdk = S)
@@ -263,6 +270,13 @@ public class ShadowTelephonyManager {
}
/** Call state may be specified via {@link #setCallState(int)}. */
+ @Implementation(minSdk = S)
+ protected int getCallStateForSubscription() {
+ checkReadPhoneStatePermission();
+ return callState;
+ }
+
+ /** Call state may be specified via {@link #setCallState(int)}. */
@Implementation
protected int getCallState() {
checkReadPhoneStatePermission();
@@ -1215,12 +1229,39 @@ public class ShadowTelephonyManager {
}
/**
+ * Implementation for {@link TelephonyManager#isDataEnabledForReason}.
+ *
+ * @return True by default, unless reason is set to false with {@link
+ * TelephonyManager#setDataEnabledForReason}.
+ */
+ @Implementation(minSdk = Build.VERSION_CODES.S)
+ protected boolean isDataEnabledForReason(@TelephonyManager.DataEnabledReason int reason) {
+ checkReadPhoneStatePermission();
+ return !dataDisabledReasons.contains(reason);
+ }
+
+ /**
* Implementation for {@link TelephonyManager#setDataEnabled}. Marked as public in order to allow
* it to be used as a test API.
*/
@Implementation(minSdk = Build.VERSION_CODES.O)
public void setDataEnabled(boolean enabled) {
- dataEnabled = enabled;
+ setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER, enabled);
+ }
+
+ /**
+ * Implementation for {@link TelephonyManager#setDataEnabledForReason}. Marked as public in order
+ * to allow it to be used as a test API.
+ */
+ @Implementation(minSdk = Build.VERSION_CODES.S)
+ public void setDataEnabledForReason(
+ @TelephonyManager.DataEnabledReason int reason, boolean enabled) {
+ if (enabled) {
+ dataDisabledReasons.remove(reason);
+ } else {
+ dataDisabledReasons.add(reason);
+ }
+ dataEnabled = dataDisabledReasons.isEmpty();
}
/**
@@ -1374,4 +1415,25 @@ public class ShadowTelephonyManager {
return sentIntent;
}
}
+
+ /**
+ * Sets the emergency numbers list returned by {@link TelephonyManager#getEmergencyNumberList}.
+ */
+ public static void setEmergencyNumberList(
+ Map<Integer, List<EmergencyNumber>> emergencyNumbersList) {
+ ShadowTelephonyManager.emergencyNumbersList = emergencyNumbersList;
+ }
+
+ /**
+ * Implementation for {@link TelephonyManager#getEmergencyNumberList}.
+ *
+ * @return an immutable map by default, unless set with {@link #setEmergencyNumberList}.
+ */
+ @Implementation(minSdk = R)
+ protected Map<Integer, List<EmergencyNumber>> getEmergencyNumberList() {
+ if (ShadowTelephonyManager.emergencyNumbersList != null) {
+ return ShadowTelephonyManager.emergencyNumbersList;
+ }
+ return ImmutableMap.of();
+ }
}
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 5c8de7314..00ad65840 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
@@ -600,6 +600,16 @@ public class ShadowUserManager {
}
@HiddenApi
+ @Implementation(minSdk = R)
+ protected List<UserHandle> getUserHandles(boolean excludeDying) {
+ ArrayList<UserHandle> userHandles = new ArrayList<>();
+ for (int id : userManagerState.userSerialNumbers.keySet()) {
+ userHandles.addAll(userManagerState.userProfilesListMap.get(id));
+ }
+ return userHandles;
+ }
+
+ @HiddenApi
@Implementation(minSdk = JELLY_BEAN_MR1)
protected static int getMaxSupportedUsers() {
return maxSupportedUsers;
@@ -998,6 +1008,9 @@ public class ShadowUserManager {
@Implementation(minSdk = JELLY_BEAN_MR1)
protected boolean removeUser(int userHandle) {
+ if (!userManagerState.userInfoMap.containsKey(userHandle)) {
+ return false;
+ }
userManagerState.userInfoMap.remove(userHandle);
userManagerState.userPidMap.remove(userHandle);
userManagerState.userSerialNumbers.remove(userHandle);
@@ -1021,6 +1034,13 @@ public class ShadowUserManager {
return removeUser(user.getIdentifier());
}
+ @Implementation(minSdk = TIRAMISU)
+ protected int removeUserWhenPossible(UserHandle user, boolean overrideDevicePolicy) {
+ return removeUser(user.getIdentifier())
+ ? UserManager.REMOVE_RESULT_REMOVED
+ : UserManager.REMOVE_RESULT_ERROR_UNKNOWN;
+ }
+
@Implementation(minSdk = N)
protected static boolean supportsMultipleUsers() {
return isMultiUserSupported;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
index b66a0a414..276a31db3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
@@ -3,18 +3,19 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.R;
import android.media.AudioAttributes;
-import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
-import android.os.vibrator.VibrationEffectSegment;
+import android.os.vibrator.PrimitiveSegment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
+import org.robolectric.util.ReflectionHelpers;
@Implements(Vibrator.class)
public class ShadowVibrator {
@@ -22,10 +23,10 @@ public class ShadowVibrator {
static boolean cancelled;
static long milliseconds;
protected static long[] pattern;
- protected static final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>();
+ protected static final List<Object> vibrationEffectSegments = new ArrayList<>();
protected static final List<PrimitiveEffect> primitiveEffects = new ArrayList<>();
protected static final List<Integer> supportedPrimitives = new ArrayList<>();
- @Nullable protected static VibrationAttributes vibrationAttributesFromLastVibration;
+ @Nullable protected static Object vibrationAttributesFromLastVibration;
@Nullable protected static AudioAttributes audioAttributesFromLastVibration;
static int repeat;
static boolean hasVibrator = true;
@@ -81,9 +82,18 @@ public class ShadowVibrator {
return repeat;
}
- /** Returns the last list of {@link VibrationEffectSegment}. */
- public List<VibrationEffectSegment> getVibrationEffectSegments() {
- return vibrationEffectSegments;
+ /** Returns the last list of {@link PrimitiveSegment} vibrations in {@link PrimitiveEffect}. */
+ @SuppressWarnings("JdkCollectors") // toImmutableList is only supported in Java 8+.
+ public List<PrimitiveEffect> getPrimitiveSegmentsInPrimitiveEffects() {
+ return vibrationEffectSegments.stream()
+ .filter(segment -> segment instanceof PrimitiveSegment)
+ .map(
+ segment ->
+ new PrimitiveEffect(
+ ReflectionHelpers.getField(segment, "mPrimitiveId"),
+ ReflectionHelpers.getField(segment, "mScale"),
+ ReflectionHelpers.getField(segment, "mDelay")))
+ .collect(Collectors.toList());
}
/** Returns the last list of {@link PrimitiveEffect}. */
@@ -108,9 +118,9 @@ public class ShadowVibrator {
supportedPrimitives.addAll(primitives);
}
- /** Returns the {@link VibrationAttributes} from the last vibration. */
+ /** Returns the {@link android.os.VibrationAttributes} from the last vibration. */
@Nullable
- public VibrationAttributes getVibrationAttributesFromLastVibration() {
+ public Object getVibrationAttributesFromLastVibration() {
return vibrationAttributesFromLastVibration;
}
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 70cb36999..5d06f587e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
@@ -524,31 +524,29 @@ public class ShadowView {
@Implementation
protected boolean post(Runnable action) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return reflector(_View_.class, realView).post(action);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
return true;
+ } else {
+ return reflector(_View_.class, realView).post(action);
}
}
@Implementation
protected boolean postDelayed(Runnable action, long delayMills) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return reflector(_View_.class, realView).postDelayed(action, delayMills);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance()
.getForegroundThreadScheduler()
.postDelayed(action, delayMills);
return true;
+ } else {
+ return reflector(_View_.class, realView).postDelayed(action, delayMills);
}
}
@Implementation
protected void postInvalidateDelayed(long delayMilliseconds) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance()
.getForegroundThreadScheduler()
.postDelayed(
@@ -559,17 +557,19 @@ public class ShadowView {
}
},
delayMilliseconds);
+ } else {
+ reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds);
}
}
@Implementation
protected boolean removeCallbacks(Runnable callback) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return reflector(_View_.class, realView).removeCallbacks(callback);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowLegacyLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
shadowLooper.getScheduler().remove(callback);
return true;
+ } else {
+ return reflector(_View_.class, realView).removeCallbacks(callback);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
index 28e668067..2f13fcf7d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
@@ -9,7 +9,7 @@ import android.view.ViewGroup;
import java.io.PrintStream;
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.shadow.api.Shadow;
import org.robolectric.util.reflector.Direct;
@@ -29,10 +29,10 @@ public class ShadowViewGroup extends ShadowView {
() -> {
reflector(ViewGroupReflector.class, realViewGroup).addView(child, index, params);
};
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- addViewRunnable.run();
- } else {
+ if (ShadowLooper.looperMode() == Mode.LEGACY) {
shadowMainLooper().runPaused(addViewRunnable);
+ } else {
+ addViewRunnable.run();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java
new file mode 100644
index 000000000..99f807b07
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java
@@ -0,0 +1,67 @@
+package org.robolectric.shadows;
+
+import android.content.Intent;
+import android.net.PlatformVpnProfile;
+import android.net.VpnManager;
+import android.net.VpnProfileState;
+import android.os.Build.VERSION_CODES;
+import java.util.UUID;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** Shadow for {@link VpnManager}. */
+@Implements(value = VpnManager.class, minSdk = VERSION_CODES.R)
+public class ShadowVpnManager {
+
+ private VpnProfileState vpnProfileState;
+ private Intent provisionVpnProfileIntent;
+
+ @Implementation
+ protected void deleteProvisionedVpnProfile() {
+ vpnProfileState = null;
+ }
+
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected VpnProfileState getProvisionedVpnProfileState() {
+ return vpnProfileState;
+ }
+
+ /**
+ * @see #setProvisionVpnProfileResult(Intent).
+ */
+ @Implementation
+ protected Intent provisionVpnProfile(PlatformVpnProfile profile) {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false);
+ }
+ return provisionVpnProfileIntent;
+ }
+
+ /** Sets the return value of #provisionVpnProfile(PlatformVpnProfile). */
+ public void setProvisionVpnProfileResult(Intent intent) {
+ provisionVpnProfileIntent = intent;
+ }
+
+ @Implementation
+ protected void startProvisionedVpnProfile() {
+ startProvisionedVpnProfileSession();
+ }
+
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected String startProvisionedVpnProfileSession() {
+ String sessionKey = UUID.randomUUID().toString();
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ vpnProfileState =
+ new VpnProfileState(VpnProfileState.STATE_CONNECTED, sessionKey, false, false);
+ }
+ return sessionKey;
+ }
+
+ @Implementation
+ protected void stopProvisionedVpnProfile() {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false);
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
index 706d86e10..1a8131f01 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
@@ -569,7 +569,7 @@ public class ShadowWebView extends ShadowViewGroup {
*
* @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()}
* @deprecated Do not depend on this method as it will be removed in a future update. The
- * preferered method is to populate a fake web history to use for going back.
+ * preferred method is to populate a fake web history to use for going back.
*/
@Deprecated
public void setCanGoBack(boolean canGoBack) {
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 8e933d93e..7221e69e7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -5,6 +5,9 @@ import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static java.util.stream.Collectors.toList;
import android.content.Context;
import android.content.Intent;
@@ -17,14 +20,19 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
+import android.net.wifi.WifiSsid;
import android.net.wifi.WifiUsabilityStatsEntry;
+import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Pair;
import com.google.common.collect.ImmutableList;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.BitSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -71,6 +79,8 @@ public class ShadowWifiManager {
@RealObject WifiManager wifiManager;
private WifiConfiguration apConfig;
private SoftApConfiguration softApConfig;
+ private final Object pnoRequestLock = new Object();
+ private PnoScanRequest outstandingPnoScanRequest = null;
@Implementation
protected boolean setWifiEnabled(boolean wifiEnabled) {
@@ -657,4 +667,176 @@ public class ShadowWifiManager {
this.predictionHorizonSec = predictionHorizonSec;
}
}
+
+ /** Informs the {@link WifiManager} of a list of PNO {@link ScanResult}. */
+ public void networksFoundFromPnoScan(List<ScanResult> scanResults) {
+ synchronized (pnoRequestLock) {
+ List<ScanResult> scanResultsCopy = List.copyOf(scanResults);
+ if (outstandingPnoScanRequest == null
+ || outstandingPnoScanRequest.ssids.stream()
+ .noneMatch(
+ ssid ->
+ scanResultsCopy.stream()
+ .anyMatch(scanResult -> scanResult.getWifiSsid().equals(ssid)))) {
+ return;
+ }
+ Executor executor = outstandingPnoScanRequest.executor;
+ InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback;
+ executor.execute(() -> callback.onScanResultsAvailable(scanResultsCopy));
+ Intent intent = createPnoScanResultsBroadcastIntent();
+ getContext().sendBroadcast(intent);
+ executor.execute(
+ () ->
+ callback.onRemoved(
+ InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED));
+ outstandingPnoScanRequest = null;
+ }
+ }
+
+ // Object needs to be used here since PnoScanResultsCallback is hidden. The looseSignatures spec
+ // requires that all args are of type Object.
+ @Implementation(minSdk = TIRAMISU)
+ @HiddenApi
+ protected void setExternalPnoScanRequest(
+ Object ssids, Object frequencies, Object executor, Object callback) {
+ synchronized (pnoRequestLock) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+
+ List<WifiSsid> pnoSsids = (List<WifiSsid>) ssids;
+ int[] pnoFrequencies = (int[]) frequencies;
+ Executor pnoExecutor = (Executor) executor;
+ InternalPnoScanResultsCallback pnoCallback = new InternalPnoScanResultsCallback(callback);
+
+ if (pnoExecutor == null) {
+ throw new IllegalArgumentException("executor cannot be null");
+ }
+ if (pnoSsids == null || pnoSsids.isEmpty()) {
+ // The real WifiServiceImpl throws an IllegalStateException in this case, so keeping it the
+ // same for consistency.
+ throw new IllegalStateException("Ssids can't be null or empty");
+ }
+ if (pnoSsids.size() > 2) {
+ throw new IllegalArgumentException("Ssid list can't be greater than 2");
+ }
+ if (pnoFrequencies != null && pnoFrequencies.length > 10) {
+ throw new IllegalArgumentException("Length of frequencies must be smaller than 10");
+ }
+ int uid = Binder.getCallingUid();
+ String packageName = getContext().getPackageName();
+
+ if (outstandingPnoScanRequest != null) {
+ pnoExecutor.execute(
+ () ->
+ pnoCallback.onRegisterFailed(
+ uid == outstandingPnoScanRequest.uid
+ ? InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED
+ : InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY));
+ return;
+ }
+
+ outstandingPnoScanRequest =
+ new PnoScanRequest(pnoSsids, pnoFrequencies, pnoExecutor, pnoCallback, packageName, uid);
+ pnoExecutor.execute(pnoCallback::onRegisterSuccess);
+ }
+ }
+
+ @Implementation(minSdk = TIRAMISU)
+ @HiddenApi
+ protected void clearExternalPnoScanRequest() {
+ synchronized (pnoRequestLock) {
+ if (outstandingPnoScanRequest != null
+ && outstandingPnoScanRequest.uid == Binder.getCallingUid()) {
+ InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback;
+ outstandingPnoScanRequest.executor.execute(
+ () ->
+ callback.onRemoved(
+ InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED));
+ outstandingPnoScanRequest = null;
+ }
+ }
+ }
+
+ private static class PnoScanRequest {
+ private final List<WifiSsid> ssids;
+ private final List<Integer> frequencies;
+ private final Executor executor;
+ private final InternalPnoScanResultsCallback callback;
+ private final String packageName;
+ private final int uid;
+
+ private PnoScanRequest(
+ List<WifiSsid> ssids,
+ int[] frequencies,
+ Executor executor,
+ InternalPnoScanResultsCallback callback,
+ String packageName,
+ int uid) {
+ this.ssids = List.copyOf(ssids);
+ this.frequencies =
+ frequencies == null ? List.of() : Arrays.stream(frequencies).boxed().collect(toList());
+ this.executor = executor;
+ this.callback = callback;
+ this.packageName = packageName;
+ this.uid = uid;
+ }
+ }
+
+ private Intent createPnoScanResultsBroadcastIntent() {
+ Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
+ intent.setPackage(outstandingPnoScanRequest.packageName);
+ return intent;
+ }
+
+ private static class InternalPnoScanResultsCallback {
+ static final int REGISTER_PNO_CALLBACK_ALREADY_REGISTERED = 1;
+ static final int REGISTER_PNO_CALLBACK_RESOURCE_BUSY = 2;
+ static final int REMOVE_PNO_CALLBACK_RESULTS_DELIVERED = 1;
+ static final int REMOVE_PNO_CALLBACK_UNREGISTERED = 2;
+
+ final Object callback;
+ final Method availableCallback;
+ final Method successCallback;
+ final Method failedCallback;
+ final Method removedCallback;
+
+ InternalPnoScanResultsCallback(Object callback) {
+ this.callback = callback;
+ try {
+ Class<?> pnoCallbackClass = callback.getClass();
+ availableCallback = pnoCallbackClass.getMethod("onScanResultsAvailable", List.class);
+ successCallback = pnoCallbackClass.getMethod("onRegisterSuccess");
+ failedCallback = pnoCallbackClass.getMethod("onRegisterFailed", int.class);
+ removedCallback = pnoCallbackClass.getMethod("onRemoved", int.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("callback is not of type PnoScanResultsCallback", e);
+ }
+ }
+
+ void onScanResultsAvailable(List<ScanResult> scanResults) {
+ invokeCallback(availableCallback, scanResults);
+ }
+
+ void onRegisterSuccess() {
+ invokeCallback(successCallback);
+ }
+
+ void onRegisterFailed(int reason) {
+ invokeCallback(failedCallback, reason);
+ }
+
+ void onRemoved(int reason) {
+ invokeCallback(removedCallback, reason);
+ }
+
+ void invokeCallback(Method method, Object... args) {
+ try {
+ method.invoke(callback, args);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException("Failed to invoke " + method.getName(), e);
+ }
+ }
+ }
}
diff --git a/shadows/httpclient/build.gradle b/shadows/httpclient/build.gradle
index 332c83e08..4e77a09c3 100644
--- a/shadows/httpclient/build.gradle
+++ b/shadows/httpclient/build.gradle
@@ -21,17 +21,17 @@ dependencies {
api project(":utils")
// We should keep httpclient version for low level API compatibility.
- earlyRuntime "org.apache.httpcomponents:httpcore:4.0.1"
- api "org.apache.httpcomponents:httpclient:4.0.3"
- compileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) { force = true }
+ earlyRuntime libs.apache.http.core
+ api libs.apache.http.client
+ compileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates)
testImplementation project(":robolectric")
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockito
testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
- testCompileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) { force = true }
+ testCompileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates)
testRuntimeOnly AndroidSdk.S.coordinates
}
diff --git a/shadows/playservices/build.gradle b/shadows/playservices/build.gradle
index c3abbba05..b00983893 100644
--- a/shadows/playservices/build.gradle
+++ b/shadows/playservices/build.gradle
@@ -14,25 +14,20 @@ shadows {
dependencies {
compileOnly project(":shadows:framework")
api project(":annotations")
- api "com.google.guava:guava:$guavaJREVersion"
+ api libs.guava
- compileOnly "androidx.fragment:fragment:1.2.0"
- compileOnly "com.google.android.gms:play-services-base:8.4.0"
- compileOnly "com.google.android.gms:play-services-basement:8.4.0"
+ compileOnly libs.bundles.play.services.base.for.shadows
compileOnly AndroidSdk.MAX_SDK.coordinates
testCompileOnly AndroidSdk.MAX_SDK.coordinates
- testCompileOnly "com.google.android.gms:play-services-base:8.4.0"
- testCompileOnly "com.google.android.gms:play-services-basement:8.4.0"
+ testCompileOnly libs.bundles.play.services.base.for.shadows
testImplementation project(":robolectric")
- testImplementation "junit:junit:$junitVersion"
- testImplementation "com.google.truth:truth:$truthVersion"
- testImplementation "org.mockito:mockito-core:$mockitoVersion"
- testRuntimeOnly "androidx.fragment:fragment:1.2.0"
- testRuntimeOnly "com.google.android.gms:play-services-base:8.4.0"
- testRuntimeOnly "com.google.android.gms:play-services-basement:8.4.0"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.mockito
+ testRuntimeOnly libs.bundles.play.services.base.for.shadows
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
}
diff --git a/shadows/versioning/Android.bp b/shadows/versioning/Android.bp
new file mode 100644
index 000000000..b630e226e
--- /dev/null
+++ b/shadows/versioning/Android.bp
@@ -0,0 +1,67 @@
+//#############################################
+// Compile Robolectric utils
+//#############################################
+
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "external_robolectric_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["external_robolectric_license"],
+}
+
+java_library_host {
+ name: "Robolectric_shadows_versioning_upstream",
+ srcs: ["src/main/java/**/*.java"],
+ static_libs: [
+ "robolectric-javax.annotation-api-1.2",
+ "Robolectric_shadowapi_upstream",
+ "Robolectric_utils_upstream",
+ "jsr305",
+ ],
+ libs: ["robolectric-host-android_all_upstream"],
+}
+
+//#############################################
+// Compile Robolectric utils tests
+//#############################################
+
+java_test_host {
+ name: "Robolectric_shadows_versioning_tests_upstream",
+ srcs: ["src/test/java/**/AndroidVersionsEdgeCaseTest.java"],
+ static_libs: [
+ "Robolectric_shadows_versioning_upstream",
+ "hamcrest",
+ "guava",
+ "junit",
+ "truth-prebuilt",
+ ],
+ test_suites: ["general-tests"],
+ test_options: {
+ unit_test: false,
+ },
+}
+
+//android_robolectric_test {
+// enabled: true,
+//
+// name: "Robolectric_shadows_versioning_tests_e2e_upstream",
+//
+// srcs: [
+// "src/**/*.AndroidVersionsTest.java",
+// ],
+//
+// java_resource_dirs: ["config"],
+//
+// libs: [
+// "androidx.test.core",
+// "androidx.test.runner",
+// ],
+//
+// instrumentation_for: "MyRoboApplication",
+//
+// upstream: true,
+//}
+
+
diff --git a/shadows/versioning/build.gradle b/shadows/versioning/build.gradle
new file mode 100644
index 000000000..68a8fb769
--- /dev/null
+++ b/shadows/versioning/build.gradle
@@ -0,0 +1,21 @@
+import org.robolectric.gradle.DeployedRoboJavaModulePlugin
+import org.robolectric.gradle.RoboJavaModulePlugin
+
+apply plugin: RoboJavaModulePlugin
+apply plugin: DeployedRoboJavaModulePlugin
+
+configurations {
+ earlyRuntime
+}
+
+dependencies {
+ api project(":shadowapi")
+ compileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK (AndroidSdk.s.coordinates) { force = true }
+ testImplementation project(":robolectric")
+ testImplementation libs.truth
+ testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
+ testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK
+ testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports
+}
+
+
diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java
new file mode 100644
index 000000000..316365b39
--- /dev/null
+++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java
@@ -0,0 +1,23 @@
+package org.robolectric.versioning;
+
+import java.io.IOException;
+import java.util.Properties;
+import java.util.jar.JarFile;
+import org.robolectric.versioning.AndroidVersions.AndroidRelease;
+
+/**
+ * Utility access method to allow robolectric to instantiate AndroidVersions without cluttering code
+ * completion for users of AndroidVersions's embedded Types of one per Android Releases.
+ */
+public final class AndroidVersionInitTools {
+
+ private AndroidVersionInitTools() {}
+
+ public static AndroidRelease computeReleaseVersion(JarFile jarFile) throws IOException {
+ return AndroidVersions.computeReleaseVersion(jarFile);
+ }
+
+ public static AndroidRelease computeCurrentSdkFromBuildProps(Properties buildProps) {
+ return AndroidVersions.computeCurrentSdkFromBuildProps(buildProps);
+ }
+}
diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java
new file mode 100644
index 000000000..631451883
--- /dev/null
+++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java
@@ -0,0 +1,779 @@
+package org.robolectric.versioning;
+
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import static java.util.Arrays.asList;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import javax.annotation.Nullable;
+import org.robolectric.util.Logger;
+import org.robolectric.util.ReflectionHelpers;
+
+/**
+ * Android versioning is complicated.<br>
+ * 1) There is a yearly letter release with an increasing of one alpha step each year A-> B, B-> C,
+ * and so on. While commonly referenced these are not the release numbers. This class calls these
+ * shortcodes. Also minor version number releases (usually within the same year) will start with the
+ * same letter.<br>
+ * 2) There is an SDK_INT field in android.os.Build.VERSION that tracks a version of the internal
+ * SDK. While useful to track the actual released versions of Android, these are not the release
+ * number. More importantly, android.os.Build.VERSION uses code names to describe future versions.
+ * Multiple code names may be in development at once on different branches of Android.<br>
+ * 3) There is a yearly release major number followed by a minor number, which may or may not be
+ * used.<br>
+ * 4) Relevant logic and reasoning should match androidx.core.os.BuildCompat.java with the caveat
+ * that this class guess at the future release version number and short of the current dev branch.
+ * <br>
+ */
+public final class AndroidVersions {
+
+ private AndroidVersions() {}
+
+ /** Representation of an android release, one that has occurred, or is expected. */
+ public abstract static class AndroidRelease implements Comparable<AndroidRelease> {
+
+ /**
+ * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt may
+ * still be that of the prior release.
+ */
+ public int getSdkInt() {
+ return ReflectionHelpers.getStaticField(this.getClass(), "SDK_INT");
+ }
+
+ /**
+ * single character short code for the release, multiple characters for minor releases (only
+ * minor version numbers increment - usually within the same year).
+ */
+ public String getShortCode() {
+ return ReflectionHelpers.getStaticField(this.getClass(), "SHORT_CODE");
+ }
+
+ /**
+ * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt will
+ * guess at the likely sdk number. Your code will need to recompile if this value changes -
+ * including most modern build tools; bazle, soong all are full build systems - and as such
+ * organizations using them have no concerns.
+ */
+ public boolean isReleased() {
+ return ReflectionHelpers.getStaticField(this.getClass(), "RELEASED");
+ }
+
+ /** major.minor version number as String. */
+ public String getVersion() {
+ return ReflectionHelpers.getStaticField(this.getClass(), "VERSION");
+ }
+
+ /**
+ * Implements comparable.
+ *
+ * @param other the object to be compared.
+ * @return 1 if this is greater than other, 0 if equal, -1 if less
+ * @throws RuntimeException if other is not an instance of AndroidRelease.
+ */
+ @Override
+ public int compareTo(AndroidRelease other) {
+ if (other == null) {
+ throw new RuntimeException(
+ "Only "
+ + AndroidVersions.class.getName()
+ + " should define Releases, illegal class "
+ + other.getClass());
+ }
+ return Integer.compare(this.getSdkInt(), other.getSdkInt());
+ }
+
+ @Override
+ public String toString() {
+ return "Android "
+ + (this.isReleased() ? "" : "Future ")
+ + "Release: "
+ + this.getVersion()
+ + " ( sdk: "
+ + this.getSdkInt()
+ + " code: "
+ + this.getShortCode()
+ + " )";
+ }
+ }
+
+ /**
+ * Version: 4.1 <br>
+ * ShortCode: J <br>
+ * SDK API Level: 16 <br>
+ * release: true <br>
+ */
+ public static final class J extends AndroidRelease {
+
+ public static final int SDK_INT = 16;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "J";
+
+ public static final String VERSION = "4.1";
+ }
+
+ /**
+ * Version: 4.2 <br>
+ * ShortCode: JMR1 <br>
+ * SDK API Level: 17 <br>
+ * release: true <br>
+ */
+ public static final class JMR1 extends AndroidRelease {
+
+ public static final int SDK_INT = 17;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "JMR1";
+
+ public static final String VERSION = "4.2";
+ }
+
+ /**
+ * Version: 4.3 <br>
+ * ShortCode: JMR2 <br>
+ * SDK API Level: 18 <br>
+ * release: true <br>
+ */
+ public static final class JMR2 extends AndroidRelease {
+
+ public static final int SDK_INT = 18;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "JMR2";
+
+ public static final String VERSION = "4.3";
+ }
+
+ /**
+ * Version: 4.4 <br>
+ * ShortCode: K <br>
+ * SDK API Level: 19 <br>
+ * release: true <br>
+ */
+ public static final class K extends AndroidRelease {
+
+ public static final int SDK_INT = 19;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "K";
+
+ public static final String VERSION = "4.4";
+ }
+
+ // Skipping K Watch release, which was 20.
+
+ /**
+ * Version: 5.0 <br>
+ * ShortCode: L <br>
+ * SDK API Level: 21 <br>
+ * release: true <br>
+ */
+ public static final class L extends AndroidRelease {
+
+ public static final int SDK_INT = 21;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "L";
+
+ public static final String VERSION = "5.0";
+ }
+
+ /**
+ * Version: 5.1 <br>
+ * ShortCode: LMR1 <br>
+ * SDK API Level: 22 <br>
+ * release: true <br>
+ */
+ public static final class LMR1 extends AndroidRelease {
+
+ public static final int SDK_INT = 22;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "LMR1";
+
+ public static final String VERSION = "5.1";
+ }
+
+ /**
+ * Version: 6.0 <br>
+ * ShortCode: M <br>
+ * SDK API Level: 23 <br>
+ * release: true <br>
+ */
+ public static final class M extends AndroidRelease {
+
+ public static final int SDK_INT = 23;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "M";
+
+ public static final String VERSION = "6.0";
+ }
+
+ /**
+ * Version: 7.0 <br>
+ * ShortCode: N <br>
+ * SDK API Level: 24 <br>
+ * release: true <br>
+ */
+ public static final class N extends AndroidRelease {
+
+ public static final int SDK_INT = 24;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "N";
+
+ public static final String VERSION = "7.0";
+ }
+
+ /**
+ * Release: 7.1 <br>
+ * ShortCode: NMR1 <br>
+ * SDK Framework: 25 <br>
+ * release: true <br>
+ */
+ public static final class NMR1 extends AndroidRelease {
+
+ public static final int SDK_INT = 25;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "NMR1";
+
+ private static final String VERSION = "7.1";
+ }
+
+ /**
+ * Release: 8.0 <br>
+ * ShortCode: O <br>
+ * SDK API Level: 26 <br>
+ * release: true <br>
+ */
+ public static final class O extends AndroidRelease {
+
+ public static final int SDK_INT = 26;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "O";
+
+ public static final String VERSION = "8.0";
+ }
+
+ /**
+ * Release: 8.1 <br>
+ * ShortCode: OMR1 <br>
+ * SDK API Level: 27 <br>
+ * release: true <br>
+ */
+ public static final class OMR1 extends AndroidRelease {
+
+ public static final int SDK_INT = 27;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "OMR1";
+
+ public static final String VERSION = "8.1";
+ }
+
+ /**
+ * Release: 9.0 <br>
+ * ShortCode: P <br>
+ * SDK API Level: 28 <br>
+ * release: true <br>
+ */
+ public static final class P extends AndroidRelease {
+
+ public static final int SDK_INT = 28;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "P";
+
+ public static final String VERSION = "9.0";
+ }
+
+ /**
+ * Release: 10.0 <br>
+ * ShortCode: Q <br>
+ * SDK API Level: 29 <br>
+ * release: true <br>
+ */
+ public static final class Q extends AndroidRelease {
+
+ public static final int SDK_INT = 29;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "Q";
+
+ public static final String VERSION = "10.0";
+ }
+
+ /**
+ * Release: 11.0 <br>
+ * ShortCode: R <br>
+ * SDK API Level: 30 <br>
+ * release: true <br>
+ */
+ public static final class R extends AndroidRelease {
+
+ public static final int SDK_INT = 30;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "R";
+
+ public static final String VERSION = "11.0";
+ }
+
+ /**
+ * Release: 12.0 <br>
+ * ShortCode: S <br>
+ * SDK API Level: 31 <br>
+ * release: true <br>
+ */
+ public static final class S extends AndroidRelease {
+
+ public static final int SDK_INT = 31;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "S";
+
+ public static final String VERSION = "12.0";
+ }
+
+ /**
+ * Release: 12.1 <br>
+ * ShortCode: Sv2 <br>
+ * SDK API Level: 32 <br>
+ * release: true <br>
+ */
+ @SuppressWarnings("UPPER_SNAKE_CASE")
+ public static final class Sv2 extends AndroidRelease {
+
+ public static final int SDK_INT = 32;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "Sv2";
+
+ public static final String VERSION = "12.1";
+ }
+
+ /**
+ * Release: 13.0 <br>
+ * ShortCode: T <br>
+ * SDK API Level: 33 <br>
+ * release: true <br>
+ */
+ public static final class T extends AndroidRelease {
+
+ public static final int SDK_INT = 33;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "T";
+
+ public static final String VERSION = "13.0";
+ }
+
+ /**
+ * Potential Release: 14.0 <br>
+ * ShortCode: U <br>
+ * SDK API Level: 34 <br>
+ * release: false <br>
+ */
+ public static final class U extends AndroidRelease {
+
+ public static final int SDK_INT = 34;
+
+ public static final boolean RELEASED = true;
+
+ public static final String SHORT_CODE = "U";
+
+ public static final String VERSION = "14.0";
+ }
+
+ /**
+ * Potential Release: 15.0 <br>
+ * ShortCode: V <br>
+ * SDK API Level: 34+ <br>
+ * release: false <br>
+ */
+ public static final class V extends AndroidRelease {
+
+ public static final int SDK_INT = 35;
+
+ public static final boolean RELEASED = false;
+
+ public static final String SHORT_CODE = "V";
+
+ public static final String VERSION = "15.0";
+ }
+
+ /** The current release this process is running on. */
+ public static final AndroidRelease CURRENT;
+
+ @Nullable
+ public static AndroidRelease getReleaseForSdkInt(@Nullable Integer sdkInt) {
+ if (sdkInt == null) {
+ return null;
+ } else {
+ return information.sdkIntToAllReleases.get(sdkInt);
+ }
+ }
+
+ public static List<AndroidRelease> getReleases() {
+ List<AndroidRelease> output = new ArrayList<>();
+ for (AndroidRelease release : information.allReleases) {
+ if (release.isReleased()) {
+ output.add(release);
+ }
+ }
+ return output;
+ }
+
+ public static List<AndroidRelease> getUnreleased() {
+ List<AndroidRelease> output = new ArrayList<>();
+ for (AndroidRelease release : information.allReleases) {
+ if (!release.isReleased()) {
+ output.add(release);
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Responsible for aggregating and interpreting the static state representing the current
+ * AndroidReleases known to AndroidVersions class.
+ */
+ static class SdkInformation {
+ final List<AndroidRelease> allReleases;
+ final List<Class<? extends AndroidRelease>> classesWithIllegalNames;
+ final AndroidRelease latestRelease;
+ final AndroidRelease earliestUnreleased;
+
+ // In the future we may need a multimap for sdkInts should they stay static across releases.
+ final Map<Integer, AndroidRelease> sdkIntToAllReleases = new HashMap<>();
+ final Map<String, AndroidRelease> shortCodeToAllReleases = new HashMap<>();
+
+ // detected errors
+ final List<Map.Entry<AndroidRelease, AndroidRelease>> sdkIntCollisions = new ArrayList<>();
+ Map.Entry<AndroidRelease, AndroidRelease> sdkApiMisordered = null;
+
+ public SdkInformation(
+ List<AndroidRelease> releases,
+ List<Class<? extends AndroidRelease>> classesWithIllegalNames) {
+ this.allReleases = releases;
+ this.classesWithIllegalNames = classesWithIllegalNames;
+ AndroidRelease latestRelease = null;
+ AndroidRelease earliestUnreleased = null;
+ for (AndroidRelease release : allReleases) {
+ if (release.isReleased()) {
+ if (latestRelease == null || latestRelease.compareTo(release) > 0) {
+ latestRelease = release;
+ }
+ } else {
+ if (earliestUnreleased == null || earliestUnreleased.compareTo(release) < 0) {
+ earliestUnreleased = release;
+ }
+ }
+ }
+ this.latestRelease = latestRelease;
+ this.earliestUnreleased = earliestUnreleased;
+ verifyStaticInformation();
+ }
+
+ private void verifyStaticInformation() {
+ for (AndroidRelease release : this.allReleases) {
+ // Construct a map of all sdkInts to releases and note duplicates
+ AndroidRelease sdkCollision = this.sdkIntToAllReleases.put(release.getSdkInt(), release);
+ if (sdkCollision != null) {
+ this.sdkIntCollisions.add(new AbstractMap.SimpleEntry<>(release, sdkCollision));
+ }
+ // Construct a map of all short codes to releases, and note duplicates
+ this.shortCodeToAllReleases.put(release.getShortCode(), release);
+ // There is no need to check for shortCode duplicates as the Field name must match the
+ // short code.
+ }
+ if (earliestUnreleased != null
+ && latestRelease != null
+ && latestRelease.getSdkInt() >= earliestUnreleased.getSdkInt()) {
+ sdkApiMisordered = new AbstractMap.SimpleEntry<>(latestRelease, earliestUnreleased);
+ }
+ }
+
+ private void throwStaticErrors() {
+ StringBuilder errors = new StringBuilder();
+ if (!this.classesWithIllegalNames.isEmpty()) {
+ errors
+ .append("The following classes do not follow the naming criteria for ")
+ .append("releases or do not have the short codes in ")
+ .append("their internal fields. Please correct them: ")
+ .append(this.classesWithIllegalNames)
+ .append("\n");
+ }
+ if (sdkApiMisordered != null) {
+ errors
+ .append("The latest released sdk ")
+ .append(sdkApiMisordered.getKey().getShortCode())
+ .append(" has a sdkInt greater than the earliest unreleased sdk ")
+ .append(sdkApiMisordered.getValue().getShortCode())
+ .append("this implies sdks were released out of order which is highly unlikely.\n");
+ }
+ if (!sdkIntCollisions.isEmpty()) {
+ errors.append(
+ "The following sdks have different shortCodes, but identical sdkInt " + "versions:\n");
+ for (Map.Entry<AndroidRelease, AndroidRelease> entry : sdkIntCollisions) {
+ errors
+ .append("Both ")
+ .append(entry.getKey().getShortCode())
+ .append(" and ")
+ .append(entry.getValue().getShortCode())
+ .append("have the same sdkInt value of ")
+ .append(entry.getKey().getSdkInt())
+ .append("\n");
+ }
+ }
+ if (errors.length() > 0) {
+ throw new RuntimeException(
+ errors
+ .append("Please check the AndroidReleases defined ")
+ .append("in ")
+ .append(AndroidVersions.class.getName())
+ .append("and ensure they are aligned with the versions of")
+ .append(" Android.")
+ .toString());
+ }
+ }
+
+ public AndroidRelease computeCurrentSdk(
+ int reportedVersion, String releaseName, String codename, List<String> activeCodeNames) {
+ Logger.info("Reported Version: " + reportedVersion);
+ Logger.info("Release Name: " + releaseName);
+ Logger.info("Code Name: " + codename);
+ Logger.info("Active Code Names: " + String.join(",", activeCodeNames));
+
+ AndroidRelease current = null;
+ // Special case "REL", which means the build is not a pre-release build.
+ if ("REL".equals(codename)) {
+ // the first letter of the code name equal to the release number.
+ current = sdkIntToAllReleases.get(reportedVersion);
+ if (current != null && !current.isReleased()) {
+ throw new RuntimeException(
+ "The current sdk "
+ + current.getShortCode()
+ + " has been released. Please update the contents of "
+ + AndroidVersions.class.getName()
+ + " to mark sdk "
+ + current.getShortCode()
+ + " as released.");
+ }
+ } else {
+ // Get known active code name letters
+
+ List<String> activeCodenameLetter = new ArrayList<>();
+ for (String name : activeCodeNames) {
+ activeCodenameLetter.add(name.toUpperCase(Locale.getDefault()).substring(0, 1));
+ }
+
+ // If the process is operating with a code name.
+ if (codename != null) {
+ StringBuilder detectedProblems = new StringBuilder();
+ // This is safe for minor releases ( X.1 ) as long as they have added an entry
+ // corresponding to the sdk of that release and the prior major release is marked as
+ // "released" on its entry in this file. If not this class will fail to initialize.
+ // The assumption is that only one of the major or minor version of a code name
+ // is under development and unreleased at any give time (S or Sv2).
+ String foundCode = codename.toUpperCase(Locale.getDefault()).substring(0, 1);
+ int loc = activeCodenameLetter.indexOf(foundCode);
+ if (loc == -1) {
+ detectedProblems
+ .append("The current codename's (")
+ .append(codename)
+ .append(") first letter (")
+ .append(foundCode)
+ .append(") is not in the list of active code's first letters: ")
+ .append(activeCodenameLetter)
+ .append("\n");
+ } else {
+ // attempt to find assume the fullname is the "shortCode", aka "Sv2", "OMR1".
+ current = shortCodeToAllReleases.get(codename);
+ // else, assume the fullname is the first letter is correct.
+ if (current == null) {
+ current = shortCodeToAllReleases.get(String.valueOf(foundCode));
+ }
+ }
+ if (current == null) {
+ detectedProblems
+ .append("No known release is associated with the shortCode of \"")
+ .append(foundCode)
+ .append("\" or \"")
+ .append(codename)
+ .append("\"\n");
+ } else if (current.isReleased()) {
+ detectedProblems
+ .append("The current sdk ")
+ .append(current.getShortCode())
+ .append(" has been been marked as released. Please update the ")
+ .append("contents of current sdk jar to the released version.\n");
+ }
+ if (detectedProblems.length() > 0) {
+ throw new RuntimeException(detectedProblems.toString());
+ }
+ }
+ }
+ return current;
+ }
+ }
+
+ /**
+ * Reads all AndroidReleases in this class and populates SdkInformation, checking for sanity in
+ * the shortCode, sdkInt, and release information.
+ *
+ * <p>All errors are stored and can be reported at once by asking the SdkInformation to throw a
+ * runtime exception after it has been populated.
+ */
+ static SdkInformation gatherStaticSdkInformationFromThisClass() {
+ List<AndroidRelease> allReleases = new ArrayList<>();
+ List<Class<? extends AndroidRelease>> classesWithIllegalNames = new ArrayList<>();
+ for (Class<?> clazz : AndroidVersions.class.getClasses()) {
+ if (AndroidRelease.class.isAssignableFrom(clazz)
+ && !clazz.isInterface()
+ && !Modifier.isAbstract(clazz.getModifiers())) {
+ try {
+ AndroidRelease rel = (AndroidRelease) clazz.getDeclaredConstructor().newInstance();
+ allReleases.add(rel);
+ // inspect field name - as this is our only chance to inspect it.
+ if (!rel.getClass().getSimpleName().equals(rel.getShortCode())) {
+ classesWithIllegalNames.add(rel.getClass());
+ }
+ } catch (NoSuchMethodException
+ | InstantiationException
+ | IllegalArgumentException
+ | IllegalAccessException
+ | InvocationTargetException ex) {
+ throw new RuntimeException(
+ "Classes "
+ + clazz.getName()
+ + "should be accessible via "
+ + AndroidVersions.class.getCanonicalName()
+ + " and have a default public no-op constructor ",
+ ex);
+ }
+ }
+ }
+ Collections.sort(allReleases, AndroidRelease::compareTo);
+
+ SdkInformation sdkInformation = new SdkInformation(allReleases, classesWithIllegalNames);
+ sdkInformation.throwStaticErrors();
+ return sdkInformation;
+ }
+
+ static AndroidRelease computeReleaseVersion(JarFile jarFile) throws IOException {
+ ZipEntry buildProp = jarFile.getEntry("build.prop");
+ Properties buildProps = new Properties();
+ buildProps.load(jarFile.getInputStream(buildProp));
+ return computeCurrentSdkFromBuildProps(buildProps);
+ }
+
+ static AndroidRelease computeCurrentSdkFromBuildProps(Properties buildProps) {
+ // 33, 34, 35 ....
+ String sdkVersionString = buildProps.getProperty("ro.build.version.sdk");
+ int sdk = sdkVersionString == null ? 0 : Integer.parseInt(sdkVersionString);
+ // "REL"
+ String release = buildProps.getProperty("ro.build.version.release");
+ // "Tiramasu", "UpsideDownCake"
+ String codename = buildProps.getProperty("ro.build.version.codename");
+ // "Tiramasu,UpsideDownCake", "UpsideDownCake", "REL"
+ String codenames = buildProps.getProperty("ro.build.version.all_codenames");
+ String[] allCodeNames = codenames == null ? new String[0] : codenames.split(",");
+ String[] activeCodeNames =
+ allCodeNames.length > 0 && allCodeNames[0].equals("REL") ? new String[0] : allCodeNames;
+ return information.computeCurrentSdk(sdk, release, codename, asList(activeCodeNames));
+ }
+
+ /**
+ * If we are working in android source, this code detects the list of active code names if any.
+ */
+ private static List<String> getActiveCodeNamesIfAny(Class<?> targetClass) {
+ try {
+ Field activeCodeFields = targetClass.getDeclaredField("ACTIVE_CODENAMES");
+ String[] activeCodeNames = (String[]) activeCodeFields.get(null);
+ return asList(activeCodeNames);
+ } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException ex) {
+ return new ArrayList<>();
+ }
+ }
+
+ private static final SdkInformation information;
+
+ static {
+ AndroidRelease currentRelease = null;
+ information = gatherStaticSdkInformationFromThisClass();
+ try {
+ Class<?> buildClass =
+ Class.forName("android.os.Build", false, Thread.currentThread().getContextClassLoader());
+ System.out.println("build class " + buildClass);
+ Class<?> versionClass = null;
+ for (Class<?> c : buildClass.getClasses()) {
+ if (c.getSimpleName().equals("VERSION")) {
+ versionClass = c;
+ System.out.println("Version class " + versionClass);
+ break;
+ }
+ }
+ if (versionClass != null) {
+ // 33, 34, etc....
+ int sdkInt = (int) ReflectionHelpers.getStaticField(versionClass, "SDK_INT");
+ // Either unset, or 13, 14, etc....
+ String release = ReflectionHelpers.getStaticField(versionClass, "RELEASE");
+ // Either REL if release is set, or Tiramasu, UpsideDownCake, etc
+ String codename = ReflectionHelpers.getStaticField(versionClass, "CODENAME");
+ List<String> activeCodeNames = getActiveCodeNamesIfAny(versionClass);
+ currentRelease = information.computeCurrentSdk(sdkInt, release, codename, activeCodeNames);
+ }
+ } catch (ClassNotFoundException | IllegalArgumentException | UnsatisfiedLinkError e) {
+ // No op, this class should be usable outside of a Robolectric sandbox.
+ }
+ CURRENT = currentRelease;
+ }
+}
diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java
new file mode 100644
index 000000000..95b2c4266
--- /dev/null
+++ b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java
@@ -0,0 +1,71 @@
+package org.robolectric.versioning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.robolectric.versioning.AndroidVersions.AndroidRelease;
+import org.robolectric.versioning.AndroidVersions.SdkInformation;
+
+/** Test more esoteric versions mismatches in sdkInt numbers, and codenames. */
+@RunWith(JUnit4.class)
+public final class AndroidVersionsEdgeCaseTest {
+
+ /**
+ * sdkInt higher than any known release, claims it's released. Expects an error message to update
+ * to update the AndroidVersions.class
+ */
+ @Test
+ public void sdkIntHigherThanKnownReleasesClaimsIsReleased_throwsException() {
+ AndroidRelease earliestUnrelease = null;
+ try {
+ SdkInformation information = AndroidVersions.gatherStaticSdkInformationFromThisClass();
+ earliestUnrelease = information.earliestUnreleased;
+ information.computeCurrentSdk(
+ earliestUnrelease.getSdkInt(), earliestUnrelease.getVersion(), "REL", Arrays.asList());
+ assertThat(this).isNull();
+ } catch (RuntimeException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .contains(
+ "The current sdk "
+ + earliestUnrelease.getShortCode()
+ + " has been released. Please update the contents of "
+ + AndroidVersions.class.getName()
+ + " to mark sdk "
+ + earliestUnrelease.getShortCode()
+ + " as released.");
+ assertThat(e).isInstanceOf(RuntimeException.class);
+ }
+ }
+
+ /**
+ * sdkInt lower than known release, claims it's released. Expects an error message to update the
+ * jar.
+ */
+ @Test
+ public void sdkIntReleasedButStillReportsCodeName_throwsException() {
+ AndroidRelease latestRelease = null;
+ try {
+ SdkInformation information = AndroidVersions.gatherStaticSdkInformationFromThisClass();
+ latestRelease = information.latestRelease;
+ information.computeCurrentSdk(
+ latestRelease.getSdkInt(),
+ null,
+ information.latestRelease.getShortCode(),
+ Arrays.asList(latestRelease.getShortCode()));
+ assertThat(this).isNull();
+ } catch (RuntimeException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .contains(
+ "The current sdk "
+ + latestRelease.getShortCode()
+ + " has been been marked as released. Please update the contents of current sdk"
+ + " jar to the released version.");
+ assertThat(e).isInstanceOf(RuntimeException.class);
+ }
+ }
+}
diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java
new file mode 100644
index 000000000..dba93cab2
--- /dev/null
+++ b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java
@@ -0,0 +1,205 @@
+package org.robolectric.versioning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build.VERSION;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.versioning.AndroidVersions.T;
+
+/**
+ * Check versions information aligns with runtime information. Primarily, selected SDK with
+ * internally detected version number.
+ */
+@RunWith(RobolectricTestRunner.class)
+public final class AndroidVersionsTest {
+
+ @Test
+ @Config(sdk = T.SDK_INT)
+ public void testStandardInitializationT() {
+ assertThat(VERSION.SDK_INT).isEqualTo(33);
+ assertThat(VERSION.RELEASE).isEqualTo("13");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.T.SHORT_CODE).isEqualTo("T");
+ assertThat(new AndroidVersions.T().getVersion()).isEqualTo("13.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("T");
+ }
+
+ @Test
+ @Config(sdk = 32)
+ public void testStandardInitializationSv2() {
+ assertThat(VERSION.SDK_INT).isEqualTo(32);
+ assertThat(VERSION.RELEASE).isEqualTo("12");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.Sv2.SHORT_CODE).isEqualTo("Sv2");
+ assertThat(new AndroidVersions.Sv2().getVersion()).isEqualTo("12.1");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Sv2");
+ }
+
+ @Test
+ @Config(sdk = 31)
+ public void testStandardInitializationS() {
+ assertThat(VERSION.SDK_INT).isEqualTo(31);
+ assertThat(VERSION.RELEASE).isEqualTo("12");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.S.SHORT_CODE).isEqualTo("S");
+ assertThat(new AndroidVersions.S().getVersion()).isEqualTo("12.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("S");
+ }
+
+ @Test
+ @Config(sdk = 30)
+ public void testStandardInitializationR() {
+ assertThat(VERSION.SDK_INT).isEqualTo(30);
+ assertThat(VERSION.RELEASE).isEqualTo("11");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.R.SHORT_CODE).isEqualTo("R");
+ assertThat(new AndroidVersions.R().getVersion()).isEqualTo("11.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("R");
+ }
+
+ @Test
+ @Config(sdk = 29)
+ public void testStandardInitializationQ() {
+ assertThat(VERSION.SDK_INT).isEqualTo(29);
+ assertThat(VERSION.RELEASE).isEqualTo("10");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.Q.SHORT_CODE).isEqualTo("Q");
+ assertThat(new AndroidVersions.Q().getVersion()).isEqualTo("10.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Q");
+ }
+
+ @Test
+ @Config(sdk = 28)
+ public void testStandardInitializationP() {
+ assertThat(VERSION.SDK_INT).isEqualTo(28);
+ assertThat(VERSION.RELEASE).isEqualTo("9");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.P.SHORT_CODE).isEqualTo("P");
+ assertThat(new AndroidVersions.P().getVersion()).isEqualTo("9.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("P");
+ }
+
+ @Test
+ @Config(sdk = 27)
+ public void testStandardInitializationOMR1() {
+ assertThat(VERSION.SDK_INT).isEqualTo(27);
+ assertThat(VERSION.RELEASE).isEqualTo("8.1.0");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.OMR1.SHORT_CODE).isEqualTo("OMR1");
+ assertThat(new AndroidVersions.OMR1().getVersion()).isEqualTo("8.1");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("OMR1");
+ }
+
+ @Test
+ @Config(sdk = 26)
+ public void testStandardInitializationO() {
+ assertThat(VERSION.SDK_INT).isEqualTo(26);
+ assertThat(VERSION.RELEASE).isEqualTo("8.0.0");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.O.SHORT_CODE).isEqualTo("O");
+ assertThat(new AndroidVersions.O().getVersion()).isEqualTo("8.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("O");
+ }
+
+ @Test
+ @Config(sdk = 25)
+ public void testStandardInitializationNMR1() {
+ assertThat(VERSION.SDK_INT).isEqualTo(25);
+ assertThat(VERSION.RELEASE).isEqualTo("7.1");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.NMR1.SHORT_CODE).isEqualTo("NMR1");
+ assertThat(new AndroidVersions.NMR1().getVersion()).isEqualTo("7.1");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("NMR1");
+ }
+
+ @Test
+ @Config(sdk = 24)
+ public void testStandardInitializationN() {
+ assertThat(VERSION.SDK_INT).isEqualTo(24);
+ assertThat(VERSION.RELEASE).isEqualTo("7.0");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.N.SHORT_CODE).isEqualTo("N");
+ assertThat(new AndroidVersions.N().getVersion()).isEqualTo("7.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("N");
+ }
+
+ @Test
+ @Config(sdk = 23)
+ public void testStandardInitializationM() {
+ assertThat(VERSION.SDK_INT).isEqualTo(23);
+ assertThat(VERSION.RELEASE).isEqualTo("6.0.1");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.M.SHORT_CODE).isEqualTo("M");
+ assertThat(new AndroidVersions.M().getVersion()).isEqualTo("6.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("M");
+ }
+
+ @Test
+ @Config(sdk = 22)
+ public void testStandardInitializationLMR1() {
+ assertThat(VERSION.SDK_INT).isEqualTo(22);
+ assertThat(VERSION.RELEASE).isEqualTo("5.1.1");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.LMR1.SHORT_CODE).isEqualTo("LMR1");
+ assertThat(new AndroidVersions.LMR1().getVersion()).isEqualTo("5.1");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("LMR1");
+ }
+
+ @Test
+ @Config(sdk = 21)
+ public void testStandardInitializationL() {
+ assertThat(VERSION.SDK_INT).isEqualTo(21);
+ assertThat(VERSION.RELEASE).isEqualTo("5.0.2");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.L.SHORT_CODE).isEqualTo("L");
+ assertThat(new AndroidVersions.L().getVersion()).isEqualTo("5.0");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("L");
+ }
+
+ @Test
+ @Config(sdk = 19)
+ public void testStandardInitializationK() {
+ assertThat(VERSION.SDK_INT).isEqualTo(19);
+ assertThat(VERSION.RELEASE).isEqualTo("4.4");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.K.SHORT_CODE).isEqualTo("K");
+ assertThat(new AndroidVersions.K().getVersion()).isEqualTo("4.4");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("K");
+ }
+
+ @Test
+ @Config(sdk = 18)
+ public void testStandardInitializationJMR2() {
+ assertThat(VERSION.SDK_INT).isEqualTo(18);
+ assertThat(VERSION.RELEASE).isEqualTo("4.3");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.JMR2.SHORT_CODE).isEqualTo("JMR2");
+ assertThat(new AndroidVersions.JMR2().getVersion()).isEqualTo("4.3");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("JMR2");
+ }
+
+ @Test
+ @Config(sdk = 17)
+ public void testStandardInitializationJMR1() {
+ assertThat(VERSION.SDK_INT).isEqualTo(17);
+ assertThat(VERSION.RELEASE).isEqualTo("4.2.2");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.JMR1.SHORT_CODE).isEqualTo("JMR1");
+ assertThat(new AndroidVersions.JMR1().getVersion()).isEqualTo("4.2");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("JMR1");
+ }
+
+ @Test
+ @Config(sdk = 16)
+ public void testStandardInitializationJ() {
+ assertThat(VERSION.SDK_INT).isEqualTo(16);
+ assertThat(VERSION.RELEASE).isEqualTo("4.1.2");
+ assertThat(VERSION.CODENAME).isEqualTo("REL");
+ assertThat(AndroidVersions.J.SHORT_CODE).isEqualTo("J");
+ assertThat(new AndroidVersions.J().getVersion()).isEqualTo("4.1");
+ assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("J");
+ }
+}
diff --git a/shadows/versioning/src/test/resources/AndroidManifest.xml b/shadows/versioning/src/test/resources/AndroidManifest.xml
new file mode 100644
index 000000000..65383ac0b
--- /dev/null
+++ b/shadows/versioning/src/test/resources/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.robolectric">
+ <uses-sdk android:targetSdkVersion="33" android:minSdkVersion="33"/>
+ <application android:name="android.app.Application">
+ </application>
+</manifest>
diff --git a/testapp/build.gradle b/testapp/build.gradle
index 651ced0ea..0abf895ae 100644
--- a/testapp/build.gradle
+++ b/testapp/build.gradle
@@ -2,6 +2,7 @@ apply plugin: 'com.android.library'
android {
compileSdk 33
+ namespace 'org.robolectric.testapp'
defaultConfig {
minSdk 16
diff --git a/utils/build.gradle b/utils/build.gradle
index c10cca279..c31c9a0e0 100644
--- a/utils/build.gradle
+++ b/utils/build.gradle
@@ -1,3 +1,4 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.robolectric.gradle.DeployedRoboJavaModulePlugin
import org.robolectric.gradle.RoboJavaModulePlugin
@@ -13,7 +14,7 @@ spotless {
}
}
-tasks.withType(GenerateModuleMetadata) {
+tasks.withType(GenerateModuleMetadata).configureEach {
// We don't want to release gradle module metadata now to avoid
// potential compatibility problems.
enabled = false
@@ -26,7 +27,7 @@ compileKotlin {
// 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 = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+ compilerOptions.jvmTarget = JvmTarget.JVM_1_8
}
afterEvaluate {
@@ -48,20 +49,19 @@ afterEvaluate {
dependencies {
api project(":annotations")
api project(":pluginapi")
- api "javax.inject:javax.inject:1"
- api "javax.annotation:javax.annotation-api:1.3.2"
+ api libs.javax.inject
+ api libs.javax.annotation.api
// For @VisibleForTesting and ByteStreams
- implementation "com.google.guava:guava:$guavaJREVersion"
- compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+ implementation libs.guava
+ compileOnly libs.findbugs.jsr305
- testCompileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
- testAnnotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
- testAnnotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
- implementation "com.google.errorprone:error_prone_annotations:$errorproneVersion"
+ testCompileOnly libs.auto.service.annotations
+ testAnnotationProcessor libs.auto.service
+ testAnnotationProcessor libs.error.prone.core
+ implementation libs.error.prone.annotations
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
+ testImplementation libs.kotlin.stdlib
}
diff --git a/utils/reflector/build.gradle b/utils/reflector/build.gradle
index 302734505..140e98700 100644
--- a/utils/reflector/build.gradle
+++ b/utils/reflector/build.gradle
@@ -5,12 +5,12 @@ apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
dependencies {
- api "org.ow2.asm:asm:${asmVersion}"
- api "org.ow2.asm:asm-commons:${asmVersion}"
- api "org.ow2.asm:asm-util:${asmVersion}"
+ api libs.asm
+ api libs.asm.commons
+ api libs.asm.util
api project(":utils")
testImplementation project(":shadowapi")
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
+ testImplementation libs.junit4
+ testImplementation libs.truth
}
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java
new file mode 100644
index 000000000..d69c39145
--- /dev/null
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java
@@ -0,0 +1,11 @@
+package org.robolectric.util.reflector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Indicates that the annotated method is a constructor. */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Constructor {}
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
index 2873e2864..12f855f2b 100644
--- a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
@@ -38,6 +38,8 @@ public class Reflector {
private static final boolean DEBUG = false;
private static final AtomicInteger COUNTER = new AtomicInteger();
private static final Map<Class<?>, Constructor<?>> cache = new ConcurrentHashMap<>();
+ private static final Map<Class<?>, Object> staticReflectorCache = new ConcurrentHashMap<>();
+
/**
* Returns an object which provides accessors for invoking otherwise inaccessible static methods
* and fields.
@@ -56,6 +58,10 @@ public class Reflector {
* @param target the target object
*/
public static <T> T reflector(Class<T> iClass, Object target) {
+ if (target == null && staticReflectorCache.containsKey(iClass)) {
+ return (T) staticReflectorCache.get(iClass);
+ }
+
Class<?> targetClass = determineTargetClass(iClass);
Constructor<? extends T> ctor = (Constructor<? extends T>) cache.get(iClass);
@@ -68,11 +74,15 @@ public class Reflector {
() -> Reflector.<T>createReflectorClass(iClass, targetClass));
ctor = reflectorClass.getConstructor(targetClass);
ctor.setAccessible(true);
+ cache.put(iClass, ctor);
}
- cache.put(iClass, ctor);
+ T instance = ctor.newInstance(target);
+ if (target == null) {
+ staticReflectorCache.put(iClass, instance);
+ }
+ return instance;
- return ctor.newInstance(target);
} catch (NoSuchMethodException
| InstantiationException
| IllegalAccessException
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
index d3e366855..ea9b45c6d 100644
--- a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
@@ -15,6 +15,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
+import javax.annotation.Nullable;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
@@ -29,6 +30,8 @@ class ReflectorClassWriter extends ClassWriter {
private static final Type CLASS_TYPE = Type.getType(Class.class);
private static final Type FIELD_TYPE = Type.getType(Field.class);
private static final Type METHOD_TYPE = Type.getType(Method.class);
+ private static final Type CONSTRUCTOR_TYPE = Type.getType(java.lang.reflect.Constructor.class);
+
private static final Type STRING_TYPE = Type.getType(String.class);
private static final Type STRINGBUILDER_TYPE = Type.getType(StringBuilder.class);
@@ -45,6 +48,8 @@ class ReflectorClassWriter extends ClassWriter {
findMethod(Class.class, "getDeclaredField", new Class<?>[] {String.class});
private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_METHOD =
findMethod(Class.class, "getDeclaredMethod", new Class<?>[] {String.class, Class[].class});
+ private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_CONSTRUCTOR =
+ findMethod(Class.class, "getDeclaredConstructor", new Class<?>[] {Class[].class});
private static final org.objectweb.asm.commons.Method ACCESSIBLE_OBJECT$SET_ACCESSIBLE =
findMethod(AccessibleObject.class, "setAccessible", new Class<?>[] {boolean.class});
private static final org.objectweb.asm.commons.Method FIELD$GET =
@@ -53,6 +58,9 @@ class ReflectorClassWriter extends ClassWriter {
findMethod(Field.class, "set", new Class<?>[] {Object.class, Object.class});
private static final org.objectweb.asm.commons.Method METHOD$INVOKE =
findMethod(Method.class, "invoke", new Class<?>[] {Object.class, Object[].class});
+ private static final org.objectweb.asm.commons.Method CONSTRUCTOR$NEWINSTANCE =
+ findMethod(
+ java.lang.reflect.Constructor.class, "newInstance", new Class<?>[] {Object[].class});
private static final org.objectweb.asm.commons.Method THROWABLE$GET_CAUSE =
findMethod(Throwable.class, "getCause", new Class<?>[] {});
private static final org.objectweb.asm.commons.Method OBJECT_INIT =
@@ -118,8 +126,11 @@ class ReflectorClassWriter extends ClassWriter {
if (method.isDefault()) continue;
Accessor accessor = method.getAnnotation(Accessor.class);
+ Constructor constructor = method.getAnnotation(Constructor.class);
if (accessor != null) {
new AccessorMethodWriter(method, accessor).write();
+ } else if (constructor != null) {
+ new ConstructorMethodWriter(method).write();
} else {
new ReflectorMethodWriter(method).write();
}
@@ -251,6 +262,135 @@ class ReflectorClassWriter extends ClassWriter {
}
}
+ private class ConstructorMethodWriter extends BaseAdapter {
+
+ private final String constructorRefName;
+ private final Type[] targetParamTypes;
+
+ private ConstructorMethodWriter(Method method) {
+ super(method);
+ int myMethodNumber = nextMethodNumber++;
+ this.constructorRefName = "constructor" + myMethodNumber;
+ this.targetParamTypes = resolveParamTypes(iMethod);
+ }
+
+ void write() {
+ // write field to hold method reference...
+ visitField(
+ ACC_PRIVATE | ACC_STATIC,
+ constructorRefName,
+ CONSTRUCTOR_TYPE.getDescriptor(),
+ null,
+ null);
+
+ visitCode();
+
+ // pseudocode:
+ // try {
+ // return constructorN.newInstance(*args);
+ // } catch (InvocationTargetException e) {
+ // throw e.getCause();
+ // } catch (ReflectiveOperationException e) {
+ // throw new AssertionError("Error invoking reflector method in ClassLoader " +
+ // Instrumentation.class.getClassLoader(), e);
+ // }
+ Label tryStart = new Label();
+ Label tryEnd = new Label();
+ Label handleInvocationTargetException = new Label();
+ visitTryCatchBlock(
+ tryStart,
+ tryEnd,
+ handleInvocationTargetException,
+ INVOCATION_TARGET_EXCEPTION_TYPE.getInternalName());
+ Label handleReflectiveOperationException = new Label();
+ visitTryCatchBlock(
+ tryStart,
+ tryEnd,
+ handleReflectiveOperationException,
+ REFLECTIVE_OPERATION_EXCEPTION_TYPE.getInternalName());
+
+ mark(tryStart);
+ loadOriginalConstructorRef();
+ loadArgArray();
+ invokeVirtual(CONSTRUCTOR_TYPE, CONSTRUCTOR$NEWINSTANCE);
+ mark(tryEnd);
+
+ castForReturn(iMethod.getReturnType());
+ returnValue();
+
+ mark(handleInvocationTargetException);
+
+ int exceptionLocalVar = newLocal(THROWABLE_TYPE);
+ storeLocal(exceptionLocalVar);
+ loadLocal(exceptionLocalVar);
+ invokeVirtual(THROWABLE_TYPE, THROWABLE$GET_CAUSE);
+ throwException();
+ mark(handleReflectiveOperationException);
+ exceptionLocalVar = newLocal(REFLECTIVE_OPERATION_EXCEPTION_TYPE);
+ storeLocal(exceptionLocalVar);
+ newInstance(STRINGBUILDER_TYPE);
+ dup();
+ invokeConstructor(STRINGBUILDER_TYPE, OBJECT_INIT);
+ push("Error invoking reflector method in ClassLoader ");
+ invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND);
+ push(targetType);
+ invokeVirtual(CLASS_TYPE, CLASS$GET_CLASS_LOADER);
+ invokeStatic(STRING_TYPE, STRING$VALUE_OF);
+ invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND);
+ invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$TO_STRING);
+ int messageLocalVar = newLocal(STRING_TYPE);
+ storeLocal(messageLocalVar);
+ newInstance(ASSERTION_ERROR_TYPE);
+ dup();
+ loadLocal(messageLocalVar);
+ loadLocal(exceptionLocalVar);
+ invokeConstructor(ASSERTION_ERROR_TYPE, ASSERTION_ERROR_INIT);
+ throwException();
+
+ endMethod();
+ }
+
+ private void loadOriginalConstructorRef() {
+ // pseudocode:
+ // if (constructorN == null) {
+ // constructorN = targetClass.getDeclaredConstructor(paramTypes);
+ // constructorN.setAccessible(true);
+ // }
+ // -> constructor reference on stack
+ getStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE);
+ dup();
+ Label haveConstructorRef = newLabel();
+ ifNonNull(haveConstructorRef);
+ pop();
+
+ // pseudocode:
+ // targetClass.getDeclaredConstructor(paramTypes);
+ push(targetType);
+ Type[] paramTypes = targetParamTypes;
+ push(paramTypes.length);
+ newArray(CLASS_TYPE);
+ for (int i = 0; i < paramTypes.length; i++) {
+ dup();
+ push(i);
+ push(paramTypes[i]);
+ arrayStore(CLASS_TYPE);
+ }
+ invokeVirtual(CLASS_TYPE, CLASS$GET_DECLARED_CONSTRUCTOR);
+
+ // pseudocode:
+ // <constructor>.setAccessible(true);
+ dup();
+ push(true);
+ invokeVirtual(CONSTRUCTOR_TYPE, ACCESSIBLE_OBJECT$SET_ACCESSIBLE);
+
+ // pseudocode:
+ // constructorN = constructor;
+ dup();
+ putStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE);
+ mark(haveConstructorRef);
+ }
+ }
+
private class ReflectorMethodWriter extends BaseAdapter {
private final String methodRefName;
@@ -375,35 +515,6 @@ class ReflectorClassWriter extends ClassWriter {
putStatic(reflectorType, methodRefName, METHOD_TYPE);
mark(haveMethodRef);
}
-
- private Type[] resolveParamTypes(Method iMethod) {
- Class<?>[] iParamTypes = iMethod.getParameterTypes();
- Annotation[][] paramAnnotations = iMethod.getParameterAnnotations();
-
- Type[] targetParamTypes = new Type[iParamTypes.length];
- for (int i = 0; i < iParamTypes.length; i++) {
- Class<?> paramType = findWithType(paramAnnotations[i]);
- if (paramType == null) {
- paramType = iParamTypes[i];
- }
- targetParamTypes[i] = Type.getType(paramType);
- }
- return targetParamTypes;
- }
-
- private Class<?> findWithType(Annotation[] paramAnnotation) {
- for (Annotation annotation : paramAnnotation) {
- if (annotation instanceof WithType) {
- String withTypeName = ((WithType) annotation).value();
- try {
- return Class.forName(withTypeName, true, iClass.getClassLoader());
- } catch (ClassNotFoundException e1) {
- // it's okay, ignore
- }
- }
- }
- return null;
- }
}
private static String[] getInternalNames(final Class<?>[] types) {
@@ -494,5 +605,35 @@ class ReflectorClassWriter extends ClassWriter {
void loadNull() {
visitInsn(Opcodes.ACONST_NULL);
}
+
+ protected Type[] resolveParamTypes(Method iMethod) {
+ Class<?>[] iParamTypes = iMethod.getParameterTypes();
+ Annotation[][] paramAnnotations = iMethod.getParameterAnnotations();
+
+ Type[] targetParamTypes = new Type[iParamTypes.length];
+ for (int i = 0; i < iParamTypes.length; i++) {
+ Class<?> paramType = findWithType(paramAnnotations[i]);
+ if (paramType == null) {
+ paramType = iParamTypes[i];
+ }
+ targetParamTypes[i] = Type.getType(paramType);
+ }
+ return targetParamTypes;
+ }
+
+ @Nullable
+ private Class<?> findWithType(Annotation[] paramAnnotation) {
+ for (Annotation annotation : paramAnnotation) {
+ if (annotation instanceof WithType) {
+ String withTypeName = ((WithType) annotation).value();
+ try {
+ return Class.forName(withTypeName, true, iClass.getClassLoader());
+ } catch (ClassNotFoundException e1) {
+ // it's okay, ignore
+ }
+ }
+ }
+ return null;
+ }
}
}
diff --git a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
index 8baf3d63e..74dc88487 100644
--- a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
+++ b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
@@ -133,6 +133,25 @@ public class ReflectorTest {
time("saved accessor", 10_000_000, () -> fieldBySavedReflector(accessor));
}
+ @Ignore
+ @Test
+ public void constructorPerf() {
+ SomeClass i = new SomeClass("c");
+
+ System.out.println("reflection = " + Collections.singletonList(methodByReflectionHelpers(i)));
+ System.out.println("accessor = " + Collections.singletonList(methodByReflector(i)));
+
+ _SomeClass_ accessor = reflector(_SomeClass_.class, i);
+
+ time("ReflectionHelpers", 10_000_000, this::constructorByReflectionHelpers);
+ time("accessor", 10_000_000, () -> constructorByReflector());
+ time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor));
+
+ time("ReflectionHelpers", 10_000_000, () -> constructorByReflectionHelpers());
+ time("accessor", 10_000_000, () -> constructorByReflector());
+ time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor));
+ }
+
@Test
public void nonExistentMethod_throwsAssertionError() {
SomeClass i = new SomeClass("c");
@@ -143,6 +162,11 @@ public class ReflectorTest {
assertThat(ex).hasCauseThat().isInstanceOf(NoSuchMethodException.class);
}
+ @Test
+ public void reflector_constructor() {
+ assertThat(staticReflector.newSomeClass("sdfsdf")).isNotNull();
+ }
+
//////////////////////
/** Accessor interface for {@link SomeClass}'s internals. */
@@ -170,6 +194,9 @@ public class ReflectorTest {
@Accessor("mD")
int getD();
+ @Constructor
+ SomeClass newSomeClass(String c);
+
String someMethod(String a, String b);
String nonExistentMethod(String a, String b, String c);
@@ -251,6 +278,20 @@ public class ReflectorTest {
return reflector.someMethod("a", "b");
}
+ private SomeClass constructorByReflectionHelpers() {
+ return ReflectionHelpers.callConstructor(
+ SomeClass.class, ClassParameter.from(String.class, "a"));
+ }
+
+ private SomeClass constructorByReflector() {
+ _SomeClass_ accessor = reflector(_SomeClass_.class);
+ return accessor.newSomeClass("a");
+ }
+
+ private SomeClass constructorBySavedReflector(_SomeClass_ reflector) {
+ return reflector.newSomeClass("a");
+ }
+
private String fieldByReflectionHelpers(SomeClass o) {
ReflectionHelpers.setField(o, "c", "abc");
return ReflectionHelpers.getField(o, "c");
diff --git a/utils/src/main/java/org/robolectric/util/Util.java b/utils/src/main/java/org/robolectric/util/Util.java
index b7292ad93..b7292ad93 100755..100644
--- a/utils/src/main/java/org/robolectric/util/Util.java
+++ b/utils/src/main/java/org/robolectric/util/Util.java