[automerger skipped] Merge branch 'upstream-google' into rng_import18 am: a99b56cbdb am: 95ace1ade5 -s ours
am skip reason: Merged-In Ife59e70205fa3b8dd3aa4cdef4689a8fd276073c with SHA-1 6660b73edd is already in history
Original change: https://googleplex-android-review.googlesource.com/c/platform/external/robolectric/+/23691011
Change-Id: I231285f45ab06c9772f2ca386dbf9365938843f4
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml
index 96b4b33..9762b9e 100644
--- a/.github/workflows/gradle_tasks_validation.yml
+++ b/.github/workflows/gradle_tasks_validation.yml
@@ -15,6 +15,23 @@
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 @@
- 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 @@
- 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 d83e804..b47f375 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -101,7 +101,7 @@
run: |
TARGET="google_apis"
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
-
+
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
@@ -147,3 +147,25 @@
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/.gitmodules b/.gitmodules
index bbbdc09..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,10 +0,0 @@
-[submodule "nativeruntime/external/sqlite"]
- path = nativeruntime/external/sqlite
- url = https://android.googlesource.com/platform/external/sqlite
- branch = android11-release
-
-[submodule "nativeruntime/external/icu"]
- path = nativeruntime/external/icu
- url = https://github.com/unicode-org/icu
- branch = release-69-1
- shallow = false
diff --git a/Android.bp b/Android.bp
index 319b5e3..135e7c1 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 @@
// 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 @@
visibility: [
":__subpackages__",
"//prebuilts/misc/common/robolectric",
+ "//test/robolectric-extensions:__subpackages__",
],
}
@@ -119,6 +126,12 @@
//#############################################
// This is a hack and should be removed with proper resource merging a la maven-shaded-plugin
+//
+// In order to use AndroidXTest APIs in Robolectric (e.g. ActivityScenario), it
+// is necessary to define the service metadata at the top-level. The classes
+// themselves (e.g. LocalUiController) do not use `@AutoService` at the moment.
+// When they are migrated to `@AutoService` the AndroidXTest service metadata
+// can be removed.
java_genrule_host {
name: "robolectric_meta_service_file",
out: ["robolectric_meta_service_file.jar"],
@@ -126,6 +139,16 @@
cmd: "mkdir -p $(genDir)/META-INF/services/ && " +
"echo -e 'org.robolectric.Shadows\norg.robolectric.shadows.httpclient.Shadows\norg.robolectric.shadows.multidex.Shadows' > " +
"$(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider &&" +
+ "echo org.robolectric.android.internal.LocalUiController > " +
+ "$(genDir)/META-INF/services/androidx.test.platform.ui.UiController &&" +
+ "echo org.robolectric.android.internal.LocalActivityInvoker > " +
+ "$(genDir)/META-INF/services/androidx.test.internal.platform.app.ActivityInvoker &&" +
+ "echo org.robolectric.android.internal.LocalPermissionGranter > " +
+ "$(genDir)/META-INF/services/androidx.test.internal.platform.content.PermissionGranter &&" +
+ "echo org.robolectric.android.internal.NoOpThreadChecker > " +
+ "$(genDir)/META-INF/services/androidx.test.internal.platform.ThreadChecker &&" +
+ "echo org.robolectric.android.internal.LocalControlledLooper > " +
+ "$(genDir)/META-INF/services/androidx.test.internal.platform.os.ControlledLooper &&" +
"$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)/META-INF/services/",
}
@@ -133,10 +156,12 @@
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",
@@ -178,6 +203,7 @@
"//prebuilts/sdk/current/aaos-libs:__pkg__",
"//packages/apps/TV/tests/common:__pkg__",
//robolectric tests
+ "//platform_testing/libraries/runner:__pkg__",
"//vendor:__subpackages__",
"//platform_testing/robolab/roboStandaloneProj/tests:__pkg__",
"//external/mobile-data-download/javatests:__pkg__",
diff --git a/README.md b/README.md
index 6b91ba3..b183647 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@
```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 @@
./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 65b4f06..d8bd113 100644
--- a/annotations/build.gradle
+++ b/annotations/build.gradle
@@ -5,6 +5,6 @@
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 1eb662b..f3ab387 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,18 +7,15 @@
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 @@
dependsOn ':aggregateJsondocs'
}
-task prefetchSdks() {
+tasks.register('prefetchSdks') {
AndroidSdk.ALL_SDKS.each { androidSdk ->
doLast {
println("Prefetching ${androidSdk.coordinates}...")
@@ -139,7 +136,7 @@
}
}
-task prefetchInstrumentedSdks() {
+tasks.register('prefetchInstrumentedSdks') {
AndroidSdk.ALL_SDKS.each { androidSdk ->
doLast {
println("Prefetching ${androidSdk.preinstrumentedCoordinates}...")
@@ -169,7 +166,7 @@
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 6123663..67279ef 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -11,8 +11,8 @@
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 0000000..6f31e6e
--- /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 c0671c5..2f1476c 100644
--- a/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
+++ b/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
@@ -28,7 +28,6 @@
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 324d04d..09e3b8d 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 @@
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 6c0e058..deb97c9 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 @@
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 b890dc1..204e3d3 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,40 +1,11 @@
ext {
- apiCompatVersion='4.9.2'
+ apiCompatVersion = libs.versions.robolectric.compat.get()
- 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'
+ // 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 1932066..5fc5616 100644
--- a/errorprone/build.gradle
+++ b/errorprone/build.gradle
@@ -20,14 +20,14 @@
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 @@
}
// 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 dc9eb66..d97ed21 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 0000000..7b43dfa
--- /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 a079d1e..55d9051 100644
--- a/integration_tests/agp/build.gradle
+++ b/integration_tests/agp/build.gradle
@@ -5,6 +5,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.agp'
defaultConfig {
minSdk 16
@@ -25,8 +26,8 @@
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 dcec3d4..e87274f 100644
--- a/integration_tests/agp/testsupport/build.gradle
+++ b/integration_tests/agp/testsupport/build.gradle
@@ -2,6 +2,7 @@
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 96535e1..10cc8c6 100644
--- a/integration_tests/androidx/build.gradle
+++ b/integration_tests/androidx/build.gradle
@@ -5,6 +5,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.androidx'
defaultConfig {
minSdk 16
@@ -25,19 +26,19 @@
}
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 7f6f621..d07ef2e 100644
--- a/integration_tests/androidx_test/build.gradle
+++ b/integration_tests/androidx_test/build.gradle
@@ -7,6 +7,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.integration.axt'
defaultConfig {
minSdk 16
@@ -41,33 +42,33 @@
}
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/androidx_test/src/main/res/layout/espresso_activity.xml b/integration_tests/androidx_test/src/main/res/layout/espresso_activity.xml
index 1cbc197..c412b90 100644
--- a/integration_tests/androidx_test/src/main/res/layout/espresso_activity.xml
+++ b/integration_tests/androidx_test/src/main/res/layout/espresso_activity.xml
@@ -50,4 +50,10 @@
android:layout_height="wrap_content"
android:inputType="phone" />
+ <EditText
+ android:id="@+id/edit_text_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="number" />
+
</LinearLayout>
diff --git a/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java b/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java
index 494ce82..0c0df8b 100644
--- a/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java
+++ b/integration_tests/androidx_test/src/sharedTest/java/org/robolectric/integrationtests/axt/EspressoTest.java
@@ -25,6 +25,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -129,6 +130,15 @@
onView(withId(R.id.edit_text_phone)).check(matches(withText("411")));
}
+ /** use typeText with a inputType number */
+ @Test
+ @Ignore // TODO(#5110): fails
+ public void typeText_number() {
+ onView(withId(R.id.edit_text_number)).perform(typeText("411"));
+
+ onView(withId(R.id.edit_text_number)).check(matches(withText("411")));
+ }
+
@Test
public void textView() {
onView(withText("Text View"))
diff --git a/integration_tests/compat-target28/build.gradle b/integration_tests/compat-target28/build.gradle
index 37a8568..1fc7485 100644
--- a/integration_tests/compat-target28/build.gradle
+++ b/integration_tests/compat-target28/build.gradle
@@ -14,6 +14,7 @@
android {
compileSdk 28
+ namespace 'org.robolectric.integrationtests.compattarget28'
defaultConfig {
minSdk 16
@@ -30,10 +31,10 @@
}
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 ee56fc6..69bbf73 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 @@
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/Android.bp b/integration_tests/ctesque/Android.bp
new file mode 100644
index 0000000..f500c35
--- /dev/null
+++ b/integration_tests/ctesque/Android.bp
@@ -0,0 +1,35 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_robolectric_test {
+ name: "CtesqueRoboTests",
+ srcs: [
+ "src/sharedTest/**/*.java",
+ "src/sharedTest/**/*.kt",
+ ],
+ exclude_srcs: [
+ // TODO(b/286353161): Re-enable this test once loading resources from XML
+ // for older SDK versions is resolved.
+ "src/sharedTest/java/android/content/res/ResourcesTest.java",
+ // TODO(b/286353719): Re-enable this test once the correct SQLite is used.
+ "src/sharedTest/java/android/database/SQLiteDatabaseTest.java",
+ ],
+ static_libs: [
+ "androidx.core_core",
+ "androidx.test.uiautomator_uiautomator",
+ "androidx.test.espresso.core",
+ "androidx.test.ext.junit",
+ "androidx.test.ext.truth",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ "truth-prebuilt",
+ "guava",
+ ],
+ upstream: true,
+ java_resource_dirs: ["src/sharedTest/resources/android"],
+ instrumentation_for: "GlobalRobolectricTestStub",
+}
diff --git a/integration_tests/ctesque/build.gradle b/integration_tests/ctesque/build.gradle
index 11f27e1..3b40c88 100644
--- a/integration_tests/ctesque/build.gradle
+++ b/integration_tests/ctesque/build.gradle
@@ -7,6 +7,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.ctesque'
defaultConfig {
minSdk 16
@@ -48,24 +49,26 @@
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/animation/AnimationTest.java b/integration_tests/ctesque/src/sharedTest/java/android/animation/AnimationTest.java
new file mode 100644
index 0000000..9e65318
--- /dev/null
+++ b/integration_tests/ctesque/src/sharedTest/java/android/animation/AnimationTest.java
@@ -0,0 +1,74 @@
+package android.animation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.animation.Animator.AnimatorListener;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.Espresso;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.lang.reflect.Modifier;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.testapp.ActivityWithoutTheme;
+
+/** Compatibility test for animation-related logic. */
+@DoNotInstrument
+@RunWith(AndroidJUnit4.class)
+public final class AnimationTest {
+
+ @Test
+ public void propertyValuesHolder() throws Exception {
+ PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat("x", 100f, 150f);
+ PropertyBag object = new PropertyBag();
+ assertThat(Modifier.isPrivate(PropertyBag.class.getModifiers())).isTrue();
+ ObjectAnimator objAnimator = ObjectAnimator.ofPropertyValuesHolder(object, pVHolder);
+ objAnimator.setDuration(500);
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.play(objAnimator);
+
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ objAnimator.addListener(
+ new AnimatorListener() {
+ @Override
+ public void onAnimationEnd(Animator a) {
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void onAnimationStart(Animator a) {}
+
+ @Override
+ public void onAnimationCancel(Animator a) {}
+
+ @Override
+ public void onAnimationRepeat(Animator a) {}
+ });
+
+ // In Android animations can only run on Looper threads.
+ try (ActivityScenario<ActivityWithoutTheme> scenario =
+ ActivityScenario.launch(ActivityWithoutTheme.class)) {
+ scenario.onActivity(
+ activity -> {
+ animatorSet.start();
+ });
+ }
+
+ Espresso.onIdle();
+ countDownLatch.await(5, TimeUnit.SECONDS);
+ assertThat(object.x).isEqualTo(150f);
+ }
+
+ /** Private class with a public member. */
+ @DoNotInstrument
+ @SuppressWarnings("unused")
+ private static class PropertyBag {
+ public float x;
+
+ public void setX(float x) {
+ this.x = x;
+ }
+ }
+}
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 c1922b6..5e4de01 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 @@
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/graphics/BitmapTest.java b/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
index 319b873..75c3288 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
@@ -10,8 +10,8 @@
import static android.os.Build.VERSION_CODES.Q;
import static androidx.test.InstrumentationRegistry.getTargetContext;
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.assumeFalse;
import android.content.res.Resources;
import android.graphics.Bitmap.CompressFormat;
@@ -51,7 +51,7 @@
@Config(minSdk = P)
@SdkSuppress(minSdkVersion = P)
@Test public void createBitmap() {
- assumeFalse(Boolean.getBoolean("robolectric.nativeruntime.enableGraphics"));
+ assume().that(System.getProperty("robolectric.graphicsMode")).isNotEqualTo("NATIVE");
// Bitmap.createBitmap(Picture) requires hardware-backed bitmaps
HardwareRendererCompat.setDrawingEnabled(true);
Picture picture = new Picture();
diff --git a/integration_tests/dependency-on-stubs/build.gradle b/integration_tests/dependency-on-stubs/build.gradle
index 6efe513..683de18 100644
--- a/integration_tests/dependency-on-stubs/build.gradle
+++ b/integration_tests/dependency-on-stubs/build.gradle
@@ -6,13 +6,13 @@
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 e5d3bb5..3db34c0 100644
--- a/integration_tests/jacoco-offline/build.gradle
+++ b/integration_tests/jacoco-offline/build.gradle
@@ -3,6 +3,8 @@
apply plugin: RoboJavaModulePlugin
apply plugin: "jacoco"
+def jacocoVersion = libs.versions.jacoco.get()
+
jacoco {
toolVersion = jacocoVersion
}
@@ -18,7 +20,7 @@
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 68c5c67..fd52d97 100644
--- a/integration_tests/kotlin/build.gradle
+++ b/integration_tests/kotlin/build.gradle
@@ -21,8 +21,8 @@
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 0000000..6d77aa9
--- /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 2c27a79..61120f2 100644
--- a/integration_tests/libphonenumber/build.gradle
+++ b/integration_tests/libphonenumber/build.gradle
@@ -4,10 +4,10 @@
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 2cc5124..91c5eb0 100644
--- a/integration_tests/memoryleaks/build.gradle
+++ b/integration_tests/memoryleaks/build.gradle
@@ -5,6 +5,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.memoryleaks'
defaultConfig {
minSdk 16
@@ -28,7 +29,7 @@
// 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 4aafcbc..f5172d6 100644
--- a/integration_tests/mockito-experimental/build.gradle
+++ b/integration_tests/mockito-experimental/build.gradle
@@ -8,7 +8,7 @@
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 ae97f1b..776f33b 100644
--- a/integration_tests/mockito-kotlin/build.gradle
+++ b/integration_tests/mockito-kotlin/build.gradle
@@ -18,8 +18,8 @@
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 e199cd7..31e6ce6 100644
--- a/integration_tests/mockito/build.gradle
+++ b/integration_tests/mockito/build.gradle
@@ -8,7 +8,7 @@
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 78344a9..1d59071 100644
--- a/integration_tests/mockk/build.gradle
+++ b/integration_tests/mockk/build.gradle
@@ -21,7 +21,7 @@
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/Android.bp b/integration_tests/nativegraphics/Android.bp
new file mode 100644
index 0000000..eb491ba
--- /dev/null
+++ b/integration_tests/nativegraphics/Android.bp
@@ -0,0 +1,60 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "NativeGraphicsTestsAssetsLib",
+ asset_dirs: ["src/main/assets"],
+ resource_dirs: ["src/main/res"],
+ min_sdk_version: "26",
+ target_sdk_version: "31",
+ platform_apis: true,
+ manifest: "AndroidManifest.xml",
+ optimize: {
+ enabled: false
+ },
+}
+
+android_app {
+ name: "NativeGraphicsPseudoApp",
+ srcs: [],
+ static_libs: ["NativeGraphicsTestsAssetsLib"],
+ manifest: "robo-manifest.xml",
+ aaptflags: [
+ "--extra-packages",
+ "org.robolectric.integrationtests.nativegraphics",
+ ],
+ dont_merge_manifests: true,
+ platform_apis: true,
+ system_ext_specific: true,
+ min_sdk_version: "26",
+ target_sdk_version: "31",
+ certificate: "platform",
+ privileged: true,
+ resource_dirs: ["src/main/res"],
+ kotlincflags: ["-Xjvm-default=all"],
+
+ plugins: ["dagger2-compiler"],
+}
+
+android_robolectric_test {
+ name: "NativeGraphicsTests",
+ srcs: [
+ "src/**/*.kt",
+ "src/**/*.java",
+ ],
+ static_libs: [
+ "androidx.core_core",
+ "androidx.test.uiautomator_uiautomator",
+ "androidx.test.ext.junit",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ "truth-prebuilt",
+ ],
+ upstream: true,
+ java_resource_dirs: ["config"],
+ instrumentation_for: "NativeGraphicsPseudoApp",
+}
diff --git a/integration_tests/nativegraphics/build.gradle b/integration_tests/nativegraphics/build.gradle
index 10e8f61..f88f53a 100644
--- a/integration_tests/nativegraphics/build.gradle
+++ b/integration_tests/nativegraphics/build.gradle
@@ -7,6 +7,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.integrationtests.nativegraphics'
defaultConfig {
minSdk 26
@@ -32,9 +33,9 @@
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/nativegraphics/config/robolectric.properties b/integration_tests/nativegraphics/config/robolectric.properties
new file mode 100644
index 0000000..0d16e6b
--- /dev/null
+++ b/integration_tests/nativegraphics/config/robolectric.properties
@@ -0,0 +1,15 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+sdk=NEWEST_SDK
diff --git a/integration_tests/nativegraphics/robo-manifest.xml b/integration_tests/nativegraphics/robo-manifest.xml
new file mode 100644
index 0000000..56ec797
--- /dev/null
+++ b/integration_tests/nativegraphics/robo-manifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--- Include all the namespaces we will ever need anywhere, because this is the source the manifest merger uses for namespaces -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="org.robolectric.integrationtests.nativegraphics"
+ android:sharedUserId="android.uid.phone"
+ coreApp="true">
+ <uses-sdk
+ android:minSdkVersion="26"
+ android:targetSdkVersion="31" />
+ <application>
+ <activity
+ android:name="com.android.myroboapplication.WelcomeActivity"
+ android:exported="true">
+ </activity>
+ </application>
+</manifest>
diff --git a/integration_tests/nativegraphics/src/main/res/color/colors.xml b/integration_tests/nativegraphics/src/main/res/color/colors.xml
deleted file mode 100644
index 64f1589..0000000
--- a/integration_tests/nativegraphics/src/main/res/color/colors.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright (C) 2008 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.
- -->
-
-<resources>
- <drawable name="red">#7f00</drawable>
- <drawable name="blue">#770000ff</drawable>
- <drawable name="black">#77ffffff</drawable>
- <drawable name="yellow">#77ffff00</drawable>
- <color name="testcolor1">#ff00ff00</color>
- <color name="testcolor2">#ffff0000</color>
- <color name="failColor">#ff0000ff</color>
- <color name="colorPrimary">#008577</color>
- <color name="colorPrimaryDark">#00574B</color>
- <color name="colorAccent">#D81B60</color>
-</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/color/styles.xml b/integration_tests/nativegraphics/src/main/res/color/styles.xml
deleted file mode 100644
index 7e05f0a..0000000
--- a/integration_tests/nativegraphics/src/main/res/color/styles.xml
+++ /dev/null
@@ -1,198 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 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.
--->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Whatever">
- <item name="type1">true</item>
- <item name="type2">false</item>
- <item name="type3">#ff0000ff</item>
- <item name="type4">#ff00ff00</item>
- <item name="type5">0.75px</item>
- <item name="type6">10px</item>
- <item name="type7">18px</item>
- <item name="type8">@drawable/pass</item>
- <item name="type9">3.14</item>
- <item name="type10">100%</item>
- <item name="type11">365</item>
- <item name="type12">86400</item>
- <item name="type13">@string/hello_android</item>
- <item name="type14">TypedArray Test!</item>
- <item name="type15">@array/difficultyLevel</item>
- <item name="type16">Typed Value!</item>
- </style>
-
- <style name="TextViewWithoutColorAndAppearance">
- <item name="android:textSize">18sp</item>
- </style>
-
- <style name="TextViewWithColorButWithOutAppearance">
- <item name="android:textColor">#ff0000ff</item>
- </style>
-
- <style name="TextViewWithColorAndAppearance">
- <item name="android:textColor">#ff0000ff</item>
- <item name="android:textAppearance">@style/TextAppearance.WithColor</item>
- </style>
-
- <style name="TextViewWithoutColorButWithAppearance">
- <item name="android:textAppearance">@style/TextAppearance.WithColor</item>
- </style>
-
- <style name="TextAppearance" parent="android:TextAppearance">
- </style>
-
- <style name="TextAppearance.WithColor">
- <item name="android:textColor">#ffff0000</item>
- </style>
-
- <style name="TextAppearance.All">
- <item name="android:textColor">@drawable/black</item>
- <item name="android:textSize">20px</item>
- <item name="android:textStyle">bold</item>
- <item name="android:textColorHint">@drawable/red</item>
- <item name="android:textColorLink">@drawable/blue</item>
- <item name="android:textColorHighlight">@drawable/yellow</item>
- </style>
-
- <style name="TextAppearance.Colors">
- <item name="android:textColor">@drawable/black</item>
- <item name="android:textColorHint">@drawable/blue</item>
- <item name="android:textColorLink">@drawable/yellow</item>
- <item name="android:textColorHighlight">@drawable/red</item>
- </style>
-
- <style name="TextAppearance.NotColors">
- <item name="android:textSize">17px</item>
- <item name="android:typeface">sans</item>
- <item name="android:textStyle">normal</item>
- </style>
-
- <style name="TextAppearance.Style">
- <item name="android:textStyle">normal</item>
- </style>
-
- <style name="TestEnum1">
- <item name="testEnum">val1</item>
- </style>
-
- <style name="TestEnum2">
- <item name="testEnum">val2</item>
- </style>
-
- <style name="TestEnum10">
- <item name="testEnum">val10</item>
- </style>
-
- <style name="TestFlag1">
- <item name="testFlags">bit1</item>
- </style>
-
- <style name="TestFlag2">
- <item name="testFlags">bit2</item>
- </style>
-
- <style name="TestFlag31">
- <item name="testFlags">bit31</item>
- </style>
-
- <style name="TestFlag1And2">
- <item name="testFlags">bit1|bit2</item>
- </style>
-
- <style name="TestFlag1And2And31">
- <item name="testFlags">bit1|bit2|bit31</item>
- </style>
-
- <style name="TestEnum1.EmptyInherit" />
-
- <style name="Theme_AlertDialog">
- <item name="android:textSize">18sp</item>
- </style>
-
- <style name="TestProgressBar">
- <item name="android:indeterminateOnly">false</item>
- <item name="android:progressDrawable">@android:drawable/progress_horizontal</item>
- <item name="android:indeterminateDrawable">@android:drawable/progress_horizontal</item>
- <item name="android:minHeight">20dip</item>
- <item name="android:maxHeight">20dip</item>
- <item name="android:focusable">true</item>
- </style>
-
- <style name="Test_Theme">
- <item name="android:windowNoTitle">true</item>
- <item name="android:panelColorForeground">#ff000000</item>
- <item name="android:panelColorBackground">#ffffffff</item>
- </style>
-
- <style name="Theme_OverrideOuter">
- <item name="themeType">1</item>
- </style>
-
- <style name="Theme_OverrideInner">
- <item name="themeType">2</item>
- <item name="themeOverrideAttr">@style/Theme_OverrideAttr</item>
- </style>
-
- <style name="Theme_OverrideAttr">
- <item name="themeType">3</item>
- </style>
-
- <style name="Theme_ThemedDrawableTest">
- <item name="themeBoolean">true</item>
- <item name="themeColor">@android:color/black</item>
- <item name="themeFloat">1.0</item>
- <item name="themeAngle">45.0</item>
- <item name="themeInteger">1</item>
- <item name="themeDimension">1px</item>
- <item name="themeDrawable">@drawable/icon_black</item>
- <item name="themeBitmap">@drawable/icon_black</item>
- <item name="themeNinePatch">@drawable/ninepatch_0</item>
- <item name="themeGravity">48</item>
- <item name="themeTileMode">2</item>
- <item name="themeType">0</item>
- <item name="themeVectorDrawableFillColor">#F00F</item>
- </style>
-
- <attr name="colorPrimary" format="reference|color" />
- <attr name="colorPrimaryDark" format="reference|color" />
- <attr name="colorAccent" format="reference|color" />
-
- <style name="Theme_MixedGradientTheme">
- <!-- Customize your theme here. -->
- <item name="colorPrimary">@color/colorPrimary</item>
- <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
- <item name="colorAccent">@color/colorAccent</item>
-
- <!-- overwrite the default gradient type to be radial in the theme -->
- <item name="GradientType">1</item>
- </style>
-
- <!-- default gradient type of linear -->
- <attr name="GradientType" format="integer">0</attr>
-
- <style name="WhiteBackgroundNoWindowAnimation"
- parent="@android:style/Theme.Holo.NoActionBar.Fullscreen">
- <item name="android:windowNoTitle">true</item>
- <item name="android:windowFullscreen">true</item>
- <item name="android:windowOverscan">true</item>
- <item name="android:fadingEdge">none</item>
- <item name="android:windowBackground">@android:color/white</item>
- <item name="android:windowContentTransitions">false</item>
- <item name="android:windowAnimationStyle">@null</item>
- </style>
-
-</resources>
diff --git a/integration_tests/play_services/build.gradle b/integration_tests/play_services/build.gradle
index f7499dd..0e05dd6 100644
--- a/integration_tests/play_services/build.gradle
+++ b/integration_tests/play_services/build.gradle
@@ -9,7 +9,7 @@
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 be4180c..6d5cf68 100644
--- a/integration_tests/powermock/build.gradle
+++ b/integration_tests/powermock/build.gradle
@@ -7,11 +7,8 @@
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 f96df56..8ac0264 100644
--- a/integration_tests/security-providers/build.gradle
+++ b/integration_tests/security-providers/build.gradle
@@ -4,12 +4,12 @@
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 7627177..1e4ba1d 100644
--- a/integration_tests/sparsearray/build.gradle
+++ b/integration_tests/sparsearray/build.gradle
@@ -14,6 +14,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.sparsearray'
defaultConfig {
minSdk 16
@@ -41,7 +42,7 @@
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 9a11978..5d58552 100644
--- a/junit/build.gradle
+++ b/junit/build.gradle
@@ -11,6 +11,6 @@
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/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java b/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java
index bfa0053..414b410 100644
--- a/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java
+++ b/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java
@@ -237,7 +237,6 @@
@Override
public void evaluate() throws Throwable {
PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance();
- perfStatsCollector.reset();
perfStatsCollector.setEnabled(!perfStatsReporters.isEmpty());
Event initialization = perfStatsCollector.startEvent("initialization");
@@ -259,6 +258,10 @@
HelperTestRunner helperTestRunner = getCachedHelperTestRunner(bootstrappedTestClass);
helperTestRunner.frameworkMethod = method;
+ // The method class may be different than the test class if the method annotated @Test
+ // is declared on a superclass of the test.
+ Class<?> bootstrappedMethodClass =
+ sandbox.bootstrappedClass(method.getMethod().getDeclaringClass());
final Method bootstrappedMethod;
try {
Class<?>[] parameterTypes =
@@ -266,7 +269,7 @@
.map(type -> type.isPrimitive() ? type : sandbox.bootstrappedClass(type))
.toArray(Class[]::new);
bootstrappedMethod =
- bootstrappedTestClass.getMethod(method.getMethod().getName(), parameterTypes);
+ bootstrappedMethodClass.getMethod(method.getMethod().getName(), parameterTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
@@ -292,11 +295,7 @@
throw Util.sneakyThrow(throwable);
} finally {
Thread.currentThread().setContextClassLoader(priorContextClassLoader);
- try {
- finallyAfterTest(method);
- } catch (Exception e) {
- e.printStackTrace();
- }
+ finallyAfterTest(method);
reportPerfStats(perfStatsCollector);
perfStatsCollector.reset();
}
diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle
index f8d496d..e784984 100644
--- a/nativeruntime/build.gradle
+++ b/nativeruntime/build.gradle
@@ -49,8 +49,8 @@
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 @@
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.1"
+ 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/main/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoader.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoader.java
index 5221a4b..3892453 100644
--- a/nativeruntime/src/main/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoader.java
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoader.java
@@ -11,6 +11,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import com.google.common.io.Resources;
+import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -137,7 +138,9 @@
}
}
System.setProperty(
- "robolectric.nativeruntime.fontdir", fontsOutputPath.toAbsolutePath().toString());
+ "robolectric.nativeruntime.fontdir",
+ // Android's FontListParser expects a trailing slash for the base font directory.
+ fontsOutputPath.toAbsolutePath() + File.separator);
if (zipfs != null) {
zipfs.close();
}
diff --git a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
index 0391159..e5d395f 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 @@
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 @@
@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 375cd10..9d78852 100644
--- a/pluginapi/build.gradle
+++ b/pluginapi/build.gradle
@@ -5,11 +5,11 @@
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 de20b2b..2aa33d9 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 @@
}
}
-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 @@
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 @@
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 60f852d..adeda9e 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 @@
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.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 @@
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 @@
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 @@
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 @@
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
old mode 100755
new mode 100644
diff --git a/preinstrumented/build.gradle b/preinstrumented/build.gradle
index 95d533e..8ffb5bb 100644
--- a/preinstrumented/build.gradle
+++ b/preinstrumented/build.gradle
@@ -17,11 +17,12 @@
}
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('sourcesJar', type: Jar) {
+tasks.register('sourcesJar', Jar) {
archiveClassifier = "sources"
}
-task('javadocJar', type: Jar) {
+tasks.register('javadocJar', Jar) {
archiveClassifier = "javadoc"
}
@@ -102,8 +103,8 @@
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 @@
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 a9c5ace..753c167 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.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.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 @@
}
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/Android.bp b/processor/Android.bp
index 46a8dc3..90a4981 100644
--- a/processor/Android.bp
+++ b/processor/Android.bp
@@ -77,12 +77,6 @@
// Disable annotation processing while compiling tests to avoid executing RobolectricProcessor.
javacflags: ["-proc:none"],
-
- // Disabling tests as they fail in the bramble build.
- test_options: {
- unit_test: false,
- },
-
}
// Workaround: java_resource_dirs ignores *.java files
diff --git a/processor/build.gradle b/processor/build.gradle
index ac14e42..9185d08 100644
--- a/processor/build.gradle
+++ b/processor/build.gradle
@@ -35,21 +35,21 @@
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/sdks.txt b/processor/sdks.txt
index b9931ca..d39d959 100644
--- a/processor/sdks.txt
+++ b/processor/sdks.txt
@@ -10,6 +10,5 @@
prebuilts/misc/common/robolectric/android-all/android-all-8.0.0_r4-robolectric-r1.jar
prebuilts/misc/common/robolectric/android-all/android-all-8.1.0-robolectric-4611349.jar
prebuilts/misc/common/robolectric/android-all/android-all-9-robolectric-4913185-2.jar
-prebuilts/misc/common/robolectric/android-all/android-all-9plus-robolectric-5616371.jar
prebuilts/misc/common/robolectric/android-all/android-all-10-robolectric-5803371.jar
-prebuilts/misc/common/robolectric/android-all/android-all-R-beta2-robolectric-6625208.jar
+prebuilts/misc/common/robolectric/android-all/android-all-11-robolectric-6757853.jar
diff --git a/resources/build.gradle b/resources/build.gradle
index 129dc20..077cdd3 100644
--- a/resources/build.gradle
+++ b/resources/build.gradle
@@ -9,11 +9,11 @@
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 cfabcbb..7402b49 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 @@
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 @@
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 @@
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 @@
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 0de3390..8b8165b 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 @@
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 139062c..e690895 100644
--- a/robolectric/Android.bp
+++ b/robolectric/Android.bp
@@ -15,6 +15,7 @@
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 faaa8b3..b826e92 100644
--- a/robolectric/build.gradle
+++ b/robolectric/build.gradle
@@ -5,8 +5,8 @@
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 @@
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 47a52c5..3ce637e 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.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 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
old mode 100755
new mode 100644
index a807d4e..f250381
--- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
@@ -86,6 +86,7 @@
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowPackageParser;
import org.robolectric.shadows.ShadowPackageParser._Package_;
+import org.robolectric.shadows.ShadowView;
import org.robolectric.util.Logger;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.ReflectionHelpers;
@@ -96,6 +97,8 @@
public class AndroidTestEnvironment implements TestEnvironment {
private static final String CONSCRYPT_PROVIDER = "Conscrypt";
+ private static final int MAX_DATA_DIR_NAME_LENGTH = 120;
+
private final Sdk runtimeSdk;
private final Sdk compileSdk;
@@ -155,7 +158,6 @@
loggingInitialized = true;
}
- Logger.debug("Robolectric Test Configuration: " + configuration.map());
ConscryptMode.Mode conscryptMode = configuration.get(ConscryptMode.Mode.class);
Security.removeProvider(CONSCRYPT_PROVIDER);
if (conscryptMode != ConscryptMode.Mode.OFF) {
@@ -178,7 +180,7 @@
androidConfiguration.fontScale = config.fontScale();
- if (Boolean.getBoolean("robolectric.nativeruntime.enableGraphics")) {
+ if (ShadowView.useRealGraphics()) {
Bitmap.setDefaultDensity(displayMetrics.densityDpi);
}
Locale locale =
@@ -201,7 +203,7 @@
RuntimeEnvironment.setAndroidFrameworkJarPath(sdkJarPath);
Bootstrap.setDisplayConfiguration(androidConfiguration, displayMetrics);
- RuntimeEnvironment.setActivityThread(ReflectionHelpers.newInstance(ActivityThread.class));
+ RuntimeEnvironment.setActivityThread(ReflectionHelpers.callConstructor(ActivityThread.class));
ReflectionHelpers.setStaticField(
ActivityThread.class, "sMainThreadHandler", new Handler(Looper.myLooper()));
@@ -321,7 +323,7 @@
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
- final Object appBindData = ReflectionHelpers.newInstance(appBindDataClass);
+ final Object appBindData = ReflectionHelpers.callConstructor(appBindDataClass);
final _AppBindData_ _appBindData_ = reflector(_AppBindData_.class, appBindData);
_appBindData_.setProcessName(parsedPackage.packageName);
_appBindData_.setAppInfo(applicationInfo);
@@ -361,7 +363,7 @@
// 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 +414,11 @@
Path packageFile = appManifest.getApkFile();
parsedPackage = ShadowPackageParser.callParsePackage(packageFile);
}
+ if (parsedPackage != null
+ && parsedPackage.applicationInfo != null
+ && RuntimeEnvironment.getApiLevel() >= P) {
+ parsedPackage.applicationInfo.appComponentFactory = appManifest.getAppComponentFactory();
+ }
return parsedPackage;
}
@@ -584,9 +591,14 @@
/** Create a file system safe directory path name for the current test. */
@SuppressWarnings("DoNotCall")
private String createTestDataDirRootPath(Method method) {
- return method.getClass().getSimpleName()
- + "_"
- + method.getName().replaceAll("[^a-zA-Z0-9.-]", "_");
+ // Cap the size to 120 to avoid unnecessarily long directory names.
+ String directoryName =
+ (method.getDeclaringClass().getSimpleName() + "_" + method.getName())
+ .replaceAll("[^a-zA-Z0-9.-]", "_");
+ if (directoryName.length() > MAX_DATA_DIR_NAME_LENGTH) {
+ directoryName = directoryName.substring(0, MAX_DATA_DIR_NAME_LENGTH);
+ }
+ return directoryName;
}
@Override
@@ -691,15 +703,39 @@
.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/LocalActivityInvoker.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java
index 31a53df..fc10913 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java
@@ -40,13 +40,13 @@
startActivity(intent, /* activityOptions= */ null);
}
- // TODO(paigemca): Omitting @Override until androidx.test.monitor version can be upgraded
+ @Override
public void startActivityForResult(Intent intent, @Nullable Bundle activityOptions) {
isActivityLaunchedForResult = true;
controller = getInstrumentation().startActivitySyncInternal(intent, activityOptions);
}
- // TODO(paigemca): Omitting @Override until androidx.test.monitor version can be upgraded
+ @Override
public void startActivityForResult(Intent intent) {
isActivityLaunchedForResult = true;
startActivityForResult(intent, /* activityOptions= */ null);
diff --git a/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java b/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java
old mode 100755
new mode 100644
diff --git a/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java b/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java
old mode 100755
new mode 100644
diff --git a/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java b/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
index 5b0b9e9..775823c 100644
--- a/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
+++ b/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
@@ -12,6 +12,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
+import org.hamcrest.BaseMatcher;
import org.hamcrest.Matcher;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
@@ -22,6 +23,52 @@
/**
* Allows tests to assert about the presence of log messages, and turns logged errors that are not
* explicitly expected into test failures.
+ *
+ * <h3>Null Expectations</h3>
+ *
+ * <p>It is permitted to pass {@code null} for any expected value or matcher (i.e. for expected
+ * tags, messages or throwables) and this cause the expectation to ignore that attribute during
+ * matching. For example:
+ *
+ * <pre>{@code
+ * // Matches any INFO level log statement with the specified tag.
+ * logged.expectLogMessage(Log.INFO, "tag", null);
+ * // Matches any INFO level log statement with the message "expected", regardless of the tag.
+ * logged.expectLogMessage(Log.INFO, null, "message");
+ * }<pre>
+ *
+ * <p>However in general it is not recommended to use this behaviour, since it can cause tests to
+ * pass when they should have failed.
+ *
+ * <h3>Matchers Vs Expected Values</h3>
+ *
+ * <p>Using a {code Matcher} which can support substring matching and other non-trivial behaviour
+ * can be a good way to avoid brittle tests. However there is a difference between the behaviour of
+ * methods which accept Hamcrest matchers and those which only accept expected value (e.g. {@code
+ * String} or {@code Pattern}).
+ *
+ * If an expected value is used to match a log statement, then duplicate expectations will be
+ * removed:
+ *
+ * <pre>{@code
+ * logged.expectLogMessage(Log.INFO, "tag", "exact message");
+ * // This call has no effect and only 1 log statements is expected.
+ * logged.expectLogMessage(Log.INFO, "tag", "exact message");
+ * }<pre>
+ *
+ * <p>When using a {@code Matcher} in any parameter, the existence of duplicate expectations can no
+ * longer be determined, so de-duplication does not occur:
+ *
+ * <pre>{@code
+ * logged.expectLogMessage(Log.INFO, "tag", Matchers.equalTo("exact message"));
+ * // This adds a 2nd expectation, so 2 log statements with the same value must be present.
+ * logged.expectLogMessage(Log.INFO, "tag", Matchers.equalTo("exact message"));
+ * }<pre>
+ *
+ * <p>This means that you may not be able to trivially convert from using one style of expectation
+ * to the other. In general it is preferable to match the number of expectations to the number of
+ * expected log messages (i.e. using the {@code Matcher} APIs) but some existing tests may rely on
+ * the older de-duplication behaviour.
*/
public final class ExpectedLogMessagesRule implements TestRule {
/** Tags that apps can't prevent. We exempt them globally. */
@@ -111,14 +158,31 @@
* Adds an expected log statement. If this log is not printed during test execution, the test case
* will fail.
*
- * <p>This will also match any log statement which contain a throwable as well. For verifying the
+ * <p>This will also match any log statement which contains a throwable as well. For verifying the
+ * throwable, please see {@link #expectLogMessageWithThrowable(int, String, Matcher<String>,
+ * Matcher<Throwable>)}.
+ *
+ * <p>Do not use this to suppress failures. Use this to test that expected error cases in your
+ * code cause log messages to be printed.
+ *
+ * <p>See class level documentation for a note about using {@code Matcher}s.
+ */
+ public void expectLogMessage(int level, String tag, Matcher<String> messageMatcher) {
+ expectLog(level, tag, messageMatcher, null);
+ }
+
+ /**
+ * Adds an expected log statement. If this log is not printed during test execution, the test case
+ * will fail.
+ *
+ * <p>This will also match any log statement which contains a throwable as well. For verifying the
* throwable, please see {@link #expectLogMessageWithThrowable(int, String, String, Throwable)}.
*
* <p>Do not use this to suppress failures. Use this to test that expected error cases in your
* code cause log messages to be printed.
*/
public void expectLogMessage(int level, String tag, String message) {
- expectedLogs.add(ExpectedLogItem.create(level, tag, message));
+ expectLogMessage(level, tag, MsgEq.of(message));
}
/**
@@ -133,21 +197,19 @@
* code cause log messages to be printed.
*/
public void expectLogMessagePattern(int level, String tag, Pattern messagePattern) {
- expectedLogs.add(ExpectedLogItem.create(level, tag, messagePattern));
+ expectLogMessage(level, tag, MsgRegex.of(messagePattern));
}
/**
- * Adds an expected log statement using a regular expression, with an extra check of {@link
- * Matcher<Throwable>}. If this log is not printed during test execution, the test case will fail.
- * When possible, log output should be made deterministic and {@link #expectLogMessage(int,
- * String, String)} used instead.
+ * Adds an expected log statement with extra check of {@link Throwable}. If this log is not
+ * printed during test execution, the test case will fail. Do not use this to suppress failures.
+ * Use this to test that expected error cases in your code cause log messages to be printed.
*
- * <p>Do not use this to suppress failures. Use this to test that expected error cases in your
- * code cause log messages to be printed.
+ * <p>See class level documentation for a note about using {@code Matcher}s.
*/
- public void expectLogMessagePatternWithThrowableMatcher(
- int level, String tag, Pattern messagePattern, Matcher<Throwable> throwableMatcher) {
- expectedLogs.add(ExpectedLogItem.create(level, tag, messagePattern, throwableMatcher));
+ public void expectLogMessageWithThrowable(
+ int level, String tag, Matcher<String> messagMatcher, Matcher<Throwable> throwableMatcher) {
+ expectLog(level, tag, messagMatcher, throwableMatcher);
}
/**
@@ -157,17 +219,37 @@
*/
public void expectLogMessageWithThrowable(
int level, String tag, String message, Throwable throwable) {
- expectLogMessageWithThrowableMatcher(level, tag, message, equalTo(throwable));
+ expectLogMessageWithThrowable(level, tag, MsgEq.of(message), equalTo(throwable));
+ }
+
+ /**
+ * Adds an expected log statement using a regular expression, with an extra check of {@link
+ * Matcher<Throwable>}. If this log is not printed during test execution, the test case will fail.
+ * When possible, log output should be made deterministic and {@link #expectLogMessage(int,
+ * String, String)} used instead.
+ *
+ * <p>See class level documentation for a note about using {@code Matcher}s.
+ */
+ public void expectLogMessagePatternWithThrowableMatcher(
+ int level, String tag, Pattern messagePattern, Matcher<Throwable> throwableMatcher) {
+ expectLogMessageWithThrowable(level, tag, MsgRegex.of(messagePattern), throwableMatcher);
}
/**
* Adds an expected log statement with extra check of {@link Matcher}. If this log is not printed
* during test execution, the test case will fail. Do not use this to suppress failures. Use this
* to test that expected error cases in your code cause log messages to be printed.
+ *
+ * <p>See class level documentation for a note about using {@code Matcher}s.
*/
public void expectLogMessageWithThrowableMatcher(
int level, String tag, String message, Matcher<Throwable> throwableMatcher) {
- expectedLogs.add(ExpectedLogItem.create(level, tag, message, throwableMatcher));
+ expectLogMessageWithThrowable(level, tag, MsgEq.of(message), throwableMatcher);
+ }
+
+ private void expectLog(
+ int level, String tag, Matcher<String> messageMatcher, Matcher<Throwable> throwableMatcher) {
+ expectedLogs.add(new ExpectedLogItem(level, tag, messageMatcher, throwableMatcher));
}
/**
@@ -196,12 +278,12 @@
shouldIgnoreMissingLoggedTags = shouldIgnore;
}
- private static boolean updateExpected(
+ private boolean updateExpected(
LogItem logItem, Map<ExpectedLogItem, Boolean> expectedLogItemMap) {
for (ExpectedLogItem expectedLogItem : expectedLogItemMap.keySet()) {
if (expectedLogItem.type == logItem.type
- && equals(expectedLogItem.tag, logItem.tag)
- && matchMessage(expectedLogItem, logItem.msg)
+ && Objects.equals(expectedLogItem.tag, logItem.tag)
+ && expectedLogItem.msgMatcher.matches(logItem.msg)
&& matchThrowable(expectedLogItem, logItem.throwable)) {
expectedLogItemMap.put(expectedLogItem, true);
return true;
@@ -211,22 +293,6 @@
return false;
}
- private static boolean equals(String a, String b) {
- return a == null ? b == null : a.equals(b);
- }
-
- private static boolean matchMessage(ExpectedLogItem logItem, String msg) {
- if (logItem.msg != null) {
- return logItem.msg.equals(msg);
- }
-
- if (logItem.msgPattern != null) {
- return logItem.msgPattern.matcher(msg).matches();
- }
-
- return msg == null;
- }
-
private static boolean matchThrowable(ExpectedLogItem logItem, Throwable throwable) {
if (logItem.throwableMatcher != null) {
return logItem.throwableMatcher.matches(throwable);
@@ -239,34 +305,15 @@
private static class ExpectedLogItem {
final int type;
final String tag;
- final String msg;
- final Pattern msgPattern;
+ // Either or both matches can be null ()
+ final Matcher<String> msgMatcher;
final Matcher<Throwable> throwableMatcher;
- static ExpectedLogItem create(int type, String tag, String msg) {
- return new ExpectedLogItem(type, tag, msg, null, null);
- }
-
- static ExpectedLogItem create(int type, String tag, Pattern msg) {
- return new ExpectedLogItem(type, tag, null, msg, null);
- }
-
- static ExpectedLogItem create(
- int type, String tag, String msg, Matcher<Throwable> throwableMatcher) {
- return new ExpectedLogItem(type, tag, msg, null, throwableMatcher);
- }
-
- static ExpectedLogItem create(
- int type, String tag, Pattern pattern, Matcher<Throwable> throwableMatcher) {
- return new ExpectedLogItem(type, tag, null, pattern, throwableMatcher);
- }
-
private ExpectedLogItem(
- int type, String tag, String msg, Pattern msgPattern, Matcher<Throwable> throwableMatcher) {
+ int type, String tag, Matcher<String> msgMatcher, Matcher<Throwable> throwableMatcher) {
this.type = type;
this.tag = tag;
- this.msg = msg;
- this.msgPattern = msgPattern;
+ this.msgMatcher = msgMatcher;
this.throwableMatcher = throwableMatcher;
}
@@ -283,47 +330,99 @@
ExpectedLogItem log = (ExpectedLogItem) o;
return type == log.type
&& !(tag != null ? !tag.equals(log.tag) : log.tag != null)
- && !(msg != null ? !msg.equals(log.msg) : log.msg != null)
- && !(msgPattern != null ? !isEqual(msgPattern, log.msgPattern) : log.msgPattern != null)
- && !(throwableMatcher != null
- ? !throwableMatcher.equals(log.throwableMatcher)
- : log.throwableMatcher != null);
+ && Objects.equals(msgMatcher, log.msgMatcher)
+ && Objects.equals(throwableMatcher, log.throwableMatcher);
}
@Override
public int hashCode() {
- return Objects.hash(type, tag, msg, hash(msgPattern), throwableMatcher);
+ return Objects.hash(type, tag, msgMatcher, throwableMatcher);
}
@Override
public String toString() {
+ String msgStr = (msgMatcher == null) ? "" : (", msg=" + msgMatcher);
String throwableStr = (throwableMatcher == null) ? "" : (", throwable=" + throwableMatcher);
- return "ExpectedLogItem{"
- + "timeString='"
- + null
- + '\''
- + ", type="
- + type
- + ", tag='"
- + tag
- + '\''
- + ", msg='"
- + (msg != null ? msg : msgPattern)
- + '\''
- + throwableStr
- + '}';
+ return String.format(
+ "ExpectedLogItem{timeString='null', type=%s, tag='%s'%s%s}",
+ type, tag, msgStr, throwableStr);
+ }
+ }
+
+ // Similar to IsEqualTo matcher in Hamcrest, but supports equals/hashCode for de-duplication.
+ private static final class MsgEq extends BaseMatcher<String> {
+ static Matcher<String> of(String msg) {
+ return msg != null ? new MsgEq(msg) : null;
+ }
+
+ private final String msg;
+
+ private MsgEq(String msg) {
+ this.msg = msg;
+ }
+
+ @Override
+ public boolean matches(Object other) {
+ // Allow direct cast since we only use this matcher in a type-safe way.
+ return msg.equals((String) other);
+ }
+
+ // Designed to match legacy toString() behaviour - do not modify.
+ @Override
+ public void describeTo(org.hamcrest.Description description) {
+ description.appendText("'" + msg + "'");
+ }
+
+ // This matches legacy behaviour to allow ExpectedLogItem to de-duplicate expectations.
+ @Override
+ public boolean equals(Object matcher) {
+ return matcher instanceof MsgEq && msg.equals(((MsgEq) matcher).msg);
+ }
+
+ @Override
+ public int hashCode() {
+ return msg.hashCode() ^ MsgEq.class.hashCode();
+ }
+ }
+
+ // Similar to MatchesPattern in Hamcrest, but supports equals/hashCode for de-duplication.
+ private static final class MsgRegex extends BaseMatcher<String> {
+ static Matcher<String> of(Pattern pattern) {
+ return pattern != null ? new MsgRegex(pattern) : null;
+ }
+
+ private final Pattern pattern;
+
+ private MsgRegex(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public boolean matches(Object other) {
+ // Allow direct cast since we only use this matcher in a type-safe way.
+ return pattern.matcher((String) other).matches();
+ }
+
+ // Designed to match legacy toString() behaviour - do not modify.
+ @Override
+ public void describeTo(org.hamcrest.Description description) {
+ description.appendText("'" + pattern + "'");
+ }
+
+ // This matches legacy behaviour to allow ExpectedLogItem to de-duplicate regex expectations.
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof MsgRegex ? isEqual(pattern, ((MsgRegex) other).pattern) : false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pattern.pattern(), pattern.flags());
}
/** Returns true if the pattern and flags compiled in a {@link Pattern} were the same. */
private static boolean isEqual(Pattern a, Pattern b) {
- return a != null && b != null
- ? a.pattern().equals(b.pattern()) && a.flags() == b.flags()
- : Objects.equals(a, b);
- }
-
- /** Returns hash for a {@link Pattern} based on the pattern and flags it was compiled with. */
- private static int hash(Pattern pattern) {
- return pattern == null ? 0 : Objects.hash(pattern.pattern(), pattern.flags());
+ return a.pattern().equals(b.pattern()) && a.flags() == b.flags();
}
}
}
diff --git a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
index c8b2693..f45a0ef 100644
--- a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
+++ b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
@@ -1,26 +1,5 @@
package org.robolectric.plugins;
-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;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
-import static android.os.Build.VERSION_CODES.M;
-import static android.os.Build.VERSION_CODES.N;
-import static android.os.Build.VERSION_CODES.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
-import static android.os.Build.VERSION_CODES.O_MR1;
-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.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;
@@ -38,6 +17,26 @@
import org.robolectric.pluginapi.Sdk;
import org.robolectric.pluginapi.SdkProvider;
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;
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+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.S_V2;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+
+import android.os.Build;
/**
* Robolectric's default {@link SdkProvider}.
@@ -85,8 +84,6 @@
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/main/java/org/robolectric/plugins/HierarchicalConfigurationStrategy.java b/robolectric/src/main/java/org/robolectric/plugins/HierarchicalConfigurationStrategy.java
index cfa250f..d172761 100644
--- a/robolectric/src/main/java/org/robolectric/plugins/HierarchicalConfigurationStrategy.java
+++ b/robolectric/src/main/java/org/robolectric/plugins/HierarchicalConfigurationStrategy.java
@@ -10,6 +10,7 @@
import javax.annotation.Priority;
import org.robolectric.pluginapi.config.ConfigurationStrategy;
import org.robolectric.pluginapi.config.Configurer;
+import org.robolectric.util.PerfStatsCollector;
/**
* Robolectric's default {@link ConfigurationStrategy}.
@@ -23,6 +24,7 @@
/** The cache is sized to avoid repeated resolutions for any node. */
private int highWaterMark = 0;
+
private final Map<String, Object[]> cache =
new LinkedHashMap<String, Object[]>() {
@Override
@@ -47,12 +49,16 @@
@Override
public ConfigurationImpl getConfig(Class<?> testClass, Method method) {
final Counter counter = new Counter();
- Object[] configs = cache(testClass.getName() + "/" + method.getName(), counter, s -> {
- counter.incr();
- Object[] methodConfigs = getConfigs(counter,
- configurer -> configurer.getConfigFor(method));
- return merge(getFirstClassConfig(testClass, counter), methodConfigs);
- });
+ Object[] configs =
+ cache(
+ testClass.getName() + "/" + method.getName(),
+ counter,
+ s -> {
+ counter.incr();
+ Object[] methodConfigs =
+ getConfigs(counter, configurer -> configurer.getConfigFor(method));
+ return merge(getFirstClassConfig(testClass, counter), methodConfigs);
+ });
ConfigurationImpl testConfig = new ConfigurationImpl();
for (int i = 0; i < configurers.length; i++) {
@@ -64,19 +70,28 @@
private Object[] getFirstClassConfig(Class<?> testClass, Counter counter) {
// todo: should parent class configs have lower precedence than package configs?
- return cache("first:" + testClass, counter, s -> {
+ return cache(
+ "first:" + testClass,
+ counter,
+ s -> {
Object[] configsForClass = getClassConfig(testClass, counter);
- Package pkg = testClass.getPackage();
- Object[] configsForPackage = getPackageConfig(pkg == null ? "" : pkg.getName(), counter);
+ Package pkg = testClass.getPackage();
+ Object[] configsForPackage =
+ PerfStatsCollector.getInstance()
+ .measure(
+ "getPackageConfig",
+ () -> getPackageConfig(pkg == null ? "" : pkg.getName(), counter));
return merge(configsForPackage, configsForClass);
- }
- );
+ });
}
private Object[] getPackageConfig(String packageName, Counter counter) {
- return cache(packageName, counter, s -> {
- Object[] packageConfigs = getConfigs(counter,
- configurer -> configurer.getConfigFor(packageName));
+ return cache(
+ packageName,
+ counter,
+ s -> {
+ Object[] packageConfigs =
+ getConfigs(counter, configurer -> configurer.getConfigFor(packageName));
String parentPackage = parentPackage(packageName);
if (parentPackage == null) {
return merge(defaultConfigs, packageConfigs);
@@ -96,16 +111,20 @@
}
private Object[] getClassConfig(Class<?> testClass, Counter counter) {
- return cache(testClass.getName(), counter, s -> {
- Object[] classConfigs = getConfigs(counter, configurer -> configurer.getConfigFor(testClass));
+ return cache(
+ testClass.getName(),
+ counter,
+ s -> {
+ Object[] classConfigs =
+ getConfigs(counter, configurer -> configurer.getConfigFor(testClass));
- Class<?> superclass = testClass.getSuperclass();
- if (superclass != Object.class) {
- Object[] superclassConfigs = getClassConfig(superclass, counter);
- return merge(superclassConfigs, classConfigs);
- }
- return classConfigs;
- });
+ Class<?> superclass = testClass.getSuperclass();
+ if (superclass != Object.class) {
+ Object[] superclassConfigs = getClassConfig(superclass, counter);
+ return merge(superclassConfigs, classConfigs);
+ }
+ return classConfigs;
+ });
}
private Object[] cache(String name, Counter counter, Function<String, Object[]> fn) {
@@ -146,11 +165,10 @@
Configurer configurer = configurers[i];
Object childConfig = childConfigs[i];
Object parentConfig = parentConfigs[i];
- objects[i] = childConfig == null
- ? parentConfig
- : parentConfig == null
- ? childConfig
- : configurer.merge(parentConfig, childConfig);
+ objects[i] =
+ childConfig == null
+ ? parentConfig
+ : parentConfig == null ? childConfig : configurer.merge(parentConfig, childConfig);
}
return objects;
}
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 0000000..22df750
--- /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 0000000..1132f6e
--- /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 c0108de..737af48 100644
--- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
+++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
@@ -268,6 +268,24 @@
}
@Test
+ public void failedTest_shouldStillReportPerfStats() throws Exception {
+ List<Metric> metrics = new ArrayList<>();
+ PerfStatsReporter reporter = (metadata, metrics1) -> metrics.addAll(metrics1);
+
+ RobolectricTestRunner runner =
+ new SingleSdkRobolectricTestRunner(
+ TestThatFails.class,
+ RobolectricTestRunner.defaultInjector()
+ .bind(PerfStatsReporter[].class, new PerfStatsReporter[] {reporter})
+ .build());
+
+ runner.run(notifier);
+
+ Set<String> metricNames = metrics.stream().map(Metric::getName).collect(toSet());
+ assertThat(metricNames).contains("initialization");
+ }
+
+ @Test
public void shouldResetThreadInterrupted() throws Exception {
RobolectricTestRunner runner = new SingleSdkRobolectricTestRunner(TestWithInterrupt.class);
runner.run(notifier);
@@ -358,6 +376,15 @@
@Ignore
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+ public static class TestThatFails {
+ @Test
+ public void first() throws Exception {
+ throw new AssertionError();
+ }
+ }
+
+ @Ignore
+ @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Config(application = TestWithBrokenAppCreate.MyTestApplication.class)
@LazyApplication(LazyLoad.OFF)
public static class TestWithBrokenAppCreate {
diff --git a/robolectric/src/test/java/org/robolectric/RuntimeEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/RuntimeEnvironmentTest.java
index 942b923..26a1970 100644
--- a/robolectric/src/test/java/org/robolectric/RuntimeEnvironmentTest.java
+++ b/robolectric/src/test/java/org/robolectric/RuntimeEnvironmentTest.java
@@ -1,18 +1,25 @@
package org.robolectric;
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import static android.os.Build.VERSION_CODES.KITKAT;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
import android.app.Application;
+import android.content.Context;
import android.content.res.Configuration;
+import android.text.format.DateUtils;
+import android.util.DisplayMetrics;
import android.view.Surface;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowDisplay;
import org.robolectric.util.Scheduler;
@@ -110,9 +117,36 @@
}
@Test
+ public void testSetFontScale_updatesFontScale() {
+ Context context = ApplicationProvider.getApplicationContext();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+
+ assertThat(context.getResources().getConfiguration().fontScale).isEqualTo(1.0f);
+ assertThat(displayMetrics.scaledDensity).isEqualTo(displayMetrics.density);
+ assertThat(RuntimeEnvironment.getFontScale()).isEqualTo(1.0f);
+
+ RuntimeEnvironment.setFontScale(1.3f);
+
+ assertThat(context.getResources().getConfiguration().fontScale).isEqualTo(1.3f);
+ assertThat(displayMetrics.scaledDensity).isEqualTo(displayMetrics.density * 1.3f);
+ assertThat(RuntimeEnvironment.getFontScale()).isEqualTo(1.3f);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
public void testGetRotation() {
RuntimeEnvironment.setQualifiers("+land");
int screenRotation = ShadowDisplay.getDefaultDisplay().getRotation();
- assertThat(screenRotation).isEqualTo(Surface.ROTATION_0);
+ assertThat(screenRotation).isEqualTo(Surface.ROTATION_90);
+ }
+
+ @Test
+ @Config(minSdk = KITKAT)
+ public void setQualifiers_resetsDateUtilsFormatCache() {
+ RuntimeEnvironment.setQualifiers("ar-rXB");
+ // Populate the DateUtils static format cache.
+ String unused = DateUtils.formatElapsedTime(120);
+ RuntimeEnvironment.setQualifiers("en-rUS");
+ assertThat(DateUtils.formatElapsedTime(120)).isEqualTo("02:00");
}
}
diff --git a/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java
index 87ddb07..0428c44 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 @@
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 @@
@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 b895d65..15ca52c 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 @@
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 @@
@Before
public void setUp() {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
optsForO = RuntimeEnvironment.getApiLevel() >= O
? "nowidecg-lowdr-"
@@ -71,7 +71,11 @@
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 @@
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 0ae4675..d4395c5 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 @@
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 b57f606..0c8d977 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 @@
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 @@
@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/controller/ActivityControllerTest.java b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java
index 7c73aaf..181a801 100644
--- a/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java
@@ -11,6 +11,7 @@
import android.app.Activity;
import android.app.Fragment;
+import android.app.WindowConfiguration;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
@@ -300,6 +301,31 @@
.isEqualTo(newFontScale);
}
+ @Config(minSdk = P)
+ @Test
+ public void configurationChange_windowConfigurationChanges_doesNotRecreateActivity() {
+ Configuration config =
+ new Configuration(
+ ApplicationProvider.getApplicationContext().getResources().getConfiguration());
+ WindowConfiguration windowConfiguration = config.windowConfiguration;
+ windowConfiguration.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
+
+ ActivityController<ConfigAwareActivity> controller =
+ Robolectric.buildActivity(ConfigAwareActivity.class).setup();
+ transcript.clear();
+ controller.configurationChange(config);
+
+ assertThat(transcript).containsAtLeast("onConfigurationChanged", "View.onConfigurationChanged");
+ assertThat(
+ controller
+ .get()
+ .getResources()
+ .getConfiguration()
+ .windowConfiguration
+ .getWindowingMode())
+ .isEqualTo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
+ }
+
@Test
@Config(maxSdk = O_MR1)
public void configurationChange_callsLifecycleMethodsAndAppliesConfigWhenAnyNonManaged_beforeP() {
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 6edc428..dc6ac31 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 @@
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 0511881..3756994 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 @@
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 @@
@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()));
@@ -356,4 +356,14 @@
assertThat(RuntimeEnvironment.getQualifiers()).contains("w640dp-h480dp");
assertThat(RuntimeEnvironment.getQualifiers()).contains("land");
}
+
+ @Test
+ public void
+ thisTestNameHasMoreThan255Characters1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890() {
+ bootstrapWrapper.callSetUpApplicationState();
+ ApplicationInfo applicationInfo =
+ ApplicationProvider.getApplicationContext().getApplicationInfo();
+ assertThat(applicationInfo.dataDir).isNotNull();
+ assertThat(new File(applicationInfo.dataDir).isDirectory()).isTrue();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
index 679b456..af1d8ba 100644
--- a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
+++ b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
@@ -187,6 +187,6 @@
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
old mode 100755
new mode 100644
diff --git a/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java b/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java
old mode 100755
new mode 100644
diff --git a/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java b/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
index a84ad06..5821af5 100644
--- a/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
+++ b/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
@@ -1,11 +1,12 @@
package org.robolectric.junit.rules;
-import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.Matchers.instanceOf;
import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.regex.Pattern;
import org.hamcrest.Description;
+import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
import org.junit.Test;
@@ -209,9 +210,24 @@
}
@Test
- public void expectLogMessageWithPattern_duplicatePatterns() {
+ public void expectLogMessage_duplicateExpectatedValues_areDeduplicated() {
+ Log.e("Mytag", "message1");
+ rule.expectLogMessage(Log.ERROR, "Mytag", "message1");
+ rule.expectLogMessage(Log.ERROR, "Mytag", "message1");
+ }
+
+ @Test
+ public void expectLogMessageWithPattern_duplicateExpectatedValues_areDeduplicated() {
Log.e("Mytag", "message1");
rule.expectLogMessagePattern(Log.ERROR, "Mytag", Pattern.compile("message1"));
rule.expectLogMessagePattern(Log.ERROR, "Mytag", Pattern.compile("message1"));
}
+
+ @Test
+ public void expectLogMessage_duplicateMatchers_areNotDeduplicated() {
+ Log.e("Mytag", "message1");
+ rule.expectLogMessage(Log.ERROR, "Mytag", Matchers.equalTo("message1"));
+ rule.expectLogMessage(Log.ERROR, "Mytag", Matchers.equalTo("message1"));
+ expectedException.expect(Matchers.isA(AssertionError.class));
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java
index 8b493ba..dc20266 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 @@
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 @@
@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/AssociationInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java
new file mode 100644
index 0000000..b1d8489
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java
@@ -0,0 +1,54 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.companion.AssociationInfo;
+import android.net.MacAddress;
+import android.os.Build.VERSION_CODES;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public final class AssociationInfoBuilderTest {
+ private static final int ID = 7;
+ private static final int USER_ID = 8;
+ private static final String PACKAGE_NAME = "com.google.foo";
+ private static final String DEVICE_MAC_ADDRESS = "AA:BB:CC:DD:EE:FF";
+ private static final String DISPLAY_NAME = "Display Name";
+ private static final String DEVICE_PROFILE = "Device Profile";
+ private static final boolean SELF_MANAGED = true;
+ private static final boolean NOTIFY_ON_DEVICE_NEARBY = true;
+ private static final long APPROVED_MS = 1234L;
+ private static final long LAST_TIME_CONNECTED_MS = 5678L;
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void obtain() {
+ AssociationInfo info =
+ AssociationInfoBuilder.newBuilder()
+ .setId(ID)
+ .setUserId(USER_ID)
+ .setPackageName(PACKAGE_NAME)
+ .setDeviceMacAddress(DEVICE_MAC_ADDRESS)
+ .setDisplayName(DISPLAY_NAME)
+ .setDeviceProfile(DEVICE_PROFILE)
+ .setSelfManaged(SELF_MANAGED)
+ .setNotifyOnDeviceNearby(NOTIFY_ON_DEVICE_NEARBY)
+ .setApprovedMs(APPROVED_MS)
+ .setLastTimeConnectedMs(LAST_TIME_CONNECTED_MS)
+ .build();
+
+ assertThat(info.getId()).isEqualTo(ID);
+ assertThat(info.getUserId()).isEqualTo(USER_ID);
+ assertThat(info.getPackageName()).isEqualTo(PACKAGE_NAME);
+ assertThat(info.getDeviceMacAddress()).isEqualTo(MacAddress.fromString(DEVICE_MAC_ADDRESS));
+ assertThat(info.getDisplayName().toString()).isEqualTo(DISPLAY_NAME);
+ assertThat(info.getDeviceProfile()).isEqualTo(DEVICE_PROFILE);
+ assertThat(info.isSelfManaged()).isEqualTo(SELF_MANAGED);
+ assertThat(info.isNotifyOnDeviceNearby()).isEqualTo(NOTIFY_ON_DEVICE_NEARBY);
+ assertThat(info.getTimeApprovedMs()).isEqualTo(APPROVED_MS);
+ assertThat(info.getLastTimeConnectedMs()).isEqualTo(LAST_TIME_CONNECTED_MS);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/BluetoothConnectionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/BluetoothConnectionManagerTest.java
new file mode 100644
index 0000000..45e377b
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/BluetoothConnectionManagerTest.java
@@ -0,0 +1,126 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class BluetoothConnectionManagerTest {
+ private static final String REMOTE_ADDRESS1 = "R-A-1";
+ private static final String REMOTE_ADDRESS2 = "R-A-2";
+
+ private final BluetoothConnectionManager manager = BluetoothConnectionManager.getInstance();
+
+ @After
+ public void tearDown() {
+ manager.resetConnections();
+ }
+
+ @Test
+ public void test_getInstance() {
+ BluetoothConnectionManager a = BluetoothConnectionManager.getInstance();
+ assertThat(a).isNotNull();
+ assertThat(a).isEqualTo(manager);
+ assertThat(a.isConnected(REMOTE_ADDRESS1)).isFalse();
+ assertThat(manager.isConnected(REMOTE_ADDRESS1)).isFalse();
+ a.registerGattClientConnection(REMOTE_ADDRESS1);
+ assertThat(a.isConnected(REMOTE_ADDRESS1)).isTrue();
+ assertThat(manager.isConnected(REMOTE_ADDRESS1)).isTrue();
+ }
+
+ @Test
+ public void test_hasGattClientConnection() {
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.hasGattClientConnection(REMOTE_ADDRESS1)).isTrue();
+ assertThat(this.manager.hasGattClientConnection(REMOTE_ADDRESS2)).isFalse();
+ }
+
+ @Test
+ public void test_hasGattClientConnection_multiple() {
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS1);
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS2);
+ assertThat(this.manager.hasGattClientConnection(REMOTE_ADDRESS1)).isTrue();
+ assertThat(this.manager.hasGattClientConnection(REMOTE_ADDRESS2)).isTrue();
+ }
+
+ @Test
+ public void test_hasGattServerConnection() {
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.hasGattServerConnection(REMOTE_ADDRESS1)).isTrue();
+ assertThat(this.manager.hasGattServerConnection(REMOTE_ADDRESS2)).isFalse();
+ }
+
+ @Test
+ public void test_hasGattServerConnection_multiple() {
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS2);
+ assertThat(this.manager.hasGattServerConnection(REMOTE_ADDRESS1)).isTrue();
+ assertThat(this.manager.hasGattServerConnection(REMOTE_ADDRESS2)).isTrue();
+ }
+
+ @Test
+ public void test_isNotConnected_noConnection() {
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isFalse();
+ }
+
+ @Test
+ public void test_isConnected_gattServerConnection() {
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isTrue();
+ }
+
+ @Test
+ public void test_isConnected_gattClientConnection() {
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isTrue();
+ }
+
+ @Test
+ public void test_isConnected_gattClientandServerConnection() {
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS1);
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isTrue();
+ }
+
+ @Test
+ public void test_isNotConnected_unregistedGattClientConnection() {
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS1);
+ this.manager.unregisterGattClientConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isFalse();
+ }
+
+ @Test
+ public void test_isNotConnected_unregistedGattServerConnection() {
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ this.manager.unregisterGattServerConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isFalse();
+ }
+
+ @Test
+ public void test_isConnected_gattClientConnection_unregistedGattServerConnection() {
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS1);
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ this.manager.unregisterGattServerConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isTrue();
+ }
+
+ @Test
+ public void test_isConnected_gattServerConnection_unregistedGattClientConnection() {
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS1);
+ this.manager.unregisterGattClientConnection(REMOTE_ADDRESS1);
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isTrue();
+ }
+
+ @Test
+ public void test_resetConnections() {
+ this.manager.registerGattServerConnection(REMOTE_ADDRESS1);
+ this.manager.registerGattClientConnection(REMOTE_ADDRESS2);
+ this.manager.resetConnections();
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS1)).isFalse();
+ assertThat(this.manager.isConnected(REMOTE_ADDRESS2)).isFalse();
+ }
+}
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 0000000..8474b21
--- /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/CellInfoNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java
new file mode 100644
index 0000000..80e7dcc
--- /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/CellSignalStrengthNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java
new file mode 100644
index 0000000..1f18ee8
--- /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/ShadowAbsSpinnerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAbsSpinnerTest.java
index ec4b985..9b1b09d 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAbsSpinnerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAbsSpinnerTest.java
@@ -3,16 +3,23 @@
import static com.google.common.truth.Truth.assertThat;
import static org.robolectric.Shadows.shadowOf;
+import android.app.Activity;
import android.content.Context;
+import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
@RunWith(AndroidJUnit4.class)
public class ShadowAbsSpinnerTest {
@@ -22,8 +29,10 @@
@Before
public void setUp() throws Exception {
+ Activity activity = Robolectric.setupActivity(Activity.class);
Context context = ApplicationProvider.getApplicationContext();
- spinner = new Spinner(context);
+ spinner = new Spinner(activity);
+ ((ViewGroup) activity.findViewById(android.R.id.content)).addView(spinner);
shadowSpinner = shadowOf(spinner);
String [] testItems = {"foo", "bar"};
arrayAdapter = new MyArrayAdapter(context, testItems);
@@ -62,6 +71,57 @@
assertThat(shadowSpinner.isAnimatedTransition()).isTrue();
}
+ @Test
+ public void useRealSelection_doesNotCauseInfiniteLoop() {
+ try {
+ System.setProperty("robolectric.useRealSpinnerSelection", "true");
+ final AtomicBoolean itemSelected = new AtomicBoolean(false);
+ spinner.setOnItemSelectedListener(
+ new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ itemSelected.set(true);
+ // This will result in an stack overflow if the fake selection ShadowAbsSpinner
+ // selection logic is used.
+ spinner.setSelection(0);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {}
+ });
+ spinner.setAdapter(arrayAdapter);
+ spinner.setSelection(1);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(itemSelected.get()).isTrue();
+ } finally {
+ System.clearProperty("robolectric.useRealSpinnerSelection");
+ }
+ }
+
+ @Test
+ public void useRealSelection_callbackCalledOnce() {
+ try {
+ System.setProperty("robolectric.useRealSpinnerSelection", "true");
+ final AtomicInteger invocations = new AtomicInteger(0);
+ spinner.setOnItemSelectedListener(
+ new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ invocations.incrementAndGet();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {}
+ });
+ spinner.setAdapter(arrayAdapter);
+ spinner.setSelection(1);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(invocations.get()).isEqualTo(1);
+ } finally {
+ System.clearProperty("robolectric.useRealSpinnerSelection");
+ }
+ }
+
private static class MyArrayAdapter extends ArrayAdapter<String> {
public MyArrayAdapter(Context context, String[] testItems) {
super(context, android.R.layout.simple_spinner_item, testItems);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityServiceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityServiceTest.java
index 9cabcdf..195d673 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityServiceTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityServiceTest.java
@@ -224,12 +224,100 @@
w2.setId(2);
AccessibilityWindowInfo w3 = AccessibilityWindowInfo.obtain();
w3.setId(3);
+
shadow.setWindows(Arrays.asList(w1, w2, w3));
+
assertThat(service.getWindows()).hasSize(3);
assertThat(service.getWindows()).containsExactly(w1, w2, w3).inOrder();
}
@Test
+ @Config(minSdk = R)
+ public void getWindowsforDefaultDisplay_returnEmptyList() {
+ assertThat(service.getWindowsOnAllDisplays().get(Display.DEFAULT_DISPLAY)).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void getWindowsforNonDefaultDisplay_returnNullList() {
+ assertThat(service.getWindowsOnAllDisplays().get(Display.DEFAULT_DISPLAY + 1)).isNull();
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void setWindowsOnDisplay_returnPopulatedWindowsOnAllDisplays() {
+ AccessibilityWindowInfo w1 = AccessibilityWindowInfo.obtain();
+ w1.setId(1);
+ AccessibilityWindowInfo w2 = AccessibilityWindowInfo.obtain();
+ w2.setId(2);
+ AccessibilityWindowInfo w3 = AccessibilityWindowInfo.obtain();
+ w3.setId(3);
+ AccessibilityWindowInfo w4 = AccessibilityWindowInfo.obtain();
+ w4.setId(4);
+ AccessibilityWindowInfo w5 = AccessibilityWindowInfo.obtain();
+ w5.setId(5);
+ AccessibilityWindowInfo w6 = AccessibilityWindowInfo.obtain();
+ w6.setId(6);
+ AccessibilityWindowInfo w7 = AccessibilityWindowInfo.obtain();
+ w7.setId(7);
+
+ shadow.setWindowsOnDisplay(Display.DEFAULT_DISPLAY, Arrays.asList(w1, w2, w3));
+ shadow.setWindowsOnDisplay(Display.DEFAULT_DISPLAY + 1, Arrays.asList(w4, w5, w6, w7));
+
+ assertThat(service.getWindows()).hasSize(3);
+ assertThat(service.getWindows()).containsExactly(w1, w2, w3).inOrder();
+ assertThat(service.getWindowsOnAllDisplays().size()).isEqualTo(2);
+ assertThat(service.getWindowsOnAllDisplays().get(Display.DEFAULT_DISPLAY))
+ .containsExactly(w1, w2, w3)
+ .inOrder();
+ assertThat(service.getWindowsOnAllDisplays().get(Display.DEFAULT_DISPLAY + 1))
+ .containsExactly(w4, w5, w6, w7)
+ .inOrder();
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void setNullWindowsOnNonDefaultDisplay_nonDefaultDisplayHasWindows_displayIsRemoved() {
+ AccessibilityWindowInfo w1 = AccessibilityWindowInfo.obtain();
+ w1.setId(1);
+ AccessibilityWindowInfo w2 = AccessibilityWindowInfo.obtain();
+ w2.setId(2);
+ AccessibilityWindowInfo w3 = AccessibilityWindowInfo.obtain();
+ w3.setId(3);
+ shadow.setWindowsOnDisplay(Display.DEFAULT_DISPLAY + 1, Arrays.asList(w1, w2, w3));
+
+ shadow.setWindowsOnDisplay(Display.DEFAULT_DISPLAY + 1, null);
+
+ assertThat(service.getWindowsOnAllDisplays().get(Display.DEFAULT_DISPLAY + 1)).isNull();
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void setWindows_nonDefaultDisplayHasWindows_nonDefaultDisplayWindowsNotRemoved() {
+ AccessibilityWindowInfo w1 = AccessibilityWindowInfo.obtain();
+ w1.setId(1);
+ AccessibilityWindowInfo w2 = AccessibilityWindowInfo.obtain();
+ w2.setId(2);
+ AccessibilityWindowInfo w3 = AccessibilityWindowInfo.obtain();
+ w3.setId(3);
+ AccessibilityWindowInfo w4 = AccessibilityWindowInfo.obtain();
+ w4.setId(4);
+ AccessibilityWindowInfo w5 = AccessibilityWindowInfo.obtain();
+ w5.setId(5);
+ AccessibilityWindowInfo w6 = AccessibilityWindowInfo.obtain();
+ w6.setId(6);
+ AccessibilityWindowInfo w7 = AccessibilityWindowInfo.obtain();
+ w7.setId(7);
+ shadow.setWindowsOnDisplay(Display.DEFAULT_DISPLAY + 1, Arrays.asList(w4, w5, w6, w7));
+
+ shadow.setWindows(Arrays.asList(w1, w2, w3));
+
+ assertThat(service.getWindowsOnAllDisplays().get(Display.DEFAULT_DISPLAY + 1))
+ .containsExactly(w4, w5, w6, w7)
+ .inOrder();
+ }
+
+ @Test
@Config(minSdk = S)
public void getSystemActions_returnsNull() {
assertThat(service.getSystemActions()).isNull();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityWindowInfoTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityWindowInfoTest.java
index 7e5b783..9407c09 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityWindowInfoTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityWindowInfoTest.java
@@ -59,4 +59,11 @@
shadow.addChild(window);
assertThat(shadow.getChild(0)).isEqualTo(window);
}
+
+ @Test
+ public void testSetPictureInPicture() {
+ assertThat(shadow.isInPictureInPictureMode()).isFalse();
+ shadow.setPictureInPicture(true);
+ assertThat(shadow.isInPictureInPictureMode()).isTrue();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java
index 9ecb63d..5de84d8 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 @@
@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 @@
@Test
public void openNonAssetShouldThrowExceptionWhenFileDoesNotExist() throws IOException {
- assumeTrue(useLegacy());
+ assume().that(useLegacy()).isTrue();
expectedException.expect(IOException.class);
expectedException.expectMessage(
@@ -90,7 +90,7 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 d5c0c46..1498562 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
@@ -50,6 +50,17 @@
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();
@@ -405,7 +416,7 @@
@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);
@@ -421,7 +432,7 @@
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);
@@ -436,7 +447,7 @@
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);
@@ -450,7 +461,7 @@
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});
@@ -465,7 +476,7 @@
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);
@@ -478,7 +489,7 @@
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);
@@ -494,7 +505,7 @@
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);
@@ -510,7 +521,7 @@
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);
@@ -526,7 +537,7 @@
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);
@@ -539,7 +550,7 @@
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);
@@ -553,7 +564,7 @@
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});
@@ -568,7 +579,7 @@
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);
@@ -581,7 +592,7 @@
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);
@@ -597,7 +608,7 @@
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);
@@ -613,7 +624,7 @@
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);
@@ -629,17 +640,130 @@
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));
@@ -650,8 +774,8 @@
@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));
@@ -662,8 +786,8 @@
@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));
@@ -674,7 +798,7 @@
@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);
@@ -683,7 +807,7 @@
@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);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
index 10cb148..9fa2ef2 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
@@ -599,7 +599,8 @@
PendingIntent.getBroadcast(
getApplicationContext(),
/* requestCode= */ 0,
- new Intent("com.dummy.action.DUMMY_ACTION"),
+ new Intent("com.dummy.action.DUMMY_ACTION")
+ .setPackage(getApplicationContext().getPackageName()),
/* flags= */ PendingIntent.FLAG_MUTABLE)));
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java
new file mode 100644
index 0000000..1216595
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java
@@ -0,0 +1,199 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import androidx.test.core.app.ApplicationProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link ShadowBluetoothGattServer}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowBluetoothGattServerTest {
+
+ private static final int INITIAL_VALUE = -99;
+ private static final String ACTION_CONNECTION = "CONNECT/DISCONNECT";
+ private static final String MOCK_MAC_ADDRESS = "00:11:22:33:AA:BB";
+ private static final byte[] RESPONSE_VALUE1 = new byte[] {'a', 'b', 'c'};
+ private static final byte[] RESPONSE_VALUE2 = new byte[] {'1', '2', '3'};
+
+ private BluetoothManager manager;
+ private Context context;
+ private BluetoothGattServer server;
+ private BluetoothDevice device;
+
+ private int resultStatus = INITIAL_VALUE;
+ private int resultState = INITIAL_VALUE;
+ private String resultAction;
+
+ private final BluetoothGattServerCallback callback =
+ new BluetoothGattServerCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+ resultStatus = status;
+ resultState = newState;
+ resultAction = ACTION_CONNECTION;
+ }
+ };
+
+ @Before
+ @Config()
+ public void setUp() {
+ context = ApplicationProvider.getApplicationContext();
+ manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
+ server = manager.openGattServer(context, new BluetoothGattServerCallback() {}, 0);
+ device = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
+ }
+
+ @After
+ public void tearDown() {
+ shadowOf(server).getBluetoothConnectionManager().resetConnections();
+ }
+
+ @Test
+ public void test_setAndGetGattServerCallback() {
+ shadowOf(server).setGattServerCallback(callback);
+ assertThat(shadowOf(server).getGattServerCallback()).isSameInstanceAs(callback);
+ }
+
+ @Test
+ public void test_getConnectionManager() {
+ assertThat(shadowOf(server).getBluetoothConnectionManager()).isNotNull();
+ }
+
+ @Test
+ public void test_isNotClosed_initially() {
+ assertThat(shadowOf(server).isClosed()).isFalse();
+ }
+
+ @Test
+ public void test_isClosed_afterClose() {
+ server.close();
+ assertThat(shadowOf(server).isClosed()).isTrue();
+ }
+
+ @Test
+ public void test_getResponses_initially() {
+ assertThat(shadowOf(server).getResponses()).isEmpty();
+ }
+
+ @Test
+ public void test_getResponses_afterSendResponses() {
+ shadowOf(server).sendResponse(device, 0, 0, 0, RESPONSE_VALUE1);
+ assertThat(shadowOf(server).getResponses()).hasSize(1);
+ shadowOf(server).sendResponse(device, 0, 0, 0, RESPONSE_VALUE2);
+ assertThat(shadowOf(server).getResponses()).hasSize(2);
+ assertThat(shadowOf(server).getResponses().get(0)).isEqualTo(RESPONSE_VALUE1);
+ assertThat(shadowOf(server).getResponses().get(1)).isEqualTo(RESPONSE_VALUE2);
+ }
+
+ @Test
+ public void test_getResponses_afterClearResponses() {
+ shadowOf(server).sendResponse(device, 0, 0, 0, RESPONSE_VALUE1);
+ shadowOf(server).sendResponse(device, 0, 0, 0, RESPONSE_VALUE2);
+ shadowOf(server).clearResponses();
+ assertThat(shadowOf(server).getResponses()).isEmpty();
+ }
+
+ @Test
+ public void test_isConnectedToDevice_initially() {
+ assertThat(shadowOf(server).isConnectedToDevice(device)).isFalse();
+ }
+
+ @Test
+ public void test_notifyConnection_withoutCallback() {
+ shadowOf(server).setGattServerCallback(null);
+ shadowOf(server).notifyConnection(device);
+ assertThat(shadowOf(server).isConnectedToDevice(device)).isTrue();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyConnection_withCallback() {
+ shadowOf(server).setGattServerCallback(callback);
+ shadowOf(server).notifyConnection(device);
+ assertThat(shadowOf(server).isConnectedToDevice(device)).isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothAdapter.STATE_CONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ public void test_notifyDisconnection_withoutCallback() {
+ shadowOf(server).setGattServerCallback(null);
+ shadowOf(server).notifyConnection(device);
+ shadowOf(server).notifyDisconnection(device);
+ assertThat(shadowOf(server).isConnectionCancelled(device)).isTrue();
+ assertThat(shadowOf(server).isConnectedToDevice(device)).isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyDisconnection_withCallback() {
+ shadowOf(server).setGattServerCallback(callback);
+ shadowOf(server).notifyConnection(device);
+ shadowOf(server).notifyDisconnection(device);
+ assertThat(shadowOf(server).isConnectionCancelled(device)).isTrue();
+ assertThat(shadowOf(server).isConnectedToDevice(device)).isFalse();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothAdapter.STATE_DISCONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ public void test_notifyDisconnection_withCallback_beforeConnection() {
+ shadowOf(server).setGattServerCallback(callback);
+ shadowOf(server).notifyDisconnection(device);
+ assertThat(shadowOf(server).isConnectionCancelled(device)).isTrue();
+ assertThat(shadowOf(server).isConnectedToDevice(device)).isFalse();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothAdapter.STATE_DISCONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ public void test_isConnectionCancelled_afterCancelConnection() {
+ server.cancelConnection(device);
+ assertThat(shadowOf(server).isConnectionCancelled(device)).isTrue();
+ }
+
+ @Test
+ public void test_isConnectionCancelled_afterCancelConnection_whileConnected() {
+ server.connect(device, true);
+ server.cancelConnection(device);
+ assertThat(shadowOf(server).isConnectionCancelled(device)).isTrue();
+ }
+
+ @Test
+ public void test_isConnectionCancelled_afterCancelConnection_aftereNotifyConnection() {
+ shadowOf(server).setGattServerCallback(callback);
+ shadowOf(server).notifyConnection(device);
+ server.cancelConnection(device);
+ assertThat(shadowOf(server).isConnectionCancelled(device)).isTrue();
+ }
+
+ @Test
+ public void test_removeCancelledDevice_afterNotifyConnection() {
+ shadowOf(server).setGattServerCallback(callback);
+ shadowOf(server).notifyDisconnection(device);
+ shadowOf(server).notifyConnection(device);
+ assertThat(shadowOf(server).isConnectionCancelled(device)).isFalse();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
index 78b9edb..01e4bdc 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
@@ -14,6 +14,7 @@
import android.bluetooth.BluetoothProfile;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.UUID;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -99,6 +100,11 @@
bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
}
+ @After
+ public void tearDown() {
+ shadowOf(bluetoothGatt).getBluetoothConnectionManager().resetConnections();
+ }
+
@Test
public void canCreateBluetoothGattViaNewInstance() {
assertThat(bluetoothGatt).isNotNull();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothManagerTest.java
index a2c5f5f..5e9848a 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothManagerTest.java
@@ -1,12 +1,17 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.robolectric.Shadows.shadowOf;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
@@ -31,14 +36,16 @@
private static final int PROFILE_STATE_CONNECTED = BluetoothProfile.STATE_CONNECTED;
private static final int PROFILE_STATE_CONNECTING = BluetoothProfile.STATE_CONNECTING;
private static final int[] CONNECTED_STATES = new int[] {PROFILE_STATE_CONNECTED};
+ private static final BluetoothGattServerCallback callback = new BluetoothGattServerCallback() {};
private BluetoothManager manager;
private BluetoothAdapter adapter;
private ShadowBluetoothManager shadowManager;
+ private Context context;
@Before
public void setUp() {
- Context context = ApplicationProvider.getApplicationContext();
+ context = ApplicationProvider.getApplicationContext();
manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
adapter = manager.getAdapter();
shadowManager = shadowOf(manager);
@@ -135,4 +142,20 @@
private BluetoothDevice createBluetoothDevice(String address) {
return adapter.getRemoteDevice(address);
}
+
+ @Test
+ @Config(minSdk = O, maxSdk = R)
+ public void openGattServer_doesNotCrash() {
+ BluetoothGattServer gattServer = manager.openGattServer(context, callback, 0);
+ assertThat(gattServer).isNotNull();
+ assertThat(shadowOf(gattServer).getGattServerCallback()).isSameInstanceAs(callback);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void openGattServerWithTransport_doesNotCrash() {
+ BluetoothGattServer gattServer = manager.openGattServer(context, callback, 0, true);
+ assertThat(gattServer).isNotNull();
+ assertThat(shadowOf(gattServer).getGattServerCallback()).isSameInstanceAs(callback);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java
index 624508b..cb3d975 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBugreportManagerTest.java
@@ -74,6 +74,7 @@
shadowMainLooper().idle();
assertThat(shadowBugreportManager.isBugreportInProgress()).isTrue();
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNotNull();
verify(callback, never()).onFinished();
verify(callback, never()).onError(anyInt());
}
@@ -217,6 +218,7 @@
assertThat(shadowBugreportManager.isBugreportInProgress()).isTrue();
verify(callback, never()).onFinished();
verify(callback, never()).onError(anyInt());
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNotNull();
shadowBugreportManager.executeOnFinished();
shadowMainLooper().idle();
@@ -224,6 +226,7 @@
assertThat(shadowBugreportManager.isBugreportInProgress()).isFalse();
verify(callback).onFinished();
verify(callback, never()).onError(anyInt());
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNull();
}
@Test
@@ -244,6 +247,7 @@
verify(callback, never()).onProgress(anyFloat());
verify(callback, never()).onFinished();
verify(callback, never()).onError(anyInt());
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNotNull();
shadowBugreportManager.executeOnProgress(50.0f);
shadowMainLooper().idle();
@@ -252,6 +256,7 @@
verify(callback).onProgress(50.0f);
verify(callback, never()).onFinished();
verify(callback, never()).onError(anyInt());
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNotNull();
shadowBugreportManager.executeOnFinished();
shadowMainLooper().idle();
@@ -263,6 +268,7 @@
verify(callback).onFinished();
verify(callback, never()).onError(anyInt());
verifyNoMoreInteractions(callback);
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNull();
}
@Test
@@ -281,6 +287,7 @@
assertThat(shadowBugreportManager.isBugreportInProgress()).isTrue();
verify(callback, never()).onFinished();
verify(callback, never()).onError(anyInt());
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNotNull();
shadowBugreportManager.executeOnFinished();
shadowMainLooper().idle();
@@ -288,6 +295,7 @@
assertThat(shadowBugreportManager.isBugreportInProgress()).isFalse();
verify(callback).onFinished();
verify(callback, never()).onError(anyInt());
+ assertThat(shadowBugreportManager.getScreenshotFd()).isNull();
}
private ParcelFileDescriptor createWriteFile(String fileName) throws IOException {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowChoreographerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowChoreographerTest.java
index 2b40685..ff359a0 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowChoreographerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowChoreographerTest.java
@@ -9,12 +9,20 @@
import java.util.concurrent.atomic.AtomicLong;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.shadow.api.Shadow;
/** Unit tests for {@link ShadowChoreographer}. */
@RunWith(AndroidJUnit4.class)
public class ShadowChoreographerTest {
@Test
+ public void isValid() {
+ ShadowPausedChoreographer shadowPausedChoreographer =
+ Shadow.extract(Choreographer.getInstance());
+ assertThat(shadowPausedChoreographer.isInitialized()).isTrue();
+ }
+
+ @Test
public void setPaused_isPaused_doesntRun() {
ShadowChoreographer.setPaused(true);
long startTime = ShadowSystem.nanoTime();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java
index 8437c7a..e49cfcd 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java
@@ -7,6 +7,7 @@
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
+import android.companion.AssociatedDevice;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
@@ -20,6 +21,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
/** Unit test for ShadowCompanionDeviceManager. */
@RunWith(AndroidJUnit4.class)
@@ -127,10 +130,13 @@
MacAddress.fromString(MAC_ADDRESS),
"displayName",
"deviceProfile",
+ /* AssociatedDevice*/ null,
/* selfManaged= */ false,
/* notifyOnDeviceNearby= */ false,
+ /* revoked */ false,
/* timeApprovedMs= */ 0,
- /* lastTimeConnectedMs= */ 0);
+ /* lastTimeConnectedMs= */ 0,
+ /* systemDataSyncFlags= */ -1);
assertThat(companionDeviceManager.getAssociations()).isEmpty();
shadowCompanionDeviceManager.addAssociation(info);
assertThat(companionDeviceManager.getMyAssociations()).contains(info);
@@ -188,6 +194,13 @@
.isSameInstanceAs(callback);
}
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void notifyDeviceAppeared() {
+ ReflectionHelpers.callInstanceMethod(
+ companionDeviceManager, "notifyDeviceAppeared", ClassParameter.from(int.class, 1));
+ }
+
private CompanionDeviceManager.Callback createCallback() {
return new CompanionDeviceManager.Callback() {
@Override
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java
index 2c9452c..6e2e2d0 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java
@@ -8,6 +8,7 @@
import static android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doReturn;
@@ -488,8 +489,25 @@
}
@Test
- public void openOutputStream_shouldReturnAnOutputStream() throws Exception {
- assertThat(contentResolver.openOutputStream(uri21)).isInstanceOf(OutputStream.class);
+ public void openOutputStream_withNoRealOrRegisteredProvider_doesNotThrow() throws Exception {
+ Uri uri = Uri.parse("content://invalidauthority/test/1");
+ assertThat(contentResolver.openOutputStream(uri)).isNotNull();
+ }
+
+ @Test
+ public void openOutputStream_withRealContentProvider_canReadBytesWrittenToOutputStream()
+ throws IOException, RemoteException {
+ Robolectric.setupContentProvider(MyContentProvider.class, AUTHORITY);
+ Uri uri = Uri.parse("content://" + AUTHORITY + "/test/1");
+
+ // Write content through given outputstream
+ try (OutputStream outputStream = contentResolver.openOutputStream(uri)) {
+ outputStream.write("foo".getBytes(UTF_8));
+ }
+
+ // Verify written content can be read back
+ InputStream inputStream = contentResolver.openInputStream(uri);
+ assertThat(new String(inputStream.readAllBytes(), UTF_8)).isEqualTo("foo");
}
@Test
@@ -565,6 +583,98 @@
}
@Test
+ public void openOutputStream_withModeWithNoRealOrRegisteredProvider_throws() {
+ Uri uri = Uri.parse("content://invalidauthority/test/1");
+ assertThrows(FileNotFoundException.class, () -> contentResolver.openOutputStream(uri, "wt"));
+ }
+
+ @Test
+ public void openOutputStream_withModeWithRealContentProvider_canReadBytesWrittenToOutputStream()
+ throws IOException, RemoteException {
+ Robolectric.setupContentProvider(MyContentProvider.class, AUTHORITY);
+ Uri uri = Uri.parse("content://" + AUTHORITY + "/test/1");
+
+ // Write content through given outputstream
+ try (OutputStream outputStream = contentResolver.openOutputStream(uri, "wt")) {
+ outputStream.write("foo".getBytes(UTF_8));
+ }
+
+ // Verify written content can be read back
+ InputStream inputStream = contentResolver.openInputStream(uri);
+ assertThat(new String(inputStream.readAllBytes(), UTF_8)).isEqualTo("foo");
+ }
+
+ @Test
+ public void openOutputStream_withModeShouldReturnRegisteredStream() throws Exception {
+ final Uri uri = Uri.parse("content://registeredProvider/path");
+
+ AtomicInteger callCount = new AtomicInteger();
+ OutputStream outputStream =
+ new OutputStream() {
+
+ @Override
+ public void write(int arg0) throws IOException {
+ callCount.incrementAndGet();
+ }
+
+ @Override
+ public String toString() {
+ return "outputstream for " + uri;
+ }
+ };
+
+ shadowOf(contentResolver).registerOutputStream(uri, outputStream);
+
+ assertThat(callCount.get()).isEqualTo(0);
+ contentResolver.openOutputStream(uri, "wt").write(5);
+ assertThat(callCount.get()).isEqualTo(1);
+ }
+
+ @Test
+ public void openOutputStream_withModeShouldReturnNewStreamFromRegisteredSupplier()
+ throws Exception {
+ final Uri uri = Uri.parse("content://registeredProvider/path");
+
+ AtomicInteger streamCreateCount = new AtomicInteger();
+ shadowOf(contentResolver)
+ .registerOutputStreamSupplier(
+ uri,
+ () -> {
+ streamCreateCount.incrementAndGet();
+ AtomicBoolean isClosed = new AtomicBoolean();
+ isClosed.set(false);
+ OutputStream outputStream =
+ new OutputStream() {
+ @Override
+ public void close() {
+ isClosed.set(true);
+ }
+
+ @Override
+ public void write(int arg0) throws IOException {
+ if (isClosed.get()) {
+ throw new IOException();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "outputstream for " + uri;
+ }
+ };
+ return outputStream;
+ });
+
+ assertThat(streamCreateCount.get()).isEqualTo(0);
+ OutputStream outputStream1 = contentResolver.openOutputStream(uri, "wt");
+ outputStream1.close();
+ assertThat(streamCreateCount.get()).isEqualTo(1);
+
+ contentResolver.openOutputStream(uri, "wt").write(5);
+ assertThat(streamCreateCount.get()).isEqualTo(2);
+ }
+
+ @Test
public void shouldTrackNotifiedUris() {
contentResolver.notifyChange(Uri.parse("foo"), null, true);
contentResolver.notifyChange(Uri.parse("bar"), null);
@@ -1182,7 +1292,7 @@
} catch (IOException e) {
throw new RuntimeException("error creating new file", e);
}
- return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
}
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextTest.java
index 366eeaa..b93dc58 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextTest.java
@@ -2,6 +2,7 @@
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.P;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -292,4 +293,10 @@
assertThat(typedArray.getString(0)).isEqualTo("^q");
assertThat(typedArray.getInt(1, -1234)).isEqualTo(1 /* ungulate */);
}
+
+ @Test
+ @Config(minSdk = P)
+ public void getWifiRttService() {
+ assertThat(context.getSystemService(Context.WIFI_RTT_RANGING_SERVICE)).isNotNull();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java
index 02b149d..03d9d73 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_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.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 @@
}
@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"));
@@ -658,16 +675,25 @@
}
@Test
- public void checkPermissionUidPid() {
- assertThat(contextWrapper.checkPermission("MY_PERMISSON", 1, 1))
+ public void checkPermission_denied() {
+ assertThat(contextWrapper.checkPermission("MY_PERMISSON", /* pid= */ 1, /* uid= */ 1))
.isEqualTo(PackageManager.PERMISSION_DENIED);
+ assertThat(contextWrapper.checkPermission("MY_PERMISSON", /* pid= */ -1, /* uid= */ 1))
+ .isEqualTo(PackageManager.PERMISSION_DENIED);
+ }
+
+ @Test
+ public void checkPermission_granted() {
shadowContextWrapper.grantPermissions(1, 1, "MY_PERMISSON");
- assertThat(contextWrapper.checkPermission("MY_PERMISSON", 2, 1))
+ assertThat(contextWrapper.checkPermission("MY_PERMISSON", /* pid= */ 1, /* uid= */ 1))
+ .isEqualTo(PackageManager.PERMISSION_GRANTED);
+
+ assertThat(contextWrapper.checkPermission("MY_PERMISSON", /* pid= */ 2, /* uid= */ 1))
.isEqualTo(PackageManager.PERMISSION_DENIED);
- assertThat(contextWrapper.checkPermission("MY_PERMISSON", 1, 1))
+ assertThat(contextWrapper.checkPermission("MY_PERMISSON", /* pid= */ -1, /* uid= */ 1))
.isEqualTo(PackageManager.PERMISSION_GRANTED);
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java
index bde0e72..f9721e4 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java
@@ -1,6 +1,6 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static com.google.common.truth.Truth.assertThat;
import android.icu.text.DateFormat;
@@ -12,13 +12,13 @@
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
-import libcore.icu.DateIntervalFormat;
+import android.text.format.DateIntervalFormat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
-@Config(minSdk = M)
+@Config(minSdk = UPSIDE_DOWN_CAKE)
public class ShadowDateIntervalFormatTest {
@Test
public void testDateInterval_FormatDateRange() throws ParseException {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
index 7d36f35..03d8247 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
@@ -818,6 +818,43 @@
}
@Test
+ @Config(minSdk = R)
+ public void getAutoTimeEnabledShouldWorkAsIntendedForDeviceOwner() {
+ // GIVEN the caller is the device owner
+ shadowOf(devicePolicyManager).setDeviceOwner(testComponent);
+
+ // WHEN setAutoTimeEnabled is called with true
+ devicePolicyManager.setAutoTimeEnabled(testComponent, true);
+
+ // THEN getAutoTimeEnabled should return true
+ assertThat(devicePolicyManager.getAutoTimeEnabled(testComponent)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void getAutoTimeEnabledShouldWorkAsIntendedForProfileOwner() {
+ // GIVEN the caller is the profile owner
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+ // WHEN setAutoTimeEnabled is called with false
+ devicePolicyManager.setAutoTimeEnabled(testComponent, false);
+
+ // THEN getAutoTimeEnabled should return false
+ assertThat(devicePolicyManager.getAutoTimeEnabled(testComponent)).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = R)
+ public void getAutoTimeEnabledShouldReturnFalseIfNotSet() {
+ // GIVEN the caller is the device owner
+ shadowOf(devicePolicyManager).setDeviceOwner(testComponent);
+
+ // WHEN setAutoTimeEnabled has not been called
+ // THEN getAutoTimeEnabled should return false
+ assertThat(devicePolicyManager.getAutoTimeEnabled(testComponent)).isFalse();
+ }
+
+ @Test
@Config(minSdk = LOLLIPOP)
public void getAutoTimeRequiredShouldWorkAsIntendedForDeviceOwner() {
// GIVEN the caller is the device owner
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayManagerTest.java
index 5ab3cbd..3e11cb8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayManagerTest.java
@@ -167,10 +167,8 @@
ShadowDisplayManager.removeDisplay(displayId);
- assertThat(events).containsExactly(
- "Added " + displayId,
- "Changed " + displayId,
- "Removed " + displayId);
+ assertThat(events)
+ .containsExactly("Added " + displayId, "Changed " + displayId, "Removed " + displayId);
}
@Test
@@ -187,9 +185,7 @@
assertThat(display.getHeight()).isEqualTo(100);
assertThat(display.getOrientation()).isEqualTo(Surface.ROTATION_90);
- assertThat(events).containsExactly(
- "Added " + displayId,
- "Changed " + displayId);
+ assertThat(events).containsExactly("Added " + displayId, "Changed " + displayId);
}
@Test
@@ -312,36 +308,44 @@
assertThat(shadowOf(instance).getSaturationLevel()).isEqualTo(1.0f);
}
- @Test @Config(minSdk = P)
+ @Test
+ @Config(minSdk = P)
public void setSaturationLevel_setToValueGreaterThanOne_shouldThrow() {
try {
instance.setSaturationLevel(1.1f);
fail("Expected IllegalArgumentException thrown");
- } catch (IllegalArgumentException expected) {}
+ } catch (IllegalArgumentException expected) {
+ }
}
- @Test @Config(minSdk = P)
+ @Test
+ @Config(minSdk = P)
public void setSaturationLevel_setToNegativeValue_shouldThrow() {
try {
instance.setSaturationLevel(-0.1f);
fail("Expected IllegalArgumentException thrown");
- } catch (IllegalArgumentException expected) {}
+ } catch (IllegalArgumentException expected) {
+ }
}
- @Test @Config(minSdk = P)
+ @Test
+ @Config(minSdk = P)
public void setSaturationLevel_setToValueGreaterThanOneViaShadow_shouldThrow() {
try {
shadowOf(instance).setSaturationLevel(1.1f);
fail("Expected IllegalArgumentException thrown");
- } catch (IllegalArgumentException expected) {}
+ } catch (IllegalArgumentException expected) {
+ }
}
- @Test @Config(minSdk = P)
+ @Test
+ @Config(minSdk = P)
public void setSaturationLevel_setToNegativevalueViaShadow_shouldThrow() {
try {
shadowOf(instance).setSaturationLevel(-0.1f);
fail("Expected IllegalArgumentException thrown");
- } catch (IllegalArgumentException expected) {}
+ } catch (IllegalArgumentException expected) {
+ }
}
@Test
@@ -450,6 +454,56 @@
assertThat(instance.getBrightnessEvents()).containsExactlyElementsIn(events);
}
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void setNaturallyPortrait_setPortrait_isRotatedWhenLandscape() {
+ ShadowDisplayManager.setNaturallyPortrait(Display.DEFAULT_DISPLAY, true);
+
+ ShadowDisplayManager.changeDisplay(Display.DEFAULT_DISPLAY, "land");
+
+ assertThat(ShadowDisplay.getDefaultDisplay().getRotation()).isEqualTo(Surface.ROTATION_90);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void setNaturallyPortrait_setPortraitWhenLandscape_isRotated() {
+ ShadowDisplayManager.changeDisplay(Display.DEFAULT_DISPLAY, "land");
+
+ ShadowDisplayManager.setNaturallyPortrait(Display.DEFAULT_DISPLAY, true);
+
+ assertThat(ShadowDisplay.getDefaultDisplay().getRotation()).isEqualTo(Surface.ROTATION_90);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void setNaturallyPortrait_setLandscape_isNotRotatedWhenLandscape() {
+ ShadowDisplayManager.setNaturallyPortrait(Display.DEFAULT_DISPLAY, false);
+
+ ShadowDisplayManager.changeDisplay(Display.DEFAULT_DISPLAY, "land");
+
+ assertThat(ShadowDisplay.getDefaultDisplay().getRotation()).isEqualTo(Surface.ROTATION_0);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void setNaturallyPortrait_setLandscape_isRotatedWhenPortrait() {
+ ShadowDisplayManager.setNaturallyPortrait(Display.DEFAULT_DISPLAY, false);
+
+ ShadowDisplayManager.changeDisplay(Display.DEFAULT_DISPLAY, "port");
+
+ assertThat(ShadowDisplay.getDefaultDisplay().getRotation()).isEqualTo(Surface.ROTATION_90);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void setNaturallyPortrait_setLandscapeWhenLandscape_isNotRotated() {
+ ShadowDisplayManager.changeDisplay(Display.DEFAULT_DISPLAY, "land");
+
+ ShadowDisplayManager.setNaturallyPortrait(Display.DEFAULT_DISPLAY, false);
+
+ assertThat(ShadowDisplay.getDefaultDisplay().getRotation()).isEqualTo(Surface.ROTATION_0);
+ }
+
// because DisplayManagerGlobal don't exist in Jelly Bean,
// and we don't want them resolved as part of the test class.
static class HideFromJB {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java
index e5d5c9d..4da4963 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java
@@ -17,6 +17,11 @@
@Config(minSdk = VERSION_CODES.LOLLIPOP)
public final class ShadowEGL14Test {
@Test
+ public void eglGetCurrentContext() {
+ assertThat(EGL14.eglGetCurrentContext()).isNotNull();
+ }
+
+ @Test
public void eglGetDisplay() {
assertThat(EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)).isNotNull();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowFingerprintManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowFingerprintManagerTest.java
index 964c04a..f0f827f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowFingerprintManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowFingerprintManagerTest.java
@@ -11,6 +11,7 @@
import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.hardware.fingerprint.FingerprintManager.CryptoObject;
+import android.os.Build.VERSION_CODES;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.security.Signature;
@@ -112,4 +113,10 @@
assertThat(manager.isHardwareDetected()).isTrue();
}
+
+ @Test
+ @Config(sdk = VERSION_CODES.S)
+ public void getSensorPropertiesInternal_notNull() {
+ assertThat(manager.getSensorPropertiesInternal()).isNotNull();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
index edff632..47c16b4 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
@@ -5,8 +5,9 @@
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
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.assertEquals;
import static org.junit.Assert.assertThrows;
@@ -18,6 +19,7 @@
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IncrementalStatesInfo;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherActivityInfoInternal;
import android.content.pm.LauncherApps;
@@ -25,6 +27,7 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -39,6 +42,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
@@ -100,12 +104,12 @@
}
@Test
- @Config(minSdk = O, maxSdk = R)
+ @Config(minSdk = O)
public void getShortcutConfigActivityList_getsShortcutsForPackageName() {
LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME, USER_HANDLE);
LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME_2, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME_2, USER_HANDLE);
shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo1);
shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo2);
@@ -114,26 +118,12 @@
}
@Test
- @Config(minSdk = S)
- public void getShortcutConfigActivityListS_getsShortcutsForPackageName() {
- LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME, USER_HANDLE);
- LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME_2, USER_HANDLE);
- shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo1);
- shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo2);
-
- assertThat(launcherApps.getShortcutConfigActivityList(TEST_PACKAGE_NAME, USER_HANDLE))
- .contains(launcherActivityInfo1);
- }
-
- @Test
- @Config(minSdk = O, maxSdk = R)
+ @Config(minSdk = O)
public void getShortcutConfigActivityList_getsShortcutsForUserHandle() {
LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME, USER_HANDLE);
LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME, UserHandle.of(10));
+ createLauncherActivityInfo(TEST_PACKAGE_NAME, UserHandle.of(10));
shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo1);
shadowOf(launcherApps).addShortcutConfigActivity(UserHandle.of(10), launcherActivityInfo2);
@@ -142,45 +132,14 @@
}
@Test
- @Config(minSdk = S)
- public void getShortcutConfigActivityListS_getsShortcutsForUserHandle() {
- LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME, USER_HANDLE);
- LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME, UserHandle.of(10));
- shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo1);
- shadowOf(launcherApps).addShortcutConfigActivity(UserHandle.of(10), launcherActivityInfo2);
-
- assertThat(launcherApps.getShortcutConfigActivityList(TEST_PACKAGE_NAME, UserHandle.of(10)))
- .contains(launcherActivityInfo2);
- }
-
- @Test
- @Config(minSdk = O, maxSdk = R)
+ @Config(minSdk = O)
public void getShortcutConfigActivityList_packageNull_getsShortcutFromAllPackagesForUser() {
LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME, USER_HANDLE);
LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME_2, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME_2, USER_HANDLE);
LauncherActivityInfo launcherActivityInfo3 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME_3, UserHandle.of(10));
- shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo1);
- shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo2);
- shadowOf(launcherApps).addShortcutConfigActivity(UserHandle.of(10), launcherActivityInfo3);
-
- assertThat(launcherApps.getShortcutConfigActivityList(null, USER_HANDLE))
- .containsExactly(launcherActivityInfo1, launcherActivityInfo2);
- }
-
- @Test
- @Config(minSdk = S)
- public void getShortcutConfigActivityListS_packageNull_getsShortcutFromAllPackagesForUser() {
- LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME, USER_HANDLE);
- LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME_2, USER_HANDLE);
- LauncherActivityInfo launcherActivityInfo3 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME_3, UserHandle.of(10));
+ createLauncherActivityInfo(TEST_PACKAGE_NAME_3, UserHandle.of(10));
shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo1);
shadowOf(launcherApps).addShortcutConfigActivity(USER_HANDLE, launcherActivityInfo2);
shadowOf(launcherApps).addShortcutConfigActivity(UserHandle.of(10), launcherActivityInfo3);
@@ -209,12 +168,12 @@
}
@Test
- @Config(minSdk = N, maxSdk = R)
+ @Config(minSdk = N)
public void testGetActivityList() {
assertThat(launcherApps.getActivityList(TEST_PACKAGE_NAME, USER_HANDLE)).isEmpty();
LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME, USER_HANDLE);
shadowOf(launcherApps).addActivity(USER_HANDLE, launcherActivityInfo1);
assertThat(launcherApps.getActivityList(TEST_PACKAGE_NAME, USER_HANDLE))
@@ -222,25 +181,14 @@
}
@Test
- @Config(minSdk = S)
- public void testGetActivityListS() {
- LauncherActivityInfo launcherActivityInfo =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME, USER_HANDLE);
- shadowOf(launcherApps).addActivity(USER_HANDLE, launcherActivityInfo);
-
- assertThat(launcherApps.getActivityList(TEST_PACKAGE_NAME, USER_HANDLE))
- .contains(launcherActivityInfo);
- }
-
- @Test
- @Config(minSdk = N, maxSdk = R)
+ @Config(minSdk = N)
public void testGetActivityList_packageNull_getsActivitiesFromAllPackagesForUser() {
LauncherActivityInfo launcherActivityInfo1 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME, USER_HANDLE);
LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME_2, USER_HANDLE);
+ createLauncherActivityInfo(TEST_PACKAGE_NAME_2, USER_HANDLE);
LauncherActivityInfo launcherActivityInfo3 =
- createLauncherActivityInfoPostN(TEST_PACKAGE_NAME_3, UserHandle.of(10));
+ createLauncherActivityInfo(TEST_PACKAGE_NAME_3, UserHandle.of(10));
shadowOf(launcherApps).addActivity(USER_HANDLE, launcherActivityInfo1);
shadowOf(launcherApps).addActivity(USER_HANDLE, launcherActivityInfo2);
shadowOf(launcherApps).addActivity(UserHandle.of(10), launcherActivityInfo3);
@@ -250,17 +198,17 @@
}
@Test
- @Config(minSdk = S)
- public void testGetActivityListS_getsActivitiesFromAllPackagesForUser() {
- LauncherActivityInfo launcherActivityInfo =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME, USER_HANDLE);
- LauncherActivityInfo launcherActivityInfo2 =
- createLauncherActivityInfoS(TEST_PACKAGE_NAME_2, USER_HANDLE);
- shadowOf(launcherApps).addActivity(USER_HANDLE, launcherActivityInfo);
- shadowOf(launcherApps).addActivity(USER_HANDLE, launcherActivityInfo2);
+ @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();
- assertThat(launcherApps.getActivityList(null, USER_HANDLE))
- .containsExactly(launcherActivityInfo, launcherActivityInfo2);
+ 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
@@ -402,6 +350,35 @@
assertThat(launcherApps.hasShortcutHostPermission()).isFalse();
}
+ @Test
+ @Config(minSdk = P)
+ public void getSuspendedPackageLauncherExtras_returnsBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt("suspended_app", 5);
+ shadowOf(launcherApps)
+ .addSuspendedPackageLauncherExtras(USER_HANDLE, TEST_PACKAGE_NAME_2, bundle);
+
+ assertThat(launcherApps.getSuspendedPackageLauncherExtras(TEST_PACKAGE_NAME_2, USER_HANDLE))
+ .isEqualTo(bundle);
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void getSuspendedPackageLauncherExtras_returnsEmptyBundle() {
+ Throwable throwable =
+ assertThrows(
+ NameNotFoundException.class,
+ () -> launcherApps.getSuspendedPackageLauncherExtras(TEST_PACKAGE_NAME, USER_HANDLE));
+
+ assertThat(throwable)
+ .hasMessageThat()
+ .isEqualTo(
+ "Suspended package extras for "
+ + TEST_PACKAGE_NAME
+ + " not found for user "
+ + USER_HANDLE.getIdentifier());
+ }
+
private List<ShortcutInfo> getPinnedShortcuts(String packageName, ComponentName activity) {
ShortcutQuery query = new ShortcutQuery();
query.setQueryFlags(ShortcutQuery.FLAG_MATCH_DYNAMIC | ShortcutQuery.FLAG_MATCH_PINNED);
@@ -410,31 +387,40 @@
return launcherApps.getShortcuts(query, Process.myUserHandle());
}
- private LauncherActivityInfo createLauncherActivityInfoS(String packageName, UserHandle user) {
- ActivityInfo info = new ActivityInfo();
- info.packageName = packageName;
- info.name = packageName;
- info.nonLocalizedLabel = packageName;
- LauncherActivityInfoInternal launcherActivityInfoInternal =
- new LauncherActivityInfoInternal(info, null);
-
- return ReflectionHelpers.callConstructor(
- LauncherActivityInfo.class,
- ClassParameter.from(Context.class, ApplicationProvider.getApplicationContext()),
- ClassParameter.from(UserHandle.class, user),
- ClassParameter.from(LauncherActivityInfoInternal.class, launcherActivityInfoInternal));
- }
-
- private LauncherActivityInfo createLauncherActivityInfoPostN(
+ private LauncherActivityInfo createLauncherActivityInfo(
String packageName, UserHandle userHandle) {
ActivityInfo info = new ActivityInfo();
info.packageName = packageName;
info.name = packageName;
info.nonLocalizedLabel = packageName;
- return ReflectionHelpers.callConstructor(
- LauncherActivityInfo.class,
- ClassParameter.from(Context.class, ApplicationProvider.getApplicationContext()),
- ClassParameter.from(ActivityInfo.class, info),
- ClassParameter.from(UserHandle.class, userHandle));
+ if (RuntimeEnvironment.getApiLevel() <= R) {
+ return ReflectionHelpers.callConstructor(
+ LauncherActivityInfo.class,
+ ClassParameter.from(Context.class, ApplicationProvider.getApplicationContext()),
+ ClassParameter.from(ActivityInfo.class, info),
+ ClassParameter.from(UserHandle.class, userHandle));
+ } else if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+ LauncherActivityInfoInternal launcherActivityInfoInternal =
+ new LauncherActivityInfoInternal(info, null, userHandle);
+
+ return ReflectionHelpers.callConstructor(
+ LauncherActivityInfo.class,
+ ClassParameter.from(Context.class, ApplicationProvider.getApplicationContext()),
+ ClassParameter.from(UserHandle.class, userHandle),
+ ClassParameter.from(LauncherActivityInfoInternal.class, launcherActivityInfoInternal));
+ } else {
+
+ LauncherActivityInfoInternal launcherActivityInfoInternal =
+ ReflectionHelpers.callConstructor(
+ LauncherActivityInfoInternal.class,
+ ClassParameter.from(ActivityInfo.class, info),
+ ClassParameter.from(IncrementalStatesInfo.class, null),
+ ClassParameter.from(UserHandle.class, userHandle));
+
+ return ReflectionHelpers.callConstructor(
+ LauncherActivityInfo.class,
+ ClassParameter.from(Context.class, ApplicationProvider.getApplicationContext()),
+ ClassParameter.from(LauncherActivityInfoInternal.class, launcherActivityInfoInternal));
+ }
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
index 2299246..772d89a 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
@@ -30,6 +30,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
@@ -37,7 +38,6 @@
/** Tests for {@link ShadowMediaController}. */
@RunWith(AndroidJUnit4.class)
-@Config(maxSdk = Q)
public final class ShadowMediaControllerTest {
private MediaController mediaController;
@@ -48,9 +48,19 @@
public void setUp() {
Context context = ApplicationProvider.getApplicationContext();
ISessionController binder = mock(ISessionController.class);
- MediaSession.Token token =
- ReflectionHelpers.callConstructor(
- MediaSession.Token.class, ClassParameter.from(ISessionController.class, binder));
+
+ MediaSession.Token token = null;
+ if (RuntimeEnvironment.getApiLevel() <= Q) {
+ token =
+ ReflectionHelpers.callConstructor(
+ MediaSession.Token.class, ClassParameter.from(ISessionController.class, binder));
+ } else {
+ token =
+ ReflectionHelpers.callConstructor(
+ MediaSession.Token.class,
+ ClassParameter.from(int.class, 0),
+ ClassParameter.from(ISessionController.class, binder));
+ }
mediaController = new MediaController(context, token);
shadowMediaController = Shadow.extract(mediaController);
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaExtractorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaExtractorTest.java
index 7a9f61a..f6b58a8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaExtractorTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaExtractorTest.java
@@ -1,10 +1,13 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.O;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.media.MediaExtractor;
+import android.media.MediaExtractor.MetricsConstants;
import android.media.MediaFormat;
+import android.os.PersistableBundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -12,6 +15,7 @@
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
import org.robolectric.shadows.util.DataSource;
/** Tests for ShadowMediaExtractor */
@@ -174,4 +178,19 @@
assertThat(mediaExtractor.getTrackCount()).isEqualTo(0);
}
+
+ @Test
+ @Config(minSdk = O)
+ public void getMetrics_returnsMetrics() throws IOException {
+ PersistableBundle metrics = new PersistableBundle();
+ metrics.putString(MetricsConstants.MIME_TYPE, "audio/mp4");
+ ShadowMediaExtractor.setMetrics(dataSource, metrics);
+
+ MediaExtractor mediaExtractor = new MediaExtractor();
+ mediaExtractor.setDataSource(path);
+
+ assertThat(mediaExtractor.getMetrics().size()).isEqualTo(1);
+ assertThat(mediaExtractor.getMetrics().getString(MetricsConstants.MIME_TYPE))
+ .isEqualTo("audio/mp4");
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMetadataRetrieverTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMetadataRetrieverTest.java
index b727f9c..9cbd7df 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMetadataRetrieverTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMetadataRetrieverTest.java
@@ -65,6 +65,14 @@
}
@Test
+ public void getFrameAtTime_withoutTime() {
+ addFrame(path, 1, bitmap);
+ addFrame(path, 2, bitmap2);
+ retriever.setDataSource(path);
+ assertThat(retriever.getFrameAtTime()).isEqualTo(bitmap);
+ }
+
+ @Test
@Config(minSdk = O_MR1)
public void getScaledFrameAtTime_shouldDependOnDataSource() {
addScaledFrame(toDataSource(path), 1, 1024, 768, bitmap);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMuxerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMuxerTest.java
index 894b518..53e3836 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMuxerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaMuxerTest.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.O;
import static com.google.common.truth.Truth.assertThat;
import android.media.MediaCodec;
@@ -9,6 +10,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -40,32 +42,59 @@
@Test
@Config(minSdk = LOLLIPOP)
public void basicMuxingFlow_sameZeroOffset() throws IOException {
- basicMuxingFlow(0, 0, INPUT_SIZE);
+ String tempFilePath =
+ tempDirectory.create("dir").resolve(UUID.randomUUID().toString()).toString();
+ MediaMuxer muxer = new MediaMuxer(tempFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+ basicMuxingFlow(muxer, tempFilePath, 0, 0, INPUT_SIZE);
}
@Test
@Config(minSdk = LOLLIPOP)
public void basicMuxingFlow_sameNonZeroOffset() throws IOException {
- basicMuxingFlow(10, 10, INPUT_SIZE);
+ String tempFilePath =
+ tempDirectory.create("dir").resolve(UUID.randomUUID().toString()).toString();
+ MediaMuxer muxer = new MediaMuxer(tempFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+ basicMuxingFlow(muxer, tempFilePath, 10, 10, INPUT_SIZE);
}
@Test
@Config(minSdk = LOLLIPOP)
public void basicMuxingFlow_nonSameButSmallerOffset() throws IOException {
- basicMuxingFlow(0, 10, INPUT_SIZE);
+ String tempFilePath =
+ tempDirectory.create("dir").resolve(UUID.randomUUID().toString()).toString();
+ MediaMuxer muxer = new MediaMuxer(tempFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+ basicMuxingFlow(muxer, tempFilePath, 0, 10, INPUT_SIZE);
}
@Test
@Config(minSdk = LOLLIPOP)
public void basicMuxingFlow_nonSameButLargerOffset() throws IOException {
- basicMuxingFlow(10, 0, INPUT_SIZE);
- }
-
- private void basicMuxingFlow(int bufInfoOffset, int bufOffset, int inputSize) throws IOException {
String tempFilePath =
tempDirectory.create("dir").resolve(UUID.randomUUID().toString()).toString();
MediaMuxer muxer = new MediaMuxer(tempFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+ basicMuxingFlow(muxer, tempFilePath, 10, 0, INPUT_SIZE);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void basicMuxingFlow_fileDescriptorConstructor_sameZeroOffset() throws IOException {
+ String tempFilePath =
+ tempDirectory.create("dir").resolve(UUID.randomUUID().toString()).toString();
+ try (FileOutputStream outputStream = new FileOutputStream(tempFilePath)) {
+ MediaMuxer muxer =
+ new MediaMuxer(outputStream.getFD(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+ basicMuxingFlow(muxer, tempFilePath, 0, 0, INPUT_SIZE);
+ }
+ }
+
+ private void basicMuxingFlow(
+ MediaMuxer muxer, String tempFilePath, int bufInfoOffset, int bufOffset, int inputSize)
+ throws IOException {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
index a5cf073..d913c99 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
@@ -212,6 +212,6 @@
}
private static Tag createMockTag() {
- return Tag.createMockTag(new byte[0], new int[0], new Bundle[0]);
+ return Tag.createMockTag(new byte[0], new int[0], new Bundle[0], 0L);
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
index 1295733..81506f3 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
@@ -16,6 +16,7 @@
import static android.content.pm.ApplicationInfo.FLAG_VM_SAFE_MODE;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID;
import static android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT;
import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED;
import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED;
@@ -30,6 +31,7 @@
import static android.content.pm.PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
import static android.content.pm.PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
import static android.content.pm.PackageManager.VERIFICATION_ALLOW;
+import static android.content.pm.PackageManager.VERIFICATION_REJECT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
@@ -48,6 +50,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -62,6 +65,7 @@
import android.app.Activity;
import android.app.Application;
import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -78,6 +82,7 @@
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.content.pm.PackageManager.ComponentEnabledSetting;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.OnPermissionsChangedListener;
import android.content.pm.PackageManager.PackageInfoFlags;
@@ -127,6 +132,7 @@
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
import org.robolectric.R;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@@ -153,6 +159,7 @@
private static final String REAL_TEST_APP_PACKAGE_NAME = "org.robolectric.exampleapp";
private static final String TEST_PACKAGE3_NAME = "com.a.third.package";
private static final int TEST_PACKAGE_VERSION_CODE = 10000;
+ public static final int INSTALL_VERIFICATION_ID = 1234;
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
private Context context;
@@ -2693,10 +2700,76 @@
@Test
@Config(minSdk = JELLY_BEAN_MR1)
- public void extendPendingInstallTimeout() {
- packageManager.extendVerificationTimeout(1234, 0, 1000);
+ public void
+ extendPendingInstallTimeout_verificationRejectAtTimeout_extendsPendingInstallTimeoutAndsetsCodeAtTimeoutToReject() {
+ packageManager.extendVerificationTimeout(
+ INSTALL_VERIFICATION_ID,
+ /* verificationCodeAtTimeout= */ VERIFICATION_REJECT,
+ /* millisecondsToDelay= */ 1000);
- assertThat(shadowOf(packageManager).getVerificationExtendedTimeout(1234)).isEqualTo(1000);
+ assertThat(shadowOf(packageManager).getVerificationExtendedTimeout(INSTALL_VERIFICATION_ID))
+ .isEqualTo(1000);
+ assertThat(
+ shadowOf(packageManager).getVerificationCodeAtTimeoutExtension(INSTALL_VERIFICATION_ID))
+ .isEqualTo(VERIFICATION_REJECT);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void
+ extendPendingInstallTimeout_verificationAllowAtTimeout_extendsPendingInstallTimeoutAndsetsCodeAtTimeoutToAllow() {
+ packageManager.extendVerificationTimeout(
+ INSTALL_VERIFICATION_ID,
+ /* verificationCodeAtTimeout= */ VERIFICATION_ALLOW,
+ /* millisecondsToDelay= */ 1000);
+
+ assertThat(shadowOf(packageManager).getVerificationExtendedTimeout(INSTALL_VERIFICATION_ID))
+ .isEqualTo(1000);
+ assertThat(
+ shadowOf(packageManager).getVerificationCodeAtTimeoutExtension(INSTALL_VERIFICATION_ID))
+ .isEqualTo(VERIFICATION_ALLOW);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void whenVerificationTimeOutNotExtended_verificationCodeAtTimeoutIsAllow() {
+ assertThat(shadowOf(packageManager).getVerificationExtendedTimeout(INSTALL_VERIFICATION_ID))
+ .isEqualTo(0);
+ // Default verdict is to allow installation.
+ assertThat(
+ shadowOf(packageManager).getVerificationCodeAtTimeoutExtension(INSTALL_VERIFICATION_ID))
+ .isEqualTo(VERIFICATION_ALLOW);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1)
+ public void triggerInstallVerificationTimeout_broadcastsPackageVerifiedIntent() {
+ ShadowPackageManager shadowPackageManagerMock =
+ mock(ShadowPackageManager.class, Mockito.CALLS_REAL_METHODS);
+
+ doReturn(VERIFICATION_REJECT)
+ .when(shadowPackageManagerMock)
+ .getVerificationCodeAtTimeoutExtension(INSTALL_VERIFICATION_ID);
+
+ List<Integer> verificationIdList = new ArrayList<>();
+ List<Integer> verificationResultList = new ArrayList<>();
+ BroadcastReceiver receiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ verificationIdList.add(intent.getIntExtra(EXTRA_VERIFICATION_ID, 0));
+ verificationResultList.add(
+ intent.getIntExtra(PackageManager.EXTRA_VERIFICATION_RESULT, 0));
+ }
+ };
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_PACKAGE_VERIFIED);
+ context.registerReceiver(receiver, intentFilter);
+
+ shadowPackageManagerMock.triggerInstallVerificationTimeout(
+ (Application) context, INSTALL_VERIFICATION_ID);
+
+ assertThat(verificationIdList).containsExactly(INSTALL_VERIFICATION_ID);
+ assertThat(verificationResultList).containsExactly(VERIFICATION_REJECT);
}
@Test
@@ -4402,6 +4475,93 @@
assertThat(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)).isFalse();
}
+ @Test
+ @Config(minSdk = S)
+ public void getProperty() throws NameNotFoundException {
+ assertThrows(
+ NameNotFoundException.class,
+ () ->
+ packageManager.getProperty(
+ "myproperty", RuntimeEnvironment.getApplication().getPackageName()));
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void getProperty_component() throws NameNotFoundException {
+ final ComponentName componentName =
+ new ComponentName(RuntimeEnvironment.getApplication().getPackageName(), "mycomponentname");
+ assertThrows(
+ NameNotFoundException.class, () -> packageManager.getProperty("myproperty", componentName));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setComponentEnabledSettingsSameSettings_getComponentEnabledSettingsConsistent() {
+ ShadowApplicationPackageManager pm = (ShadowApplicationPackageManager) shadowOf(packageManager);
+ ComponentName componentName0 = new ComponentName(context, "mycomponentname0");
+ ComponentName componentName1 = new ComponentName(context, "mycomponentname1");
+ ComponentEnabledSetting componentEnabledSetting0 =
+ new ComponentEnabledSetting(
+ componentName0,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ ComponentEnabledSetting componentEnabledSetting1 =
+ new ComponentEnabledSetting(
+ componentName1,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ ImmutableList<ComponentEnabledSetting> componentEnabledSettings =
+ ImmutableList.of(componentEnabledSetting0, componentEnabledSetting1);
+
+ pm.setComponentEnabledSettings(componentEnabledSettings);
+
+ for (ComponentEnabledSetting setting : componentEnabledSettings) {
+ assertThat(pm.getComponentEnabledSetting(setting.getComponentName()))
+ .isEqualTo(setting.getEnabledState());
+ }
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setComponentEnabledSettingsDifferentSettings_getComponentEnabledSettingsConsistent() {
+ ShadowApplicationPackageManager pm = (ShadowApplicationPackageManager) shadowOf(packageManager);
+ ComponentName componentName0 = new ComponentName(context, "mycomponentname0");
+ ComponentName componentName1 = new ComponentName(context, "mycomponentname1");
+ ComponentEnabledSetting componentEnabledSetting0 =
+ new ComponentEnabledSetting(
+ componentName0,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ ComponentEnabledSetting componentEnabledSetting1 =
+ new ComponentEnabledSetting(
+ componentName1,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ ImmutableList<ComponentEnabledSetting> componentEnabledSettings =
+ ImmutableList.of(componentEnabledSetting0, componentEnabledSetting1);
+
+ pm.setComponentEnabledSettings(componentEnabledSettings);
+
+ for (ComponentEnabledSetting setting : componentEnabledSettings) {
+ assertThat(pm.getComponentEnabledSetting(setting.getComponentName()))
+ .isEqualTo(setting.getEnabledState());
+ }
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void setSyntheticAppDetailsActivityEnabled_getConsistent() {
+ ShadowApplicationPackageManager pm = (ShadowApplicationPackageManager) shadowOf(packageManager);
+
+ pm.setSyntheticAppDetailsActivityEnabled(TEST_PACKAGE_NAME, true);
+ boolean before = pm.getSyntheticAppDetailsActivityEnabled(TEST_PACKAGE_NAME);
+ pm.setSyntheticAppDetailsActivityEnabled(TEST_PACKAGE_NAME, false);
+ boolean after = pm.getSyntheticAppDetailsActivityEnabled(TEST_PACKAGE_NAME);
+
+ assertThat(before).isTrue();
+ assertThat(after).isFalse();
+ }
+
public String[] setPackagesSuspended(
String[] packageNames,
boolean suspended,
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
index 6ae57f9..7744b95 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 @@
}
@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 8801f1e..71e85c9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
@@ -14,6 +14,7 @@
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;
@@ -33,6 +34,7 @@
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;
@@ -621,6 +623,43 @@
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/ShadowPendingIntentTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java
index 7d50e6f..02219fe 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java
@@ -934,4 +934,30 @@
.toString())
.startsWith("PendingIntent");
}
+
+ @Test
+ public void isTargetedToPackage_returnsFalse() {
+ Intent embedded = new Intent().setComponent(new ComponentName("pkg", "cls")).setPackage("pkg");
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, embedded, 0);
+
+ assertThat(pendingIntent.isTargetedToPackage()).isFalse();
+ }
+
+ @Test
+ public void isTargetedToPackage_returnsTrue_whenOnlyComponentSet() {
+ Intent embedded = new Intent().setComponent(new ComponentName("pkg", "cls"));
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, embedded, 0);
+
+ // This is weird but the implementation checks both component and package and this method should
+ // really be named `isNotExplicitIntentWithPackageSet`.
+ assertThat(pendingIntent.isTargetedToPackage()).isTrue();
+ }
+
+ @Test
+ public void isTargetedToPackage_returnsTrue() {
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, new Intent("ACTION!"), 0);
+
+ // This is weird as well. `isTargetedToPackage` is really `isNotExplicitIntent`.
+ assertThat(pendingIntent.isTargetedToPackage()).isTrue();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPreferenceGroupTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPreferenceGroupTest.java
index f21c21f..f70bcd2 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPreferenceGroupTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPreferenceGroupTest.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.KITKAT;
import static com.google.common.truth.Truth.assertThat;
import static org.robolectric.Robolectric.buildActivity;
import static org.robolectric.Shadows.shadowOf;
@@ -15,6 +16,10 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
@RunWith(AndroidJUnit4.class)
public class ShadowPreferenceGroupTest {
@@ -32,7 +37,7 @@
group = new TestPreferenceGroup(activity, attrs);
shadow = shadowOf(group);
- shadow.callOnAttachedToHierarchy(new PreferenceManager(activity, 0));
+ shadow.callOnAttachedToHierarchy(newPreferenceManager(activity, 0));
pref1 = new Preference(activity);
pref1.setKey("pref1");
@@ -41,6 +46,18 @@
pref2.setKey("pref2");
}
+ private static PreferenceManager newPreferenceManager(Activity activity, int firstRequestCode) {
+ if (RuntimeEnvironment.getApiLevel() >= KITKAT) {
+ return new PreferenceManager(activity, 0);
+ } else {
+ // Constructor was not public before KITKAT.
+ return ReflectionHelpers.callConstructor(
+ PreferenceManager.class,
+ ClassParameter.from(Activity.class, activity),
+ ClassParameter.from(int.class, 0));
+ }
+ }
+
@Test
public void shouldInheritFromPreference() {
assertThat(shadow).isInstanceOf(ShadowPreference.class);
@@ -136,6 +153,7 @@
assertThat(group.findPreference(pref2.getKey())).isSameInstanceAs(pref2);
}
+ @Config(minSdk = KITKAT)
@Test
public void shouldFindPreferenceRecursively() {
TestPreferenceGroup group2 = new TestPreferenceGroup(activity, attrs);
@@ -148,6 +166,7 @@
assertThat(group.findPreference(pref2.getKey())).isSameInstanceAs(pref2);
}
+ @Config(minSdk = KITKAT)
@Test
public void shouldSetEnabledRecursively() {
boolean[] values = {false, true};
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java
index da34401..a4732b9 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 @@
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 @@
@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 @@
@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 323511f..878777c 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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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/ShadowScrollViewTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowScrollViewTest.java
index 1b0970b..536e6dc 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowScrollViewTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowScrollViewTest.java
@@ -36,7 +36,7 @@
@Test
public void realCode_shouldSmoothScrollTo() {
try {
- System.setProperty("robolectric.nativeruntime.enableGraphics", "true");
+ System.setProperty("robolectric.useRealScrolling", "true");
Activity activity = Robolectric.setupActivity(Activity.class);
ScrollView scrollView = new ScrollView(activity);
View view = new View(activity);
@@ -51,14 +51,14 @@
assertEquals(7, scrollView.getScrollX());
assertEquals(6, scrollView.getScrollY());
} finally {
- System.clearProperty("robolectric.nativeruntime.enableGraphics");
+ System.clearProperty("robolectric.useRealScrolling");
}
}
@Test
public void realCode_shouldSmoothScrollBy() {
try {
- System.setProperty("robolectric.nativeruntime.enableGraphics", "true");
+ System.setProperty("robolectric.useRealScrolling", "true");
Activity activity = Robolectric.setupActivity(Activity.class);
ScrollView scrollView = new ScrollView(activity);
View view = new View(activity);
@@ -74,7 +74,7 @@
assertEquals(17, scrollView.getScrollX());
assertEquals(26, scrollView.getScrollY());
} finally {
- System.clearProperty("robolectric.nativeruntime.enableGraphics");
+ System.clearProperty("robolectric.useRealScrolling");
}
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowServiceManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowServiceManagerTest.java
index cf1bf5b..68778c5 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowServiceManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowServiceManagerTest.java
@@ -4,6 +4,7 @@
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
+import android.os.Build.VERSION_CODES;
import android.os.IBinder;
import android.os.ServiceManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -13,17 +14,24 @@
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
/** Tests for {@link ShadowServiceManager}. */
@RunWith(AndroidJUnit4.class)
public final class ShadowServiceManagerTest {
-
+
@Test
public void getService_available_shouldReturnNonNull() {
assertThat(ServiceManager.getService(Context.INPUT_METHOD_SERVICE)).isNotNull();
}
@Test
+ @Config(sdk = VERSION_CODES.S)
+ public void getSensorPrivacyService_notNull() {
+ assertThat(ServiceManager.getService(Context.SENSOR_PRIVACY_SERVICE)).isNotNull();
+ }
+
+ @Test
public void getService_unavailableService_shouldReturnNull() {
ShadowServiceManager.setServiceAvailability(Context.INPUT_METHOD_SERVICE, false);
assertThat(ServiceManager.getService(Context.INPUT_METHOD_SERVICE)).isNull();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSoundTriggerManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSoundTriggerManagerTest.java
new file mode 100644
index 0000000..a5c8a61
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSoundTriggerManagerTest.java
@@ -0,0 +1,80 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.robolectric.shadow.api.Shadow.extract;
+
+import android.hardware.soundtrigger.SoundTrigger;
+import android.media.soundtrigger.SoundTriggerManager;
+import android.os.Build.VERSION_CODES;
+import androidx.test.core.app.ApplicationProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = VERSION_CODES.N)
+public final class ShadowSoundTriggerManagerTest {
+ private ShadowSoundTriggerManager instance;
+
+ @Before
+ public void setUp() {
+ instance =
+ extract(
+ ApplicationProvider.getApplicationContext()
+ .getSystemService(SoundTriggerManager.class));
+ }
+
+ @Config(sdk = VERSION_CODES.S)
+ @Test
+ public void getModuleProperties_nullModuleProperties() throws Exception {
+ SoundTrigger.ModuleProperties moduleProperties = instance.getModuleProperties();
+ assertThat(moduleProperties).isNull();
+ }
+
+ @Config(sdk = VERSION_CODES.R)
+ @Test
+ public void getModuleProperties_nullPointExceptionAtAndroidR() throws Exception {
+ try {
+ SoundTrigger.ModuleProperties unused = instance.getModuleProperties();
+ fail("Expect NullPointException");
+ } catch (NullPointerException e) {
+ assertThat(e).isNotNull();
+ }
+ }
+
+ @Config(sdk = VERSION_CODES.R)
+ @Test
+ public void getModuleProperties_nonNullProperties() throws Exception {
+ instance.setModuleProperties(
+ (SoundTrigger.ModuleProperties) getModuleProperties("supportedModelArch", 1234));
+ SoundTrigger.ModuleProperties moduleProperties = instance.getModuleProperties();
+ assertThat(moduleProperties).isNotNull();
+ assertThat(moduleProperties.getSupportedModelArch()).isEqualTo("supportedModelArch");
+ assertThat(moduleProperties.getVersion()).isEqualTo(1234);
+ }
+
+ // Construct a dummuy {@code SoundTrigger.ModuleProperties}. Return Object because {@code
+ // SoundTrigger.ModuleProperties} is not exist in public Android SDK.
+ private Object getModuleProperties(String supportedModelArch, int version) {
+ return new SoundTrigger.ModuleProperties(
+ /* id= */ 0,
+ /* implementor= */ "implementor",
+ /* description= */ "description",
+ /* uuid= */ "11111111-1111-1111-1111-111111111111",
+ version,
+ supportedModelArch,
+ /* maxSoundModels= */ 0,
+ /* maxKeyphrases= */ 0,
+ /* maxUsers= */ 0,
+ /* recognitionModes= */ 0,
+ /* supportsCaptureTransition= */ false,
+ /* maxBufferMs= */ 0,
+ /* supportsConcurrentCapture= */ false,
+ /* powerConsumptionMw= */ 0,
+ /* returnsTriggerInEvent= */ false,
+ /* audioCapabilities= */ 0);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
index e3b48f0..a527ec9 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 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 @@
}
@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 @@
}
@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();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
index f6b0995..ad3adeb 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_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.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;
@@ -1101,4 +1105,36 @@
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 7c9bfaf..9845a59 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_MR1;
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.fail;
@@ -196,13 +197,9 @@
shadowOf(userManager)
.addProfile(
0, PROFILE_USER_HANDLE, PROFILE_USER_NAME, ShadowUserManager.FLAG_MANAGED_PROFILE);
-
assertThat(userManager.isManagedProfile()).isFalse();
- Application application = ApplicationProvider.getApplicationContext();
- ShadowContextImpl shadowContext = Shadow.extract(application.getBaseContext());
- shadowContext.setUserId(PROFILE_USER_HANDLE);
-
+ setUserIdInContext(PROFILE_USER_HANDLE);
assertThat(userManager.isManagedProfile()).isTrue();
}
@@ -216,6 +213,35 @@
}
@Test
+ @Config(minSdk = S)
+ public void isCloneProfile_withSetter() {
+ shadowOf(userManager).setCloneProfile(false);
+ assertThat(userManager.isCloneProfile()).isFalse();
+
+ shadowOf(userManager).setCloneProfile(true);
+ assertThat(userManager.isCloneProfile()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void isCloneProfile_usesContextUser() {
+ assertThat(userManager.isCloneProfile()).isFalse();
+
+ UserInfo profileUserInfo =
+ new UserInfo(
+ PROFILE_USER_HANDLE,
+ PROFILE_USER_NAME,
+ /* iconPath= */ null,
+ /* profileFlags= */ 0,
+ UserManager.USER_TYPE_PROFILE_CLONE);
+ shadowOf(userManager).addProfile(0, PROFILE_USER_HANDLE, profileUserInfo);
+ assertThat(userManager.isCloneProfile()).isFalse();
+
+ setUserIdInContext(PROFILE_USER_HANDLE);
+ assertThat(userManager.isCloneProfile()).isTrue();
+ }
+
+ @Test
@Config(minSdk = R)
public void isProfile_fullUser_returnsFalse() {
assertThat(userManager.isProfile()).isFalse();
@@ -230,10 +256,7 @@
userManager.createProfile(PROFILE_USER_NAME, UserManager.USER_TYPE_PROFILE_MANAGED, null);
assertThat(userManager.isProfile()).isFalse();
- Application application = ApplicationProvider.getApplicationContext();
- ShadowContextImpl shadowContext = Shadow.extract(application.getBaseContext());
- shadowContext.setUserId(profileHandle.getIdentifier());
-
+ setUserIdInContext(profileHandle.getIdentifier());
assertThat(userManager.isProfile()).isTrue();
}
@@ -750,9 +773,7 @@
shadowOf(userManager).setMaxSupportedUsers(2);
userManager.createProfile(PROFILE_USER_NAME, UserManager.USER_TYPE_PROFILE_MANAGED, null);
- Application application = ApplicationProvider.getApplicationContext();
- ShadowContextImpl shadowContext = Shadow.extract(application.getBaseContext());
- shadowContext.setUserId(ShadowUserManager.DEFAULT_SECONDARY_USER_ID);
+ setUserIdInContext(ShadowUserManager.DEFAULT_SECONDARY_USER_ID);
assertThat(userManager.getUserName()).isEqualTo(PROFILE_USER_NAME);
}
@@ -851,13 +872,11 @@
userManager.setUserName("new user name");
- Application application = ApplicationProvider.getApplicationContext();
- ShadowContextImpl shadowContext = Shadow.extract(application.getBaseContext());
- shadowContext.setUserId(PROFILE_USER_HANDLE);
+ setUserIdInContext(PROFILE_USER_HANDLE);
userManager.setUserName("new profile name");
assertThat(userManager.getUserName()).isEqualTo("new profile name");
- shadowContext.setUserId(TEST_USER_HANDLE);
+ setUserIdInContext(TEST_USER_HANDLE);
assertThat(userManager.getUserName()).isEqualTo("new user name");
}
@@ -870,10 +889,7 @@
userManager.createProfile(PROFILE_USER_NAME, UserManager.USER_TYPE_PROFILE_MANAGED, null);
assertThat(userManager.isUserOfType(UserManager.USER_TYPE_PROFILE_MANAGED)).isFalse();
- Application application = ApplicationProvider.getApplicationContext();
- ShadowContextImpl shadowContext = Shadow.extract(application.getBaseContext());
- shadowContext.setUserId(newUser.getIdentifier());
-
+ setUserIdInContext(newUser.getIdentifier());
assertThat(userManager.isUserOfType(UserManager.USER_TYPE_PROFILE_MANAGED)).isTrue();
}
@@ -1098,6 +1114,18 @@
assertThat(receivedHandle.get()).isNull();
}
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void someUserHasAccount() {
+ assertThat(userManager.someUserHasAccount(SEED_ACCOUNT_NAME, SEED_ACCOUNT_TYPE)).isFalse();
+
+ shadowOf(userManager).setSomeUserHasAccount(SEED_ACCOUNT_NAME, SEED_ACCOUNT_TYPE);
+ assertThat(userManager.someUserHasAccount(SEED_ACCOUNT_NAME, SEED_ACCOUNT_TYPE)).isTrue();
+
+ shadowOf(userManager).removeSomeUserHasAccount(SEED_ACCOUNT_NAME, SEED_ACCOUNT_TYPE);
+ assertThat(userManager.someUserHasAccount(SEED_ACCOUNT_NAME, SEED_ACCOUNT_TYPE)).isFalse();
+ }
+
// Create user handle from parcel since UserHandle.of() was only added in later APIs.
private static UserHandle newUserHandle(int uid) {
Parcel userParcel = Parcel.obtain();
@@ -1106,6 +1134,12 @@
return new UserHandle(userParcel);
}
+ private static void setUserIdInContext(int userId) {
+ Application application = ApplicationProvider.getApplicationContext();
+ ShadowContextImpl shadowContext = Shadow.extract(application.getBaseContext());
+ shadowContext.setUserId(userId);
+ }
+
private static void setPermissions(String... permissions) {
Application context = ApplicationProvider.getApplicationContext();
PackageInfo packageInfo =
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java
index b8b5785..ef5527a 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.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 @@
@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 @@
.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 @@
.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/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index 15dd9ec..fc5a5e5 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -33,6 +33,7 @@
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.AddNetworkResult;
import android.net.wifi.WifiManager.MulticastLock;
import android.net.wifi.WifiManager.PnoScanResultsCallback;
import android.net.wifi.WifiSsid;
@@ -51,6 +52,7 @@
import org.mockito.ArgumentCaptor;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
@RunWith(AndroidJUnit4.class)
public class ShadowWifiManagerTest {
@@ -68,11 +70,19 @@
@Test
public void setWifiInfo_shouldUpdateWifiInfo() {
- WifiInfo wifiInfo = new WifiInfo();
+ WifiInfo wifiInfo = newWifiInfo();
shadowOf(wifiManager).setConnectionInfo(wifiInfo);
assertThat(wifiManager.getConnectionInfo()).isSameInstanceAs(wifiInfo);
}
+ private static WifiInfo newWifiInfo() {
+ if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) {
+ return new WifiInfo();
+ } else {
+ return ReflectionHelpers.callConstructor(WifiInfo.class);
+ }
+ }
+
@Test
public void setWifiEnabled_shouldThrowSecurityExceptionWhenAccessWifiStatePermissionNotGranted() {
shadowOf(wifiManager).setAccessWifiStatePermission(false);
@@ -263,6 +273,87 @@
}
@Test
+ @Config(minSdk = S)
+ public void addNetworkPrivileged_nullConfig_shouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> wifiManager.addNetworkPrivileged(null));
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void addNetworkPrivileged_nonNullConfig_shouldAddNetworkSuccessfully() {
+ WifiConfiguration wifiConfiguration = new WifiConfiguration();
+
+ AddNetworkResult addNetworkResult = wifiManager.addNetworkPrivileged(wifiConfiguration);
+
+ assertThat(addNetworkResult).isNotNull();
+ assertThat(addNetworkResult.statusCode).isEqualTo(AddNetworkResult.STATUS_SUCCESS);
+ assertThat(wifiManager.getConfiguredNetworks()).hasSize(1);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ getCallerConfiguredNetworks_noAccessWifiStatePermission_shouldThrowSecurityException() {
+ shadowOf(wifiManager).setAccessWifiStatePermission(false);
+
+ assertThrows(SecurityException.class, () -> wifiManager.getCallerConfiguredNetworks());
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void getCallerConfiguredNetworks_noNetworksConfigured_returnsEmptyList() {
+ assertThat(wifiManager.getCallerConfiguredNetworks()).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void getCallerConfiguredNetworks_networksAddedAndRemoved_returnsConfiguredNetworks() {
+ WifiConfiguration wifiConfiguration = new WifiConfiguration();
+ wifiManager.addNetwork(wifiConfiguration);
+
+ assertThat(wifiManager.getCallerConfiguredNetworks()).hasSize(1);
+
+ wifiManager.removeNetwork(0);
+
+ assertThat(wifiManager.getCallerConfiguredNetworks()).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ removeNonCallerConfiguredNetworks_noChangeWifiStatePermission_shouldThrowSecurityException() {
+ setDeviceOwner();
+ shadowOf(wifiManager).setChangeWifiStatePermission(false);
+
+ assertThrows(SecurityException.class, () -> wifiManager.removeNonCallerConfiguredNetworks());
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void removeNonCallerConfiguredNetworks_notDeviceOwner_shouldThrowSecurityException() {
+ assertThrows(SecurityException.class, () -> wifiManager.removeNonCallerConfiguredNetworks());
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void removeNonCallerConfiguredNetworks_noConfiguredNetworks_returnsFalse() {
+ setDeviceOwner();
+
+ assertThat(wifiManager.removeNonCallerConfiguredNetworks()).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void removeNonCallerConfiguredNetworks_hasConfiguredNetworks_removesConfiguredNetworks() {
+ setDeviceOwner();
+ wifiManager.addNetwork(new WifiConfiguration());
+ wifiManager.addNetwork(new WifiConfiguration());
+
+ assertThat(wifiManager.removeNonCallerConfiguredNetworks()).isTrue();
+ assertThat(wifiManager.getConfiguredNetworks()).isEmpty();
+ }
+
+ @Test
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
public void getPrivilegedConfiguredNetworks_shouldReturnConfiguredNetworks() {
WifiConfiguration wifiConfiguration = new WifiConfiguration();
diff --git a/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java b/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java
index 3b06a4e..612fd3b 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 @@
@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 0000000..cbda17e
--- /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 64accd7..358b027 100644
--- a/sandbox/build.gradle
+++ b/sandbox/build.gradle
@@ -5,24 +5,24 @@
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/config/AndroidConfigurer.java b/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
index ce81f2a..c03b882 100644
--- a/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
+++ b/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
@@ -98,6 +98,11 @@
builder.addClassNameTranslation("sun.misc.Cleaner", "java.lang.ref.Cleaner$Cleanable");
}
+ // Don't acquire legacy support packages.
+ builder
+ .doNotInstrumentPackage("android.support.constraint.")
+ .doNotInstrumentPackage("android.support.v7.view.");
+
// Instrumenting these classes causes a weird failure.
builder.doNotInstrumentClass("android.R").doNotInstrumentClass("android.R$styleable");
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 e1463a1..53d4e57 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -379,6 +379,7 @@
* @param method the constructor to instrument
*/
protected void instrumentConstructor(MutableClass mutableClass, MethodNode method) {
+ int methodAccess = method.access;
makeMethodPrivate(method);
InsnList callSuper = extractCallToSuperConstructor(mutableClass, method);
@@ -388,8 +389,7 @@
String[] exceptions = exceptionArray(method);
MethodNode initMethodNode =
- new MethodNode(method.access, "<init>", method.desc, method.signature, exceptions);
- makeMethodPublic(initMethodNode);
+ new MethodNode(methodAccess, "<init>", method.desc, method.signature, exceptions);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
initMethodNode.instructions.add(callSuper);
generator.loadThis();
@@ -682,12 +682,6 @@
(clazz.access | Opcodes.ACC_PUBLIC) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
}
- /** Replaces protected and private method modifiers with public. */
- protected void makeMethodPublic(MethodNode method) {
- method.access =
- (method.access | Opcodes.ACC_PUBLIC) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
- }
-
/** Replaces protected and public class modifiers with private. */
protected void makeMethodPrivate(MethodNode method) {
method.access =
diff --git a/sandbox/src/test/java/org/robolectric/internal/bytecode/SandboxClassLoaderTest.java b/sandbox/src/test/java/org/robolectric/internal/bytecode/SandboxClassLoaderTest.java
index 016e883..5c9411c 100644
--- a/sandbox/src/test/java/org/robolectric/internal/bytecode/SandboxClassLoaderTest.java
+++ b/sandbox/src/test/java/org/robolectric/internal/bytecode/SandboxClassLoaderTest.java
@@ -103,7 +103,7 @@
public void shouldDelegateToHandlerForConstructors() throws Exception {
Class<?> clazz = loadClass(AClassWithNoDefaultConstructor.class);
Constructor<?> ctor = clazz.getDeclaredConstructor(String.class);
- assertTrue(Modifier.isPublic(ctor.getModifiers()));
+ assertThat(Modifier.isPublic(ctor.getModifiers())).isFalse();
ctor.setAccessible(true);
Object instance = ctor.newInstance("new one");
assertThat(transcript)
diff --git a/settings.gradle b/settings.gradle
index 20f8fae..1894b8c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -16,6 +16,7 @@
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 f63d048..3f0064f 100644
--- a/shadowapi/build.gradle
+++ b/shadowapi/build.gradle
@@ -5,11 +5,11 @@
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 eaaee1a..8ae6399 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.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 @@
* <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 @@
* @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 @@
* @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 @@
}
/**
+ * 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 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 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 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 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 56c489d..e5f281b 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 @@
}
@Test
- public void callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() {
+ public void
+ callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() {
ExampleDescendant example = new ExampleDescendant();
int returnNumber =
ReflectionHelpers.callInstanceMethod(
@@ -282,23 +283,35 @@
}
@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 @@
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 a273d5a..a2230b0 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -15,6 +15,8 @@
sqlite4java
}
+def sqlite4javaVersion = libs.versions.sqlite4java.get()
+
task copySqliteNatives(type: Copy) {
from project.configurations.sqlite4java {
include '**/*.dll'
@@ -45,22 +47,20 @@
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
old mode 100755
new mode 100644
index 33276d9..0385636
--- a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
+++ b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
@@ -1,5 +1,6 @@
package org.robolectric;
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
@@ -13,11 +14,14 @@
import android.graphics.Bitmap;
import android.os.Build;
import android.util.DisplayMetrics;
+import android.view.Display;
import com.google.common.base.Supplier;
import java.nio.file.Path;
import org.robolectric.android.Bootstrap;
import org.robolectric.android.ConfigurationV25;
import org.robolectric.res.ResourceTable;
+import org.robolectric.shadows.ShadowDisplayManager;
+import org.robolectric.shadows.ShadowView;
import org.robolectric.util.Scheduler;
import org.robolectric.util.TempDirectory;
@@ -193,6 +197,10 @@
* @param newQualifiers the qualifiers to apply
*/
public static void setQualifiers(String newQualifiers) {
+ if (getApiLevel() >= JELLY_BEAN_MR1) {
+ ShadowDisplayManager.changeDisplay(Display.DEFAULT_DISPLAY, newQualifiers);
+ }
+
Configuration configuration;
DisplayMetrics displayMetrics = new DisplayMetrics();
@@ -203,10 +211,31 @@
configuration = new Configuration();
}
Bootstrap.applyQualifiers(newQualifiers, getApiLevel(), configuration, displayMetrics);
- if (Boolean.getBoolean("robolectric.nativeruntime.enableGraphics")) {
+ if (ShadowView.useRealGraphics()) {
Bitmap.setDefaultDensity(displayMetrics.densityDpi);
}
+ updateConfiguration(configuration, displayMetrics);
+ }
+
+ public static void setFontScale(float fontScale) {
+ Resources systemResources = getApplication().getResources();
+ DisplayMetrics displayMetrics = systemResources.getDisplayMetrics();
+ Configuration configuration = systemResources.getConfiguration();
+
+ displayMetrics.scaledDensity = displayMetrics.density * fontScale;
+ configuration.fontScale = fontScale;
+
+ updateConfiguration(configuration, displayMetrics);
+ }
+
+ public static float getFontScale() {
+ Resources systemResources = getApplication().getResources();
+ return systemResources.getConfiguration().fontScale;
+ }
+
+ private static void updateConfiguration(
+ Configuration configuration, DisplayMetrics displayMetrics) {
// Update the resources last so that listeners will have a consistent environment.
// TODO(paulsowden): Can we call ResourcesManager.getInstance().applyConfigurationToResources()?
if (Build.VERSION.SDK_INT >= KITKAT
@@ -223,7 +252,6 @@
}
}
-
public static int getApiLevel() {
return apiLevel;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/android/Bootstrap.java b/shadows/framework/src/main/java/org/robolectric/android/Bootstrap.java
index b349ed5..1ed8d21 100644
--- a/shadows/framework/src/main/java/org/robolectric/android/Bootstrap.java
+++ b/shadows/framework/src/main/java/org/robolectric/android/Bootstrap.java
@@ -8,6 +8,7 @@
import android.util.DisplayMetrics;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.res.Qualifiers;
+import org.robolectric.shadows.ShadowDateUtils;
import org.robolectric.shadows.ShadowDisplayManager;
import org.robolectric.shadows.ShadowWindowManagerImpl;
@@ -101,6 +102,9 @@
DeviceConfig.applyRules(configuration, displayMetrics, apiLevel);
fixJellyBean(configuration, displayMetrics);
+
+ // DateUtils has a static cache of the last Configuration, so it may need to be reset.
+ ShadowDateUtils.resetLastConfig();
}
private static void fixJellyBean(Configuration configuration, DisplayMetrics displayMetrics) {
diff --git a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java
index 74d5b3f..56b0f4c 100644
--- a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java
+++ b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java
@@ -67,6 +67,9 @@
DESTROYED
}
+ // ActivityInfo constant.
+ private static final int CONFIG_WINDOW_CONFIGURATION = 0x20000000;
+
private _Activity_ _component_;
private LifecycleState currentState = LifecycleState.INITIAL;
@@ -428,11 +431,12 @@
Configuration newConfiguration, DisplayMetrics newMetrics, @Config int changedConfig) {
component.getResources().updateConfiguration(newConfiguration, newMetrics);
+ int filteredChanges = filterConfigChanges(changedConfig);
// TODO: throw on changedConfig == 0 since it non-intuitively calls onConfigurationChanged
// Can the activity handle itself ALL configuration changes?
- if ((getActivityInfo(component.getApplication()).configChanges & changedConfig)
- == changedConfig) {
+ if ((getActivityInfo(component.getApplication()).configChanges & filteredChanges)
+ == filteredChanges) {
shadowMainLooper.runPaused(
() -> {
component.onConfigurationChanged(newConfiguration);
@@ -460,7 +464,7 @@
() -> {
// Set flags
_component_.setChangingConfigurations(true);
- _component_.setConfigChangeFlags(changedConfig);
+ _component_.setConfigChangeFlags(filteredChanges);
// Perform activity destruction
final Bundle outState = new Bundle();
@@ -690,6 +694,16 @@
destroy();
}
+ // See ActivityRecord#getConfigurationChanges for the config changes that are considered for
+ // activity recreation by the window manager.
+ private static int filterConfigChanges(int changedConfig) {
+ // We don't want window configuration to cause relaunches.
+ if ((changedConfig & CONFIG_WINDOW_CONFIGURATION) != 0) {
+ changedConfig &= ~CONFIG_WINDOW_CONFIGURATION;
+ }
+ return changedConfig;
+ }
+
/** Accessor interface for android.app.Activity.NonConfigurationInstances's internals. */
@ForType(className = "android.app.Activity$NonConfigurationInstances")
interface _NonConfigurationInstances_ {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
index 73c36a5..e2b8f0d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
@@ -80,19 +80,40 @@
public AssociationInfo build() {
try {
if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
- 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*/),
- ClassParameter.from(long.class, approvedMs),
- ClassParameter.from(long.class, lastTimeConnectedMs));
+ // 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,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
index 70e54b1..d445460 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
@@ -136,4 +136,4 @@
void resetConnections() {
this.remoteAddressConnectionMap.clear();
}
-}
\ No newline at end of file
+}
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 0000000..22a0e75
--- /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
index 412d8c7..6f3f934 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
@@ -57,13 +57,17 @@
}
public CellInfoLte build() {
+ int apiLevel = RuntimeEnvironment.getApiLevel();
if (cellIdentity == null) {
- cellIdentity = CellIdentityLteBuilder.getDefaultInstance();
+ if (apiLevel > Build.VERSION_CODES.Q) {
+ cellIdentity = CellIdentityLteBuilder.getDefaultInstance();
+ } else {
+ cellIdentity = CellIdentityLteBuilder.newBuilder().build();
+ }
}
if (cellSignalStrength == null) {
cellSignalStrength = CellSignalStrengthLteBuilder.getDefaultInstance();
}
- int apiLevel = RuntimeEnvironment.getApiLevel();
CellInfoLteReflector cellInfoLteReflector = reflector(CellInfoLteReflector.class);
if (apiLevel < Build.VERSION_CODES.TIRAMISU) {
CellInfoLte cellInfo = cellInfoLteReflector.newCellInfoLte();
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 0000000..7819524
--- /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/CellSignalStrengthNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java
new file mode 100644
index 0000000..4f3f859
--- /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/ResourceModeShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
index d23045b..4af12cd 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
@@ -23,11 +23,12 @@
this.binary14ShadowClass = binary9ShadowClass;
}
- public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass,
- Class<? extends T> binaryShadowClass,
- Class<? extends T> binary9ShadowClass,
- Class<? extends T> binary10ShadowClass,
- Class<? extends T> binary14ShadowClass) {
+ public ResourceModeShadowPicker(
+ Class<? extends T> legacyShadowClass,
+ Class<? extends T> binaryShadowClass,
+ Class<? extends T> binary9ShadowClass,
+ Class<? extends T> binary10ShadowClass,
+ Class<? extends T> binary14ShadowClass) {
this.legacyShadowClass = legacyShadowClass;
this.binaryShadowClass = binaryShadowClass;
this.binary9ShadowClass = binary9ShadowClass;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbsSpinner.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbsSpinner.java
index f0c5b6f..e1cc9f9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbsSpinner.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbsSpinner.java
@@ -25,12 +25,38 @@
@Implementation
protected void setSelection(int position) {
reflector(AbsSpinnerReflector.class, realAbsSpinner).setSelection(position);
- SpinnerAdapter adapter = realAbsSpinner.getAdapter();
- if (getItemSelectedListener() != null && adapter != null) {
- getItemSelectedListener().onItemSelected(realAbsSpinner, null, position, adapter.getItemId(position));
+ if (!useRealSpinnerSelection()) {
+ // Use the broken legacy spinner selection callback invocation logic.
+ SpinnerAdapter adapter = realAbsSpinner.getAdapter();
+ if (getItemSelectedListener() != null && adapter != null) {
+ getItemSelectedListener()
+ .onItemSelected(realAbsSpinner, null, position, adapter.getItemId(position));
+ }
}
}
+ /**
+ * Currently the shadow method {@link #setSelection(int)} broken and low-fidelity. In real
+ * Android, the item selected callback is invoked during layout passes. However, in Robolectric,
+ * the callback is always invoked explicitly, which leads to many issues:
+ *
+ * <ol>
+ * <li>Item selection callbacks are invoked even if the selection hasn't changed, which can lead
+ * to infinite loops.
+ * <li>Item selection callbacks are still invoked if spinners are not attached to an Activity.
+ * <li>Item selection callbacks are invoked even if spinners are non-visible.
+ * <li>Item selection callbacks are invoked twice if spinners are attached to an Activity and
+ * visible.
+ * </ol>
+ *
+ * <p>Long-term we want to eliminate this broken shadow method, but in the mean time the real
+ * scrolling behavior is only enabled if a system property is set, due to tests depending on the
+ * broken behavior.
+ */
+ private static boolean useRealSpinnerSelection() {
+ return Boolean.getBoolean("robolectric.useRealSpinnerSelection");
+ }
+
// Non-implementation helper method
public boolean isAnimatedTransition() {
return animatedTransition;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityService.java
index e5620df..9e51896 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityService.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityService.java
@@ -16,11 +16,15 @@
import android.hardware.HardwareBuffer;
import android.os.Handler;
import android.os.SystemClock;
+import android.util.SparseArray;
+import android.view.Display;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
+import com.google.common.collect.ArrayListMultimap;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
@@ -35,7 +39,8 @@
private final List<Integer> globalActionsPerformed = new ArrayList<>();
private List<AccessibilityNodeInfo.AccessibilityAction> systemActions;
- private final List<AccessibilityWindowInfo> windows = new ArrayList<>();
+ private final ArrayListMultimap<Integer, AccessibilityWindowInfo> windows =
+ ArrayListMultimap.create();
private final List<GestureDispatch> gesturesDispatched = new ArrayList<>();
private boolean canDispatchGestures = true;
@@ -66,13 +71,41 @@
}
/**
- * Returns a representation of interactive windows shown on the device screen. Mirrors the values
- * provided to {@link #setWindows(List<AccessibilityWindowInfo>)}. Returns an empty List if not
- * set.
+ * Returns a representation of interactive windows shown on the device's default display. Mirrors
+ * the values provided to {@link #setWindows(List<AccessibilityWindowInfo>)}. Returns an empty
+ * list if not set.
*/
@Implementation(minSdk = LOLLIPOP)
protected List<AccessibilityWindowInfo> getWindows() {
- return new ArrayList<>(windows);
+ List<AccessibilityWindowInfo> windowInfos = windows.get(Display.DEFAULT_DISPLAY);
+ if (windowInfos != null) {
+ return new ArrayList<>(windowInfos);
+ } else {
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * Returns a representation of interactive windows shown on the device's all displays. An empty
+ * list will be returned for default display and {@code null} will be return for other displays if
+ * they are not set by {@link #setWindowsOnDisplay(int, List)}.
+ */
+ @Implementation(minSdk = R)
+ protected SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() {
+ return cloneWindowOnAllDisplays();
+ }
+
+ private SparseArray<List<AccessibilityWindowInfo>> cloneWindowOnAllDisplays() {
+ SparseArray<List<AccessibilityWindowInfo>> anotherArray = new SparseArray<>();
+ for (int displayId : windows.keySet()) {
+ anotherArray.put(displayId, windows.get(displayId));
+ }
+
+ if (anotherArray.get(Display.DEFAULT_DISPLAY) == null) {
+ anotherArray.put(Display.DEFAULT_DISPLAY, new ArrayList<>());
+ }
+
+ return anotherArray;
}
@Implementation(minSdk = N)
@@ -130,13 +163,23 @@
}
/**
- * Sets the list of interactive windows shown on the device screen as reported by {@link
- * #getWindows()}
+ * Sets the list of interactive windows shown on the device's default display as reported by
+ * {@link #getWindows()}
*/
public void setWindows(List<AccessibilityWindowInfo> windowList) {
- windows.clear();
+ setWindowsOnDisplay(Display.DEFAULT_DISPLAY, windowList);
+ }
+
+ /**
+ * Sets the list of interactive windows shown on the device's {@code displayId} display. If the
+ * {@code windowList} is null, we will remove the list with given {@code displayId} display as
+ * reported by {@link #getWindowsOnAllDisplays()}.
+ */
+ public void setWindowsOnDisplay(
+ int displayId, @Nullable List<AccessibilityWindowInfo> windowList) {
+ windows.removeAll(displayId);
if (windowList != null) {
- windows.addAll(windowList);
+ windows.putAll(displayId, windowList);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityWindowInfo.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityWindowInfo.java
index 77f1a30..c20012e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityWindowInfo.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityWindowInfo.java
@@ -2,6 +2,7 @@
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.Q;
import static org.robolectric.util.reflector.Reflector.reflector;
@@ -52,6 +53,8 @@
private boolean isFocused = false;
+ private boolean isPictureInPicture = false;
+
@RealObject private AccessibilityWindowInfo mRealAccessibilityWindowInfo;
@Implementation
@@ -256,6 +259,11 @@
return isAccessibilityFocused;
}
+ @Implementation(minSdk = O)
+ protected boolean isInPictureInPictureMode() {
+ return isPictureInPicture;
+ }
+
@Implementation
protected void recycle() {
// This shadow does not track recycling of windows.
@@ -314,6 +322,11 @@
isFocused = focused;
}
+ @Implementation(minSdk = O)
+ public void setPictureInPicture(boolean pictureInPicture) {
+ isPictureInPicture = pictureInPicture;
+ }
+
public void addChild(AccessibilityWindowInfo child) {
if (children == null) {
children = new ArrayList<>();
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 70464bf..852919a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
@@ -2,6 +2,7 @@
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
@@ -157,7 +158,12 @@
/** Update's ActivityThread's list of active Activities */
void registerActivityLaunch(
Intent intent, ActivityInfo activityInfo, Activity activity, IBinder token) {
- ActivityClientRecord record = new ActivityClientRecord();
+ ActivityClientRecord record;
+ if (RuntimeEnvironment.getApiLevel() >= P) {
+ record = new ActivityClientRecord();
+ } else {
+ record = ReflectionHelpers.callConstructor(ActivityClientRecord.class);
+ }
ActivityClientRecordReflector recordReflector =
reflector(ActivityClientRecordReflector.class, record);
recordReflector.setToken(token);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
index 4084825..290d785 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
@@ -5,7 +5,9 @@
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DONT_KILL_APP;
import static android.content.pm.PackageManager.GET_ACTIVITIES;
import static android.content.pm.PackageManager.GET_META_DATA;
import static android.content.pm.PackageManager.GET_PROVIDERS;
@@ -67,7 +69,6 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.ComponentEnabledSetting;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.OnPermissionsChangedListener;
@@ -336,6 +337,33 @@
componentList.put(componentName, new ComponentState(newState, flags));
}
+ @Implementation(minSdk = TIRAMISU)
+ protected void setComponentEnabledSettings(List<ComponentEnabledSetting> settings) {
+ for (ComponentEnabledSetting setting : settings) {
+ componentList.put(
+ setting.getComponentName(),
+ new ComponentState(setting.getEnabledState(), setting.getEnabledFlags()));
+ }
+ }
+
+ @Implementation(minSdk = Q)
+ protected void setSyntheticAppDetailsActivityEnabled(String packageName, boolean enabled) {
+ ComponentName componentName =
+ new ComponentName(packageName, PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME);
+ setComponentEnabledSetting(
+ componentName,
+ enabled ? COMPONENT_ENABLED_STATE_DEFAULT : COMPONENT_ENABLED_STATE_DISABLED,
+ DONT_KILL_APP);
+ }
+
+ @Implementation(minSdk = Q)
+ protected boolean getSyntheticAppDetailsActivityEnabled(String packageName) {
+ ComponentName componentName =
+ new ComponentName(packageName, PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME);
+ int state = getComponentEnabledSetting(componentName);
+ return state == COMPONENT_ENABLED_STATE_ENABLED || state == COMPONENT_ENABLED_STATE_DEFAULT;
+ }
+
@Implementation
protected void setApplicationEnabledSetting(String packageName, int newState, int flags) {
applicationEnabledSettingMap.put(packageName, newState);
@@ -1096,7 +1124,10 @@
@Implementation(minSdk = JELLY_BEAN_MR1)
protected void extendVerificationTimeout(
int id, int verificationCodeAtTimeout, long millisecondsToDelay) {
- verificationTimeoutExtension.put(id, millisecondsToDelay);
+ synchronized (lock) {
+ verificationTimeoutExtension.put(id, millisecondsToDelay);
+ verificationCodeAtTimeoutExtension.put(id, verificationCodeAtTimeout);
+ }
}
@Override
@@ -1109,7 +1140,9 @@
@Implementation
protected void setInstallerPackageName(String targetPackage, String installerPackageName) {
- packageInstallerMap.put(targetPackage, installerPackageName);
+ synchronized (lock) {
+ packageInstallerMap.put(targetPackage, installerPackageName);
+ }
}
@Implementation(minSdk = KITKAT)
@@ -2246,6 +2279,22 @@
return reflector(ReflectorApplicationPackageManager.class, realObject).getContext();
}
+ /** Stub that will always throw. */
+ @Implementation(minSdk = S)
+ protected Object /* PackageManager.Property */ getProperty(
+ String propertyName, String packageName) throws NameNotFoundException {
+ // TODO: in future read this value from parsed manifest
+ throw new NameNotFoundException("unsupported");
+ }
+
+ /** Stub that will always throw. */
+ @Implementation(minSdk = S)
+ protected Object /* PackageManager.Property */ getProperty(
+ String propertyName, ComponentName name) throws NameNotFoundException {
+ // TODO: in future read this value from parsed manifest
+ throw new NameNotFoundException("unsupported");
+ }
+
/** Reflector interface for {@link ApplicationPackageManager}'s internals. */
@ForType(ApplicationPackageManager.class)
private interface ReflectorApplicationPackageManager {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
old mode 100755
new mode 100644
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
old mode 100755
new mode 100644
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 2234351..5f2d4fd 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 @@
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 @@
}
/**
+ * 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 @@
}
/**
+ * 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 @@
*
* @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 @@
*
* @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 @@
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/ShadowBluetoothGatt.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
index 7b6ff7e..188c60d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
@@ -35,7 +35,7 @@
public class ShadowBluetoothGatt {
private static final String NULL_CALLBACK_MSG = "BluetoothGattCallback can not be null.";
-
+
private BluetoothGattCallback bluetoothGattCallback;
private int connectionPriority = BluetoothGatt.CONNECTION_PRIORITY_BALANCED;
private boolean isConnected = false;
@@ -143,7 +143,7 @@
@Implementation(minSdk = JELLY_BEAN_MR2)
protected void disconnect() {
bluetoothGattReflector.disconnect();
- if (this.getGattCallback() != null && this.isConnected) {
+ if (this.isCallbackAppropriate()) {
this.getGattCallback()
.onConnectionStateChange(
this.realBluetoothGatt,
@@ -382,7 +382,6 @@
return this.getGattCallback() != null && this.isConnected;
}
-
@ForType(BluetoothGatt.class)
private interface BluetoothGattReflector {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java
new file mode 100644
index 0000000..de2e76e
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java
@@ -0,0 +1,159 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow of {@link BluetoothGattServer}. */
+@Implements(value = BluetoothGattServer.class, minSdk = O)
+public class ShadowBluetoothGattServer {
+ private BluetoothGattServerCallback callback;
+ private final List<byte[]> responses = new ArrayList<>();
+ private final Set<BluetoothDevice> cancelledDevices = new HashSet<>();
+ private boolean isClosed;
+
+ @ReflectorObject protected BluetoothGattServerReflector bluetoothGattServerReflector;
+
+ /** Close this GATT server instance. */
+ @Implementation
+ protected void close() {
+ bluetoothGattServerReflector.close();
+ this.isClosed = true;
+ }
+
+ /**
+ * Disconnects an established connection, or cancels a connection attempt currently in progress.
+ *
+ * @param device Remote device
+ */
+ @Implementation
+ protected void cancelConnection(BluetoothDevice device) {
+ this.bluetoothGattServerReflector.cancelConnection(device);
+ this.cancelledDevices.add(device);
+ }
+
+ /**
+ * Send a response to a read or write request to a remote device.
+ *
+ * @param device The remote device to send this response to
+ * @param requestId The ID of the request that was received with the callback
+ * @param status The status of the request to be sent to the remote devices
+ * @param offset Value offset for partial read/write response
+ * @param value The value of the attribute that was read/written (optional)
+ */
+ @Implementation
+ protected boolean sendResponse(
+ BluetoothDevice device, int requestId, int status, int offset, byte[] value) {
+ this.responses.add(value);
+ return this.bluetoothGattServerReflector.sendResponse(device, requestId, status, offset, value);
+ }
+
+ /**
+ * Simulate a successful Gatt Server Connection with {@link BluetoothConnectionManager}. Performs
+ * a {@link BluetoothGattCallback#onConnectionStateChange} if available.
+ *
+ * @param device remote device
+ */
+ public void notifyConnection(BluetoothDevice device) {
+ BluetoothConnectionManager.getInstance().registerGattServerConnection(device.getAddress());
+ this.cancelledDevices.remove(device);
+
+ if (this.callback != null) {
+ this.callback.onConnectionStateChange(
+ device, BluetoothGatt.GATT_SUCCESS, BluetoothAdapter.STATE_CONNECTED);
+ }
+ }
+
+ /**
+ * Simulate a successful Gatt Server Disconnection with {@link BluetoothConnectionManager}.
+ * Performs a {@link BluetoothGattCallback#onConnectionStateChange} if available, even when device
+ * was not connected initially.
+ *
+ * @param device remote device
+ */
+ public void notifyDisconnection(BluetoothDevice device) {
+ BluetoothConnectionManager.getInstance().unregisterGattServerConnection(device.getAddress());
+ this.cancelledDevices.add(device);
+
+ if (this.callback != null) {
+ this.callback.onConnectionStateChange(
+ device, BluetoothGatt.GATT_SUCCESS, BluetoothAdapter.STATE_DISCONNECTED);
+ }
+ }
+
+ /**
+ * Get whether the device's connection has been cancelled.
+ *
+ * @param device remote device
+ */
+ public boolean isConnectionCancelled(BluetoothDevice device) {
+ return this.cancelledDevices.contains(device);
+ }
+
+ /**
+ * Returns true if the connection status of remote device is connected
+ *
+ * @param device remote device
+ */
+ public boolean isConnectedToDevice(BluetoothDevice device) {
+ return BluetoothConnectionManager.getInstance().hasGattServerConnection(device.getAddress());
+ }
+
+ /** Get a copy of the list of responses that have been sent. */
+ public List<byte[]> getResponses() {
+ List<byte[]> responsesCopy = new ArrayList<>();
+ for (byte[] response : this.responses) {
+ responsesCopy.add(response.clone());
+ }
+ return responsesCopy;
+ }
+
+ /** Clear the list of responses. */
+ public void clearResponses() {
+ this.responses.clear();
+ }
+
+ /** Get whether server has been closed. */
+ public boolean isClosed() {
+ return this.isClosed;
+ }
+
+ public void setGattServerCallback(BluetoothGattServerCallback callback) {
+ this.callback = callback;
+ }
+
+ public BluetoothGattServerCallback getGattServerCallback() {
+ return this.callback;
+ }
+
+ public BluetoothConnectionManager getBluetoothConnectionManager() {
+ return BluetoothConnectionManager.getInstance();
+ }
+
+ @ForType(BluetoothGattServer.class)
+ private interface BluetoothGattServerReflector {
+
+ @Direct
+ void close();
+
+ @Direct
+ void cancelConnection(BluetoothDevice device);
+
+ @Direct
+ boolean sendResponse(
+ BluetoothDevice device, int requestId, int status, int offset, byte[] value);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothManager.java
index a5dd7bf..db9d1bd 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothManager.java
@@ -1,19 +1,31 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
import static com.google.common.base.Preconditions.checkArgument;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothGatt;
+import android.content.Context;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.ImmutableIntArray;
import java.util.ArrayList;
import java.util.List;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.PerfStatsCollector;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
/** Shadow of {@link BluetoothManager} that makes the testing possible. */
@Implements(value = BluetoothManager.class, minSdk = JELLY_BEAN_MR2)
@@ -57,6 +69,11 @@
}
}
+ /**
+ * Get the {@link BluetoothAdapter} for this device.
+ *
+ * @return BluetoothAdapter instance
+ */
@Implementation
protected BluetoothAdapter getAdapter() {
return BluetoothAdapter.getDefaultAdapter();
@@ -101,4 +118,45 @@
private boolean isProfileValid(int profile) {
return profile == BluetoothProfile.GATT || profile == BluetoothProfile.GATT_SERVER;
}
+
+ @Implementation(minSdk = O, maxSdk = R)
+ protected BluetoothGattServer openGattServer(
+ Context context, BluetoothGattServerCallback callback, int transport) {
+ return createGattServer(context, callback, transport);
+ }
+
+ /**
+ * Overrides behavior of {@link openGattServer} and returns {@link ShadowBluetoothGattServer}
+ * after creating and using a nullProxy for {@link IBluetoothGatt}.
+ */
+ @Implementation(minSdk = S)
+ protected BluetoothGattServer openGattServer(
+ Context context, BluetoothGattServerCallback callback, int transport, boolean eattSupport) {
+ return createGattServer(context, callback, transport);
+ }
+
+ private BluetoothGattServer createGattServer(
+ Context unusedContext, BluetoothGattServerCallback callback, int transport) {
+ IBluetoothGatt iGatt = ReflectionHelpers.createNullProxy(IBluetoothGatt.class);
+ BluetoothGattServer gattServer;
+
+ if (RuntimeEnvironment.getApiLevel() <= R) {
+ gattServer =
+ ReflectionHelpers.callConstructor(
+ BluetoothGattServer.class,
+ ClassParameter.from(IBluetoothGatt.class, iGatt),
+ ClassParameter.from(int.class, transport));
+ } else {
+ gattServer =
+ ReflectionHelpers.callConstructor(
+ BluetoothGattServer.class,
+ ClassParameter.from(IBluetoothGatt.class, iGatt),
+ ClassParameter.from(int.class, transport),
+ ClassParameter.from(BluetoothAdapter.class, this.getAdapter()));
+ }
+ PerfStatsCollector.getInstance().incrementCount("constructShadowBluetoothGattServer");
+ ShadowBluetoothGattServer shadowBluetoothGattServer = Shadow.extract(gattServer);
+ shadowBluetoothGattServer.setGattServerCallback(callback);
+ return gattServer;
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBugreportManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBugreportManager.java
index 22df7cf..7be474b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBugreportManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBugreportManager.java
@@ -140,6 +140,12 @@
return shareDescription;
}
+ /** Returns the screenshot file descriptor if set with {@code startBugreport}, else null. */
+ @Nullable
+ public ParcelFileDescriptor getScreenshotFd() {
+ return screenshotFd;
+ }
+
private void resetParams() {
try {
bugreportFd.close();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java
index da43162..ffe0097 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamera.java
@@ -17,7 +17,6 @@
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
@Implements(Camera.class)
public class ShadowCamera {
@@ -300,7 +299,7 @@
/** Add custom preview sizes to supportedPreviewSizes. */
public void addSupportedPreviewSize(int width, int height) {
- Camera.Size newSize = ReflectionHelpers.newInstance(Camera.class).new Size(width, height);
+ Camera.Size newSize = newInstanceOf(Camera.class).new Size(width, height);
supportedPreviewSizes.add(newSize);
}
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 a4b1fb7..cff9382 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
@@ -7,6 +7,7 @@
import android.view.Choreographer;
import android.view.Choreographer.FrameCallback;
+import android.view.DisplayEventReceiver;
import java.time.Duration;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -171,5 +172,8 @@
@Direct
void doFrame(long frameTimeNanos, int frame);
+
+ @Accessor("mDisplayEventReceiver")
+ DisplayEventReceiver getReceiver();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java
index 7937487..6201869 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java
@@ -3,6 +3,9 @@
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.companion.AssociatedDevice;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
@@ -21,6 +24,10 @@
import java.util.concurrent.Executor;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Reflector;
+
/** Shadow for CompanionDeviceManager. */
@Implements(value = CompanionDeviceManager.class, minSdk = VERSION_CODES.O)
@@ -136,13 +143,17 @@
MacAddress.fromString(info.deviceMacAddress()),
info.displayName(),
info.deviceProfile(),
+ info.associatedDevice(),
info.selfManaged(),
info.notifyOnDeviceNearby(),
+ info.revoked(),
info.timeApprovedMs(),
- info.lastTimeConnectedMs());
+ info.lastTimeConnectedMs(),
+ info.systemDataSyncFlags());
}
private RoboAssociationInfo createShadowAssociationInfo(AssociationInfo info) {
+ var ref_info = Reflector.reflector(_AssociationInfo_.class, info);
return RoboAssociationInfo.create(
info.getId(),
info.getUserId(),
@@ -150,10 +161,20 @@
info.getDeviceMacAddress().toString(),
info.getDisplayName(),
info.getDeviceProfile(),
+ ref_info.getFullAssociatedDevice(),
info.isSelfManaged(),
info.isNotifyOnDeviceNearby(),
+ info.isRevoked(),
info.getTimeApprovedMs(),
- info.getLastTimeConnectedMs());
+ info.getLastTimeConnectedMs(),
+ info.getSystemDataSyncFlags());
+ }
+
+ @ForType(AssociationInfo.class)
+ public interface _AssociationInfo_ {
+
+ @Accessor("mAssociatedDevice")
+ AssociatedDevice getFullAssociatedDevice();
}
/**
@@ -178,14 +199,20 @@
@Nullable
public abstract String deviceProfile();
+ public abstract AssociatedDevice associatedDevice();
+
public abstract boolean selfManaged();
public abstract boolean notifyOnDeviceNearby();
+ public abstract boolean revoked();
+
public abstract long timeApprovedMs();
public abstract long lastTimeConnectedMs();
+ public abstract int systemDataSyncFlags();
+
public static Builder builder() {
return new AutoValue_ShadowCompanionDeviceManager_RoboAssociationInfo.Builder()
.setId(1)
@@ -193,7 +220,8 @@
.setSelfManaged(false)
.setNotifyOnDeviceNearby(false)
.setTimeApprovedMs(0)
- .setLastTimeConnectedMs(0);
+ .setLastTimeConnectedMs(0)
+ .setSystemDataSyncFlags(-1);
}
public static RoboAssociationInfo create(
@@ -203,10 +231,13 @@
String deviceMacAddress,
CharSequence displayName,
String deviceProfile,
+ AssociatedDevice associatedDevice,
boolean selfManaged,
boolean notifyOnDeviceNearby,
+ boolean revoked,
long timeApprovedMs,
- long lastTimeConnectedMs) {
+ long lastTimeConnectedMs,
+ int systemDataSyncFlags) {
return RoboAssociationInfo.builder()
.setId(id)
.setUserId(userId)
@@ -214,10 +245,13 @@
.setDeviceMacAddress(deviceMacAddress)
.setDisplayName(displayName)
.setDeviceProfile(deviceProfile)
+ .setAssociatedDevice(associatedDevice)
.setSelfManaged(selfManaged)
.setNotifyOnDeviceNearby(notifyOnDeviceNearby)
.setTimeApprovedMs(timeApprovedMs)
+ .setRevoked(revoked)
.setLastTimeConnectedMs(lastTimeConnectedMs)
+ .setSystemDataSyncFlags(systemDataSyncFlags)
.build();
}
@@ -238,12 +272,18 @@
public abstract Builder setSelfManaged(boolean selfManaged);
+ public abstract Builder setAssociatedDevice(AssociatedDevice device);
+
public abstract Builder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby);
+ public abstract Builder setRevoked(boolean revoked);
+
public abstract Builder setTimeApprovedMs(long timeApprovedMs);
public abstract Builder setLastTimeConnectedMs(long lastTimeConnectedMs);
+ public abstract Builder setSystemDataSyncFlags(int flags);
+
public abstract RoboAssociationInfo build();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
index 0b86844..bb2672c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
@@ -7,6 +7,7 @@
import static android.content.ContentResolver.SCHEME_CONTENT;
import static android.content.ContentResolver.SCHEME_FILE;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.Q;
@@ -201,7 +202,25 @@
}
@Implementation
- protected final OutputStream openOutputStream(final Uri uri) {
+ protected final OutputStream openOutputStream(final Uri uri) throws FileNotFoundException {
+ try {
+ return openOutputStream(uri, "w");
+ } catch (SecurityException | FileNotFoundException e) {
+ // This is legacy behavior is only supported because existing users require it.
+ return new OutputStream() {
+ @Override
+ public void write(int arg0) throws IOException {}
+
+ @Override
+ public String toString() {
+ return "outputstream for " + uri;
+ }
+ };
+ }
+ }
+
+ @Implementation
+ protected final OutputStream openOutputStream(Uri uri, String mode) throws FileNotFoundException {
Supplier<OutputStream> supplier = outputStreamMap.get(uri);
if (supplier != null) {
OutputStream outputStream = supplier.get();
@@ -209,15 +228,8 @@
return outputStream;
}
}
- return new OutputStream() {
- @Override
- public void write(int arg0) throws IOException {}
-
- @Override
- public String toString() {
- return "outputstream for " + uri;
- }
- };
+ return reflector(ContentResolverReflector.class, realContentResolver)
+ .openOutputStream(uri, mode);
}
/**
@@ -553,7 +565,7 @@
}
for (Map.Entry<Account, Status> mp : map.getValue().entrySet()) {
if (isSyncActive(mp.getKey(), map.getKey())) {
- SyncInfo si = new SyncInfo(0, mp.getKey(), map.getKey(), 0);
+ SyncInfo si = newSyncInfo(0, mp.getKey(), map.getKey(), 0);
list.add(si);
}
}
@@ -561,6 +573,20 @@
return list;
}
+ private static SyncInfo newSyncInfo(
+ int authorityId, Account account, String authority, long startTime) {
+ if (RuntimeEnvironment.getApiLevel() >= JELLY_BEAN_MR2) {
+ return new SyncInfo(authorityId, account, authority, startTime);
+ } else {
+ return ReflectionHelpers.callConstructor(
+ SyncInfo.class,
+ ClassParameter.from(int.class, authorityId),
+ ClassParameter.from(Account.class, account),
+ ClassParameter.from(String.class, authority),
+ ClassParameter.from(long.class, startTime));
+ }
+ }
+
@Implementation
protected static void setIsSyncable(Account account, String authority, int syncable) {
getStatus(account, authority, true).state = syncable;
@@ -1160,6 +1186,6 @@
InputStream openInputStream(Uri uri) throws FileNotFoundException;
@Direct
- OutputStream openOutputStream(Uri uri) throws FileNotFoundException;
+ OutputStream openOutputStream(Uri uri, String mode) throws FileNotFoundException;
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormat.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormat.java
index abbed64..83d5235 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormat.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormat.java
@@ -2,16 +2,17 @@
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import java.text.FieldPosition;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
-import libcore.icu.DateIntervalFormat;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-@Implements(value = DateIntervalFormat.class, isInAndroidSdk = false, minSdk = KITKAT)
+@Implements(className = "libcore.icu.DateIntervalFormat", isInAndroidSdk = false, minSdk = KITKAT,
+ maxSdk = TIRAMISU)
public class ShadowDateIntervalFormat {
private static long address;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java
new file mode 100644
index 0000000..ac435c4
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java
@@ -0,0 +1,42 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
+import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+
+import java.text.FieldPosition;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import android.text.format.DateIntervalFormat;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(value = DateIntervalFormat.class, isInAndroidSdk = false, minSdk = UPSIDE_DOWN_CAKE)
+public class ShadowDateIntervalFormatU {
+
+ private static long address;
+ private static Map<Long, com.ibm.icu.text.DateIntervalFormat> INTERVAL_CACHE = new HashMap<>();
+
+ @Implementation
+ public static long createDateIntervalFormat(String skeleton, String localeName, String tzName) {
+ address++;
+ INTERVAL_CACHE.put(address, com.ibm.icu.text.DateIntervalFormat.getInstance(skeleton, new Locale(localeName)));
+ return address;
+ }
+
+ @Implementation
+ public static void destroyDateIntervalFormat(long address) {
+ INTERVAL_CACHE.remove(address);
+ }
+
+ @Implementation
+ @SuppressWarnings("JdkObsolete")
+ public static String formatDateInterval(long address, long fromDate, long toDate) {
+ StringBuffer buffer = new StringBuffer();
+
+ FieldPosition pos = new FieldPosition(0);
+ INTERVAL_CACHE.get(address).format(new com.ibm.icu.util.DateInterval(fromDate, toDate), buffer, pos);
+
+ return buffer.toString();
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateUtils.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateUtils.java
new file mode 100644
index 0000000..1a5114a
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateUtils.java
@@ -0,0 +1,30 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.text.format.DateUtils;
+import org.robolectric.annotation.Implements;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
+
+/** Shadow for {@link DateUtils}. */
+@Implements(value = DateUtils.class, isInAndroidSdk = false)
+public class ShadowDateUtils {
+
+ /**
+ * internal only
+ *
+ * <p>Does not need to be a resetter method because Configuration at test startup.
+ */
+ public static void resetLastConfig() {
+ reflector(DateUtilsReflector.class).setLastConfig(null);
+ }
+
+ @ForType(DateUtils.class)
+ interface DateUtilsReflector {
+ @Static
+ @Accessor("sLastConfig")
+ void setLastConfig(String lastConfig);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
index dbaa353..422f96f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
@@ -89,6 +89,7 @@
private Map<String, Bundle> applicationRestrictionsMap = new HashMap<>();
private CharSequence organizationName;
private int organizationColor;
+ private boolean isAutoTimeEnabled;
private boolean isAutoTimeRequired;
private boolean isAutoTimeZoneEnabled;
private String timeZone;
@@ -602,6 +603,18 @@
return organizationColor;
}
+ @Implementation(minSdk = R)
+ protected void setAutoTimeEnabled(ComponentName admin, boolean enabled) {
+ enforceDeviceOwnerOrProfileOwner(admin);
+ isAutoTimeEnabled = enabled;
+ }
+
+ @Implementation(minSdk = R)
+ protected boolean getAutoTimeEnabled(ComponentName admin) {
+ enforceDeviceOwnerOrProfileOwner(admin);
+ return isAutoTimeEnabled;
+ }
+
@Implementation(minSdk = LOLLIPOP)
protected void setAutoTimeRequired(ComponentName admin, boolean required) {
enforceDeviceOwnerOrProfileOwner(admin);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplay.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplay.java
index c4f5770..75d3579 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplay.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplay.java
@@ -405,6 +405,11 @@
throw new UnsupportedOperationException("HDR capabilities are not supported below Android N");
}
+ if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) {
+ reflector(DisplayModeReflector.class, realObject.getMode())
+ .setSupportedHdrTypes(supportedHdrTypes);
+ }
+
ShadowDisplayManager.changeDisplay(
displayId,
displayConfig -> {
@@ -474,4 +479,10 @@
@Accessor("mFlags")
void setFlags(int flags);
}
+
+ @ForType(Display.Mode.class)
+ interface DisplayModeReflector {
+ @Accessor("mSupportedHdrTypes")
+ void setSupportedHdrTypes(int[] types);
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManager.java
index 9959ca1..d15da6b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManager.java
@@ -1,7 +1,10 @@
package org.robolectric.shadows;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.P;
+import static java.util.Objects.requireNonNull;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
@@ -17,8 +20,10 @@
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.auto.value.AutoBuilder;
+import java.util.HashMap;
import java.util.List;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.Bootstrap;
@@ -27,6 +32,7 @@
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.Resetter;
import org.robolectric.res.Qualifiers;
import org.robolectric.util.Consumer;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
@@ -44,6 +50,13 @@
private Context context;
+ private static final HashMap<Integer, Boolean> displayIsNaturallyPortrait = new HashMap<>();
+
+ @Resetter
+ public static void reset() {
+ displayIsNaturallyPortrait.clear();
+ }
+
@Implementation
protected void __constructor__(Context context) {
this.context = context;
@@ -74,11 +87,12 @@
throw new IllegalStateException("this method should only be called by Robolectric");
}
- shadowDisplayManagerGlobal.addDisplay(createDisplayInfo(configuration, displayMetrics));
+ shadowDisplayManagerGlobal.addDisplay(
+ createDisplayInfo(configuration, displayMetrics, /* isNaturallyPortrait= */ true));
}
private static DisplayInfo createDisplayInfo(
- Configuration configuration, DisplayMetrics displayMetrics) {
+ Configuration configuration, DisplayMetrics displayMetrics, boolean isNaturallyPortrait) {
int widthPx = (int) (configuration.screenWidthDp * displayMetrics.density);
int heightPx = (int) (configuration.screenHeightDp * displayMetrics.density);
@@ -93,9 +107,9 @@
displayInfo.logicalWidth = widthPx;
displayInfo.logicalHeight = heightPx;
displayInfo.rotation =
- configuration.orientation == Configuration.ORIENTATION_PORTRAIT
- ? Surface.ROTATION_0
- : Surface.ROTATION_90;
+ configuration.orientation == ORIENTATION_PORTRAIT
+ ? (isNaturallyPortrait ? Surface.ROTATION_0 : Surface.ROTATION_90)
+ : (isNaturallyPortrait ? Surface.ROTATION_90 : Surface.ROTATION_0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
displayInfo.modeId = 0;
displayInfo.defaultModeId = 0;
@@ -115,16 +129,19 @@
return displayInfo;
}
- private static DisplayInfo createDisplayInfo(String qualifiersStr, DisplayInfo baseDisplayInfo) {
+ private static DisplayInfo createDisplayInfo(String qualifiersStr, @Nullable Integer displayId) {
+ DisplayInfo baseDisplayInfo =
+ displayId != null ? DisplayManagerGlobal.getInstance().getDisplayInfo(displayId) : null;
Configuration configuration = new Configuration();
DisplayMetrics displayMetrics = new DisplayMetrics();
+ boolean isNaturallyPortrait =
+ requireNonNull(displayIsNaturallyPortrait.getOrDefault(displayId, true));
if (qualifiersStr.startsWith("+") && baseDisplayInfo != null) {
configuration.orientation =
- (baseDisplayInfo.rotation == Surface.ROTATION_0
- || baseDisplayInfo.rotation == Surface.ROTATION_180)
- ? Configuration.ORIENTATION_PORTRAIT
- : Configuration.ORIENTATION_LANDSCAPE;
+ isRotated(baseDisplayInfo.rotation)
+ ? (isNaturallyPortrait ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT)
+ : (isNaturallyPortrait ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE);
configuration.screenWidthDp =
baseDisplayInfo.logicalWidth
* DisplayMetrics.DENSITY_DEFAULT
@@ -142,7 +159,11 @@
Bootstrap.applyQualifiers(
qualifiersStr, RuntimeEnvironment.getApiLevel(), configuration, displayMetrics);
- return createDisplayInfo(configuration, displayMetrics);
+ return createDisplayInfo(configuration, displayMetrics, isNaturallyPortrait);
+ }
+
+ private static boolean isRotated(int rotation) {
+ return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
}
private static void fixNominalDimens(DisplayInfo displayInfo) {
@@ -168,13 +189,33 @@
* display
*/
public static void changeDisplay(int displayId, String qualifiersStr) {
- DisplayInfo baseDisplayInfo = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId);
- DisplayInfo displayInfo = createDisplayInfo(qualifiersStr, baseDisplayInfo);
+ DisplayInfo displayInfo = createDisplayInfo(qualifiersStr, displayId);
getShadowDisplayManagerGlobal().changeDisplay(displayId, displayInfo);
shadowMainLooper().idle();
}
/**
+ * Changes the display to be naturally portrait or landscape. This will ensure that the rotation
+ * is configured consistently with orientation when the orientation is configured by {@link
+ * #changeDisplay}, e.g. if the display is naturally portrait and the orientation is configured as
+ * landscape the rotation will be set to {@link Surface#ROTATION_90}.
+ */
+ public static void setNaturallyPortrait(int displayId, boolean isNaturallyPortrait) {
+ displayIsNaturallyPortrait.put(displayId, isNaturallyPortrait);
+ changeDisplay(
+ displayId,
+ config -> {
+ boolean isRotated = isRotated(config.rotation);
+ boolean isPortrait = config.logicalHeight > config.logicalWidth;
+ if ((isNaturallyPortrait ^ isPortrait) != isRotated) {
+ config.rotation =
+ (isNaturallyPortrait ^ isPortrait) ? Surface.ROTATION_90 : Surface.ROTATION_0;
+ }
+ });
+ shadowMainLooper().idle();
+ }
+
+ /**
* Sets supported modes to the specified display with ID {@code displayId}.
*
* <p>Idles the main looper to ensure all listeners are notified.
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 f7f5f5b..d0fdab0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
@@ -18,16 +18,12 @@
import android.view.Display;
import android.view.DisplayInfo;
import com.google.common.annotations.VisibleForTesting;
-
import java.lang.reflect.Field;
-import java.sql.Array;
-import java.text.Format;
import java.util.ArrayList;
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;
@@ -84,7 +80,7 @@
private static DisplayManagerGlobal newDisplayManagerGlobal(IDisplayManager displayManager) {
instance = Shadow.newInstanceOf(DisplayManagerGlobal.class);
DisplayManagerGlobalReflector displayManagerGlobal =
- reflector(DisplayManagerGlobalReflector.class, instance);
+ reflector(DisplayManagerGlobalReflector.class, instance);
displayManagerGlobal.setDm(displayManager);
displayManagerGlobal.setLock(new Object());
List<Handler> displayListeners = createDisplayListeners();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java
index 475607a..4f488b5 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java
@@ -84,6 +84,11 @@
}
@Implementation
+ protected static EGLContext eglGetCurrentContext() {
+ return createEglContext(3);
+ }
+
+ @Implementation
protected static boolean eglMakeCurrent(
EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
return true;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java
index 42f7711..2972ed7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java
@@ -10,6 +10,8 @@
import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.hardware.fingerprint.FingerprintManager.CryptoObject;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.os.Build.VERSION_CODES;
import android.os.CancellationSignal;
import android.os.Handler;
import android.util.Log;
@@ -183,4 +185,9 @@
protected boolean isHardwareDetected() {
return this.isHardwareDetected;
}
+
+ @Implementation(minSdk = VERSION_CODES.S)
+ protected List<FingerprintSensorPropertiesInternal> getSensorPropertiesInternal() {
+ return new ArrayList<>();
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java
index e76b797..da5070a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java
@@ -1011,10 +1011,20 @@
}
int checkPermission(String permission, int pid, int uid) {
- Set<String> grantedPermissionsForPidUid = grantedPermissionsMap.get(new Pair(pid, uid));
- return grantedPermissionsForPidUid != null && grantedPermissionsForPidUid.contains(permission)
- ? PERMISSION_GRANTED
- : PERMISSION_DENIED;
+ if (pid == -1) {
+ for (Map.Entry<Pair<Integer, Integer>, Set<String>> entry :
+ grantedPermissionsMap.entrySet()) {
+ if (entry.getKey().second == uid && entry.getValue().contains(permission)) {
+ return PERMISSION_GRANTED;
+ }
+ }
+ return PERMISSION_DENIED;
+ } else {
+ Set<String> grantedPermissionsForPidUid = grantedPermissionsMap.get(new Pair(pid, uid));
+ return grantedPermissionsForPidUid != null && grantedPermissionsForPidUid.contains(permission)
+ ? PERMISSION_GRANTED
+ : PERMISSION_DENIED;
+ }
}
void grantPermissions(String... permissionNames) {
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 4b28baf..0cbc9fb 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
@@ -49,10 +49,13 @@
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();
private final Map<UserHandle, Map<String, ApplicationInfo>> applicationInfoList = new HashMap<>();
+ private final Map<UserHandle, Map<String, Bundle>> suspendedPackageLauncherExtras =
+ new HashMap<>();
private final List<Pair<LauncherApps.Callback, Handler>> callbacks = new ArrayList<>();
private boolean hasShortcutHostPermission = false;
@@ -100,6 +103,17 @@
}
/**
+ * 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)}.
*
@@ -205,11 +219,36 @@
"Package " + packageName + " not found for user " + user.getIdentifier());
}
+ /**
+ * Adds a {@link Bundle} to be retrieved by {@link #getSuspendedPackageLauncherExtras(String,
+ * UserHandle)}.
+ *
+ * @param userHandle the user handle to be added.
+ * @param packageName the package name to be added.
+ * @param bundle the bundle for the extras.
+ */
+ public void addSuspendedPackageLauncherExtras(
+ UserHandle userHandle, String packageName, Bundle bundle) {
+ if (!suspendedPackageLauncherExtras.containsKey(userHandle)) {
+ suspendedPackageLauncherExtras.put(userHandle, new HashMap<>());
+ }
+ suspendedPackageLauncherExtras.get(userHandle).put(packageName, bundle);
+ }
+
@Implementation(minSdk = P)
@Nullable
- protected Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user) {
- throw new UnsupportedOperationException(
- "This method is not currently supported in Robolectric.");
+ protected Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user)
+ throws NameNotFoundException {
+ Map<String, Bundle> map = suspendedPackageLauncherExtras.get(user);
+ if (map != null && map.containsKey(packageName)) {
+ return map.get(packageName);
+ }
+
+ throw new NameNotFoundException(
+ "Suspended package extras for "
+ + packageName
+ + " not found for user "
+ + user.getIdentifier());
}
@Implementation(minSdk = Q)
@@ -219,10 +258,9 @@
"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/ShadowLoadedApk.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
index 2a3a61b..f954ac2 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 @@
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 @@
@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/ShadowLocationManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java
index 9cbf5ee..57fc6e9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java
@@ -390,16 +390,16 @@
if (locationProviderConstructor == null) {
if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) {
locationProviderConstructor =
- LocationProvider.class.getConstructor(
+ LocationProvider.class.getDeclaredConstructor(
String.class, android.location.provider.ProviderProperties.class);
} else {
locationProviderConstructor =
- LocationProvider.class.getConstructor(
+ LocationProvider.class.getDeclaredConstructor(
String.class,
Class.forName("com.android.internal.location.ProviderProperties"));
}
- locationProviderConstructor.setAccessible(true);
}
+ locationProviderConstructor.setAccessible(true);
if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) {
return locationProviderConstructor.newInstance(name, properties.getProviderProperties());
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaExtractor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaExtractor.java
index bd06be2..aa1763e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaExtractor.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaExtractor.java
@@ -2,6 +2,7 @@
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.O;
import static java.lang.Math.min;
import static org.robolectric.shadows.util.DataSource.toDataSource;
@@ -11,6 +12,7 @@
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
+import android.os.PersistableBundle;
import java.io.FileDescriptor;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -48,8 +50,10 @@
}
private static final Map<DataSource, List<TrackInfo>> tracksMap = new HashMap<>();
+ private static final Map<DataSource, PersistableBundle> metricsMap = new HashMap<>();
private List<TrackInfo> tracks;
+ private PersistableBundle metrics;
private int[] trackSampleReadPositions;
private int[] trackLastReadSize;
private int selectedTrackIndex = -1;
@@ -70,6 +74,15 @@
tracks.add(trackInfo);
}
+ /**
+ * Sets metrics for an associated {@link org.robolectric.shadows.util.DataSource}.
+ *
+ * @param metrics the data which will be returned by {@link MediaExtractor#getMetrics()}.
+ */
+ public static void setMetrics(DataSource dataSource, PersistableBundle metrics) {
+ metricsMap.put(dataSource, metrics);
+ }
+
private void setDataSource(DataSource dataSource) {
if (tracksMap.containsKey(dataSource)) {
this.tracks = tracksMap.get(dataSource);
@@ -81,6 +94,8 @@
Arrays.fill(trackSampleReadPositions, 0);
this.trackLastReadSize = new int[tracks.size()];
Arrays.fill(trackLastReadSize, 0);
+
+ this.metrics = metricsMap.get(dataSource);
}
@Implementation(minSdk = N)
@@ -203,9 +218,15 @@
selectedTrackIndex = -1;
}
+ @Implementation(minSdk = O)
+ protected PersistableBundle getMetrics() {
+ return metrics;
+ }
+
@Resetter
public static void reset() {
tracksMap.clear();
+ metricsMap.clear();
DataSource.reset();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMetadataRetriever.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMetadataRetriever.java
index b38da28..14de44a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMetadataRetriever.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMetadataRetriever.java
@@ -72,6 +72,11 @@
return (frames.containsKey(dataSource) ? frames.get(dataSource).get(timeUs) : null);
}
+ @Implementation
+ protected Bitmap getFrameAtTime() {
+ return getFrameAtTime(/* timeUs= */ 1, /* option= */ 0);
+ }
+
@Implementation(minSdk = O_MR1)
protected Bitmap getScaledFrameAtTime(long timeUs, int option, int dstWidth, int dstHeight) {
return (scaledFrames.containsKey(dataSource)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMuxer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMuxer.java
index 56e893c..ee0bdff 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMuxer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaMuxer.java
@@ -93,6 +93,12 @@
@Implementation
protected static long nativeSetup(@NonNull FileDescriptor fd, int format) throws IOException {
FileOutputStream outputStream = fdToStream.get(fd);
+ if (outputStream == null) {
+ // If this MediaMuxer was constructed with the un-shadowed MediaMuxer(FileDescriptor, int), no
+ // FileOutputStream will be associated with the FileDescriptor, so just create one.
+ outputStream = new FileOutputStream(fd);
+ fdToStream.put(fd, outputStream);
+ }
long potentialKey;
do {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
index cddbd44..0359bc3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
@@ -364,7 +364,12 @@
p.writeInt(realBitmap.getDensity());
p.writeBoolean(realBitmap.isMutable());
p.writeSerializable(realBitmap.getConfig());
- p.writeString(realBitmap.getColorSpace().getName());
+ ColorSpace colorSpace = realBitmap.getColorSpace();
+ boolean hasColorSpace = colorSpace != null;
+ p.writeBoolean(hasColorSpace);
+ if (hasColorSpace) {
+ p.writeString(colorSpace.getName());
+ }
p.writeBoolean(realBitmap.hasAlpha());
int[] pixels = new int[width * height];
realBitmap.getPixels(pixels, 0, width, 0, 0, width, height);
@@ -378,20 +383,28 @@
int density = p.readInt();
boolean mutable = p.readBoolean();
Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();
- String colorSpaceName = p.readString();
- boolean hasAlpha = p.readBoolean();
ColorSpace colorSpace = null;
- ColorSpace[] namedColorSpaces = reflector(ColorSpaceReflector.class).getNamedColorSpaces();
- for (ColorSpace named : namedColorSpaces) {
- if (named.getName().equals(colorSpaceName)) {
- colorSpace = named;
- break;
+ boolean hasColorSpace = p.readBoolean();
+ if (hasColorSpace) {
+ String colorSpaceName = p.readString();
+ ColorSpace[] namedColorSpaces = reflector(ColorSpaceReflector.class).getNamedColorSpaces();
+ for (ColorSpace named : namedColorSpaces) {
+ if (named.getName().equals(colorSpaceName)) {
+ colorSpace = named;
+ break;
+ }
}
}
+ boolean hasAlpha = p.readBoolean();
int[] parceledColors = new int[parceledHeight * parceledWidth];
p.readIntArray(parceledColors);
- Bitmap bitmap =
- Bitmap.createBitmap(parceledWidth, parceledHeight, parceledConfig, hasAlpha, colorSpace);
+ Bitmap bitmap;
+ if (colorSpace != null) {
+ bitmap =
+ Bitmap.createBitmap(parceledWidth, parceledHeight, parceledConfig, hasAlpha, colorSpace);
+ } else {
+ bitmap = Bitmap.createBitmap(parceledWidth, parceledHeight, parceledConfig, hasAlpha);
+ }
bitmap.setPixels(parceledColors, 0, parceledWidth, 0, 0, parceledWidth, parceledHeight);
bitmap.setDensity(density);
if (!mutable) {
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 7b045e8..b2da827 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
@@ -57,7 +57,7 @@
FontFamilyBuilderNatives.nAddFont(builderPtr, fontPtr);
}
- @Implementation(maxSdk=TIRAMISU)
+ @Implementation(maxSdk = TIRAMISU)
protected static long nBuild(
long builderPtr, String langTags, int variant, boolean isCustomFallback) {
return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
@@ -65,8 +65,12 @@
@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);
+ long builderPtr,
+ String langTags,
+ int variant,
+ boolean isCustomFallback,
+ boolean isDefaultFallback) {
+ return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
}
@Implementation
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePropertyValuesHolder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePropertyValuesHolder.java
index d9a8f7d..f5e9b8e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePropertyValuesHolder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePropertyValuesHolder.java
@@ -5,6 +5,7 @@
import android.animation.PropertyValuesHolder;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
import org.robolectric.nativeruntime.PropertyValuesHolderNatives;
import org.robolectric.shadows.ShadowNativePropertyValuesHolder.Picker;
@@ -12,6 +13,10 @@
@Implements(value = PropertyValuesHolder.class, minSdk = O, shadowPicker = Picker.class)
public class ShadowNativePropertyValuesHolder {
+ static {
+ DefaultNativeRuntimeLoader.injectAndLoad();
+ }
+
@Implementation
protected static long nGetIntMethod(Class<?> targetClass, String methodName) {
return PropertyValuesHolderNatives.nGetIntMethod(targetClass, methodName);
@@ -79,7 +84,7 @@
/** Shadow picker for {@link PropertyValuesHolder}. */
public static final class Picker extends GraphicsShadowPicker<Object> {
public Picker() {
- super(null, ShadowNativePropertyValuesHolder.class);
+ super(ShadowPropertyValuesHolder.class, ShadowNativePropertyValuesHolder.class);
}
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java
index 3c9f578..7977df1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID;
import static android.content.pm.PackageManager.GET_ACTIVITIES;
import static android.content.pm.PackageManager.GET_CONFIGURATIONS;
import static android.content.pm.PackageManager.GET_GIDS;
@@ -25,6 +26,7 @@
import static android.content.pm.PackageManager.SIGNATURE_NEITHER_SIGNED;
import static android.content.pm.PackageManager.SIGNATURE_NO_MATCH;
import static android.content.pm.PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
+import static android.content.pm.PackageManager.VERIFICATION_ALLOW;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.N;
@@ -34,6 +36,8 @@
import android.Manifest;
import android.annotation.UserIdInt;
+import android.app.Application;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -143,7 +147,13 @@
static final Map<String, Integer> uidForPackage = new HashMap<>();
static final Map<Integer, String> namesForUid = new HashMap<>();
static final Map<Integer, Integer> verificationResults = new HashMap<>();
+
+ @GuardedBy("lock")
static final Map<Integer, Long> verificationTimeoutExtension = new HashMap<>();
+
+ @GuardedBy("lock")
+ static final Map<Integer, Integer> verificationCodeAtTimeoutExtension = new HashMap<>();
+
static final Map<String, String> currentToCanonicalNames = new HashMap<>();
static final Map<String, String> canonicalToCurrentNames = new HashMap<>();
static final Map<ComponentName, ComponentState> componentList = new LinkedHashMap<>();
@@ -545,7 +555,6 @@
private static PersistableBundle deepCopyNullablePersistableBundle(PersistableBundle bundle) {
return bundle == null ? null : bundle.deepCopy();
}
-
}
static final Map<String, PackageSetting> packageSettings = new HashMap<>();
@@ -1008,11 +1017,31 @@
}
public long getVerificationExtendedTimeout(int id) {
- Long result = verificationTimeoutExtension.get(id);
- if (result == null) {
- return 0;
+ synchronized (lock) {
+ return verificationTimeoutExtension.getOrDefault(id, 0L);
}
- return result;
+ }
+
+ public int getVerificationCodeAtTimeoutExtension(int id) {
+ synchronized (lock) {
+ return verificationCodeAtTimeoutExtension.getOrDefault(id, VERIFICATION_ALLOW);
+ }
+ }
+
+ public void triggerInstallVerificationTimeout(Application appContext, int id) {
+ Intent intent = new Intent(Intent.ACTION_PACKAGE_VERIFIED);
+ intent.putExtra(EXTRA_VERIFICATION_ID, id);
+ intent.putExtra(
+ PackageManager.EXTRA_VERIFICATION_RESULT, getVerificationCodeAtTimeoutExtension(id));
+
+ // Send PACKAGE_VERIFIED broadcast to trigger the verification timeout.
+ // Replacement api does not return the actual receiver objects.
+ @SuppressWarnings("deprecation")
+ List<BroadcastReceiver> receivers =
+ ShadowApplication.getShadowInstrumentation().getReceiversForIntent(intent);
+ for (BroadcastReceiver receiver : receivers) {
+ receiver.onReceive(appContext, intent);
+ }
}
public void setShouldShowRequestPermissionRationale(String permission, boolean show) {
@@ -1694,6 +1723,7 @@
namesForUid.clear();
verificationResults.clear();
verificationTimeoutExtension.clear();
+ verificationCodeAtTimeoutExtension.clear();
currentToCanonicalNames.clear();
canonicalToCurrentNames.clear();
componentList.clear();
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 8fec646..16a7398 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 @@
}
@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/ShadowParcelFileDescriptor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java
index 3f27850..d898b43 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.KITKAT;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
@@ -13,7 +14,6 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
-import java.lang.reflect.Constructor;
import java.util.UUID;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
@@ -21,6 +21,7 @@
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
@@ -51,13 +52,15 @@
@Implementation
protected static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException {
- ParcelFileDescriptor pfd;
- try {
- Constructor<ParcelFileDescriptor> constructor =
- ParcelFileDescriptor.class.getDeclaredConstructor(FileDescriptor.class);
- pfd = constructor.newInstance(new FileDescriptor());
- } catch (Exception e) {
- throw new RuntimeException(e);
+ ParcelFileDescriptor pfd = null;
+ if (RuntimeEnvironment.getApiLevel() > JELLY_BEAN) {
+ pfd = new ParcelFileDescriptor(new FileDescriptor());
+ } else {
+ // In Jelly Bean, the ParcelFileDescriptor(FileDescriptor) constructor was non-public.
+ pfd =
+ ReflectionHelpers.callConstructor(
+ ParcelFileDescriptor.class,
+ ClassParameter.from(FileDescriptor.class, new FileDescriptor()));
}
ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(pfd);
shadowParcelFileDescriptor.file = new RandomAccessFile(file, getFileMode(mode));
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedChoreographer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedChoreographer.java
index 8a0a949..8d1f042 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedChoreographer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedChoreographer.java
@@ -3,9 +3,12 @@
import static org.robolectric.util.reflector.Reflector.reflector;
import android.view.Choreographer;
+import android.view.DisplayEventReceiver;
+import androidx.annotation.VisibleForTesting;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.Resetter;
+import org.robolectric.shadows.ShadowDisplayEventReceiver.DisplayEventReceiverReflector;
/**
* A {@link Choreographer} shadow for {@link LooperMode.Mode.PAUSED}.
@@ -25,4 +28,16 @@
public static void reset() {
reflector(ChoreographerReflector.class).getThreadInstance().remove();
}
+
+ /**
+ * Returns true if choreographer has been initialized properly.
+ *
+ * @return
+ */
+ @VisibleForTesting
+ boolean isInitialized() {
+ DisplayEventReceiver receiver =
+ reflector(ChoreographerReflector.class, realObject).getReceiver();
+ return reflector(DisplayEventReceiverReflector.class, receiver).getReceiverPtr() != 0;
+ }
}
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 416033b..5caf016 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
@@ -59,7 +59,16 @@
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);
}
@@ -307,7 +316,9 @@
return Duration.ZERO;
}
while (next != null) {
- when = shadowOfMsg(next).getWhen();
+ if (next.getTarget() != null) {
+ when = shadowOfMsg(next).getWhen();
+ }
next = shadowOfMsg(next).internalGetNext();
}
}
@@ -333,7 +344,9 @@
synchronized (realQueue) {
Message next = getMessages();
while (next != null) {
- count++;
+ if (next.getTarget() != null) {
+ count++;
+ }
next = shadowOfMsg(next).internalGetNext();
}
}
@@ -347,12 +360,24 @@
*/
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;
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java
index 8f031d8..61f3c3c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java
@@ -333,6 +333,18 @@
return (flags & FLAG_IMMUTABLE) > 0;
}
+ @Implementation
+ protected boolean isTargetedToPackage() {
+ // This is weird and we know it. See:
+ // https://googleplex-android.googlesource.com/platform/frameworks/base/+/f24a737c89de326199eb6d9f5912eae24b5514e6/services/core/java/com/android/server/am/ActivityManagerService.java#5377
+ for (Intent intent : savedIntents) {
+ if (intent.getPackage() != null && intent.getComponent() != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* @return {@code true} iff sending this PendingIntent will start an activity
* @deprecated prefer {@link #isActivity} which was added to {@link PendingIntent} in API 31
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java
new file mode 100644
index 0000000..cbac333
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java
@@ -0,0 +1,141 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.PixelCopy;
+import android.view.PixelCopy.OnPixelCopyFinishedListener;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow for PixelCopy that uses View.draw to create screenshots. The real PixelCopy performs a
+ * full hardware capture of the screen at the given location, which is impossible in Robolectric.
+ *
+ * <p>If listenerThread is backed by a paused looper, make sure to call ShadowLooper.idle() to
+ * ensure the screenshot finishes.
+ */
+@Implements(value = PixelCopy.class, minSdk = P)
+public class ShadowPixelCopy {
+ @Implementation
+ protected static void request(
+ @NonNull SurfaceView source,
+ @Nullable Rect srcRect,
+ @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ Activity activity = getActivity(source);
+ if (srcRect != null && srcRect.isEmpty()) {
+ throw new IllegalArgumentException("sourceRect is empty");
+ }
+ if (activity == null) {
+ throw new IllegalArgumentException("SourceView was not attached to an activity");
+ }
+ takeScreenshot(activity.getWindow(), dest, srcRect);
+ alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
+ }
+
+ @Implementation
+ protected static void request(
+ @NonNull Window source,
+ @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ takeScreenshot(source, dest, null);
+ alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
+ }
+
+ @Implementation
+ protected static void request(
+ @NonNull Window source,
+ @Nullable Rect srcRect,
+ @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ if (srcRect != null && srcRect.isEmpty()) {
+ throw new IllegalArgumentException("sourceRect is empty");
+ }
+ takeScreenshot(source, dest, srcRect);
+ alertFinished(listener, listenerThread, PixelCopy.SUCCESS);
+ }
+
+ private static void takeScreenshot(Window window, Bitmap screenshot, @Nullable Rect srcRect) {
+ validateBitmap(screenshot);
+
+ // Draw the view to a bitmap in the canvas that is the size of the view itself.
+ View decorView = window.getDecorView();
+ Bitmap bitmap =
+ Bitmap.createBitmap(decorView.getWidth(), decorView.getHeight(), Bitmap.Config.ARGB_8888);
+ Canvas screenshotCanvas = new Canvas(bitmap);
+ decorView.draw(screenshotCanvas);
+
+ Rect dst = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight());
+
+ Canvas resizingCanvas = new Canvas(screenshot);
+ Paint paint = new Paint();
+ resizingCanvas.drawBitmap(bitmap, srcRect, dst, paint);
+ }
+
+ private static void alertFinished(
+ OnPixelCopyFinishedListener listener, Handler listenerThread, int result) {
+ if (listenerThread.getLooper() == Looper.getMainLooper()) {
+ listener.onPixelCopyFinished(result);
+ return;
+ }
+ listenerThread.post(() -> listener.onPixelCopyFinished(result));
+ }
+
+ private static void validateBitmap(Bitmap bitmap) {
+ if (bitmap == null) {
+ throw new IllegalArgumentException("Bitmap cannot be null");
+ }
+ if (bitmap.isRecycled()) {
+ throw new IllegalArgumentException("Bitmap is recycled");
+ }
+ if (!bitmap.isMutable()) {
+ throw new IllegalArgumentException("Bitmap is immutable");
+ }
+ }
+
+ private static Activity getActivity(Context context) {
+ if (context instanceof Activity) {
+ return (Activity) context;
+ } else if (context instanceof ContextWrapper) {
+ return getActivity(((ContextWrapper) context).getBaseContext());
+ } else {
+ return null;
+ }
+ }
+
+ private static Activity getActivity(View view) {
+ Activity activity = getActivity(view.getContext());
+ if (activity != null) {
+ return activity;
+ }
+
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ if (viewGroup.getChildCount() > 0) {
+ // getActivity is known to fail if View is a DecorView such as specified via espresso's
+ // isRoot().
+ // Make another attempt to find the activity from its first child view
+ return getActivity(viewGroup.getChildAt(0).getContext());
+ }
+ }
+ return null;
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPropertyValuesHolder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPropertyValuesHolder.java
new file mode 100644
index 0000000..1e52981
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPropertyValuesHolder.java
@@ -0,0 +1,50 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.animation.PropertyValuesHolder;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+
+/**
+ * Shadow for {@link PropertyValuesHolder} that works around the ART/JVM differences of accessing
+ * methods.
+ */
+@Implements(value = PropertyValuesHolder.class, isInAndroidSdk = false)
+public class ShadowPropertyValuesHolder {
+
+ @RealObject PropertyValuesHolder realPropertyValuesHolder;
+
+ @Implementation
+ protected Method setupSetterOrGetter(
+ Class<?> targetClass,
+ HashMap<Class<?>, HashMap<String, Method>> propertyMapMap,
+ String prefix,
+ Class<?> valueType) {
+ Method result =
+ reflector(PropertyValuesHolderReflector.class, realPropertyValuesHolder)
+ .setupSetterOrGetter(targetClass, propertyMapMap, prefix, valueType);
+ if (result != null && Modifier.isPublic(result.getModifiers())) {
+ // ART allows calling public methods on private or package-private classes, but the
+ // JVM does not. Calling setAccessible(true) is required.
+ result.setAccessible(true);
+ }
+ return result;
+ }
+
+ @ForType(PropertyValuesHolder.class)
+ interface PropertyValuesHolderReflector {
+ @Direct
+ Method setupSetterOrGetter(
+ Class<?> targetClass,
+ HashMap<Class<?>, HashMap<String, Method>> propertyMapMap,
+ String prefix,
+ Class<?> valueType);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScrollView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScrollView.java
index a96c00e..d7126d2 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScrollView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScrollView.java
@@ -16,7 +16,7 @@
@Implementation
protected void smoothScrollTo(int x, int y) {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
reflector(ScrollViewReflector.class, realScrollView).smoothScrollTo(x, y);
} else {
scrollTo(x, y);
@@ -25,7 +25,7 @@
@Implementation
protected void smoothScrollBy(int x, int y) {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
reflector(ScrollViewReflector.class, realScrollView).smoothScrollBy(x, y);
} else {
scrollBy(x, y);
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 cbf52ae..2785e11 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
@@ -34,6 +34,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.IBluetooth;
import android.bluetooth.IBluetoothManager;
+import android.companion.ICompanionDeviceManager;
import android.content.Context;
import android.content.IClipboard;
import android.content.IRestrictionsManager;
@@ -88,6 +89,7 @@
import android.view.translation.ITranslationManager;
import com.android.internal.app.IAppOpsService;
import com.android.internal.app.IBatteryStats;
+import com.android.internal.app.ISoundTriggerService;
import com.android.internal.appwidget.IAppWidgetService;
import com.android.internal.os.IDropBoxManagerService;
import com.android.internal.statusbar.IStatusBar;
@@ -167,6 +169,7 @@
}
if (RuntimeEnvironment.getApiLevel() >= N) {
addBinderService(Context.CONTEXTHUB_SERVICE, IContextHubService.class);
+ addBinderService(Context.SOUND_TRIGGER_SERVICE, ISoundTriggerService.class);
}
if (RuntimeEnvironment.getApiLevel() >= N_MR1) {
addBinderService(Context.SHORTCUT_SERVICE, IShortcutService.class);
@@ -175,6 +178,7 @@
addBinderService("mount", IStorageManager.class);
addBinderService(Context.WIFI_AWARE_SERVICE, IWifiAwareManager.class);
addBinderService(Context.STORAGE_STATS_SERVICE, IStorageStatsManager.class);
+ addBinderService(Context.COMPANION_DEVICE_SERVICE, ICompanionDeviceManager.class);
} else {
addBinderService("mount", "android.os.storage.IMountService");
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundTriggerManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundTriggerManager.java
new file mode 100644
index 0000000..8983213
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundTriggerManager.java
@@ -0,0 +1,36 @@
+package org.robolectric.shadows;
+
+import android.annotation.RequiresPermission;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.os.Build.VERSION_CODES;
+import javax.annotation.Nullable;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** A Shadow SoundTriggerManager in Android O+. */
+@Implements(
+ className = "android.media.soundtrigger.SoundTriggerManager",
+ minSdk = VERSION_CODES.N,
+ isInAndroidSdk = false)
+public final class ShadowSoundTriggerManager {
+ private SoundTrigger.ModuleProperties moduleProperties = null;
+
+ /**
+ * Set {@code SoundTrigger.ModuleProperties}, value will returned for the following {@code
+ * getModuleProperties} call.
+ */
+ public void setModuleProperties(@Nullable SoundTrigger.ModuleProperties moduleProperties) {
+ this.moduleProperties = moduleProperties;
+ }
+
+ @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+ @Implementation(minSdk = VERSION_CODES.R)
+ protected SoundTrigger.ModuleProperties getModuleProperties() {
+ if (RuntimeEnvironment.getApiLevel() == VERSION_CODES.R && moduleProperties == null) {
+ throw new NullPointerException(
+ "Throw NullPointException in Android R when moduleProperties is null.");
+ }
+ return moduleProperties;
+ }
+}
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 fb5c1c2..1b239d1 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.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 @@
}
/**
+ * 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 @@
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
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 63d12e6..49a93bc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
@@ -84,11 +84,7 @@
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
- }
+ surfaceControlReflector.setFreeNativeResources(() -> {});
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemServiceRegistry.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemServiceRegistry.java
index f8e7bc6..9ab967e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemServiceRegistry.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemServiceRegistry.java
@@ -54,7 +54,7 @@
static _ServiceFetcher_ get(String key, Object serviceFetcher) {
String serviceFetcherClassName = getConcreteClassName(serviceFetcher);
if (serviceFetcherClassName == null) {
- throw new IllegalStateException("no idea what to do with " + key + " " + serviceFetcher);
+ throw new IllegalStateException("could not find class name for serviceFetcher " + key);
}
switch (serviceFetcherClassName) {
@@ -69,8 +69,15 @@
default:
if (key.equals(Context.INPUT_METHOD_SERVICE)) {
return o -> {}; // handled by ShadowInputMethodManager.reset()
+ } else if (key.equals(Context.INPUT_SERVICE)) {
+ return o -> {}; // handled by ShadowInputManager.reset()
}
- throw new IllegalStateException("no idea what to do with " + key + " " + serviceFetcher);
+ throw new IllegalStateException(
+ "did not recognize serviceFetcher class name "
+ + serviceFetcherClassName
+ + " for key '"
+ + key
+ + "'");
}
}
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 cce1990..41fbf4e 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.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.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 @@
@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/ShadowTelecomManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java
index 3fd351f..ef52021 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java
@@ -6,6 +6,7 @@
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.R;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Verify.verifyNotNull;
import android.annotation.SystemApi;
@@ -615,6 +616,7 @@
@Implementation(minSdk = M)
@HiddenApi
public void enablePhoneAccount(PhoneAccountHandle handle, boolean isEnabled) {
+ checkNotNull(getPhoneAccount(handle)).setIsEnabled(isEnabled);
}
/**
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 0ab3730..048abf0 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.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,6 +60,7 @@
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;
@@ -155,6 +157,7 @@
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
@@ -172,6 +175,7 @@
@Resetter
public static void reset() {
callComposerStatus = 0;
+ emergencyNumbersList = null;
}
@Implementation(minSdk = S)
@@ -1411,4 +1415,25 @@
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/ShadowTimeManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java
index 7a77328..56b7eb0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java
@@ -1,13 +1,19 @@
package org.robolectric.shadows;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
+
import android.annotation.SystemApi;
import android.app.time.Capabilities;
import android.app.time.Capabilities.CapabilityState;
import android.app.time.ExternalTimeSuggestion;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.app.time.TelephonyTimeZoneAlgorithmStatus;
import android.app.time.TimeManager;
import android.app.time.TimeZoneCapabilities;
import android.app.time.TimeZoneCapabilitiesAndConfig;
import android.app.time.TimeZoneConfiguration;
+import android.app.time.TimeZoneDetectorStatus;
import android.os.Build.VERSION_CODES;
import android.os.UserHandle;
import java.util.Objects;
@@ -25,10 +31,19 @@
private TimeZoneCapabilities timeZoneCapabilities =
new TimeZoneCapabilities.Builder(UserHandle.CURRENT)
.setConfigureAutoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED)
+ .setUseLocationEnabled(true)
.setConfigureGeoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED)
- .setSuggestManualTimeZoneCapability(Capabilities.CAPABILITY_POSSESSED)
+ .setSetManualTimeZoneCapability(Capabilities.CAPABILITY_POSSESSED)
.build();
+ private TimeZoneDetectorStatus detectorStatus =
+ new TimeZoneDetectorStatus(
+ DETECTOR_STATUS_RUNNING,
+ new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING),
+ new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+ LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null,
+ LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null));
+
private TimeZoneConfiguration timeZoneConfiguration;
/**
@@ -54,7 +69,8 @@
protected TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig() {
Objects.requireNonNull(timeZoneConfiguration, "timeZoneConfiguration was not set");
- return new TimeZoneCapabilitiesAndConfig(timeZoneCapabilities, timeZoneConfiguration);
+ return new TimeZoneCapabilitiesAndConfig(
+ detectorStatus, timeZoneCapabilities, timeZoneConfiguration);
}
@Implementation
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 00ad658..4eb7f18 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
@@ -19,6 +19,7 @@
import static org.robolectric.util.reflector.Reflector.reflector;
import android.Manifest.permission;
+import android.accounts.Account;
import android.annotation.UserIdInt;
import android.app.Application;
import android.content.Context;
@@ -39,6 +40,7 @@
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -81,6 +83,7 @@
@RealObject private UserManager realObject;
private UserManagerState userManagerState;
private Boolean managedProfile;
+ private Boolean cloneProfile;
private boolean userUnlocked = true;
private boolean isSystemUser = true;
@@ -97,6 +100,8 @@
private boolean enforcePermissions;
private int userSwitchability = UserManager.SWITCHABILITY_STATUS_OK;
+ private final Set<Account> userAccounts = new HashSet<>();
+
/**
* Global UserManager state. Shared across {@link UserManager}s created in different {@link
* Context}s.
@@ -122,10 +127,13 @@
private int nextUserId = DEFAULT_SECONDARY_USER_ID;
+ // TODO: use UserInfo.FLAG_MAIN when available
+ private static final int FLAG_MAIN = 0x00004000;
+
public UserManagerState() {
int id = UserHandle.USER_SYSTEM;
String name = "system_user";
- int flags = UserInfo.FLAG_PRIMARY | UserInfo.FLAG_ADMIN;
+ int flags = UserInfo.FLAG_PRIMARY | UserInfo.FLAG_ADMIN | FLAG_MAIN;
userSerialNumbers.put(id, (long) id);
// Start the user as shut down.
@@ -298,15 +306,20 @@
}
}
- /** Add a profile to be returned by {@link #getProfiles(int)}.* */
+ /** Add a profile to be returned by {@link #getProfiles(int)}. */
public void addProfile(
int userHandle, int profileUserHandle, String profileName, int profileFlags) {
+ UserInfo profileUserInfo = new UserInfo(profileUserHandle, profileName, profileFlags);
+ addProfile(userHandle, profileUserHandle, profileUserInfo);
+ }
+
+ /** Add a profile to be returned by {@link #getProfiles(int)}. */
+ public void addProfile(int userHandle, int profileUserHandle, UserInfo profileUserInfo) {
// Don't override serial number set by setSerialNumberForUser()
if (!userManagerState.userSerialNumbers.containsKey(profileUserHandle)) {
// use UserHandle id as serial number unless setSerialNumberForUser() is used
userManagerState.userSerialNumbers.put(profileUserHandle, (long) profileUserHandle);
}
- UserInfo profileUserInfo = new UserInfo(profileUserHandle, profileName, profileFlags);
if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) {
profileUserInfo.profileGroupId = userHandle;
UserInfo parentUserInfo = getUserInfo(userHandle);
@@ -401,6 +414,34 @@
this.managedProfile = managedProfile;
}
+ /**
+ * If permissions are enforced (see {@link #enforcePermissionChecks(boolean)}) and the application
+ * doesn't have the {@link android.Manifest.permission#MANAGE_USERS} permission, throws a {@link
+ * SecurityManager} exception.
+ *
+ * @return true if the user is clone, or the value specified via {@link #setCloneProfile(boolean)}
+ * @see #enforcePermissionChecks(boolean)
+ * @see #setCloneProfile(boolean)
+ */
+ @Implementation(minSdk = S)
+ protected boolean isCloneProfile() {
+ if (enforcePermissions && !hasManageUsersPermission()) {
+ throw new SecurityException("You need MANAGE_USERS permission to: check isCloneProfile");
+ }
+
+ if (cloneProfile != null) {
+ return cloneProfile;
+ }
+
+ UserInfo info = getUserInfo(context.getUserId());
+ return info != null && info.isCloneProfile();
+ }
+
+ /** Setter for {@link UserManager#isCloneProfile()}. */
+ public void setCloneProfile(boolean cloneProfile) {
+ this.cloneProfile = cloneProfile;
+ }
+
@Implementation(minSdk = R)
protected boolean isProfile() {
if (enforcePermissions && !hasManageUsersPermission()) {
@@ -1195,4 +1236,19 @@
@Accessor("mUserId")
void setUserId(int userId);
}
+
+ @Implementation(minSdk = TIRAMISU)
+ protected boolean someUserHasAccount(String accountName, String accountType) {
+ return userAccounts.contains(new Account(accountName, accountType));
+ }
+
+ /** Setter for {@link UserManager#someUserHasAccount(String, String)}. */
+ public void setSomeUserHasAccount(String accountName, String accountType) {
+ userAccounts.add(new Account(accountName, accountType));
+ }
+
+ /** Removes user account set via {@link #setSomeUserHasAccount(String, String)}. */
+ public void removeSomeUserHasAccount(String accountName, String accountType) {
+ userAccounts.remove(new Account(accountName, accountType));
+ }
}
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 b66a0a4..276a31d 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 @@
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 @@
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 @@
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 @@
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 5d06f58..5d4f329 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
@@ -36,6 +36,7 @@
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.Transformation;
+import com.google.common.annotations.Beta;
import com.google.common.collect.ImmutableList;
import java.io.PrintStream;
import java.util.ArrayList;
@@ -44,12 +45,15 @@
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.GraphicsMode;
+import org.robolectric.annotation.GraphicsMode.Mode;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.annotation.Resetter;
+import org.robolectric.config.ConfigurationRegistry;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.TimeUtils;
@@ -575,7 +579,7 @@
@Implementation
protected void scrollTo(int x, int y) {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
reflector(_View_.class, realView).scrollTo(x, y);
} else {
reflector(_View_.class, realView)
@@ -588,7 +592,7 @@
@Implementation
protected void scrollBy(int x, int y) {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
reflector(_View_.class, realView).scrollBy(x, y);
} else {
scrollTo(getScrollX() + x, getScrollY() + y);
@@ -597,7 +601,7 @@
@Implementation
protected int getScrollX() {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
return reflector(_View_.class, realView).getScrollX();
} else {
return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
@@ -606,7 +610,7 @@
@Implementation
protected int getScrollY() {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
return reflector(_View_.class, realView).getScrollY();
} else {
return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
@@ -615,7 +619,7 @@
@Implementation
protected void setScrollX(int scrollX) {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
reflector(_View_.class, realView).setScrollX(scrollX);
} else {
scrollTo(scrollX, scrollToCoordinates.y);
@@ -624,7 +628,7 @@
@Implementation
protected void setScrollY(int scrollY) {
- if (useRealGraphics()) {
+ if (useRealScrolling()) {
reflector(_View_.class, realView).setScrollY(scrollY);
} else {
scrollTo(scrollToCoordinates.x, scrollY);
@@ -1054,7 +1058,26 @@
void setWindowId(WindowId windowId);
}
- static boolean useRealGraphics() {
- return Boolean.getBoolean("robolectric.nativeruntime.enableGraphics");
+ /**
+ * Internal API to determine if native graphics is enabled.
+ *
+ * <p>This is currently public because it has to be accessed from multiple packages, but it is not
+ * recommended to depend on this API.
+ */
+ @Beta
+ public static boolean useRealGraphics() {
+ GraphicsMode.Mode graphicsMode = ConfigurationRegistry.get(GraphicsMode.Mode.class);
+ return graphicsMode == Mode.NATIVE && RuntimeEnvironment.getApiLevel() >= O;
+ }
+
+ /**
+ * Currently the default View scrolling implementation is broken and low-fidelty. For instance,
+ * even if a View has no children, Robolectric will still happily set the scroll position of a
+ * View. Long-term we want to eliminate this broken behavior, but in the mean time the real
+ * scrolling behavior is enabled when native graphics are enabled, or when a system property is
+ * set.
+ */
+ static boolean useRealScrolling() {
+ return useRealGraphics() || Boolean.getBoolean("robolectric.useRealScrolling");
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
index cfe2bc7..2dd2e9a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
@@ -123,7 +123,30 @@
}
public void callDispatchResized() {
- if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.S_V2) {
+ if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) {
+ Display display = getDisplay();
+ Rect frame = new Rect();
+ display.getRectSize(frame);
+
+ ClientWindowFrames frames = new ClientWindowFrames();
+ // set the final field
+ ReflectionHelpers.setField(frames, "frame", frame);
+
+ ReflectionHelpers.callInstanceMethod(
+ ViewRootImpl.class,
+ realObject,
+ "dispatchResized",
+ ClassParameter.from(ClientWindowFrames.class, frames),
+ ClassParameter.from(boolean.class, true), /* reportDraw */
+ ClassParameter.from(
+ MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */
+ ClassParameter.from(InsetsState.class, new InsetsState()), /* insetsState */
+ ClassParameter.from(boolean.class, false), /* forceLayout */
+ ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */
+ ClassParameter.from(int.class, 0), /* displayId */
+ ClassParameter.from(int.class, 0), /* syncSeqId */
+ ClassParameter.from(boolean.class, false) /* dragResizing */);
+ } else if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.S_V2) {
Display display = getDisplay();
Rect frame = new Rect();
display.getRectSize(frame);
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 706d86e..1a8131f 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 @@
*
* @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 7221e69..cc6cfcc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -9,6 +9,7 @@
import static android.os.Build.VERSION_CODES.TIRAMISU;
import static java.util.stream.Collectors.toList;
+import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
@@ -19,6 +20,7 @@
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.AddNetworkResult;
import android.net.wifi.WifiManager.MulticastLock;
import android.net.wifi.WifiSsid;
import android.net.wifi.WifiUsabilityStatsEntry;
@@ -58,6 +60,7 @@
private static float sSignalLevelInPercent = 1f;
private boolean accessWifiStatePermission = true;
+ private boolean changeWifiStatePermission = true;
private int wifiState = WifiManager.WIFI_STATE_ENABLED;
private boolean wasSaved = false;
private WifiInfo wifiInfo;
@@ -151,6 +154,19 @@
return scanResults;
}
+ /**
+ * The original implementation allows this to be called by the Device Owner (DO), Profile Owner
+ * (PO), callers with carrier privilege and system apps, but this shadow can be called by all apps
+ * carrying the ACCESS_WIFI_STATE permission.
+ *
+ * <p>This shadow is a wrapper for getConfiguredNetworks() and does not actually check the caller.
+ */
+ @Implementation(minSdk = S)
+ protected List<WifiConfiguration> getCallerConfiguredNetworks() {
+ checkAccessWifiStatePermission();
+ return getConfiguredNetworks();
+ }
+
@Implementation
protected List<WifiConfiguration> getConfiguredNetworks() {
final ArrayList<WifiConfiguration> wifiConfigurations = new ArrayList<>();
@@ -176,6 +192,21 @@
return networkId;
}
+ /**
+ * The new version of {@link #addNetwork(WifiConfiguration)} which returns a more detailed failure
+ * codes. The original implementation of this API is limited to Device Owner (DO), Profile Owner
+ * (PO), system app, and privileged apps but this shadow can be called by all apps.
+ */
+ @Implementation(minSdk = S)
+ protected AddNetworkResult addNetworkPrivileged(WifiConfiguration config) {
+ if (config == null) {
+ throw new IllegalArgumentException("config cannot be null");
+ }
+
+ int networkId = addNetwork(config);
+ return new AddNetworkResult(AddNetworkResult.STATUS_SUCCESS, networkId);
+ }
+
@Implementation
protected boolean removeNetwork(int netId) {
networkIdToConfiguredNetworks.remove(netId);
@@ -183,6 +214,21 @@
}
/**
+ * Removes all configured networks regardless of the app that created the network. Can only be
+ * called by a Device Owner (DO) app.
+ *
+ * @return {@code true} if at least one network is removed, {@code false} otherwise
+ */
+ @Implementation(minSdk = S)
+ protected boolean removeNonCallerConfiguredNetworks() {
+ checkChangeWifiStatePermission();
+ checkDeviceOwner();
+ int previousSize = networkIdToConfiguredNetworks.size();
+ networkIdToConfiguredNetworks.clear();
+ return networkIdToConfiguredNetworks.size() < previousSize;
+ }
+
+ /**
* Adds or updates a network which can later be retrieved with {@link #getWifiConfiguration(int)}
* method. A null {@param config}, or one with a networkId less than 0, or a networkId that had
* its updatePermission removed using the {@link #setUpdateNetworkPermission(int, boolean)} will
@@ -365,6 +411,10 @@
this.accessWifiStatePermission = accessWifiStatePermission;
}
+ public void setChangeWifiStatePermission(boolean changeWifiStatePermission) {
+ this.changeWifiStatePermission = changeWifiStatePermission;
+ }
+
/**
* Prevents a networkId from being updated using the {@link updateNetwork(WifiConfiguration)}
* method. This is to simulate the case where a separate application creates a network, and the
@@ -409,7 +459,21 @@
private void checkAccessWifiStatePermission() {
if (!accessWifiStatePermission) {
- throw new SecurityException();
+ throw new SecurityException("Caller does not hold ACCESS_WIFI_STATE permission");
+ }
+ }
+
+ private void checkChangeWifiStatePermission() {
+ if (!changeWifiStatePermission) {
+ throw new SecurityException("Caller does not hold CHANGE_WIFI_STATE permission");
+ }
+ }
+
+ private void checkDeviceOwner() {
+ if (!getContext()
+ .getSystemService(DevicePolicyManager.class)
+ .isDeviceOwnerApp(getContext().getPackageName())) {
+ throw new SecurityException("Caller is not device owner");
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/SystemFeatureListInitializer.java b/shadows/framework/src/main/java/org/robolectric/shadows/SystemFeatureListInitializer.java
index 23dc71c..ee6b903 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/SystemFeatureListInitializer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/SystemFeatureListInitializer.java
@@ -19,10 +19,12 @@
if (apiLevel >= VERSION_CODES.O) {
features.put(PackageManager.FEATURE_WIFI_AWARE, true);
+ features.put(PackageManager.FEATURE_COMPANION_DEVICE_SETUP, true);
}
if (apiLevel >= VERSION_CODES.P) {
features.put(PackageManager.FEATURE_WIFI_DIRECT, true);
+ features.put(PackageManager.FEATURE_WIFI_RTT, true);
}
return ImmutableMap.copyOf(features);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
index fc63fc9..4c38710 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
@@ -2,6 +2,7 @@
import android.net.wifi.WifiUsabilityStatsEntry;
import android.net.wifi.WifiUsabilityStatsEntry.ContentionTimeStats;
+import android.net.wifi.WifiUsabilityStatsEntry.LinkStats;
import android.net.wifi.WifiUsabilityStatsEntry.RadioStats;
import android.net.wifi.WifiUsabilityStatsEntry.RateStats;
import android.os.Build.VERSION_CODES;
@@ -152,7 +153,9 @@
cellularDataNetworkType,
cellularSignalStrengthDbm,
cellularSignalStrengthDb,
- isSameRegisteredCell);
+ isSameRegisteredCell,
+ new SparseArray<LinkStats>());
+
}
}
diff --git a/shadows/httpclient/build.gradle b/shadows/httpclient/build.gradle
index 332c83e..4e77a09 100644
--- a/shadows/httpclient/build.gradle
+++ b/shadows/httpclient/build.gradle
@@ -21,17 +21,17 @@
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 c3abbba..b009838 100644
--- a/shadows/playservices/build.gradle
+++ b/shadows/playservices/build.gradle
@@ -14,25 +14,20 @@
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 0000000..b630e22
--- /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 0000000..68a8fb7
--- /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 0000000..316365b
--- /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 0000000..6314518
--- /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 0000000..95b2c42
--- /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 0000000..dba93ca
--- /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 0000000..65383ac
--- /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/Android.bp b/testapp/Android.bp
new file mode 100644
index 0000000..a11ba44
--- /dev/null
+++ b/testapp/Android.bp
@@ -0,0 +1,41 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "GlobalRobolectricAssetsLib",
+ asset_dirs: ["src/main/assets"],
+ resource_dirs: ["src/main/res"],
+ min_sdk_version: "16",
+ target_sdk_version: "33",
+ platform_apis: true,
+ manifest: "src/main/AndroidManifest.xml",
+ optimize: {
+ enabled: false
+ },
+}
+
+android_app {
+ name: "GlobalRobolectricTestStub",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ static_libs: ["GlobalRobolectricAssetsLib"],
+ manifest: "src/main/AndroidManifest.xml",
+ aaptflags: [
+ "--extra-packages",
+ "org.robolectric.testapp",
+ ],
+ dont_merge_manifests: true,
+ platform_apis: true,
+ system_ext_specific: true,
+ min_sdk_version: "16",
+ target_sdk_version: "33",
+ certificate: "platform",
+ privileged: true,
+ resource_dirs: ["src/main/res"],
+ kotlincflags: ["-Xjvm-default=all"],
+
+ plugins: ["dagger2-compiler"],
+}
diff --git a/testapp/build.gradle b/testapp/build.gradle
index 651ced0..0abf895 100644
--- a/testapp/build.gradle
+++ b/testapp/build.gradle
@@ -2,6 +2,7 @@
android {
compileSdk 33
+ namespace 'org.robolectric.testapp'
defaultConfig {
minSdk 16
diff --git a/utils/build.gradle b/utils/build.gradle
index c10cca2..c31c9a0 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 @@
}
}
-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 @@
// 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 @@
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 3027345..140e987 100644
--- a/utils/reflector/build.gradle
+++ b/utils/reflector/build.gradle
@@ -5,12 +5,12 @@
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/src/main/java/org/robolectric/util/Util.java b/utils/src/main/java/org/robolectric/util/Util.java
old mode 100755
new mode 100644