diff options
184 files changed, 7471 insertions, 771 deletions
diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml index 96b4b3307..9762b9e51 100644 --- a/.github/workflows/gradle_tasks_validation.yml +++ b/.github/workflows/gradle_tasks_validation.yml @@ -15,6 +15,23 @@ permissions: contents: read jobs: + run_checkForApiChanges: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 11 + + - uses: gradle/gradle-build-action@v2 + + - name: Run checkForApiChanges + run: ./gradlew checkForApiChanges + run_aggregateDocs: runs-on: ubuntu-20.04 @@ -30,7 +47,7 @@ jobs: - uses: gradle/gradle-build-action@v2 - name: Run aggregateDocs - run: SKIP_NATIVERUNTIME_BUILD=true ./gradlew clean aggregateDocs # building the native runtime is not required for checking javadoc + run: ./gradlew clean aggregateDocs run_instrumentAll: runs-on: ubuntu-20.04 @@ -50,7 +67,7 @@ jobs: - uses: gradle/gradle-build-action@v2 - name: Run :preinstrumented:instrumentAll - run: SKIP_NATIVERUNTIME_BUILD=true ./gradlew :preinstrumented:instrumentAll + run: ./gradlew :preinstrumented:instrumentAll - name: Run :preinstrumented:instrumentAll with SDK 33 - run: SKIP_NATIVERUNTIME_BUILD=true PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll + run: PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d83e804a7..b47f37522 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -101,7 +101,7 @@ jobs: run: | TARGET="google_apis" echo "TARGET=$TARGET" >> $GITHUB_OUTPUT - + - name: AVD cache uses: actions/cache@v3 id: avd-cache @@ -147,3 +147,25 @@ jobs: path: | **/build/reports/* **/build/outputs/*/connected/* + + publish-to-snapshots: + runs-on: ubuntu-20.04 + env: + SONATYPE_LOGIN: ${{ secrets.SONATYPE_LOGIN }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + needs: unit-tests + if: github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 11 + + - uses: gradle/gradle-build-action@v2 + + - name: Publish + run: | + ./gradlew publish --stacktrace --no-watch-fs diff --git a/Android.bp b/Android.bp index 319b5e3df..2487cafaf 100644 --- a/Android.bp +++ b/Android.bp @@ -13,7 +13,10 @@ // limitations under the License. package { - default_visibility: [":__subpackages__"], + default_visibility: [ + "//external/robolectric:__subpackages__", + "//test/robolectric-extensions:__subpackages__", + ], default_applicable_licenses: ["external_robolectric_license"], } @@ -33,7 +36,10 @@ package { // See: http://go/android-license-faq license { name: "external_robolectric_license", - visibility: [":__subpackages__"], + visibility: [ + ":__subpackages__", + "//test/robolectric-extensions:__subpackages__", + ], license_kinds: [ "SPDX-license-identifier-Apache-2.0", "SPDX-license-identifier-MIT", @@ -111,6 +117,7 @@ java_library_host { visibility: [ ":__subpackages__", "//prebuilts/misc/common/robolectric", + "//test/robolectric-extensions:__subpackages__", ], } @@ -133,10 +140,12 @@ java_library_host { name: "Robolectric_all_upstream", static_libs: [ + "Robolectric-aosp-plugins", "robolectric_meta_service_file", "Robolectric_shadows_httpclient_upstream", "Robolectric_shadows_framework_upstream", "Robolectric_shadows_multidex_upstream", + "Robolectric_shadows_versioning_upstream", "Robolectric_robolectric_upstream", "Robolectric_annotations_upstream", "Robolectric_resources_upstream", @@ -40,7 +40,7 @@ If you'd like to start a new project with Robolectric tests you can refer to `de ```groovy testImplementation "junit:junit:4.13.2" -testImplementation "org.robolectric:robolectric:4.10-alpha-1" +testImplementation "org.robolectric:robolectric:4.10.3" ``` ## Building And Contributing @@ -79,18 +79,3 @@ Run compatibility test suites on opening Emulator: ./gradlew connectedCheck -### Using Snapshots - -If you would like to live on the bleeding edge, you can try running against a snapshot build. Keep in mind that snapshots represent the most recent changes on master and may contain bugs. - -#### build.gradle: - -```groovy -repositories { - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } -} - -dependencies { - testImplementation "org.robolectric:robolectric:4.10-SNAPSHOT" -} -``` diff --git a/annotations/build.gradle b/annotations/build.gradle index 65b4f06bb..d8bd113c5 100644 --- a/annotations/build.gradle +++ b/annotations/build.gradle @@ -5,6 +5,6 @@ apply plugin: RoboJavaModulePlugin apply plugin: DeployedRoboJavaModulePlugin dependencies { - compileOnly "com.google.code.findbugs:jsr305:3.0.2" + compileOnly libs.findbugs.jsr305 compileOnly AndroidSdk.MAX_SDK.coordinates } diff --git a/build.gradle b/build.gradle index 1eb662b1f..f3ab387aa 100644 --- a/build.gradle +++ b/build.gradle @@ -7,18 +7,15 @@ buildscript { google() mavenCentral() gradlePluginPortal() - maven { - url "https://plugins.gradle.org/m2/" - } } dependencies { gradle - classpath 'com.android.tools.build:gradle:7.4.2' - classpath 'net.ltgt.gradle:gradle-errorprone-plugin:3.0.1' - classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath "com.diffplug.spotless:spotless-plugin-gradle:6.17.0" + classpath libs.android.gradle + classpath libs.error.prone.gradle + classpath libs.aggregate.javadocs.gradle + classpath libs.kotlin.gradle + classpath libs.spotless.gradle } } @@ -120,7 +117,7 @@ task aggregateDocs { dependsOn ':aggregateJsondocs' } -task prefetchSdks() { +tasks.register('prefetchSdks') { AndroidSdk.ALL_SDKS.each { androidSdk -> doLast { println("Prefetching ${androidSdk.coordinates}...") @@ -139,7 +136,7 @@ task prefetchSdks() { } } -task prefetchInstrumentedSdks() { +tasks.register('prefetchInstrumentedSdks') { AndroidSdk.ALL_SDKS.each { androidSdk -> doLast { println("Prefetching ${androidSdk.preinstrumentedCoordinates}...") @@ -169,7 +166,7 @@ private void shellExec(String mvnCommand) { if (process.exitValue() != 0) System.exit(1) } -task prefetchDependencies() { +tasks.register('prefetchDependencies') { doLast { allprojects.each { p -> p.configurations.each { config -> diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 612366308..67279efee 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -11,8 +11,8 @@ dependencies { implementation gradleApi() implementation localGroovy() - api "com.google.guava:guava:31.1-jre" - api 'org.jetbrains:annotations:24.0.1' - implementation "org.ow2.asm:asm-tree:9.4" - implementation 'com.android.tools.build:gradle:7.4.2' + api libs.guava + api libs.jetbrains.annotations + implementation libs.asm.tree + implementation libs.android.gradle } diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 000000000..6f31e6ef7 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy b/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy index c0671c598..2f1476cf3 100644 --- a/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy +++ b/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy @@ -28,7 +28,6 @@ class CheckApiChangesPlugin implements Plugin<Project> { project.checkApiChanges.from.each { project.dependencies.checkApiChangesFrom(it) { transitive = false - force = true } } diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy index 324d04d44..09e3b8d4a 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy @@ -94,8 +94,8 @@ class DeployedRoboJavaModulePlugin implements Plugin<Project> { url = project.version.endsWith("-SNAPSHOT") ? snapshotsRepoUrl : releasesRepoUrl credentials { - username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin'] - password = System.properties["sonatype-password"] ?: System.env['sonatypePassword'] + username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN'] + password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD'] } } } diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy index 6c0e05894..deb97c994 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy @@ -13,8 +13,8 @@ class RoboJavaModulePlugin implements Plugin<Project> { if (!skipErrorprone) { apply plugin: "net.ltgt.errorprone" project.dependencies { - errorprone("com.google.errorprone:error_prone_core:$errorproneVersion") - errorproneJavac("com.google.errorprone:javac:$errorproneJavacVersion") + errorprone(libs.error.prone.core) + errorproneJavac(libs.error.prone.javac) } } diff --git a/dependencies.gradle b/dependencies.gradle index b890dc16d..204e3d354 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,40 +1,11 @@ ext { - apiCompatVersion='4.9.2' - - errorproneVersion='2.18.0' - errorproneJavacVersion='9+181-r4173-1' - - // AndroidX test versions - axtMonitorVersion='1.6.1' - axtRunnerVersion='1.5.2' - axtRulesVersion='1.5.0' - axtCoreVersion='1.5.0' - axtTruthVersion='1.5.0' - espressoVersion='3.5.1' - axtJunitVersion='1.1.4' - axtTestServicesVersion='1.4.2' - - // AndroidX versions - coreVersion='1.9.0' - appCompatVersion='1.6.1' - constraintlayoutVersion='2.1.4' - windowVersion='1.0.0' - fragmentVersion='1.5.5' - - truthVersion='1.1.3' - - junitVersion='4.13.2' - - mockitoVersion='4.11.0' - - jacocoVersion='0.8.8' - - guavaJREVersion='31.1-jre' - - asmVersion='9.4' - - kotlinVersion='1.8.10' - autoServiceVersion='1.0.1' - multidexVersion='2.0.1' - sqlite4javaVersion='1.0.392' + apiCompatVersion = libs.versions.robolectric.compat.get() + + // https://github.com/gradle/gradle/issues/21267 + axtCoreVersion = libs.versions.androidx.test.core.get() + axtJunitVersion = libs.versions.androidx.test.ext.junit.get() + axtMonitorVersion = libs.versions.androidx.test.monitor.get() + axtRunnerVersion = libs.versions.androidx.test.runner.get() + axtTruthVersion = libs.versions.androidx.test.ext.truth.get() + espressoVersion = libs.versions.androidx.test.espresso.get() } diff --git a/errorprone/build.gradle b/errorprone/build.gradle index 1932066ae..5fc561605 100644 --- a/errorprone/build.gradle +++ b/errorprone/build.gradle @@ -20,14 +20,14 @@ dependencies { implementation project(":shadowapi") // Compile dependencies - implementation "com.google.errorprone:error_prone_annotation:$errorproneVersion" - implementation "com.google.errorprone:error_prone_refaster:$errorproneVersion" - implementation "com.google.errorprone:error_prone_check_api:$errorproneVersion" - compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion" - compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true } + implementation libs.error.prone.annotations + implementation libs.error.prone.refaster + implementation libs.error.prone.check.api + compileOnly libs.auto.service.annotations + compileOnly(AndroidSdk.MAX_SDK.coordinates) - annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion" - annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion" + annotationProcessor libs.auto.service + annotationProcessor libs.error.prone.core // in jdk 9, tools.jar disappears! def toolsJar = Jvm.current().getToolsJar() @@ -36,10 +36,10 @@ dependencies { } // Testing dependencies - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation("com.google.errorprone:error_prone_test_helpers:${errorproneVersion}") { + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation(libs.error.prone.test.helpers) { exclude group: 'junit', module: 'junit' // because it depends on a snapshot!? } - testCompileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true } + testCompileOnly(AndroidSdk.MAX_SDK.coordinates) } diff --git a/gradle.properties b/gradle.properties index dc9eb66c0..d97ed211a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -thisVersion=4.10-SNAPSHOT +thisVersion=4.11-SNAPSHOT android.useAndroidX=true kotlin.stdlib.default.dependency=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..7b43dfa1f --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,237 @@ +[versions] +robolectric-compat = "4.10.2" +robolectric-nativeruntime-dist-compat = "1.0.1" + +# https://developer.android.com/studio/releases +android-gradle = "7.4.2" + +# https://github.com/google/conscrypt/tags +conscrypt = "2.5.2" + +# https://github.com/bcgit/bc-java/tags +bouncycastle = "1.73" + +# https://github.com/findbugsproject/findbugs/tags +findbugs-jsr305 = "3.0.2" + +# https://github.com/hamcrest/JavaHamcrest/releases +hamcrest = "2.0.0.0" + +# https://github.com/nebula-plugins/gradle-aggregate-javadocs-plugin/releases +aggregate-javadocs-gradle = "3.0.1" + +# https://github.com/google/error-prone/releases +error-prone = "2.19.1" +error-prone-javac = "9+181-r4173-1" + +# https://github.com/tbroyer/gradle-errorprone-plugin/releases +error-prone-gradle = "3.1.0" + +# https://kotlinlang.org/docs/releases.html#release-details +kotlin = "1.8.10" + +# https://github.com/diffplug/spotless/blob/main/CHANGES.md +spotless-gradle = "6.18.0" + +# https://hc.apache.org/news.html +apache-http-core = "4.0.1" +apache-http-client = "4.0.3" + +# https://asm.ow2.io/versions.html +asm = "9.5" + +# https://github.com/google/auto/releases +auto-common = "1.2.1" +auto-service = "1.0.1" +auto-value = "1.10.1" + +compile-testing = "0.21.0" + +# https://github.com/google/guava/releases +guava-jre = "31.1-jre" + +# https://github.com/google/gson/releases +gson = "2.10.1" + +# https://github.com/google/truth/releases +truth = "1.1.3" + +# https://github.com/unicode-org/icu/releases +icu4j = "73.1" + +jacoco = "0.8.10" + +# https://github.com/javaee/javax.annotation/tags +javax-annotation-api = "1.3.2" +javax-annotation-jsr250-api = "1.0" +javax-inject = "1" + +# https://github.com/JetBrains/java-annotations/releases +jetbrains-annotations = "24.0.1" + +# https://junit.org/junit4/ +junit4 = "4.13.2" + +# https://github.com/google/libphonenumber/releases +libphonenumber = "8.13.11" + +# https://github.com/mockito/mockito/releases +mockito = "4.11.0" + +# https://github.com/mockk/mockk/releases +mockk = "1.13.5" + +# https://square.github.io/okhttp/changelogs/changelog/ +okhttp = "4.11.0" + +# https://github.com/powermock/powermock/releases +powermock = "2.0.9" + +sqlite4java = "1.0.392" + +# https://developer.android.com/jetpack/androidx/versions +androidx-annotation = "1.3.0" +androidx-appcompat = "1.6.1" +androidx-constraintlayout = "2.1.4" +androidx-core = "1.10.1" +androidx-fragment = "1.5.7" +androidx-multidex = "2.0.1" +androidx-window = "1.0.0" + +# https://github.com/android/android-test/tags +androidx-test-annotation = "1.0.1" +androidx-test-core = "1.5.0" +androidx-test-espresso = "3.5.1" +androidx-test-ext-junit = "1.1.5" +androidx-test-ext-truth = "1.5.0" +androidx-test-monitor="1.6.1" +androidx-test-orchestrator="1.4.2" +androidx-test-runner = "1.5.2" +androidx-test-services = "1.4.2" + +# for shadows/playservices/build.gradle +androidx-fragment-for-shadows = "1.2.0" +play-services-base-for-shadows = "8.4.0" + +[libraries] +android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" } +kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless-gradle" } + +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } + +auto-common = { module = "com.google.auto:auto-common", version.ref = "auto-common" } +auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "auto-service" } +auto-service = { module = "com.google.auto.service:auto-service", version.ref = "auto-service" } +auto-value-annotations = { module = "com.google.auto.value:auto-value-annotations", version.ref = "auto-value" } +auto-value = { module = "com.google.auto.value:auto-value", version.ref = "auto-value" } + +apache-http-core = { module = "org.apache.httpcomponents:httpcore", version.ref = "apache-http-core" } +apache-http-client = { module = "org.apache.httpcomponents:httpclient", version.ref = "apache-http-client" } + +asm = { module = "org.ow2.asm:asm", version.ref = "asm" } +asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } +asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" } +asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" } + +compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" } + +aggregate-javadocs-gradle = { module = "com.netflix.nebula:gradle-aggregate-javadocs-plugin", version.ref = "aggregate-javadocs-gradle" } + +error-prone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" } +error-prone-annotations = { module = "com.google.errorprone:error_prone_annotation", version.ref = "error-prone" } +error-prone-refaster= { module = "com.google.errorprone:error_prone_refaster", version.ref = "error-prone" } +error-prone-check-api = { module = "com.google.errorprone:error_prone_check_api", version.ref = "error-prone" } +error-prone-test-helpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "error-prone" } +error-prone-javac = { module = "com.google.errorprone:javac", version.ref = "error-prone-javac" } + +error-prone-gradle = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "error-prone-gradle" } + +conscrypt-openjdk-uber = { module = "org.conscrypt:conscrypt-openjdk-uber", version.ref = "conscrypt" } +bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" } +findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs-jsr305" } + +guava = { module = "com.google.guava:guava", version.ref = "guava-jre" } +guava-testlib = { module = "com.google.guava:guava-testlib", version.ref = "guava-jre" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +hamcrest-junit = { module = "org.hamcrest:hamcrest-junit", version.ref = "hamcrest" } + +icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" } + +jacoco-agent = { module = "org.jacoco:org.jacoco.agent", version.ref = "jacoco" } +junit4 = { module = "junit:junit", version.ref = "junit4" } + +javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation-api" } +javax-annotation-jsr250-api = { module = "javax.annotation:jsr250-api", version.ref = "javax-annotation-jsr250-api" } +javax-inject = { module = "javax.inject:javax.inject", version.ref = "javax.inject" } + +jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } + +libphonenumber = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "libphonenumber" } + +okhttp = { module = "com.squareup.okhttp3:okhttp" } +okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" } + +powermock-module-junit4 = { module = "org.powermock:powermock-module-junit4", version.ref = "powermock" } +powermock-module-junit4-rule = { module = "org.powermock:powermock-module-junit4-rule", version.ref = "powermock" } +powermock-api-mockito2 = { module = "org.powermock:powermock-api-mockito2", version.ref = "powermock" } +powermock-classloading-xstream = { module = "org.powermock:powermock-classloading-xstream", version.ref = "powermock" } + +robolectric-nativeruntime-dist-compat = { module = "org.robolectric:nativeruntime-dist-compat", version.ref = "robolectric-nativeruntime-dist-compat" } + +sqlite4java = { module = "com.almworks.sqlite4java:sqlite4java", version.ref = "sqlite4java" } +sqlite4java-osx = { module = "com.almworks.sqlite4java:libsqlite4java-osx", version.ref = "sqlite4java" } +sqlite4java-linux-amd64 = { module = "com.almworks.sqlite4java:libsqlite4java-linux-amd64", version.ref = "sqlite4java" } +sqlite4java-win32-x64 = { module = "com.almworks.sqlite4java:sqlite4java-win32-x64", version.ref = "sqlite4java" } +sqlite4java-linux-i386 = { module = "com.almworks.sqlite4java:libsqlite4java-linux-i386", version.ref = "sqlite4java" } +sqlite4java-win32-x86 = { module = "com.almworks.sqlite4java:sqlite4java-win32-x86", version.ref = "sqlite4java" } + +truth = { module = "com.google.truth:truth", version.ref = "truth" } +truth-java8-extension = { module = "com.google.truth.extensions:truth-java8-extension", version.ref = "truth" } + +mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } + +androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } +androidx-fragment = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment" } +androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "androidx-fragment" } +androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "androidx-multidex" } +androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } + +androidx-test-annotation = { module = "androidx.test:annotation", version.ref = "androidx-test-annotation" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } +androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidx-test-monitor" } +androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-core" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +androidx-test-services = { module = "androidx.test.services:test-services", version.ref = "androidx-test-services" } +androidx-test-services-storage = { module = "androidx.test.services:storage", version.ref = "androidx-test-services" } + +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } +androidx-test-espresso-accessibility = { module = "androidx.test.espresso:espresso-accessibility", version.ref = "androidx-test-espresso" } +androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "androidx-test-espresso" } +androidx-test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "androidx-test-espresso" } +androidx-test-espresso-remote = { module = "androidx.test.espresso:espresso-remote", version.ref = "androidx-test-espresso" } +androidx-test-espresso-web = { module = "androidx.test.espresso:espresso-web", version.ref = "androidx-test-espresso" } + +androidx-test-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "androidx-test-espresso" } +androidx-test-espresso-idling-concurrent = { module = "androidx.test.espresso.idling:idling-concurrent", version.ref = "androidx-test-espresso" } +androidx-test-espresso-idling-net = { module = "androidx.test.espresso.idling:idling-net", version.ref = "androidx-test-espresso" } + +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } +androidx-test-ext-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-ext-truth" } + +androidx-fragment-for-shadows = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment-for-shadows" } +play-services-base-for-shadows = { module = "com.google.android.gms:play-services-base", version.ref = "play-services-base-for-shadows" } +play-services-basement-for-shadows = { module = "com.google.android.gms:play-services-basement", version.ref = "play-services-base-for-shadows" } + +[bundles] +play-services-base-for-shadows = [ "androidx-fragment-for-shadows", "play-services-base-for-shadows", "play-services-basement-for-shadows" ] +powermock = [ "powermock-module-junit4", "powermock-module-junit4-rule", "powermock-api-mockito2", "powermock-classloading-xstream" ] +sqlite4java-native = [ "sqlite4java-osx", "sqlite4java-linux-amd64", "sqlite4java-win32-x64", "sqlite4java-linux-i386", "sqlite4java-win32-x86" ] + +[plugins] diff --git a/integration_tests/agp/build.gradle b/integration_tests/agp/build.gradle index a079d1ecf..55d90516f 100644 --- a/integration_tests/agp/build.gradle +++ b/integration_tests/agp/build.gradle @@ -5,6 +5,7 @@ apply plugin: AndroidProjectConfigPlugin android { compileSdk 33 + namespace 'org.robolectric.integrationtests.agp' defaultConfig { minSdk 16 @@ -25,8 +26,8 @@ dependencies { testImplementation project(":robolectric") testImplementation project(":integration_tests:agp:testsupport") - testImplementation "junit:junit:${junitVersion}" - testImplementation("androidx.test:core:$axtCoreVersion") - testImplementation("androidx.test:runner:$axtRunnerVersion") - testImplementation("androidx.test.ext:junit:$axtJunitVersion") + testImplementation libs.junit4 + testImplementation libs.androidx.test.core + testImplementation libs.androidx.test.runner + testImplementation libs.androidx.test.ext.junit } diff --git a/integration_tests/agp/testsupport/build.gradle b/integration_tests/agp/testsupport/build.gradle index dcec3d41d..e87274f2b 100644 --- a/integration_tests/agp/testsupport/build.gradle +++ b/integration_tests/agp/testsupport/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'com.android.library' android { compileSdk 33 + namespace 'org.robolectric.integrationtests.agp.testsupport' defaultConfig { minSdk 16 diff --git a/integration_tests/androidx/build.gradle b/integration_tests/androidx/build.gradle index 96535e1dd..10cc8c650 100644 --- a/integration_tests/androidx/build.gradle +++ b/integration_tests/androidx/build.gradle @@ -5,6 +5,7 @@ apply plugin: AndroidProjectConfigPlugin android { compileSdk 33 + namespace 'org.robolectric.integrationtests.androidx' defaultConfig { minSdk 16 @@ -25,19 +26,19 @@ android { } dependencies { - implementation("androidx.appcompat:appcompat:$appCompatVersion") - implementation("androidx.window:window:$windowVersion") + implementation libs.androidx.appcompat + implementation libs.androidx.window // Testing dependencies testImplementation project(path: ':testapp') testImplementation project(":robolectric") - testImplementation "junit:junit:$junitVersion" - testImplementation("androidx.test:core:$axtCoreVersion") - testImplementation("androidx.core:core:$coreVersion") - testImplementation("androidx.test:runner:$axtRunnerVersion") - testImplementation("androidx.test:rules:$axtRulesVersion") - testImplementation("androidx.test.espresso:espresso-intents:$espressoVersion") - testImplementation("androidx.test.ext:truth:$axtTruthVersion") - testImplementation("androidx.test.ext:junit:$axtJunitVersion") - testImplementation("com.google.truth:truth:$truthVersion") + testImplementation libs.junit4 + testImplementation libs.androidx.test.core + testImplementation libs.androidx.core + testImplementation libs.androidx.test.runner + testImplementation libs.androidx.test.rules + testImplementation libs.androidx.test.espresso.intents + testImplementation libs.androidx.test.ext.truth + testImplementation libs.androidx.test.ext.junit + testImplementation libs.truth } diff --git a/integration_tests/androidx_test/build.gradle b/integration_tests/androidx_test/build.gradle index 7f6f621b6..d07ef2ed6 100644 --- a/integration_tests/androidx_test/build.gradle +++ b/integration_tests/androidx_test/build.gradle @@ -7,6 +7,7 @@ apply plugin: GradleManagedDevicePlugin android { compileSdk 33 + namespace 'org.robolectric.integration.axt' defaultConfig { minSdk 16 @@ -41,33 +42,33 @@ android { } dependencies { - implementation "androidx.appcompat:appcompat:$appCompatVersion" - implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion" - implementation "androidx.multidex:multidex:$multidexVersion" + implementation libs.androidx.appcompat + implementation libs.androidx.constraintlayout + implementation libs.androidx.multidex // Testing dependencies testImplementation project(":robolectric") - testImplementation "androidx.test:runner:$axtRunnerVersion" - testImplementation "junit:junit:$junitVersion" - testImplementation "androidx.test:rules:$axtRulesVersion" - testImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" - testImplementation "androidx.test.espresso:espresso-core:$espressoVersion" - testImplementation "androidx.test.ext:truth:$axtTruthVersion" - testImplementation "androidx.test:core:$axtCoreVersion" - testImplementation "androidx.fragment:fragment:$fragmentVersion" - testImplementation "androidx.fragment:fragment-testing:$fragmentVersion" - testImplementation "androidx.test.ext:junit:$axtJunitVersion" - testImplementation "com.google.truth:truth:$truthVersion" + testImplementation libs.androidx.test.runner + testImplementation libs.junit4 + testImplementation libs.androidx.test.rules + testImplementation libs.androidx.test.espresso.intents + testImplementation libs.androidx.test.espresso.core + testImplementation libs.androidx.test.ext.truth + testImplementation libs.androidx.test.core + testImplementation libs.androidx.fragment + testImplementation libs.androidx.fragment.testing + testImplementation libs.androidx.test.ext.junit + testImplementation libs.truth androidTestImplementation project(':annotations') - androidTestImplementation "androidx.test:runner:$axtRunnerVersion" - androidTestImplementation "junit:junit:$junitVersion" - androidTestImplementation "androidx.test:rules:$axtRulesVersion" - androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" - androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" - androidTestImplementation "androidx.test.ext:truth:$axtTruthVersion" - androidTestImplementation "androidx.test:core:$axtCoreVersion" - androidTestImplementation "androidx.test.ext:junit:$axtJunitVersion" - androidTestImplementation "com.google.truth:truth:$truthVersion" - androidTestUtil "androidx.test.services:test-services:$axtTestServicesVersion" + androidTestImplementation libs.androidx.test.runner + androidTestImplementation libs.junit4 + androidTestImplementation libs.androidx.test.rules + androidTestImplementation libs.androidx.test.espresso.intents + androidTestImplementation libs.androidx.test.espresso.core + androidTestImplementation libs.androidx.test.ext.truth + androidTestImplementation libs.androidx.test.core + androidTestImplementation libs.androidx.test.ext.junit + androidTestImplementation libs.truth + androidTestUtil libs.androidx.test.services } diff --git a/integration_tests/compat-target28/build.gradle b/integration_tests/compat-target28/build.gradle index 37a856856..1fc7485ae 100644 --- a/integration_tests/compat-target28/build.gradle +++ b/integration_tests/compat-target28/build.gradle @@ -14,6 +14,7 @@ spotless { android { compileSdk 28 + namespace 'org.robolectric.integrationtests.compattarget28' defaultConfig { minSdk 16 @@ -30,10 +31,10 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + implementation libs.kotlin.stdlib testImplementation project(path: ':testapp') testImplementation project(":robolectric") - testImplementation "junit:junit:$junitVersion" - testImplementation "com.google.truth:truth:$truthVersion" + testImplementation libs.junit4 + testImplementation libs.truth } diff --git a/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt b/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt index ee56fc6d2..69bbf73e0 100644 --- a/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt +++ b/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt @@ -1,7 +1,9 @@ package org.robolectric.integration.compat.target28 import android.content.Context +import android.content.Context.VIBRATOR_SERVICE import android.os.Build +import android.os.Vibrator import android.speech.SpeechRecognizer import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -47,4 +49,9 @@ class NormalCompatibilityTest { fun `Create speech recognizer succeed`() { assertThat(SpeechRecognizer.createSpeechRecognizer(application)).isNotNull() } + + @Test + fun `Get default Vibrator succeed`() { + assertThat(application.getSystemService(VIBRATOR_SERVICE) as Vibrator).isNotNull() + } } diff --git a/integration_tests/ctesque/build.gradle b/integration_tests/ctesque/build.gradle index 11f27e1d6..3b40c88de 100644 --- a/integration_tests/ctesque/build.gradle +++ b/integration_tests/ctesque/build.gradle @@ -7,6 +7,7 @@ apply plugin: GradleManagedDevicePlugin android { compileSdk 33 + namespace 'org.robolectric.integrationtests.ctesque' defaultConfig { minSdk 16 @@ -48,24 +49,26 @@ dependencies { implementation project(':testapp') testImplementation project(':robolectric') - testImplementation "junit:junit:${junitVersion}" - testImplementation("androidx.test:monitor:$axtMonitorVersion") - testImplementation("androidx.test:runner:$axtRunnerVersion") - testImplementation("androidx.test:rules:$axtRulesVersion") - testImplementation("androidx.test.ext:junit:$axtJunitVersion") - testImplementation("androidx.test.ext:truth:$axtTruthVersion") - testImplementation("androidx.test:core:$axtCoreVersion") - testImplementation("com.google.truth:truth:${truthVersion}") - testImplementation("com.google.guava:guava:$guavaJREVersion") + testImplementation libs.junit4 + testImplementation libs.androidx.test.monitor + testImplementation libs.androidx.test.runner + testImplementation libs.androidx.test.rules + testImplementation libs.androidx.test.ext.junit + testImplementation libs.androidx.test.ext.truth + testImplementation libs.androidx.test.core + testImplementation libs.androidx.test.espresso.core + testImplementation libs.truth + testImplementation libs.guava // Testing dependencies androidTestImplementation project(':shadowapi') - androidTestImplementation("androidx.test:monitor:$axtMonitorVersion") - androidTestImplementation("androidx.test:runner:$axtRunnerVersion") - androidTestImplementation("androidx.test:rules:$axtRulesVersion") - androidTestImplementation("androidx.test.ext:junit:$axtJunitVersion") - androidTestImplementation("androidx.test.ext:truth:$axtTruthVersion") - androidTestImplementation("com.google.truth:truth:${truthVersion}") - androidTestImplementation("com.google.guava:guava:$guavaJREVersion") - androidTestUtil "androidx.test.services:test-services:$axtTestServicesVersion" + androidTestImplementation libs.androidx.test.monitor + androidTestImplementation libs.androidx.test.runner + androidTestImplementation libs.androidx.test.rules + androidTestImplementation libs.androidx.test.ext.junit + androidTestImplementation libs.androidx.test.ext.truth + androidTestImplementation libs.androidx.test.espresso.core + androidTestImplementation libs.truth + androidTestImplementation libs.guava + androidTestUtil libs.androidx.test.services } diff --git a/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java b/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java index c1922b60d..5e4de013c 100644 --- a/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java +++ b/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java @@ -503,6 +503,14 @@ public class ResourcesTest { assertThat(id).isEqualTo(0); } + @Test + @SdkSuppress(minSdkVersion = LOLLIPOP) + @Config(minSdk = LOLLIPOP) + public void getIdentifier_material() { + int id = Resources.getSystem().getIdentifier("btn_check_material_anim", "drawable", "android"); + assertThat(id).isGreaterThan(0); + } + /** * Public framework symbols are defined here: * https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml diff --git a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java index fa11ce7e2..eea5deaee 100644 --- a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java +++ b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java @@ -12,6 +12,7 @@ import android.database.sqlite.SQLiteException; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; +import androidx.test.filters.Suppress; import com.google.common.base.Ascii; import com.google.common.base.Throwables; import com.google.common.io.ByteStreams; @@ -174,7 +175,8 @@ public class SQLiteDatabaseTest { } // TODO(hoisie): This test crashes in emulators, enable when it is fixed in Android. - @SdkSuppress(minSdkVersion = 34) + // Use Suppress here to stop it from running on emulators, but not on Robolectric + @Suppress @Test public void cursorWindow_finalize_concurrentStressTest() throws Throwable { final PrintStream originalErr = System.err; @@ -223,4 +225,18 @@ public class SQLiteDatabaseTest { c.close(); assertThat(sorted).containsExactly("aaa", "abc", "ABC", "bbb").inOrder(); } + + @Test + @Config(minSdk = LOLLIPOP) + @SdkSuppress(minSdkVersion = LOLLIPOP) + public void regex_selection() { + ContentValues values = new ContentValues(); + values.put("first_column", "test"); + database.insert("table_name", null, values); + String select = "first_column regexp ?"; + String[] selectArgs = { + "test", + }; + assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1); + } } diff --git a/integration_tests/dependency-on-stubs/build.gradle b/integration_tests/dependency-on-stubs/build.gradle index 6efe51373..683de182d 100644 --- a/integration_tests/dependency-on-stubs/build.gradle +++ b/integration_tests/dependency-on-stubs/build.gradle @@ -6,13 +6,13 @@ apply plugin: RoboJavaModulePlugin dependencies { api project(":robolectric") - api "junit:junit:${junitVersion}" + api libs.junit4 testImplementation files("${System.getenv("ANDROID_HOME")}/platforms/android-29/android.jar") testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0" + testImplementation libs.truth + testImplementation libs.mockito + testImplementation libs.hamcrest.junit } diff --git a/integration_tests/jacoco-offline/build.gradle b/integration_tests/jacoco-offline/build.gradle index e5d3bb5eb..3db34c00a 100644 --- a/integration_tests/jacoco-offline/build.gradle +++ b/integration_tests/jacoco-offline/build.gradle @@ -3,6 +3,8 @@ import org.robolectric.gradle.RoboJavaModulePlugin apply plugin: RoboJavaModulePlugin apply plugin: "jacoco" +def jacocoVersion = libs.versions.jacoco.get() + jacoco { toolVersion = jacocoVersion } @@ -18,7 +20,7 @@ dependencies { testRuntimeOnly AndroidSdk.MAX_SDK.coordinates testImplementation project(":robolectric") - testImplementation "junit:junit:$junitVersion" + testImplementation libs.junit4 testImplementation "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" } diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle index 68c5c677f..fd52d973d 100644 --- a/integration_tests/kotlin/build.gradle +++ b/integration_tests/kotlin/build.gradle @@ -21,8 +21,8 @@ dependencies { testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - testImplementation "junit:junit:$junitVersion" - testImplementation "com.google.truth:truth:$truthVersion" + testImplementation libs.kotlin.stdlib + testImplementation libs.junit4 + testImplementation libs.truth testImplementation "androidx.test:core:$axtCoreVersion@aar" } diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt new file mode 100644 index 000000000..6d77aa91f --- /dev/null +++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt @@ -0,0 +1,28 @@ +package org.robolectric.integrationtests.kotlin + +import android.net.Uri +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters +import org.robolectric.annotation.Config + +@RunWith(ParameterizedRobolectricTestRunner::class) +class ParameterizedRobolectricTestRunnerTest(private var uri: Uri) { + @Test + @Config(manifest = Config.NONE) + fun parse() { + val currentUri = Uri.parse("http://host/") + assertThat(currentUri).isEqualTo(uri) + } + + companion object { + @Parameters + @JvmStatic + fun getTestData(): Collection<*> { + val data = arrayOf<Any>(Uri.parse("http://host/")) + return listOf(data) + } + } +} diff --git a/integration_tests/libphonenumber/build.gradle b/integration_tests/libphonenumber/build.gradle index 2c27a7968..61120f227 100644 --- a/integration_tests/libphonenumber/build.gradle +++ b/integration_tests/libphonenumber/build.gradle @@ -4,10 +4,10 @@ apply plugin: RoboJavaModulePlugin dependencies { api project(":robolectric") - api "junit:junit:${junitVersion}" + api libs.junit4 compileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation 'com.googlecode.libphonenumber:libphonenumber:8.13.8' -}
\ No newline at end of file + testImplementation libs.truth + testImplementation libs.libphonenumber +} diff --git a/integration_tests/memoryleaks/build.gradle b/integration_tests/memoryleaks/build.gradle index 2cc51247c..91c5eb01e 100644 --- a/integration_tests/memoryleaks/build.gradle +++ b/integration_tests/memoryleaks/build.gradle @@ -5,6 +5,7 @@ apply plugin: AndroidProjectConfigPlugin android { compileSdk 33 + namespace 'org.robolectric.integrationtests.memoryleaks' defaultConfig { minSdk 16 @@ -28,7 +29,7 @@ dependencies { // Testing dependencies testImplementation project(path: ':testapp') testImplementation project(":robolectric") - testImplementation "junit:junit:$junitVersion" - testImplementation "com.google.guava:guava-testlib:$guavaJREVersion" - testImplementation "androidx.fragment:fragment:$fragmentVersion" + testImplementation libs.junit4 + testImplementation libs.guava.testlib + testImplementation libs.androidx.fragment } diff --git a/integration_tests/mockito-experimental/build.gradle b/integration_tests/mockito-experimental/build.gradle index 4aafcbc08..f5172d6af 100644 --- a/integration_tests/mockito-experimental/build.gradle +++ b/integration_tests/mockito-experimental/build.gradle @@ -8,7 +8,7 @@ dependencies { testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-inline:${mockitoVersion}" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockito.inline } diff --git a/integration_tests/mockito-kotlin/build.gradle b/integration_tests/mockito-kotlin/build.gradle index ae97f1b7a..776f33bd2 100644 --- a/integration_tests/mockito-kotlin/build.gradle +++ b/integration_tests/mockito-kotlin/build.gradle @@ -18,8 +18,8 @@ dependencies { testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar" - testImplementation "junit:junit:$junitVersion" - testImplementation "com.google.truth:truth:$truthVersion" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.kotlin.stdlib + testImplementation libs.mockito } diff --git a/integration_tests/mockito/build.gradle b/integration_tests/mockito/build.gradle index e199cd74d..31e6ce675 100644 --- a/integration_tests/mockito/build.gradle +++ b/integration_tests/mockito/build.gradle @@ -8,7 +8,7 @@ dependencies { testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" -}
\ No newline at end of file + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockito +} diff --git a/integration_tests/mockk/build.gradle b/integration_tests/mockk/build.gradle index 78344a9aa..1d590714e 100644 --- a/integration_tests/mockk/build.gradle +++ b/integration_tests/mockk/build.gradle @@ -21,7 +21,7 @@ dependencies { testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation 'io.mockk:mockk:1.13.4' + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockk } diff --git a/integration_tests/nativegraphics/build.gradle b/integration_tests/nativegraphics/build.gradle index 10e8f6138..f88f53a3f 100644 --- a/integration_tests/nativegraphics/build.gradle +++ b/integration_tests/nativegraphics/build.gradle @@ -7,6 +7,7 @@ apply plugin: GradleManagedDevicePlugin android { compileSdk 33 + namespace 'org.robolectric.integrationtests.nativegraphics' defaultConfig { minSdk 26 @@ -32,9 +33,9 @@ dependencies { testImplementation AndroidSdk.MAX_SDK.coordinates testImplementation project(':robolectric') - testImplementation "androidx.core:core:$coreVersion" - testImplementation "androidx.test.ext:junit:$axtJunitVersion" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "junit:junit:${junitVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation libs.androidx.core + testImplementation libs.androidx.test.ext.junit + testImplementation libs.truth + testImplementation libs.junit4 + testImplementation libs.mockito } diff --git a/integration_tests/play_services/build.gradle b/integration_tests/play_services/build.gradle index f7499dd73..0e05dd6b3 100644 --- a/integration_tests/play_services/build.gradle +++ b/integration_tests/play_services/build.gradle @@ -9,7 +9,7 @@ dependencies { testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "junit:junit:$junitVersion" - testImplementation "com.google.truth:truth:$truthVersion" + testImplementation libs.junit4 + testImplementation libs.truth testImplementation "com.google.android.gms:play-services-basement:18.0.1" -}
\ No newline at end of file +} diff --git a/integration_tests/powermock/build.gradle b/integration_tests/powermock/build.gradle index be4180cf5..6d5cf689d 100644 --- a/integration_tests/powermock/build.gradle +++ b/integration_tests/powermock/build.gradle @@ -7,11 +7,8 @@ dependencies { compileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" + testImplementation libs.junit4 + testImplementation libs.truth - testImplementation "org.powermock:powermock-module-junit4:2.0.9" - testImplementation "org.powermock:powermock-module-junit4-rule:2.0.9" - testImplementation "org.powermock:powermock-api-mockito2:2.0.9" - testImplementation "org.powermock:powermock-classloading-xstream:2.0.9" -}
\ No newline at end of file + testImplementation libs.bundles.powermock +} diff --git a/integration_tests/security-providers/build.gradle b/integration_tests/security-providers/build.gradle index f96df56b9..8ac0264a8 100644 --- a/integration_tests/security-providers/build.gradle +++ b/integration_tests/security-providers/build.gradle @@ -4,12 +4,12 @@ apply plugin: RoboJavaModulePlugin dependencies { api project(":robolectric") - api "junit:junit:${junitVersion}" + api libs.junit4 compileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.4.0" - testImplementation "com.squareup.okhttp3:okhttp" - testImplementation platform("com.squareup.okhttp3:okhttp-bom:4.10.0") + testImplementation libs.truth + testImplementation libs.conscrypt.openjdk.uber + testImplementation libs.okhttp + testImplementation platform(libs.okhttp.bom) } diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle index 762717794..1e4ba1ddf 100644 --- a/integration_tests/sparsearray/build.gradle +++ b/integration_tests/sparsearray/build.gradle @@ -14,6 +14,7 @@ spotless { android { compileSdk 33 + namespace 'org.robolectric.sparsearray' defaultConfig { minSdk 16 @@ -41,7 +42,7 @@ dependencies { testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates testImplementation project(":robolectric") - testImplementation "junit:junit:$junitVersion" - testImplementation "com.google.truth:truth:$truthVersion" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.kotlin.stdlib } diff --git a/junit/build.gradle b/junit/build.gradle index 9a1197871..5d5855254 100644 --- a/junit/build.gradle +++ b/junit/build.gradle @@ -11,6 +11,6 @@ dependencies { api project(":shadowapi") api project(":utils:reflector") - compileOnly "com.google.code.findbugs:jsr305:3.0.2" - compileOnly "junit:junit:${junitVersion}" + compileOnly libs.findbugs.jsr305 + compileOnly libs.junit4 } diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle index 1ef93175a..e784984ab 100644 --- a/nativeruntime/build.gradle +++ b/nativeruntime/build.gradle @@ -49,8 +49,8 @@ if (System.getenv('PUBLISH_NATIVERUNTIME_DIST_COMPAT') == "true") { url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" credentials { - username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin'] - password = System.properties["sonatype-password"] ?: System.env['sonatypePassword'] + username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN'] + password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD'] } } } @@ -64,17 +64,17 @@ if (System.getenv('PUBLISH_NATIVERUNTIME_DIST_COMPAT') == "true") { dependencies { api project(":utils") api project(":utils:reflector") - api "com.google.guava:guava:$guavaJREVersion" + api libs.guava - implementation "org.robolectric:nativeruntime-dist-compat:1.0.0" + implementation libs.robolectric.nativeruntime.dist.compat - annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion" - compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion" + annotationProcessor libs.auto.service + compileOnly libs.auto.service.annotations compileOnly AndroidSdk.MAX_SDK.coordinates testCompileOnly AndroidSdk.MAX_SDK.coordinates testRuntimeOnly AndroidSdk.MAX_SDK.coordinates testImplementation project(":robolectric") - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" + testImplementation libs.junit4 + testImplementation libs.truth } diff --git a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java index 03911593e..e5d395f2e 100644 --- a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java +++ b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java @@ -2,7 +2,7 @@ package org.robolectric.nativeruntime; import static android.os.Build.VERSION_CODES.O; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeTrue; +import static com.google.common.truth.TruthJUnit.assume; import android.database.CursorWindow; import android.database.sqlite.SQLiteDatabase; @@ -33,8 +33,8 @@ public final class DefaultNativeRuntimeLoaderTest { @Test public void extracts_fontsAndIcuData() { - assumeTrue(hasResource("fonts")); - assumeTrue(hasResource("icu/icudt68l.dat")); + assume().that(hasResource("fonts")).isTrue(); + assume().that(hasResource("icu/icudt68l.dat")).isTrue(); DefaultNativeRuntimeLoader defaultNativeRuntimeLoader = new DefaultNativeRuntimeLoader(); defaultNativeRuntimeLoader.ensureLoaded(); // Check that extraction of some key files worked. diff --git a/pluginapi/build.gradle b/pluginapi/build.gradle index 375cd10e4..9d7885291 100644 --- a/pluginapi/build.gradle +++ b/pluginapi/build.gradle @@ -5,11 +5,11 @@ apply plugin: RoboJavaModulePlugin apply plugin: DeployedRoboJavaModulePlugin dependencies { - compileOnly 'com.google.code.findbugs:jsr305:3.0.2' + compileOnly libs.findbugs.jsr305 api project(":annotations") - api "com.google.guava:guava:$guavaJREVersion" + api libs.guava - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockito } diff --git a/plugins/maven-dependency-resolver/build.gradle b/plugins/maven-dependency-resolver/build.gradle index de20b2b8f..2aa33d9f5 100644 --- a/plugins/maven-dependency-resolver/build.gradle +++ b/plugins/maven-dependency-resolver/build.gradle @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.robolectric.gradle.DeployedRoboJavaModulePlugin import org.robolectric.gradle.RoboJavaModulePlugin @@ -13,7 +14,7 @@ spotless { } } -tasks.withType(GenerateModuleMetadata) { +tasks.withType(GenerateModuleMetadata).configureEach { // We don't want to release gradle module metadata now to avoid // potential compatibility problems. enabled = false @@ -22,11 +23,11 @@ tasks.withType(GenerateModuleMetadata) { compileKotlin { // Use java/main classes directory to replace default kotlin/main to // avoid d8 error when dexing & desugaring kotlin classes with non-exist - // kotlin/main directory because utils module doesn't have kotlin code + // kotlin/main directory because this module doesn't have kotlin code // in production. If utils module starts to add Kotlin code in main source // set, we can remove this destinationDirectory modification. destinationDirectory = file("${projectDir}/build/classes/java/main") - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } afterEvaluate { @@ -48,10 +49,12 @@ afterEvaluate { dependencies { api project(":pluginapi") api project(":utils") - api "com.google.guava:guava:$guavaJREVersion" + api libs.auto.value.annotations + api libs.guava + annotationProcessor libs.auto.value - testImplementation "junit:junit:$junitVersion" - testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "com.google.truth:truth:$truthVersion" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + testImplementation libs.junit4 + testImplementation libs.mockito + testImplementation libs.truth + testImplementation libs.kotlin.stdlib } diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java index 60f852dbc..adeda9e78 100644 --- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java +++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java @@ -2,6 +2,7 @@ package org.robolectric.internal.dependency; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.auto.value.AutoValue; import com.google.common.base.Strings; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; @@ -24,6 +25,7 @@ import java.net.URLConnection; import java.util.Base64; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import javax.annotation.Nonnull; import org.robolectric.util.Logger; /** @@ -82,15 +84,27 @@ public class MavenArtifactFetcher { return Futures.immediateFuture(null); } createArtifactSubdirectory(artifact, localRepositoryDir); - boolean pomValid = + ValidationResult pomResult = validateStagedFiles(artifact.pomPath(), artifact.pomSha512Path()); - if (!pomValid) { - throw new AssertionError("SHA512 mismatch for POM file fetched in " + artifact); + if (!pomResult.isSuccess()) { + throw new AssertionError( + "SHA-512 mismatch for POM file for " + + artifact + + ", expected SHA-512=" + + pomResult.expectedHashCode() + + ", actual SHA-512=" + + pomResult.calculatedHashCode()); } - boolean jarValid = + ValidationResult jarResult = validateStagedFiles(artifact.jarPath(), artifact.jarSha512Path()); - if (!jarValid) { - throw new AssertionError("SHA512 mismatch for JAR file fetched in " + artifact); + if (!jarResult.isSuccess()) { + throw new AssertionError( + "SHA-512 mismatch for POM file for " + + artifact + + ", expected SHA-512=" + + jarResult.expectedHashCode() + + ", actual SHA-512=" + + jarResult.calculatedHashCode()); } Logger.info( String.format( @@ -123,7 +137,8 @@ public class MavenArtifactFetcher { new File(repositoryDir, artifact.pomSha512Path()).delete(); } - private boolean validateStagedFiles(String filePath, String sha512Path) throws IOException { + private ValidationResult validateStagedFiles(String filePath, String sha512Path) + throws IOException { File tempFile = new File(this.stagingRepositoryDir, filePath); File sha512File = new File(this.stagingRepositoryDir, sha512Path); @@ -131,7 +146,24 @@ public class MavenArtifactFetcher { HashCode.fromString(new String(Files.asByteSource(sha512File).read(), UTF_8)); HashCode actual = Files.asByteSource(tempFile).hash(Hashing.sha512()); - return expected.equals(actual); + return ValidationResult.create(expected.equals(actual), expected.toString(), actual.toString()); + } + + @AutoValue + abstract static class ValidationResult { + abstract boolean isSuccess(); + + @Nonnull + abstract String expectedHashCode(); + + @Nonnull + abstract String calculatedHashCode(); + + static ValidationResult create( + boolean isSuccess, String expectedHashCode, String calculatedHashCode) { + return new AutoValue_MavenArtifactFetcher_ValidationResult( + isSuccess, expectedHashCode, calculatedHashCode); + } } private void createArtifactSubdirectory(MavenJarArtifact artifact, File repositoryDir) @@ -218,6 +250,9 @@ public class MavenArtifactFetcher { try (InputStream inputStream = connection.getInputStream(); FileOutputStream outputStream = new FileOutputStream(localFile)) { ByteStreams.copy(inputStream, outputStream); + // Ensure all contents are written to disk. + outputStream.flush(); + outputStream.getFD().sync(); } return Futures.immediateFuture(null); } diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java index bb5604d80..bb5604d80 100755..100644 --- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java +++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java diff --git a/preinstrumented/build.gradle b/preinstrumented/build.gradle index 95d533e4d..8ffb5bbf2 100644 --- a/preinstrumented/build.gradle +++ b/preinstrumented/build.gradle @@ -17,11 +17,12 @@ java { } dependencies { - implementation "com.google.guava:guava:$guavaJREVersion" + implementation libs.guava implementation project(":sandbox") + implementation project(":shadows:versioning") } -task instrumentAll { +tasks.register('instrumentAll') { dependsOn ':prefetchSdks' dependsOn 'build' @@ -42,11 +43,11 @@ task instrumentAll { } } -task('sourcesJar', type: Jar) { +tasks.register('sourcesJar', Jar) { archiveClassifier = "sources" } -task('javadocJar', type: Jar) { +tasks.register('javadocJar', Jar) { archiveClassifier = "javadoc" } @@ -102,8 +103,8 @@ if (System.getenv('PUBLISH_PREINSTRUMENTED_JARS') == "true") { url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" credentials { - username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin'] - password = System.properties["sonatype-password"] ?: System.env['sonatypePassword'] + username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN'] + password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD'] } } } @@ -132,4 +133,4 @@ clean.doFirst { AndroidSdk.ALL_SDKS.each { androidSdk -> delete "${buildDir}/${androidSdk.preinstrumentedJarFileName}" } -}
\ No newline at end of file +} diff --git a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java index a9c5aceab..753c1670d 100644 --- a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java +++ b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import java.util.Locale; -import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; @@ -21,6 +20,8 @@ import org.robolectric.internal.bytecode.ClassNodeProvider; import org.robolectric.internal.bytecode.InstrumentationConfiguration; import org.robolectric.internal.bytecode.Interceptors; import org.robolectric.util.inject.Injector; +import org.robolectric.versioning.AndroidVersionInitTools; +import org.robolectric.versioning.AndroidVersions.AndroidRelease; /** Runs Robolectric invokedynamic instrumentation on an android-all jar. */ public class JarInstrumentor { @@ -146,14 +147,7 @@ public class JarInstrumentor { } private int getJarAndroidSDKVersion(JarFile jarFile) throws IOException { - ZipEntry buildProp = jarFile.getEntry("build.prop"); - Properties buildProps = new Properties(); - buildProps.load(jarFile.getInputStream(buildProp)); - String codename = buildProps.getProperty("ro.build.version.codename"); - // Check for a prerelease SDK. - if (!"REL".equals(codename)) { - return 10000; - } - return Integer.parseInt(buildProps.getProperty("ro.build.version.sdk")); + AndroidRelease release = AndroidVersionInitTools.computeReleaseVersion(jarFile); + return release.getSdkInt(); } } diff --git a/processor/build.gradle b/processor/build.gradle index ac14e4282..9185d088a 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -35,21 +35,21 @@ dependencies { api project(":annotations") api project(":shadowapi") - compileOnly "com.google.code.findbugs:jsr305:3.0.2" - api "org.ow2.asm:asm:${asmVersion}" - api "org.ow2.asm:asm-commons:${asmVersion}" - api "com.google.guava:guava:$guavaJREVersion" - api "com.google.code.gson:gson:2.10.1" - implementation 'com.google.auto:auto-common:1.1.2' + compileOnly libs.findbugs.jsr305 + api libs.asm + api libs.asm.commons + api libs.guava + api libs.gson + implementation libs.auto.common def toolsJar = Jvm.current().getToolsJar() if (toolsJar != null) { implementation files(toolsJar) } - testImplementation "javax.annotation:jsr250-api:1.0" - testImplementation "junit:junit:${junitVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testImplementation "com.google.testing.compile:compile-testing:0.21.0" - testImplementation "com.google.truth:truth:${truthVersion}" + testImplementation libs.javax.annotation.jsr250.api + testImplementation libs.junit4 + testImplementation libs.mockito + testImplementation libs.compile.testing + testImplementation libs.truth } diff --git a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java index fd5e77dd2..d9905f6cf 100644 --- a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java +++ b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java @@ -1,5 +1,6 @@ package org.robolectric.annotation.processing; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Maps.newTreeMap; import static com.google.common.collect.Sets.newTreeSet; @@ -127,6 +128,10 @@ public class RobolectricModel { } public void addResetter(TypeElement shadowTypeElement, ExecutableElement elem) { + checkState( + !resetterMap.containsKey(shadowTypeElement.getQualifiedName().toString()), + "Trying to register a duplicate resetter on %s", + shadowTypeElement.getQualifiedName()); registerType(shadowTypeElement); resetterMap.put(shadowTypeElement.getQualifiedName().toString(), diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java index d409f8396..39df257f9 100644 --- a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java +++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java @@ -1,6 +1,9 @@ package org.robolectric.annotation.processing.validator; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; @@ -13,6 +16,9 @@ import org.robolectric.annotation.processing.RobolectricModel; * Validator that checks usages of {@link org.robolectric.annotation.Resetter}. */ public class ResetterValidator extends FoundOnImplementsValidator { + + private final Map<TypeElement, ExecutableElement> resetterMethodsByClass = new HashMap<>(); + public ResetterValidator(RobolectricModel.Builder modelBuilder, ProcessingEnvironment env) { super(modelBuilder, env, "org.robolectric.annotation.Resetter"); } @@ -35,7 +41,19 @@ public class ResetterValidator extends FoundOnImplementsValidator { error("@Resetter methods must not have parameters"); error = true; } + if (resetterMethodsByClass.containsKey(parent)) { + error( + String.format( + Locale.US, + "Duplicate @Resetter methods found on %s: %s() and %s(). Only one @Resetter method" + + " is permitted on each shadow.", + parent.getQualifiedName(), + resetterMethodsByClass.get(parent).getSimpleName(), + elem.getSimpleName())); + error = true; + } if (!error) { + resetterMethodsByClass.put(parent, elem); modelBuilder.addResetter(parent, elem); } } diff --git a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java index 68900409b..c7924e8b0 100644 --- a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java +++ b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java @@ -12,7 +12,8 @@ import org.junit.runners.JUnit4; public class ResetterValidatorTest { @Test public void resetterWithoutImplements_shouldNotCompile() { - final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements"; + final String testClass = + "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements"; assertAbout(singleClass()) .that(testClass) .failsToCompile() @@ -22,7 +23,8 @@ public class ResetterValidatorTest { @Test public void nonStaticResetter_shouldNotCompile() { - final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic"; + final String testClass = + "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic"; assertAbout(singleClass()) .that(testClass) .failsToCompile() @@ -32,7 +34,8 @@ public class ResetterValidatorTest { @Test public void nonPublicResetter_shouldNotCompile() { - final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic"; + final String testClass = + "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic"; assertAbout(singleClass()) .that(testClass) .failsToCompile() @@ -42,7 +45,8 @@ public class ResetterValidatorTest { @Test public void resetterWithParameters_shouldNotCompile() { - final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters"; + final String testClass = + "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters"; assertAbout(singleClass()) .that(testClass) .failsToCompile() @@ -51,6 +55,20 @@ public class ResetterValidatorTest { } @Test + public void twoValidResetters_shouldNotCompile() { + final String testClass = "org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters"; + + assertAbout(singleClass()) + .that(testClass) + .failsToCompile() + .withErrorContaining( + "Duplicate @Resetter methods found on" + + " org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters:" + + " resetter_method_one() and resetter_method_two().") + .onLine(13); + } + + @Test public void goodResetter_shouldCompile() { final String testClass = "org.robolectric.annotation.processing.shadows.ShadowDummy"; assertAbout(singleClass()) diff --git a/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java new file mode 100644 index 000000000..8183073b6 --- /dev/null +++ b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java @@ -0,0 +1,15 @@ +package org.robolectric.annotation.processing.shadows; + +import com.example.objects.Dummy; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +@Implements(Dummy.class) +public class ShadowWithTwoResetters { + + @Resetter + public static void resetter_method_one() {} + + @Resetter + public static void resetter_method_two() {} +} diff --git a/resources/build.gradle b/resources/build.gradle index 129dc20ad..077cdd3b3 100644 --- a/resources/build.gradle +++ b/resources/build.gradle @@ -9,11 +9,11 @@ dependencies { api project(":annotations") api project(":pluginapi") - api "com.google.guava:guava:$guavaJREVersion" - compileOnly "com.google.code.findbugs:jsr305:3.0.2" + api libs.guava + compileOnly libs.findbugs.jsr305 - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "com.google.testing.compile:compile-testing:0.21.0" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.compile.testing + testImplementation libs.mockito } diff --git a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java index cfabcbb2b..7402b4982 100644 --- a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java +++ b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java @@ -53,6 +53,7 @@ public class AndroidManifest implements UsesSdk { private String processName; private String themeRef; private String labelRef; + private String appComponentFactory; // Added from SDK 28 private Integer minSdkVersion; private Integer targetSdkVersion; private Integer maxSdkVersion; @@ -191,6 +192,7 @@ public class AndroidManifest implements UsesSdk { rClassName = packageName + ".R"; Node applicationNode = findApplicationNode(manifestDocument); + // Parse application node of the AndroidManifest.xml if (applicationNode != null) { NamedNodeMap attributes = applicationNode.getAttributes(); int attrCount = attributes.getLength(); @@ -204,6 +206,7 @@ public class AndroidManifest implements UsesSdk { processName = applicationAttributes.get("android:process"); themeRef = applicationAttributes.get("android:theme"); labelRef = applicationAttributes.get("android:label"); + appComponentFactory = applicationAttributes.get("android:appComponentFactory"); parseReceivers(applicationNode); parseServices(applicationNode); @@ -605,6 +608,11 @@ public class AndroidManifest implements UsesSdk { return labelRef; } + public String getAppComponentFactory() { + parseAndroidManifest(); + return appComponentFactory; + } + /** * Returns the minimum Android SDK version that this package expects to be runnable on, as * specified in the manifest. diff --git a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java index 0de3390e7..8b8165bd1 100644 --- a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java +++ b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java @@ -526,32 +526,79 @@ public class LoadedArsc { return 0; } + // for (const auto& type_entry : type_spec->type_entries) { for (ResTable_type iter : type_spec.types) { + // const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type; ResTable_type type = iter; + // const size_t entry_count = dtohl(type->entryCount); int entry_count = type.entryCount; + // const auto entry_offsets = type.offset(dtohs(type->header.headerSize)); + // for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) { for (int entry_idx = 0; entry_idx < entry_count; entry_idx++) { - // const uint32_t* entry_offsets = reinterpret_cast<const uint32_t*>( - // reinterpret_cast<const uint8_t*>(type.type) + dtohs(type.type.header.headerSize)); - // ResTable_type entry_offsets = new ResTable_type(type.myBuf(), - // type.myOffset() + type.header.headerSize); - // int offset = dtohl(entry_offsets[entry_idx]); - int offset = dtohl(type.entryOffset(entry_idx)); + // uint32_t offset; + int offset; + // uint16_t res_idx; + short res_idx; + // if (type->flags & ResTable_type::FLAG_SPARSE) { + if (isTruthy(type.flags & ResTable_type.FLAG_SPARSE)) { + // auto sparse_entry = entry_offsets.convert<ResTable_sparseTypeEntry>() + entry_idx; + + ResTable_sparseTypeEntry sparse_entry = + new ResTable_sparseTypeEntry( + type.myBuf(), type.myOffset() + entry_idx * ResTable_sparseTypeEntry.SIZEOF); + // if (!sparse_entry) { + // return base::unexpected(IOError::PAGES_MISSING); + // } + // TODO: implement above + // offset = dtohs(sparse_entry->offset) * 4u; + offset = dtohs(sparse_entry.offset) * 4; + // res_idx = dtohs(sparse_entry->idx); + res_idx = dtohs(sparse_entry.idx); + // } else if (type->flags & ResTable_type::FLAG_OFFSET16) { + } else if (isTruthy(type.flags & ResTable_type.FLAG_OFFSET16)) { + // auto entry = entry_offsets.convert<uint16_t>() + entry_idx; + int entry = type.entryOffset(entry_idx); + // if (!entry) { + // return base::unexpected(IOError::PAGES_MISSING); + // } + // offset = offset_from16(entry.value()); + offset = entry; + // res_idx = entry_idx; + res_idx = (short) entry_idx; + } else { + // auto entry = entry_offsets.convert<uint32_t>() + entry_idx; + int entry = type.entryOffset(entry_idx); + // if (!entry) { + // return base::unexpected(IOError::PAGES_MISSING); + // } + // offset = dtohl(entry.value()); + offset = dtohl(entry); + res_idx = (short) entry_idx; + } + if (offset != ResTable_type.NO_ENTRY) { - // const ResTable_entry* entry = - // reinterpret_cast<const ResTable_entry*>(reinterpret_cast<const uint8_t*>(type.type) + - // dtohl(type.type.entriesStart) + offset); + // auto entry = type.offset(dtohl(type->entriesStart) + + // offset).convert<ResTable_entry>(); ResTable_entry entry = - new ResTable_entry(type.myBuf(), type.myOffset() + - dtohl(type.entriesStart) + offset); + new ResTable_entry( + type.myBuf(), type.myOffset() + dtohl(type.entriesStart) + offset); + // if (!entry) { + // return base::unexpected(IOError::PAGES_MISSING); + // } + // TODO implement above + // if (entry->key() == static_cast<uint32_t>(*key_idx)) { if (dtohl(entry.getKeyIndex()) == key_idx) { - // The package ID will be overridden by the caller (due to runtime assignment of package + // The package ID will be overridden by the caller (due to runtime assignment of + // package // IDs for shared libraries). - return make_resid((byte) 0x00, (byte) (type_idx + type_id_offset_ + 1), (short) entry_idx); + // return make_resid(0x00, *type_idx + type_id_offset_ + 1, res_idx); + return make_resid((byte) 0x00, (byte) (type_idx + type_id_offset_ + 1), res_idx); } } } } + // return base::unexpected(std::nullopt); return 0; } diff --git a/robolectric/Android.bp b/robolectric/Android.bp index 139062ca6..e690895a3 100644 --- a/robolectric/Android.bp +++ b/robolectric/Android.bp @@ -15,6 +15,7 @@ java_library_host { name: "Robolectric_robolectric_upstream", libs: [ "Robolectric_shadows_framework_upstream", + "Robolectric_shadows_versioning_upstream", "Robolectric_annotations_upstream", "Robolectric_shadowapi_upstream", "Robolectric_resources_upstream", diff --git a/robolectric/build.gradle b/robolectric/build.gradle index faaa8b3a0..b826e9232 100644 --- a/robolectric/build.gradle +++ b/robolectric/build.gradle @@ -5,8 +5,8 @@ apply plugin: RoboJavaModulePlugin apply plugin: DeployedRoboJavaModulePlugin dependencies { - annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion" - annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion" + annotationProcessor libs.auto.service + annotationProcessor libs.error.prone.core api project(":annotations") api project(":junit") @@ -16,33 +16,34 @@ dependencies { api project(":utils") api project(":utils:reflector") api project(":plugins:maven-dependency-resolver") - api "javax.inject:javax.inject:1" - compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion" - api "javax.annotation:javax.annotation-api:1.3.2" + api libs.javax.inject + compileOnly libs.auto.service.annotations + api libs.javax.annotation.api // We need to have shadows-framework.jar on the runtime system classpath so ServiceLoader // can find its META-INF/services/org.robolectric.shadows.ShadowAdapter. api project(":shadows:framework") - implementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' - api "org.bouncycastle:bcprov-jdk18on:1.72" - compileOnly "com.google.code.findbugs:jsr305:3.0.2" + implementation libs.conscrypt.openjdk.uber + api libs.bcprov.jdk18on + compileOnly libs.findbugs.jsr305 compileOnly AndroidSdk.MAX_SDK.coordinates - compileOnly "junit:junit:${junitVersion}" + compileOnly libs.junit4 + api "androidx.test:monitor:$axtMonitorVersion@aar" implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion@aar" - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.truth.java8.extension + testImplementation libs.mockito + testImplementation libs.hamcrest.junit testImplementation "androidx.test:core:$axtCoreVersion@aar" testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar" testImplementation "androidx.test.ext:truth:$axtTruthVersion@aar" testImplementation "androidx.test:runner:$axtRunnerVersion@aar" - testImplementation("com.google.guava:guava:$guavaJREVersion") + testImplementation libs.guava testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports } diff --git a/robolectric/src/main/java/org/robolectric/Robolectric.java b/robolectric/src/main/java/org/robolectric/Robolectric.java index 47a52c54e..3ce637e76 100644 --- a/robolectric/src/main/java/org/robolectric/Robolectric.java +++ b/robolectric/src/main/java/org/robolectric/Robolectric.java @@ -1,5 +1,6 @@ package org.robolectric; +import static com.google.common.base.Preconditions.checkState; import static org.robolectric.shadows.ShadowAssetManager.useLegacy; import android.annotation.IdRes; @@ -11,6 +12,7 @@ import android.app.backup.BackupAgent; import android.content.ContentProvider; import android.content.Intent; import android.os.Bundle; +import android.os.Looper; import android.util.AttributeSet; import android.view.View; import javax.annotation.Nullable; @@ -104,6 +106,9 @@ public class Robolectric { */ public static <T extends Activity> ActivityController<T> buildActivity( Class<T> activityClass, Intent intent, @Nullable Bundle activityOptions) { + checkState( + Thread.currentThread() == Looper.getMainLooper().getThread(), + "buildActivity must be called on main Looper thread"); return ActivityController.of( ReflectionHelpers.callConstructor(activityClass), intent, activityOptions); } diff --git a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java index a807d4e68..ed859dd23 100755..100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java @@ -361,7 +361,7 @@ public class AndroidTestEnvironment implements TestEnvironment { // Preload fonts resources FontsContract.setApplicationContextForResources(application); } - registerBroadcastReceivers(application, appManifest); + registerBroadcastReceivers(application, appManifest, loadedApk); appResources.updateConfiguration(androidConfiguration, displayMetrics); // propagate any updates to configuration via RuntimeEnvironment.setQualifiers @@ -412,6 +412,11 @@ public class AndroidTestEnvironment implements TestEnvironment { Path packageFile = appManifest.getApkFile(); parsedPackage = ShadowPackageParser.callParsePackage(packageFile); } + if (parsedPackage != null + && parsedPackage.applicationInfo != null + && RuntimeEnvironment.getApiLevel() >= P) { + parsedPackage.applicationInfo.appComponentFactory = appManifest.getAppComponentFactory(); + } return parsedPackage; } @@ -691,15 +696,39 @@ public class AndroidTestEnvironment implements TestEnvironment { .toString(); } + private static BroadcastReceiver newBroadcastReceiverFromP( + String receiverClassName, LoadedApk loadedApk) { + ClassLoader classLoader = Shadow.class.getClassLoader(); + if (loadedApk == null || loadedApk.getAppFactory() == null) { + return (BroadcastReceiver) newInstanceOf(receiverClassName); + } else { + try { + return loadedApk.getAppFactory().instantiateReceiver(classLoader, receiverClassName, null); + } catch (ReflectiveOperationException e) { + Logger.warn( + "Failed to initialize receiver %s with AppComponentFactory %s: %s", + receiverClassName, loadedApk.getAppFactory(), e); + } + } + return null; + } + // TODO move/replace this with packageManager @VisibleForTesting - static void registerBroadcastReceivers(Application application, AndroidManifest androidManifest) { + static void registerBroadcastReceivers( + Application application, AndroidManifest androidManifest, LoadedApk loadedApk) { for (BroadcastReceiverData receiver : androidManifest.getBroadcastReceivers()) { IntentFilter filter = new IntentFilter(); for (String action : receiver.getActions()) { filter.addAction(action); } - application.registerReceiver((BroadcastReceiver) newInstanceOf(receiver.getName()), filter); + String receiverClassName = receiver.getName(); + if (loadedApk != null && RuntimeEnvironment.getApiLevel() >= P) { + application.registerReceiver( + newBroadcastReceiverFromP(receiverClassName, loadedApk), filter); + } else { + application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter); + } } } diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java index a6908af09..dd1bc5cca 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java @@ -157,7 +157,7 @@ public class LocalUiController implements UiController { @Override public void loopMainThreadUntilIdle() { - if (!ShadowLooper.looperMode().equals(LooperMode.Mode.PAUSED)) { + if (ShadowLooper.looperMode().equals(LooperMode.Mode.LEGACY)) { shadowMainLooper().idle(); } else { ImmutableSet<IdlingResourceProxy> idlingResources = syncIdlingResources(); diff --git a/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java b/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java index 69adb67e6..69adb67e6 100755..100644 --- a/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java +++ b/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java diff --git a/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java b/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java index 837e966e5..837e966e5 100755..100644 --- a/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java +++ b/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java diff --git a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java index c8b2693ea..cf3af7d55 100644 --- a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java +++ b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java @@ -1,5 +1,23 @@ package org.robolectric.plugins; +import com.google.auto.service.AutoService; +import com.google.common.base.Preconditions; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.SortedMap; +import java.util.TreeMap; +import javax.annotation.Priority; +import javax.inject.Inject; +import org.robolectric.internal.dependency.DependencyJar; +import org.robolectric.internal.dependency.DependencyResolver; +import org.robolectric.pluginapi.Sdk; +import org.robolectric.pluginapi.SdkProvider; +import org.robolectric.shadows.ShadowBuild; +import org.robolectric.util.Util; import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; @@ -17,28 +35,9 @@ import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; import static android.os.Build.VERSION_CODES.S_V2; import static android.os.Build.VERSION_CODES.TIRAMISU; -import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; import android.os.Build; -import com.google.auto.service.AutoService; -import com.google.common.base.Preconditions; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Collections; -import java.util.Locale; -import java.util.SortedMap; -import java.util.TreeMap; -import javax.annotation.Priority; -import javax.inject.Inject; -import org.robolectric.internal.dependency.DependencyJar; -import org.robolectric.internal.dependency.DependencyResolver; -import org.robolectric.pluginapi.Sdk; -import org.robolectric.pluginapi.SdkProvider; -import org.robolectric.util.Util; - /** * Robolectric's default {@link SdkProvider}. * @@ -85,8 +84,6 @@ public class DefaultSdkProvider implements SdkProvider { knownSdks.put(S, new DefaultSdk(S, "12", "7732740", "REL", 9)); knownSdks.put(S_V2, new DefaultSdk(S_V2, "12.1", "8229987", "REL", 9)); knownSdks.put(TIRAMISU, new DefaultSdk(TIRAMISU, "13", "9030017", "Tiramisu", 9)); - // TODO(rexhoffman): should this have a dedicated mechanism? Should we maintain a known good version? - knownSdks.put(CUR_DEVELOPMENT, new DefaultSdk(CUR_DEVELOPMENT, "current", "r0", "UpsideDownCake", 9)); } @Override diff --git a/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java b/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java new file mode 100644 index 000000000..22df750a9 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java @@ -0,0 +1,22 @@ +package org.robolectric; + +import android.app.AppComponentFactory; +import android.content.BroadcastReceiver; +import android.content.Intent; +import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver; +import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver; + +public final class CustomAppComponentFactory extends AppComponentFactory { + @Override + public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + if (className != null) { + if (className.contains(CustomConstructorWithOneActionReceiver.class.getName())) { + return new CustomConstructorWithOneActionReceiver(100); + } else if (className.contains(CustomConstructorWithEmptyActionReceiver.class.getName())) { + return new CustomConstructorWithEmptyActionReceiver(100); + } + } + return super.instantiateReceiver(cl, className, intent); + } +} diff --git a/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java b/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java new file mode 100644 index 000000000..1132f6eb7 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java @@ -0,0 +1,32 @@ +package org.robolectric; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class CustomConstructorReceiverWrapper { + private static class CustomConstructorReceiver extends BroadcastReceiver { + private final int intValue; + + public CustomConstructorReceiver(int intValue) { + // We don't use intValue actually, and we only want to use this class to test the + // initialization of BroadcastReceiver with a custom constructor. + this.intValue = intValue; + } + + @Override + public void onReceive(Context context, Intent intent) {} + } + + public static class CustomConstructorWithOneActionReceiver extends CustomConstructorReceiver { + public CustomConstructorWithOneActionReceiver(int intValue) { + super(intValue); + } + } + + public static class CustomConstructorWithEmptyActionReceiver extends CustomConstructorReceiver { + public CustomConstructorWithEmptyActionReceiver(int intValue) { + super(intValue); + } + } +} diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java index 0ba55d893..c0108de97 100644 --- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java +++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java @@ -50,6 +50,7 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.Config.Implementation; import org.robolectric.annotation.experimental.LazyApplication; import org.robolectric.annotation.experimental.LazyApplication.LazyLoad; +import org.robolectric.config.ConfigurationRegistry; import org.robolectric.internal.AndroidSandbox.TestEnvironmentSpec; import org.robolectric.internal.ResourcesMode; import org.robolectric.internal.ShadowProvider; @@ -163,10 +164,10 @@ public class RobolectricTestRunnerTest { assertThat(events) .containsExactly( "started: first", - "failure: ShadowActivityThread.reset: ActivityThread not set", + "failure: fake error in setUpApplicationState", "finished: first", "started: second", - "failure: ShadowActivityThread.reset: ActivityThread not set", + "failure: fake error in setUpApplicationState", "finished: second") .inOrder(); } @@ -319,6 +320,9 @@ public class RobolectricTestRunnerTest { @Override public void setUpApplicationState(Method method, Configuration configuration, AndroidManifest appManifest) { + // ConfigurationRegistry.instance is required for resetters. + Config config = configuration.get(Config.class); + ConfigurationRegistry.instance = new ConfigurationRegistry(configuration.map()); throw new RuntimeException("fake error in setUpApplicationState"); } } diff --git a/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java index 87ddb071b..0428c44e1 100644 --- a/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java +++ b/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java @@ -3,9 +3,9 @@ package org.robolectric.android; import static android.os.Build.VERSION_CODES.KITKAT_WATCH; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeTrue; import static org.robolectric.shadows.ShadowAssetManager.useLegacy; import android.animation.Animator; @@ -31,7 +31,7 @@ public class DrawableResourceLoaderTest { @Before public void setup() throws Exception { - assumeTrue(useLegacy()); + assume().that(useLegacy()).isTrue(); resources = ApplicationProvider.getApplicationContext().getResources(); } diff --git a/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java index b895d6503..15ca52c68 100644 --- a/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java +++ b/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java @@ -2,7 +2,7 @@ package org.robolectric.android; import static android.os.Build.VERSION_CODES.O; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeTrue; +import static com.google.common.truth.TruthJUnit.assume; import static org.robolectric.shadows.ShadowAssetManager.useLegacy; import android.content.res.Configuration; @@ -32,7 +32,7 @@ public class ResourceLoaderTest { @Before public void setUp() { - assumeTrue(useLegacy()); + assume().that(useLegacy()).isTrue(); optsForO = RuntimeEnvironment.getApiLevel() >= O ? "nowidecg-lowdr-" @@ -71,7 +71,11 @@ public class ResourceLoaderTest { private void checkForPollutionHelper() { assertThat(RuntimeEnvironment.getQualifiers()) - .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav-v" + Build.VERSION.RESOURCES_SDK_INT); + .isEqualTo( + "en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav-v" + + Build.VERSION.RESOURCES_SDK_INT); View view = LayoutInflater.from(ApplicationProvider.getApplicationContext()) @@ -97,7 +101,10 @@ public class ResourceLoaderTest { assertThat(resId).isNotNull(); assertThat(resourceProvider.getResName(resId)).isEqualTo(internalResource); - Class<?> internalRIdClass = Robolectric.class.getClassLoader().loadClass("com.android.internal.R$" + internalResource.type); + Class<?> internalRIdClass = + Robolectric.class + .getClassLoader() + .loadClass("com.android.internal.R$" + internalResource.type); int internalResourceId; internalResourceId = (Integer) internalRIdClass.getDeclaredField(internalResource.name).get(null); assertThat(resId).isEqualTo(internalResourceId); diff --git a/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java b/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java index 0ae467557..d4395c5a7 100644 --- a/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java +++ b/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java @@ -1,7 +1,7 @@ package org.robolectric.android; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeTrue; +import static com.google.common.truth.TruthJUnit.assume; import static org.robolectric.shadows.ShadowAssetManager.useLegacy; import android.os.Build; @@ -17,7 +17,7 @@ import org.robolectric.res.ResName; public class ResourceTableFactoryIntegrationTest { @Test public void shouldIncludeStyleableAttributesThatDoNotHaveACorrespondingEntryInAttrClass() throws Exception { - assumeTrue(useLegacy()); + assume().that(useLegacy()).isTrue(); // This covers a corner case in Framework resources where an attribute is mentioned in a styleable array, e.g: R.styleable.Toolbar_buttonGravity but there is no corresponding R.attr.buttonGravity assertThat(RuntimeEnvironment.getSystemResourceTable() .getResourceId(new ResName("android", "attr", "buttonGravity"))).isGreaterThan(0); diff --git a/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java b/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java index b57f606ee..0c8d977d0 100644 --- a/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java +++ b/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java @@ -2,11 +2,11 @@ package org.robolectric.android; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.common.truth.TruthJUnit.assume; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; import android.app.Application; import android.content.res.XmlResourceParser; @@ -276,7 +276,7 @@ public class XmlResourceParserImplTest { @Test public void testIsWhitespace() throws Exception { - assumeTrue(RuntimeEnvironment.useLegacyResources()); + assume().that(RuntimeEnvironment.useLegacyResources()).isTrue(); XmlResourceParserImpl parserImpl = (XmlResourceParserImpl) parser; assertThat(parserImpl.isWhitespace("bar")).isFalse(); diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java index 6edc42886..dc6ac3138 100644 --- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java +++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java @@ -88,7 +88,7 @@ public class AndroidTestEnvironmentCreateApplicationTest { Application application = AndroidTestEnvironment.createApplication(appManifest, null, new ApplicationInfo()); shadowOf(application).callAttach(RuntimeEnvironment.systemContext); - registerBroadcastReceivers(application, appManifest); + registerBroadcastReceivers(application, appManifest, null); List<ShadowApplication.Wrapper> receivers = shadowOf(application).getRegisteredReceivers(); assertThat(receivers).hasSize(1); diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java index 051188139..70dd385b4 100644 --- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java +++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java @@ -2,8 +2,8 @@ package org.robolectric.android.internal; import static android.os.Build.VERSION_CODES.O; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; import static org.robolectric.annotation.ConscryptMode.Mode.OFF; import static org.robolectric.annotation.ConscryptMode.Mode.ON; import static org.robolectric.annotation.LooperMode.Mode.LEGACY; @@ -241,7 +241,7 @@ public class AndroidTestEnvironmentTest { @Test public void testResourceNotFound() { // not relevant for binary resources mode - assumeTrue(bootstrapWrapper.isLegacyResources()); + assume().that(bootstrapWrapper.isLegacyResources()).isTrue(); try { bootstrapWrapper.changeAppManifest(new ThrowingManifest(bootstrapWrapper.getAppManifest())); diff --git a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java index e3163ccdc..af1d8ba86 100644 --- a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java +++ b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java @@ -66,10 +66,10 @@ public class AndroidInterceptorsIntegrationTest { @Test public void systemNanoTime_shouldReturnShadowClockTime() throws Throwable { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - SystemClock.setCurrentTimeMillis(200); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos()); + } else { + SystemClock.setCurrentTimeMillis(200); } long nanoTime = invokeDynamic(System.class, "nanoTime", long.class); @@ -78,10 +78,10 @@ public class AndroidInterceptorsIntegrationTest { @Test public void systemCurrentTimeMillis_shouldReturnShadowClockTime() throws Throwable { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - SystemClock.setCurrentTimeMillis(200); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos()); + } else { + SystemClock.setCurrentTimeMillis(200); } long currentTimeMillis = invokeDynamic(System.class, "currentTimeMillis", long.class); @@ -187,6 +187,6 @@ public class AndroidInterceptorsIntegrationTest { callsite .dynamicInvoker() .invokeWithArguments( - Arrays.stream(params).map(param -> param.val).collect(Collectors.toList())); + Arrays.stream(params).map(param -> param.value).collect(Collectors.toList())); } } diff --git a/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java b/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java index 414fdb09d..414fdb09d 100755..100644 --- a/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java +++ b/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java diff --git a/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java b/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java index f4562f5d2..f4562f5d2 100755..100644 --- a/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java +++ b/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java diff --git a/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java index 8b493ba71..dc2026601 100644 --- a/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java +++ b/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java @@ -2,7 +2,7 @@ package org.robolectric.res; import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeTrue; +import static com.google.common.truth.TruthJUnit.assume; import static org.robolectric.util.TestUtil.sdkResources; import org.junit.Before; @@ -18,14 +18,16 @@ public class StyleResourceLoaderTest { @Before public void setUp() throws Exception { - assumeTrue(RuntimeEnvironment.useLegacyResources()); + assume().that(RuntimeEnvironment.useLegacyResources()).isTrue(); ResourcePath resourcePath = sdkResources(JELLY_BEAN); resourceTable = new ResourceTableFactory().newResourceTable("android", resourcePath); } @Test public void testStyleDataIsLoadedCorrectly() throws Exception { - TypedResource typedResource = resourceTable.getValue(new ResName("android", "style", "Theme_Holo"), new ResTable_config()); + TypedResource typedResource = + resourceTable.getValue( + new ResName("android", "style", "Theme_Holo"), new ResTable_config()); StyleData styleData = (StyleData) typedResource.getData(); assertThat(styleData.getName()).isEqualTo("Theme_Holo"); assertThat(styleData.getParent()).isEqualTo("Theme"); diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java new file mode 100644 index 000000000..72887ddb5 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java @@ -0,0 +1,114 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build; +import android.telephony.CellIdentityLte; +import android.telephony.CellInfo; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Test for {@link CellIdentityLteBuilder} */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1) +public class CellIdentityLteBuilderTest { + + private static final String MCC = "310"; + private static final String MNC = "260"; + private static final int CI = 0; + private static final int PCI = 1; + private static final int TAC = 2; + private static final int EARFCN = 4; + private static final int[] BANDS = new int[] {2, 4}; + private static final int BANDWIDTH = 5; + private static final String SHORT_OPERATOR_NAME = "short operator name"; + private static final String LONG_OPERATOR_NAME = "long operator name"; + private static final ImmutableList<String> ADDITIONAL_PLMNS = ImmutableList.of("310240"); + + @Test + public void build_noArguments() { + // The intent is to primarily verify that there are no issues setting default values i.e., no + // exceptions thrown or invalid inputs. + CellIdentityLte cellIdentity = CellIdentityLteBuilder.newBuilder().build(); + + assertThat(cellIdentity.getCi()).isEqualTo(CellInfo.UNAVAILABLE); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.M) + public void build_sdkJtoM() { + CellIdentityLte cellIdentity = getCellIdentityLte(); + + assertCellIdentityFieldsForAllSdks(cellIdentity); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.N, maxSdk = Build.VERSION_CODES.O_MR1) + public void build_sdkNtoO() { + CellIdentityLte cellIdentity = getCellIdentityLte(); + + assertCellIdentityFieldsForAllSdks(cellIdentity); + assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R) + public void build_sdkPtoQ() { + CellIdentityLte cellIdentity = getCellIdentityLte(); + + assertCellIdentityFieldsForAllSdks(cellIdentity); + assertThat(cellIdentity.getMccString()).isEqualTo(MCC); + assertThat(cellIdentity.getMncString()).isEqualTo(MNC); + assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN); + assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH); + assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME); + assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.S) + public void build_fromSdkS() { + CellIdentityLte cellIdentity = getCellIdentityLte(); + + assertCellIdentityFieldsForAllSdks(cellIdentity); + assertThat(cellIdentity.getMccString()).isEqualTo(MCC); + assertThat(cellIdentity.getMncString()).isEqualTo(MNC); + assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN); + assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH); + assertThat(cellIdentity.getBands()).isEqualTo(BANDS); + assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME); + assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME); + assertThat(cellIdentity.getAdditionalPlmns()).containsExactlyElementsIn(ADDITIONAL_PLMNS); + } + + /** + * Assertions on {@link android.telephony.CellIdentityLte} values that are common across all + * tested SDKs. + */ + private void assertCellIdentityFieldsForAllSdks(CellIdentityLte cellIdentity) { + assertThat(cellIdentity.getMcc()).isEqualTo(Integer.parseInt(MCC)); + assertThat(cellIdentity.getMnc()).isEqualTo(Integer.parseInt(MNC)); + assertThat(cellIdentity.getCi()).isEqualTo(CI); + assertThat(cellIdentity.getPci()).isEqualTo(PCI); + assertThat(cellIdentity.getTac()).isEqualTo(TAC); + } + + private CellIdentityLte getCellIdentityLte() { + return CellIdentityLteBuilder.newBuilder() + .setMcc(MCC) + .setMnc(MNC) + .setCi(CI) + .setPci(PCI) + .setTac(TAC) + .setEarfcn(EARFCN) + .setBands(BANDS) + .setBandwidth(BANDWIDTH) + .setLongOperatorName(LONG_OPERATOR_NAME) + .setShortOperatorName(SHORT_OPERATOR_NAME) + .setAdditionalPlmns(ADDITIONAL_PLMNS) + .build(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java new file mode 100644 index 000000000..8474b2136 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java @@ -0,0 +1,90 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build; +import android.telephony.AccessNetworkConstants; +import android.telephony.CellIdentityNr; +import android.telephony.CellInfo; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Test for {@link CellIdentityNrBuilder} */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = Build.VERSION_CODES.Q) +public class CellIdentityNrBuilderTest { + + private static final int PCI = 1; + private static final int TAC = 2; + private static final int NRARFCN = 4; + private static final int[] BANDS = + new int[] { + AccessNetworkConstants.NgranBands.BAND_1, AccessNetworkConstants.NgranBands.BAND_2 + }; + private static final String MCC = "310"; + private static final String MNC = "260"; + private static final int NCI = 0; + private static final String LONG_OPERATOR_NAME = "long operator name"; + private static final String SHORT_OPERATOR_NAME = "short operator name"; + private static final ImmutableList<String> ADDITIONAL_PLMNS = ImmutableList.of("310240"); + + @Test + public void build_noArguments() { + // The intent is to primarily verify that there are no issues setting default values i.e., no + // exceptions thrown or invalid inputs. + CellIdentityNr cellIdentity = CellIdentityNrBuilder.newBuilder().build(); + + assertThat(cellIdentity.getPci()).isEqualTo(CellInfo.UNAVAILABLE); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R) + public void build_sdkQtoR() { + CellIdentityNr cellIdentity = getCellIdentityNr(); + + assertCellIdentityFieldsForAllSdks(cellIdentity); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.S) + public void build_fromSdkS() { + CellIdentityNr cellIdentity = getCellIdentityNr(); + + assertCellIdentityFieldsForAllSdks(cellIdentity); + assertThat(cellIdentity.getBands()).isEqualTo(BANDS); + assertThat(cellIdentity.getAdditionalPlmns()).containsExactlyElementsIn(ADDITIONAL_PLMNS); + } + + /** + * Assertions on {@link android.telephony.CellIdentityNr} values that are common across all tested + * SDKs. + */ + private void assertCellIdentityFieldsForAllSdks(CellIdentityNr cellIdentity) { + assertThat(cellIdentity.getPci()).isEqualTo(PCI); + assertThat(cellIdentity.getTac()).isEqualTo(TAC); + assertThat(cellIdentity.getNrarfcn()).isEqualTo(NRARFCN); + assertThat(cellIdentity.getMccString()).isEqualTo(MCC); + assertThat(cellIdentity.getMncString()).isEqualTo(MNC); + assertThat(cellIdentity.getNci()).isEqualTo(NCI); + assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME); + assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME); + } + + private CellIdentityNr getCellIdentityNr() { + return CellIdentityNrBuilder.newBuilder() + .setPci(PCI) + .setTac(TAC) + .setNrarfcn(NRARFCN) + .setBands(BANDS) + .setMcc(MCC) + .setMnc(MNC) + .setNci(NCI) + .setLongOperatorName(LONG_OPERATOR_NAME) + .setShortOperatorName(SHORT_OPERATOR_NAME) + .setAdditionalPlmns(ADDITIONAL_PLMNS) + .build(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java new file mode 100644 index 000000000..f61abad90 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java @@ -0,0 +1,81 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build; +import android.telephony.CellIdentityLte; +import android.telephony.CellInfoLte; +import android.telephony.CellSignalStrengthLte; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Test for {@link CellInfoLteBuilder} */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1) +public class CellInfoLteBuilderTest { + + private static final boolean REGISTERED = false; + private static final long TIMESTAMP_NANOS = 123L; + private static final long TIMESTAMP_MILLIS = Duration.ofNanos(TIMESTAMP_NANOS).toMillis(); + private static final int CELL_CONNECTION_STATUS = 1; + + private static final CellIdentityLte cellIdentity = + CellIdentityLteBuilder.newBuilder().setMcc("310").build(); + private static final CellSignalStrengthLte cellSignalStrength = + CellSignalStrengthLteBuilder.newBuilder().setRsrp(-120).build(); + + @Test + public void build_noArguments() { + // The intent is to primarily verify that there are no issues setting default values i.e., no + // exceptions thrown or invalid inputs. + CellInfoLte cellInfo = CellInfoLteBuilder.newBuilder().build(); + + assertThat(cellInfo.getTimeStamp()).isEqualTo(0); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1) + public void build_sdkJtoN() { + CellInfoLte cellInfo = getCellInfoLte(); + + assertThat(cellInfo.isRegistered()).isFalse(); + assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS); + assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.P, maxSdk = Build.VERSION_CODES.Q) + public void build_fromSdkPtoQ() { + CellInfoLte cellInfo = getCellInfoLte(); + + assertThat(cellInfo.isRegistered()).isFalse(); + assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS); + assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS); + assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.R, maxSdk = Config.NEWEST_SDK) + public void build_fromSdkR() { + CellInfoLte cellInfo = getCellInfoLte(); + + assertThat(cellInfo.isRegistered()).isFalse(); + assertThat(cellInfo.getTimestampMillis()).isEqualTo(TIMESTAMP_MILLIS); + assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS); + assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength); + assertThat(cellInfo.getCellIdentity()).isEqualTo(cellIdentity); + } + + private CellInfoLte getCellInfoLte() { + return CellInfoLteBuilder.newBuilder() + .setRegistered(REGISTERED) + .setTimeStampNanos(TIMESTAMP_NANOS) + .setCellConnectionStatus(CELL_CONNECTION_STATUS) + .setCellIdentity(cellIdentity) + .setCellSignalStrength(cellSignalStrength) + .build(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java new file mode 100644 index 000000000..80e7dcc6e --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java @@ -0,0 +1,76 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build; +import android.telephony.CellIdentityNr; +import android.telephony.CellInfoNr; +import android.telephony.CellSignalStrengthNr; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Test for {@link CellInfoNrBuilder} */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = Build.VERSION_CODES.Q) +public class CellInfoNrBuilderTest { + + private static final boolean REGISTERED = false; + private static final long TIMESTAMP_NANOS = 123L; + private static final long TIMESTAMP_MILLIS = Duration.ofNanos(TIMESTAMP_NANOS).toMillis(); + private static final int CELL_CONNECTION_STATUS = 1; + + private static final CellIdentityNr cellIdentity = + CellIdentityNrBuilder.newBuilder().setMcc("310").build(); + private static final CellSignalStrengthNr cellSignalStrength = + CellSignalStrengthNrBuilder.newBuilder().setCsiRsrp(-100).build(); + + @Test + public void build_noArguments() { + // The intent is to primarily verify that there are no issues setting default values i.e., no + // exceptions thrown or invalid inputs. + CellInfoNr cellInfo = CellInfoNrBuilder.newBuilder().build(); + + assertThat(cellInfo.getTimeStamp()).isEqualTo(0); + } + + @Test + @Config(sdk = Build.VERSION_CODES.Q) + public void build_sdkQ() { + CellInfoNr cellInfo = getCellInfoNr(); + + assertCellInfoFieldsForAllSdks(cellInfo); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.R) + public void build_fromSdkR() { + CellInfoNr cellInfo = getCellInfoNr(); + + assertCellInfoFieldsForAllSdks(cellInfo); + assertThat(cellInfo.getTimestampMillis()).isEqualTo(TIMESTAMP_MILLIS); + } + + /** + * Assertions on {@link android.telephony.CellInfo} values that are common across all tested SDKs. + */ + private void assertCellInfoFieldsForAllSdks(CellInfoNr cellInfo) { + assertThat(cellInfo.isRegistered()).isFalse(); + assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS); + assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS); + assertThat(cellInfo.getCellIdentity()).isEqualTo(cellIdentity); + assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength); + } + + private CellInfoNr getCellInfoNr() { + return CellInfoNrBuilder.newBuilder() + .setRegistered(REGISTERED) + .setTimeStampNanos(TIMESTAMP_NANOS) + .setCellConnectionStatus(CELL_CONNECTION_STATUS) + .setCellIdentity(cellIdentity) + .setCellSignalStrength(cellSignalStrength) + .build(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java new file mode 100644 index 000000000..cfd3bbeeb --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java @@ -0,0 +1,95 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build; +import android.telephony.CellInfo; +import android.telephony.CellSignalStrengthLte; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Test for {@link CellSignalStrengthLteBuilder} */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1) +public class CellSignalStrengthLteBuilderTest { + + // The platform enforces that some of these values are within a certain range - otherwise, it will + // default to {@link android.telephony.CellInfo.UNAVAILABLE}. + private static final int RSSI = -100; + private static final int RSRP = -120; + private static final int RSRQ = -10; + private static final int RSSNR = 30; + private static final int CQI_TABLE_INDEX = 4; + private static final int CQI = 5; + private static final int TIMING_ADVANCE = 6; + + @Test + public void build_noArguments() { + // The intent is to primarily verify that there are no issues setting default values i.e., no + // exceptions thrown or invalid inputs. + CellSignalStrengthLte cellSignalStrength = CellSignalStrengthLteBuilder.newBuilder().build(); + + assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(CellInfo.UNAVAILABLE); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1) + public void build_sdkJtoN() { + CellSignalStrengthLte cellSignalStrength = getCellSignalStrength(); + + assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.O, maxSdk = Build.VERSION_CODES.P) + public void build_sdkOToP() { + CellSignalStrengthLte cellSignalStrength = getCellSignalStrength(); + + assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP); + assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR); + assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ); + assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI); + assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R) + public void build_sdkQtoR() { + CellSignalStrengthLte cellSignalStrength = getCellSignalStrength(); + + assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI); + assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP); + assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ); + assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR); + assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI); + assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.S) + public void build_fromSdkS() { + CellSignalStrengthLte cellSignalStrength = getCellSignalStrength(); + + assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI); + assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP); + assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ); + assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR); + assertThat(cellSignalStrength.getCqiTableIndex()).isEqualTo(CQI_TABLE_INDEX); + assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI); + assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE); + } + + private CellSignalStrengthLte getCellSignalStrength() { + return CellSignalStrengthLteBuilder.newBuilder() + .setRssi(RSSI) + .setRsrp(RSRP) + .setRsrq(RSRQ) + .setRssnr(RSSNR) + .setCqi(CQI) + .setCqiTableIndex(CQI_TABLE_INDEX) + .setTimingAdvance(TIMING_ADVANCE) + .build(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java new file mode 100644 index 000000000..1f18ee8dc --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java @@ -0,0 +1,84 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build; +import android.telephony.CellInfo; +import android.telephony.CellSignalStrengthNr; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Test for {@link CellSignalStrengthNrBuilder} */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = Build.VERSION_CODES.Q) +public class CellSignalStrengthNrBuilderTest { + + // The platform enforces that some of these values are within a certain range - otherwise, it will + // default to {@link android.telephony.CellInfo.UNAVAILABLE}. + private static final int CSI_RSRP = -100; + private static final int CSI_RSRQ = -10; + private static final int CSI_SINR = -20; + private static final int CSI_CQI_TABLE_INDEX = 1; + private static final ImmutableList<Byte> CSI_CQI_REPORT = ImmutableList.of((byte) 7); + private static final int SS_RSRP = -140; + private static final int SS_RSRQ = -15; + private static final int SS_SINR = -20; + private static final int TIMING_ADVANCE = 10; + + @Test + public void build_noArguments() { + // The intent is to primarily verify that there are no issues setting default values i.e., no + // exceptions thrown or invalid inputs. + CellSignalStrengthNr cellSignalStrength = CellSignalStrengthNrBuilder.newBuilder().build(); + + assertThat(cellSignalStrength.getCsiRsrp()).isEqualTo(CellInfo.UNAVAILABLE); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.S_V2) + public void build_sdkQtoS() { + CellSignalStrengthNr cellSignalStrength = getCellSignalStrength(); + + assertCellSignalStrengthFieldsForAllSdks(cellSignalStrength); + } + + @Test + @Config(minSdk = Build.VERSION_CODES.TIRAMISU) + public void build_fromSdkT() { + CellSignalStrengthNr cellSignalStrength = getCellSignalStrength(); + + assertCellSignalStrengthFieldsForAllSdks(cellSignalStrength); + assertThat(cellSignalStrength.getCsiCqiTableIndex()).isEqualTo(CSI_CQI_TABLE_INDEX); + assertThat(cellSignalStrength.getCsiCqiReport()).containsExactly(7); + } + + /** + * Assertions on {@link android.telephony.CellSignalStrengthNr} values that are common across all + * tested SDKs. + */ + private void assertCellSignalStrengthFieldsForAllSdks(CellSignalStrengthNr cellSignalStrength) { + assertThat(cellSignalStrength.getCsiRsrp()).isEqualTo(CSI_RSRP); + assertThat(cellSignalStrength.getCsiRsrq()).isEqualTo(CSI_RSRQ); + assertThat(cellSignalStrength.getCsiSinr()).isEqualTo(CSI_SINR); + assertThat(cellSignalStrength.getSsRsrp()).isEqualTo(SS_RSRP); + assertThat(cellSignalStrength.getSsRsrq()).isEqualTo(SS_RSRQ); + assertThat(cellSignalStrength.getSsSinr()).isEqualTo(SS_SINR); + } + + private CellSignalStrengthNr getCellSignalStrength() { + return CellSignalStrengthNrBuilder.newBuilder() + .setCsiRsrp(CSI_RSRP) + .setCsiRsrq(CSI_RSRQ) + .setCsiSinr(CSI_SINR) + .setCsiCqiTableIndex(CSI_CQI_TABLE_INDEX) + .setCsiCqiReport(CSI_CQI_REPORT) + .setSsRsrp(SS_RSRP) + .setSsRsrq(SS_RSRQ) + .setSsSinr(SS_SINR) + .setTimingAdvance(TIMING_ADVANCE) + .build(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java index 27e635c8f..06769c66e 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java @@ -14,6 +14,7 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; import android.media.MediaFormat; +import android.util.Range; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,6 +30,10 @@ public class MediaCodecInfoBuilderTest { private static final String VP9_DECODER_NAME = "test.decoder.vp9"; private static final String MULTIFORMAT_ENCODER_NAME = "test.encoder.multiformat"; + private static final int WIDTH = 1920; + private static final int HEIGHT = 1080; + private static final Range<Integer> DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE = new Range<>(2, 896); + private static final MediaFormat AAC_MEDIA_FORMAT = createMediaFormat( MIMETYPE_AUDIO_AAC, new String[] {CodecCapabilities.FEATURE_DynamicTimestamp}); @@ -37,6 +42,9 @@ public class MediaCodecInfoBuilderTest { MIMETYPE_AUDIO_OPUS, new String[] {CodecCapabilities.FEATURE_AdaptivePlayback}); private static final MediaFormat AVC_MEDIA_FORMAT = createMediaFormat(MIMETYPE_VIDEO_AVC, new String[] {CodecCapabilities.FEATURE_IntraRefresh}); + private static final MediaFormat AVC_MEDIA_FORMAT_WITH_RESOLUTION = + createMediaFormat( + MIMETYPE_VIDEO_AVC, WIDTH, HEIGHT, new String[] {CodecCapabilities.FEATURE_IntraRefresh}); private static final MediaFormat VP9_MEDIA_FORMAT = createMediaFormat( MIMETYPE_VIDEO_VP9, @@ -123,6 +131,10 @@ public class MediaCodecInfoBuilderTest { assertThat(codecCapabilities.getMimeType()).isEqualTo(MIMETYPE_VIDEO_AVC); assertThat(codecCapabilities.getAudioCapabilities()).isNull(); assertThat(codecCapabilities.getVideoCapabilities()).isNotNull(); + assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths()) + .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE); + assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights()) + .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE); assertThat(codecCapabilities.getEncoderCapabilities()).isNotNull(); assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_IntraRefresh)) .isTrue(); @@ -136,6 +148,24 @@ public class MediaCodecInfoBuilderTest { @Test @Config(minSdk = Q) + public void canCreateVideoEncoderCapabilities_supportedFormatResolutionIsSet() { + CodecCapabilities codecCapabilities = + MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() + .setMediaFormat(AVC_MEDIA_FORMAT_WITH_RESOLUTION) + .setIsEncoder(true) + .setProfileLevels(AVC_PROFILE_LEVELS) + .setColorFormats(AVC_COLOR_FORMATS) + .build(); + + assertThat(codecCapabilities.getVideoCapabilities()).isNotNull(); + assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths()) + .isEqualTo(new Range<>(1, WIDTH)); + assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights()) + .isEqualTo(new Range<>(1, HEIGHT)); + } + + @Test + @Config(minSdk = Q) public void canCreateVideoDecoderCapabilities() { CodecCapabilities codecCapabilities = MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() @@ -353,4 +383,24 @@ public class MediaCodecInfoBuilderTest { } return mediaFormat; } + + /** + * Create a sample {@link MediaFormat}. + * + * @param mime one of MIMETYPE_* from {@link MediaFormat}. + * @param width The width of the content (in pixels). + * @param height The height of the content (in pixels). + * @param features an array of CodecCapabilities.FEATURE_ features to be enabled. + */ + private static MediaFormat createMediaFormat( + String mime, int width, int height, String[] features) { + MediaFormat mediaFormat = new MediaFormat(); + mediaFormat.setString(MediaFormat.KEY_MIME, mime); + mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width); + mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height); + for (String feature : features) { + mediaFormat.setFeatureEnabled(feature, true); + } + return mediaFormat; + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java index 9ecb63d06..5de84d80d 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java @@ -1,8 +1,8 @@ package org.robolectric.shadows; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.robolectric.shadows.ShadowAssetManager.legacyShadowOf; @@ -46,7 +46,7 @@ public class ShadowAssetManagerTest { @Test public void openFd_shouldProvideFileDescriptorForDeflatedAsset() throws Exception { - assumeTrue(!useLegacy()); + assume().that(useLegacy()).isFalse(); expectedException.expect(FileNotFoundException.class); expectedException.expectMessage( "This file can not be opened as a file descriptor; it is probably compressed"); @@ -79,7 +79,7 @@ public class ShadowAssetManagerTest { @Test public void openNonAssetShouldThrowExceptionWhenFileDoesNotExist() throws IOException { - assumeTrue(useLegacy()); + assume().that(useLegacy()).isTrue(); expectedException.expect(IOException.class); expectedException.expectMessage( @@ -90,7 +90,7 @@ public class ShadowAssetManagerTest { @Test public void unknownResourceIdsShouldReportPackagesSearched() throws IOException { - assumeTrue(useLegacy()); + assume().that(useLegacy()).isTrue(); expectedException.expect(Resources.NotFoundException.class); expectedException.expectMessage("Resource ID #0xffffffff"); @@ -102,7 +102,7 @@ public class ShadowAssetManagerTest { @Test public void forSystemResources_unknownResourceIdsShouldReportPackagesSearched() throws IOException { - if (!useLegacy()) return; + assume().that(useLegacy()).isTrue(); expectedException.expect(Resources.NotFoundException.class); expectedException.expectMessage("Resource ID #0xffffffff"); @@ -113,8 +113,7 @@ public class ShadowAssetManagerTest { @Test @Config(qualifiers = "mdpi") public void openNonAssetShouldOpenCorrectAssetBasedOnQualifierMdpi() throws IOException { - if (!useLegacy()) return; - + assume().that(useLegacy()).isTrue(); InputStream inputStream = assetManager.openNonAsset(0, "res/drawable/robolectric.png", 0); assertThat(countBytes(inputStream)).isEqualTo(8141); } @@ -122,8 +121,7 @@ public class ShadowAssetManagerTest { @Test @Config(qualifiers = "hdpi") public void openNonAssetShouldOpenCorrectAssetBasedOnQualifierHdpi() throws IOException { - if (!useLegacy()) return; - + assume().that(useLegacy()).isTrue(); InputStream inputStream = assetManager.openNonAsset(0, "res/drawable/robolectric.png", 0); assertThat(countBytes(inputStream)).isEqualTo(23447); } @@ -178,8 +176,7 @@ public class ShadowAssetManagerTest { @Test public void attrsToTypedArray_shouldAllowMockedAttributeSets() { - if (!useLegacy()) return; - + assume().that(useLegacy()).isTrue(); AttributeSet mockAttributeSet = mock(AttributeSet.class); when(mockAttributeSet.getAttributeCount()).thenReturn(1); when(mockAttributeSet.getAttributeNameResource(0)).thenReturn(android.R.attr.windowBackground); @@ -191,7 +188,7 @@ public class ShadowAssetManagerTest { @Test public void whenStyleAttrResolutionFails_attrsToTypedArray_returnsNiceErrorMessage() { - if (!useLegacy()) return; + assume().that(useLegacy()).isTrue(); expectedException.expect(RuntimeException.class); expectedException.expectMessage( "no value for org.robolectric:attr/styleNotSpecifiedInAnyTheme in theme with applied" diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java index b798a74a3..149856254 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java @@ -8,6 +8,7 @@ import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -24,6 +25,7 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; +import android.media.AudioSystem; import android.media.MediaRecorder.AudioSource; import android.media.audiopolicy.AudioPolicy; import androidx.test.core.app.ApplicationProvider; @@ -48,6 +50,17 @@ public class ShadowAudioManagerTest { private Context appContext; private AudioManager audioManager; + // When creating Audio Device Info, we need to pass external device type instead of internal input + // device(e.g. AudioDeviceInfo.TYPE_BLUETOOTH_SCO) + // The mapping between external device type and internal input device is: + // http://shortn/_7pV0nML4Cr + // Copied from + // http://cs/android-internal/frameworks/base/media/java/android/media/AudioSystem.java;l=989 + private static final int DEVICE_OUT_BLUETOOTH_SCO = 0x10; + // Copied from + // http://cs/android-internal/frameworks/base/media/java/android/media/AudioSystem.java;l=1000 + private static final int DEVICE_OUT_BLUETOOTH_A2DP = 0x80; + @Before public void setUp() { appContext = ApplicationProvider.getApplicationContext(); @@ -403,7 +416,7 @@ public class ShadowAudioManagerTest { @Config(minSdk = M) public void registerAudioDeviceCallback_availableDevices_onAudioDevicesAddedCallback() throws Exception { - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setInputDevices(Collections.singletonList(device)); AudioDeviceCallback callback = mock(AudioDeviceCallback.class); @@ -419,7 +432,7 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setInputDevices(Collections.singletonList(device)); verifyNoMoreInteractions(callback); @@ -434,7 +447,7 @@ public class ShadowAudioManagerTest { audioManager.unregisterAudioDeviceCallback(callback); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true); verifyNoMoreInteractions(callback); @@ -448,7 +461,7 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device}); @@ -463,7 +476,7 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ false); verifyNoMoreInteractions(callback); @@ -476,7 +489,7 @@ public class ShadowAudioManagerTest { AudioDeviceCallback callback = mock(AudioDeviceCallback.class); audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setInputDevices(Collections.singletonList(device)); shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true); @@ -492,7 +505,7 @@ public class ShadowAudioManagerTest { AudioDeviceCallback callback = mock(AudioDeviceCallback.class); audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setInputDevices(Collections.singletonList(device)); shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true); @@ -508,7 +521,7 @@ public class ShadowAudioManagerTest { AudioDeviceCallback callback = mock(AudioDeviceCallback.class); audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setInputDevices(Collections.singletonList(device)); shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ false); @@ -524,7 +537,7 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true); verifyNoMoreInteractions(callback); @@ -537,7 +550,7 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setOutputDevices(Collections.singletonList(device)); verifyNoMoreInteractions(callback); @@ -551,7 +564,7 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device}); @@ -566,7 +579,7 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false); verifyNoMoreInteractions(callback); @@ -579,7 +592,7 @@ public class ShadowAudioManagerTest { AudioDeviceCallback callback = mock(AudioDeviceCallback.class); audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setOutputDevices(Collections.singletonList(device)); shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true); @@ -595,7 +608,7 @@ public class ShadowAudioManagerTest { AudioDeviceCallback callback = mock(AudioDeviceCallback.class); audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setOutputDevices(Collections.singletonList(device)); shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true); @@ -611,7 +624,7 @@ public class ShadowAudioManagerTest { AudioDeviceCallback callback = mock(AudioDeviceCallback.class); audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setOutputDevices(Collections.singletonList(device)); shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false); @@ -627,17 +640,130 @@ public class ShadowAudioManagerTest { audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration - AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true); verifyNoMoreInteractions(callback); } @Test + @Config(minSdk = S) + public void setAvailableCommunicationDevices_withCallbackRegistered_noNotificationCallback() + throws Exception { + AudioDeviceCallback callback = mock(AudioDeviceCallback.class); + audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration + + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device)); + + verifyNoMoreInteractions(callback); + } + + @Test + @Config(minSdk = S) + public void + addAvailableCommunicationDevice_withCallbackRegisteredAndNoDevice_deviceAddedAndNotifiesCallback() + throws Exception { + AudioDeviceCallback callback = mock(AudioDeviceCallback.class); + audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration + + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + shadowOf(audioManager) + .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true); + + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device}); + } + + @Test + @Config(minSdk = S) + public void + addAvailableCommunicationDeviceNoCallbackNotification_withCallbackRegisteredAndNoDevice_noNotificationCallback() + throws Exception { + AudioDeviceCallback callback = mock(AudioDeviceCallback.class); + audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration + + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + shadowOf(audioManager) + .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ false); + + verifyNoMoreInteractions(callback); + } + + @Test + @Config(minSdk = S) + public void + addAvailableCommunicationDevice_withCallbackRegisteredAndDevicePresent_noNotificationCallback() + throws Exception { + AudioDeviceCallback callback = mock(AudioDeviceCallback.class); + audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device)); + + shadowOf(audioManager) + .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true); + + verifyNoMoreInteractions(callback); + } + + @Test + @Config(minSdk = S) + public void + removeAvailableCommunicationDevice_withCallbackRegisteredAndDevicePresent_deviceRemovedAndNotifiesCallback() + throws Exception { + AudioDeviceCallback callback = mock(AudioDeviceCallback.class); + audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device)); + + shadowOf(audioManager) + .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true); + + verify(callback).onAudioDevicesRemoved(new AudioDeviceInfo[] {device}); + } + + @Test + @Config(minSdk = S) + public void + removeAvailableCommunicationDeviceNoCallbackNotification_withCallbackRegisteredAndDevicePresent_noNotificationCallback() + throws Exception { + AudioDeviceCallback callback = mock(AudioDeviceCallback.class); + audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device)); + + shadowOf(audioManager) + .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ false); + + verifyNoMoreInteractions(callback); + } + + @Test + @Config(minSdk = S) + public void + removeAvailableCommunicationDevice_withCallbackRegisteredAndNoDevice_noNotificationCallback() + throws Exception { + AudioDeviceCallback callback = mock(AudioDeviceCallback.class); + audioManager.registerAudioDeviceCallback(callback, /* handler= */ null); + verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration + + AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + shadowOf(audioManager) + .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true); + + verifyNoMoreInteractions(callback); + } + + @Test @Config(minSdk = M) public void getDevices_criteriaInputs_getsAllInputDevices() throws Exception { - AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); - AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); + AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP); shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice)); shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice)); @@ -648,8 +774,8 @@ public class ShadowAudioManagerTest { @Test @Config(minSdk = M) public void getDevices_criteriaOutputs_getsAllOutputDevices() throws Exception { - AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); - AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); + AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP); shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice)); shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice)); @@ -660,8 +786,8 @@ public class ShadowAudioManagerTest { @Test @Config(minSdk = M) public void getDevices_criteriaInputsAndOutputs_getsAllDevices() throws Exception { - AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); - AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); + AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); + AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP); shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice)); shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice)); @@ -672,7 +798,7 @@ public class ShadowAudioManagerTest { @Test @Config(minSdk = S) public void setCommunicationDevice_updatesCommunicationDevice() throws Exception { - AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setCommunicationDevice(scoDevice); assertThat(audioManager.getCommunicationDevice()).isEqualTo(scoDevice); @@ -681,7 +807,7 @@ public class ShadowAudioManagerTest { @Test @Config(minSdk = S) public void clearCommunicationDevice_clearsCommunicationDevice() throws Exception { - AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); + AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO); shadowOf(audioManager).setCommunicationDevice(scoDevice); assertThat(audioManager.getCommunicationDevice()).isEqualTo(scoDevice); @@ -991,6 +1117,190 @@ public class ShadowAudioManagerTest { assertThat(audioSessionId).isNotEqualTo(audioSessionId2); } + @Test + @Config(minSdk = Q) + public void isOffloadSupported_withoutSupport() { + assertThat( + AudioManager.isOffloadedPlaybackSupported( + new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build(), + new AudioAttributes.Builder().build())) + .isFalse(); + } + + @Test + @Config(minSdk = Q, maxSdk = R) + public void isOffloadSupported_withSetOffloadSupported() { + AudioFormat format = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + AudioAttributes attributes = new AudioAttributes.Builder().build(); + assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse(); + + ShadowAudioSystem.setOffloadSupported(format, attributes, true); + + assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue(); + } + + @Test + @Config(minSdk = Q, maxSdk = R) + public void isOffloadSupported_withSetOffloadSupportedAddedAndRemoved() { + AudioFormat format = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + AudioAttributes attributes = new AudioAttributes.Builder().build(); + ShadowAudioSystem.setOffloadSupported(format, attributes, true); + assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue(); + + ShadowAudioSystem.setOffloadSupported(format, attributes, false); + + assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse(); + } + + @Test + @Config(minSdk = S) + public void isOffloadSupported_withSetOffloadPlaybackSupport() { + AudioFormat format = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + AudioAttributes attributes = new AudioAttributes.Builder().build(); + assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse(); + + ShadowAudioSystem.setOffloadPlaybackSupport(format, attributes, AudioSystem.OFFLOAD_SUPPORTED); + + assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue(); + } + + @Test + @Config(minSdk = S) + public void getPlaybackOffloadSupport_withSetOffloadSupport_returnsOffloadSupported() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setSampleRate(48_000) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .setEncoding(AudioFormat.ENCODING_AAC_HE_V2) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + ShadowAudioSystem.setOffloadPlaybackSupport( + audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED); + + int playbackOffloadSupport = + AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes); + + assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED); + } + + @Test + @Config(minSdk = S) + public void + getPlaybackOffloadSupport_withoutSetDirectPlaybackSupport_returnsOffloadNotSupported() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setSampleRate(48_000) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .setEncoding(AudioFormat.ENCODING_AAC_HE_V2) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); + + int playbackOffloadSupport = + AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes); + + assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_NOT_SUPPORTED); + } + + @Test + @Config(minSdk = S) + public void getPlaybackOffloadSupport_withSameAudioAttrUsage_returnsOffloadSupported() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setSampleRate(48_000) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .setEncoding(AudioFormat.ENCODING_AAC_HE_V2) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + ShadowAudioSystem.setOffloadPlaybackSupport( + audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED); + + AudioAttributes audioAttributes2 = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + int playbackOffloadSupport = + AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes2); + + assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED); + } + + @Test + @Config(minSdk = TIRAMISU) + public void getDirectPlaybackSupport_withSetDirectPlaybackSupport_returnsOffloadSupported() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setSampleRate(48_000) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .setEncoding(AudioFormat.ENCODING_AAC_HE_V2) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + ShadowAudioSystem.setDirectPlaybackSupport( + audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED); + + int playbackOffloadSupport = + AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes); + + assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_OFFLOAD_SUPPORTED); + } + + @Test + @Config(minSdk = TIRAMISU) + public void getDirectPlaybackSupport_withShadowAudioSystemReset_returnsOffloadNotSupported() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setSampleRate(48_000) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .setEncoding(AudioFormat.ENCODING_AAC_HE_V2) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + ShadowAudioSystem.setDirectPlaybackSupport( + audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED); + ShadowAudioSystem.reset(); + + int playbackOffloadSupport = + AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes); + + assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_NOT_SUPPORTED); + } + private static AudioDeviceInfo createAudioDevice(int type) throws ReflectiveOperationException { AudioDeviceInfo info = Shadow.newInstanceOf(AudioDeviceInfo.class); Field portField = AudioDeviceInfo.class.getDeclaredField("mPort"); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java index adffa0111..e8869bd49 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java @@ -4,13 +4,20 @@ import static android.media.AudioTrack.ERROR_BAD_VALUE; import static android.media.AudioTrack.WRITE_BLOCKING; import static android.media.AudioTrack.WRITE_NON_BLOCKING; import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; +import android.media.AudioSystem; import android.media.AudioTrack; +import android.media.PlaybackParams; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.nio.ByteBuffer; import org.junit.Test; @@ -172,6 +179,360 @@ public class ShadowAudioTrackTest implements ShadowAudioTrack.OnAudioDataWritten assertThat(written).isEqualTo(ERROR_BAD_VALUE); } + @Test + @Config(minSdk = M) + public void getPlaybackParams_withSetPlaybackParams_returnsSetPlaybackParams() { + PlaybackParams playbackParams = + new PlaybackParams() + .allowDefaults() + .setSpeed(1.0f) + .setPitch(1.0f) + .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL); + AudioTrack audioTrack = getSampleAudioTrack(); + audioTrack.setPlaybackParams(playbackParams); + + assertThat(audioTrack.getPlaybackParams()).isEqualTo(playbackParams); + } + + @Test + public void addDirectPlaybackSupport_forPcmEncoding_throws() { + AudioAttributes attributes = new AudioAttributes.Builder().build(); + assertThrows( + IllegalArgumentException.class, + () -> + ShadowAudioTrack.addDirectPlaybackSupport( + getAudioFormat(AudioFormat.ENCODING_PCM_8BIT), attributes)); + assertThrows( + IllegalArgumentException.class, + () -> + ShadowAudioTrack.addDirectPlaybackSupport( + getAudioFormat(AudioFormat.ENCODING_PCM_16BIT), attributes)); + assertThrows( + IllegalArgumentException.class, + () -> + ShadowAudioTrack.addDirectPlaybackSupport( + getAudioFormat(AudioFormat.ENCODING_PCM_24BIT_PACKED), attributes)); + assertThrows( + IllegalArgumentException.class, + () -> + ShadowAudioTrack.addDirectPlaybackSupport( + getAudioFormat(AudioFormat.ENCODING_PCM_32BIT), attributes)); + assertThrows( + IllegalArgumentException.class, + () -> + ShadowAudioTrack.addDirectPlaybackSupport( + getAudioFormat(AudioFormat.ENCODING_PCM_FLOAT), attributes)); + } + + @Test + @Config(minSdk = Q) + public void isDirectPlaybackSupported() { + AudioFormat ac3Format = getAudioFormat(AudioFormat.ENCODING_AC3); + AudioAttributes audioAttributes = new AudioAttributes.Builder().build(); + + assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse(); + + ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes); + + assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue(); + } + + @Test + @Config(minSdk = Q) + public void isDirectPlaybackSupported_differentFormatOrAttributeFields() { + AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build(); + AudioAttributes audioAttributes = new AudioAttributes.Builder().build(); + + ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes); + + assertThat( + AudioTrack.isDirectPlaybackSupported( + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(65000) + .build(), + audioAttributes)) + .isFalse(); + assertThat( + AudioTrack.isDirectPlaybackSupported( + ac3Format, + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .build())) + .isFalse(); + } + + @Test + @Config(minSdk = Q) + public void clearDirectPlaybackSupportedEncodings() { + AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build(); + AudioAttributes audioAttributes = new AudioAttributes.Builder().build(); + ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes); + assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue(); + + ShadowAudioTrack.clearDirectPlaybackSupportedFormats(); + + assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse(); + } + + @Test + public void addAllowedNonPcmEncoding_forPcmEncoding_throws() { + assertThrows( + IllegalArgumentException.class, + () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_8BIT)); + assertThrows( + IllegalArgumentException.class, + () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_16BIT)); + assertThrows( + IllegalArgumentException.class, + () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_24BIT_PACKED)); + assertThrows( + IllegalArgumentException.class, + () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_32BIT)); + assertThrows( + IllegalArgumentException.class, + () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_FLOAT)); + } + + @Test + @Config(minSdk = Q) + public void createInstance_withNonPcmEncodingNotAllowed_throws() { + assertThrows( + UnsupportedOperationException.class, + () -> + new AudioTrack.Builder() + .setAudioFormat( + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build()) + .setBufferSizeInBytes(65536) + .build()); + } + + @Test + @Config(minSdk = Q) + public void createInstance_withNonPcmEncodingAllowed() { + ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3); + + new AudioTrack.Builder() + .setAudioFormat( + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build()) + .setBufferSizeInBytes(65536) + .build(); + } + + @Test + @Config(minSdk = Q) + public void createInstance_withOffloadAndEncodingNotOffloaded_throws() { + assertThrows( + UnsupportedOperationException.class, + () -> + new AudioTrack.Builder() + .setAudioFormat( + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build()) + .setBufferSizeInBytes(65536) + .setOffloadedPlayback(true) + .build()); + } + + @Test + @Config(minSdk = Q, maxSdk = R) + public void createInstance_withOffloadAndEncodingIsOffloadSupported() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + AudioAttributes attributes = new AudioAttributes.Builder().build(); + ShadowAudioSystem.setOffloadSupported(audioFormat, attributes, /* supported= */ true); + + AudioTrack audioTrack = + new AudioTrack.Builder() + .setAudioFormat(audioFormat) + .setAudioAttributes(attributes) + .setBufferSizeInBytes(65536) + .setOffloadedPlayback(true) + .build(); + + assertThat(audioTrack.isOffloadedPlayback()).isTrue(); + } + + @Test + @Config(sdk = S) + public void createInstance_withOffloadAndGetOffloadSupport() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + AudioAttributes attributes = new AudioAttributes.Builder().build(); + ShadowAudioSystem.setOffloadPlaybackSupport( + audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED); + + AudioTrack audioTrack = + new AudioTrack.Builder() + .setAudioFormat(audioFormat) + .setAudioAttributes(attributes) + .setBufferSizeInBytes(65536) + .setOffloadedPlayback(true) + .build(); + + assertThat(audioTrack.isOffloadedPlayback()).isTrue(); + } + + @Test + @Config(minSdk = TIRAMISU) + public void createInstance_withOffloadAndGetDirectPlaybackSupport() { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + AudioAttributes attributes = new AudioAttributes.Builder().build(); + ShadowAudioSystem.setDirectPlaybackSupport( + audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED); + + AudioTrack audioTrack = + new AudioTrack.Builder() + .setAudioFormat(audioFormat) + .setAudioAttributes(attributes) + .setBufferSizeInBytes(65536) + .setOffloadedPlayback(true) + .build(); + + assertThat(audioTrack.isOffloadedPlayback()).isTrue(); + } + + @Test + @Config(minSdk = Q) + public void clearAllowedNonPcmEncodings() { + AudioFormat surroundAudioFormat = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3); + new AudioTrack.Builder() + .setAudioFormat(surroundAudioFormat) + .setBufferSizeInBytes(65536) + .build(); + + ShadowAudioTrack.clearAllowedNonPcmEncodings(); + + assertThrows( + UnsupportedOperationException.class, + () -> + new AudioTrack.Builder() + .setAudioFormat(surroundAudioFormat) + .setBufferSizeInBytes(65536) + .build()); + } + + @Test + @Config(minSdk = Q) + public void write_withNonPcmEncodingSupported_succeeds() { + ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3); + + AudioTrack audioTrack = + new AudioTrack.Builder() + .setAudioFormat( + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build()) + .setAudioAttributes(new AudioAttributes.Builder().build()) + .setBufferSizeInBytes(32 * 1024) + .build(); + + assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128); + assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING)) + .isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING)) + .isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L)) + .isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L)) + .isEqualTo(128); + } + + @Test + @Config(minSdk = Q, maxSdk = R) + public void write_withOffloadUntilApi30_succeeds() { + ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3); + AudioFormat ac3Format = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + AudioAttributes attributes = new AudioAttributes.Builder().build(); + ShadowAudioSystem.setOffloadSupported(ac3Format, attributes, /* supported= */ true); + + AudioTrack audioTrack = + new AudioTrack.Builder() + .setAudioFormat(ac3Format) + .setAudioAttributes(new AudioAttributes.Builder().build()) + .setBufferSizeInBytes(32 * 1024) + .setOffloadedPlayback(true) + .build(); + + assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128); + assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING)) + .isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING)) + .isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L)) + .isEqualTo(128); + assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L)) + .isEqualTo(128); + } + + @Test + @Config(minSdk = Q) + public void write_withNonPcmEncodingNoLongerSupported_returnsErrorDeadObject() { + ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3); + AudioTrack audioTrack = + new AudioTrack.Builder() + .setAudioFormat( + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_AC3) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build()) + .setAudioAttributes(new AudioAttributes.Builder().build()) + .setBufferSizeInBytes(32 * 1024) + .build(); + + ShadowAudioTrack.clearAllowedNonPcmEncodings(); + + assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(AudioTrack.ERROR_DEAD_OBJECT); + assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)) + .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT); + assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING)) + .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT); + assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING)) + .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT); + assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L)) + .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT); + } + @Override @Config(minSdk = Q) public void onAudioDataWritten( @@ -195,4 +556,8 @@ public class ShadowAudioTrackTest implements ShadowAudioTrack.OnAudioDataWritten .build()) .build(); } + + private AudioFormat getAudioFormat(int encoding) { + return new AudioFormat.Builder().setEncoding(encoding).build(); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java index caed17812..78b9edbd9 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java @@ -31,6 +31,7 @@ public class ShadowBluetoothGattTest { private static final String ACTION_DISCOVER = "DISCOVER"; private static final String ACTION_READ = "READ"; private static final String ACTION_WRITE = "WRITE"; + private static final String REMOTE_ADDRESS = "R-A"; private int resultStatus = INITIAL_VALUE; private int resultState = INITIAL_VALUE; @@ -274,6 +275,15 @@ public class ShadowBluetoothGattTest { @Test @Config(minSdk = O) + public void getService_afterAddService() { + shadowOf(bluetoothGatt).addDiscoverableService(service1); + assertThat(bluetoothGatt.discoverServices()).isFalse(); + assertThat(bluetoothGatt.getService(service1.getUuid())).isEqualTo(service1); + assertThat(bluetoothGatt.getService(service2.getUuid())).isNull(); + } + + @Test + @Config(minSdk = O) public void discoverServices_clearsService() { shadowOf(bluetoothGatt).setGattCallback(callback); shadowOf(bluetoothGatt).addDiscoverableService(service1); @@ -471,4 +481,103 @@ public class ShadowBluetoothGattTest { assertThat(resultCharacteristic).isEqualTo(characteristic); assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE); } + + @Test + public void test_getBluetoothConnectionManager() { + assertThat(shadowOf(bluetoothGatt).getBluetoothConnectionManager()).isNotNull(); + } + + @Test + public void test_notifyConnection_connects() { + shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS); + assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue(); + assertThat( + shadowOf(bluetoothGatt) + .getBluetoothConnectionManager() + .hasGattClientConnection(REMOTE_ADDRESS)) + .isTrue(); + assertThat(resultStatus).isEqualTo(INITIAL_VALUE); + assertThat(resultState).isEqualTo(INITIAL_VALUE); + assertThat(resultAction).isNull(); + } + + @Test + public void test_notifyConnection_connectsWithCallbackSet() { + shadowOf(bluetoothGatt).setGattCallback(callback); + shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS); + assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue(); + assertThat( + shadowOf(bluetoothGatt) + .getBluetoothConnectionManager() + .hasGattClientConnection(REMOTE_ADDRESS)) + .isTrue(); + assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS); + assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED); + assertThat(resultAction).isEqualTo(ACTION_CONNECTION); + } + + @Test + public void test_notifyDisconnection_disconnects() { + shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS); + assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse(); + assertThat( + shadowOf(bluetoothGatt) + .getBluetoothConnectionManager() + .hasGattClientConnection(REMOTE_ADDRESS)) + .isFalse(); + assertThat(resultStatus).isEqualTo(INITIAL_VALUE); + assertThat(resultState).isEqualTo(INITIAL_VALUE); + assertThat(resultAction).isNull(); + } + + @Test + public void test_notifyDisconnection_disconnectsWithCallbackSet() { + shadowOf(bluetoothGatt).setGattCallback(callback); + shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS); + assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse(); + assertThat( + shadowOf(bluetoothGatt) + .getBluetoothConnectionManager() + .hasGattClientConnection(REMOTE_ADDRESS)) + .isFalse(); + assertThat(resultStatus).isEqualTo(INITIAL_VALUE); + assertThat(resultState).isEqualTo(INITIAL_VALUE); + assertThat(resultAction).isNull(); + } + + @Test + public void test_notifyDisconnection_disconnectsWithCallbackSet_connectedInitially() { + shadowOf(bluetoothGatt).setGattCallback(callback); + shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS); + shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS); + assertThat( + shadowOf(bluetoothGatt) + .getBluetoothConnectionManager() + .hasGattClientConnection(REMOTE_ADDRESS)) + .isFalse(); + assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse(); + assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS); + assertThat(resultState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED); + assertThat(resultAction).isEqualTo(ACTION_CONNECTION); + } + + @Test + @Config(minSdk = O) + public void allowCharacteristicNotification_canSetNotification() { + service1.addCharacteristic(characteristicWithReadProperty); + shadowOf(bluetoothGatt).addDiscoverableService(service1); + shadowOf(bluetoothGatt).allowCharacteristicNotification(characteristicWithReadProperty); + assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true)) + .isTrue(); + } + + @Test + @Config(minSdk = O) + public void disallowCharacteristicNotification_cannotSetNotification() { + service1.addCharacteristic(characteristicWithReadProperty); + shadowOf(bluetoothGatt).addDiscoverableService(service1); + shadowOf(bluetoothGatt).disallowCharacteristicNotification(characteristicWithReadProperty); + assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true)) + .isFalse(); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java index 9482ba8d7..cae11d9fa 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java @@ -22,9 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.ArrayList; import java.util.List; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; @@ -37,8 +35,6 @@ public class ShadowBluetoothHeadsetTest { private BluetoothHeadset bluetoothHeadset; private Application context; - @Rule public ExpectedException thrown = ExpectedException.none(); - @Before public void setUp() throws Exception { device1 = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:AA:BB"); @@ -61,6 +57,41 @@ public class ShadowBluetoothHeadsetTest { } @Test + public void getConnectedDevices_doesNotReturnDevicesInNonConnectedStates() { + shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING); + shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING); + + assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty(); + } + + @Test + public void getConnectionState_returnsStoredConnectionState() { + shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING); + shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING); + + assertThat(bluetoothHeadset.getConnectionState(device1)) + .isEqualTo(BluetoothProfile.STATE_CONNECTING); + assertThat(bluetoothHeadset.getConnectionState(device2)) + .isEqualTo(BluetoothProfile.STATE_DISCONNECTING); + } + + @Test + public void removeDevice_getConnectionStateReturnsDisconnected() { + shadowOf(bluetoothHeadset).addConnectedDevice(device1); + shadowOf(bluetoothHeadset).removeDevice(device1); + + assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty(); + } + + @Test + public void removeDevice_getConnectedDevicesReturnsEmpty() { + shadowOf(bluetoothHeadset).addConnectedDevice(device1); + shadowOf(bluetoothHeadset).removeDevice(device1); + + assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty(); + } + + @Test public void getConnectionState_defaultsToDisconnected() { shadowOf(bluetoothHeadset).addConnectedDevice(device1); shadowOf(bluetoothHeadset).addConnectedDevice(device2); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java index 02b149da7..cf3eba8a5 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java @@ -4,6 +4,7 @@ import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.M; +import static android.os.Build.VERSION_CODES.P; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; @@ -46,6 +47,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.ConfigTestReceiver; +import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver; +import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver; import org.robolectric.R; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; @@ -95,6 +98,20 @@ public class ShadowContextWrapperTest { } @Test + @Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P) + public void registerReceiver_shouldGetReceiverWithCustomConstructorEmptyAction() { + BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithEmptyActionReceiver.class); + assertThat(receiver).isInstanceOf(CustomConstructorWithEmptyActionReceiver.class); + } + + @Test + @Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P) + public void registerReceiver_shouldGetReceiverWithCustomConstructorAndOneAction() { + BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithOneActionReceiver.class); + assertThat(receiver).isInstanceOf(CustomConstructorWithOneActionReceiver.class); + } + + @Test public void registerReceiver_shouldRegisterForAllIntentFilterActions() throws Exception { BroadcastReceiver receiver = broadcastReceiver("Larry"); contextWrapper.registerReceiver(receiver, intentFilter("foo", "baz")); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java index 8ee669ae1..8000cc656 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java @@ -1,10 +1,14 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.hardware.input.InputManager; +import android.hardware.input.InputManager.InputDeviceListener; +import android.os.Handler; +import android.os.Looper; import android.view.MotionEvent; import android.view.VerifiedMotionEvent; import androidx.test.core.app.ApplicationProvider; @@ -16,7 +20,7 @@ import org.robolectric.annotation.Config; /** Unit tests for {@link ShadowInputManager}. */ @RunWith(AndroidJUnit4.class) -@Config(minSdk = R) +@Config(minSdk = R, maxSdk = TIRAMISU) public class ShadowInputManagerTest { private InputManager inputManager; @@ -38,4 +42,22 @@ public class ShadowInputManagerTest { assertThat(verifiedMotionEvent.getEventTimeNanos()).isEqualTo(23456000000L); assertThat(verifiedMotionEvent.getDownTimeNanos()).isEqualTo(12345000000L); } + + static class InputDeviceListenerNoOp implements InputDeviceListener { + @Override + public void onInputDeviceAdded(int deviceId) {} + + @Override + public void onInputDeviceRemoved(int deviceId) {} + + @Override + public void onInputDeviceChanged(int deviceId) {} + } + + @Test + public void testRegisterInputDeviceListener_doesNotCrash() { + InputDeviceListenerNoOp listener = new InputDeviceListenerNoOp(); + inputManager.registerInputDeviceListener(listener, new Handler(Looper.getMainLooper())); + inputManager.unregisterInputDeviceListener(listener); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java index edff632ed..323cb57fc 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java @@ -264,6 +264,20 @@ public class ShadowLauncherAppsTest { } @Test + @Config(minSdk = L) + public void testIsActivityEnabled() { + ComponentName c1 = new ComponentName(ApplicationProvider.getApplicationContext(), "Activity1"); + ComponentName c2 = new ComponentName(ApplicationProvider.getApplicationContext(), "Activity2"); + ComponentName c3 = new ComponentName("other", "Activity1"); + assertThat(launcherApps.isActivityEnabled(c1, USER_HANDLE)).isFalse(); + + shadowOf(launcherApps).setActivityEnabled(USER_HANDLE, c1); + assertThat(launcherApps.isActivityEnabled(c1, USER_HANDLE)).isTrue(); + assertThat(launcherApps.isActivityEnabled(c2, USER_HANDLE)).isFalse(); + assertThat(launcherApps.isActivityEnabled(c3, USER_HANDLE)).isFalse(); + } + + @Test @Config(minSdk = O) public void testGetApplicationInfo_packageNotFound() throws Exception { Throwable throwable = diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java index 6ae57f951..7744b9507 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java @@ -71,6 +71,15 @@ public class ShadowPaintTest { } @Test + public void shouldSetStrikeThruText() { + Paint paint = new Paint(); + paint.setStrikeThruText(true); + assertThat(paint.isStrikeThruText()).isTrue(); + paint.setStrikeThruText(false); + assertThat(paint.isStrikeThruText()).isFalse(); + } + + @Test public void measureTextActuallyMeasuresLength() { Paint paint = new Paint(); assertThat(paint.measureText("Hello")).isEqualTo(5.0f); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java index cadc3e969..71e85c9b6 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java @@ -4,6 +4,7 @@ import static android.os.Looper.getMainLooper; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.Executors.newSingleThreadExecutor; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; @@ -13,6 +14,7 @@ import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; +import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -32,6 +34,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; import org.robolectric.res.android.Ref; import org.robolectric.shadow.api.Shadow; @@ -526,10 +529,8 @@ public class ShadowPausedLooperTest { } @Test - public void testIdleNotStuck_whenThreadCrashes() throws Exception { - HandlerThread thread = new HandlerThread("WillCrash"); - thread.start(); - Looper looper = thread.getLooper(); + public void idle_looperPaused_idleHandlerThrowsException() throws Exception { + Looper looper = handlerThread.getLooper(); shadowOf(looper).pause(); new Handler(looper) .post( @@ -537,12 +538,69 @@ public class ShadowPausedLooperTest { Looper.myQueue() .addIdleHandler( () -> { - throw new RuntimeException(); + throw new IllegalStateException(); }); }); - shadowOf(looper).idle(); - thread.join(5_000); - assertThat(thread.getState()).isEqualTo(Thread.State.TERMINATED); + assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle()); + handlerThread.join(5_000); + assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED); + } + + @Test + public void idle_looperPaused_runnableThrowsException() throws Exception { + Looper looper = handlerThread.getLooper(); + shadowOf(looper).pause(); + new Handler(looper) + .post( + () -> { + throw new IllegalStateException(); + }); + + assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle()); + handlerThread.join(5_000); + assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED); + } + + @Test + public void idle_looperRunning_runnableThrowsException() throws Exception { + Looper looper = handlerThread.getLooper(); + new Handler(looper) + .post( + () -> { + throw new IllegalStateException(); + }); + + assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle()); + handlerThread.join(5_000); + assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED); + } + + @Test + public void post_throws_if_looper_died() throws Exception { + Looper looper = handlerThread.getLooper(); + new Handler(looper) + .post( + () -> { + throw new IllegalStateException(); + }); + handlerThread.join(5_000); + assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED); + + assertThrows(IllegalStateException.class, () -> new Handler(looper).post(() -> {})); + } + + @Test + public void idle_throws_if_looper_died() throws Exception { + Looper looper = handlerThread.getLooper(); + new Handler(looper) + .post( + () -> { + throw new IllegalStateException(); + }); + handlerThread.join(5_000); + assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED); + + assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle()); } @Test @@ -565,6 +623,43 @@ public class ShadowPausedLooperTest { assertThat(foregroundThreadReceived.get()).isTrue(); } + @Test + @Config(minSdk = VERSION_CODES.M) + public void runOneTask_ignoreSyncBarrier() { + int barrier = Looper.getMainLooper().getQueue().postSyncBarrier(); + + final AtomicBoolean wasRun = new AtomicBoolean(false); + new Handler(Looper.getMainLooper()).post(() -> wasRun.set(true)); + + ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper()); + shadowPausedLooper.runOneTask(); + + // tasks should not be executed when blocked by a sync barrier + assertThat(wasRun.get()).isFalse(); + // sync barrier will throw if the barrier was not found. + Looper.getMainLooper().getQueue().removeSyncBarrier(barrier); + + shadowPausedLooper.runOneTask(); + assertThat(wasRun.get()).isTrue(); + } + + @Test + @Config(minSdk = VERSION_CODES.P) + public void runOneTask_ignoreSyncBarrier_with_async() { + int barrier = Looper.getMainLooper().getQueue().postSyncBarrier(); + + final AtomicBoolean wasRun = new AtomicBoolean(false); + Handler.createAsync(Looper.getMainLooper()).post(() -> wasRun.set(true)); + + ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper()); + shadowPausedLooper.runOneTask(); + + // tasks should be executed as the handler is async + assertThat(wasRun.get()).isTrue(); + // sync barrier will throw if the barrier was not found. + Looper.getMainLooper().getQueue().removeSyncBarrier(barrier); + } + private static class BlockingRunnable implements Runnable { CountDownLatch latch = new CountDownLatch(1); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java index da3440139..a4732b920 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java @@ -2,8 +2,7 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.N_MR1; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeFalse; -import static org.junit.Assume.assumeTrue; +import static com.google.common.truth.TruthJUnit.assume; import static org.robolectric.Shadows.shadowOf; import static org.robolectric.shadows.ShadowAssetManager.useLegacy; @@ -85,7 +84,7 @@ public class ShadowResourcesTest { @Test public void openRawResourceFd_shouldReturnsNullForLegacyResource() throws Exception { - assumeTrue(useLegacy()); + assume().that(useLegacy()).isTrue(); try (AssetFileDescriptor afd = resources.openRawResourceFd(R.raw.raw_resource)) { assertThat(afd).isNull(); } @@ -93,7 +92,7 @@ public class ShadowResourcesTest { @Test public void openRawResourceFd_shouldReturnsValidFdForUnCompressFile() throws Exception { - assumeFalse(useLegacy()); + assume().that(useLegacy()).isFalse(); try (AssetFileDescriptor afd = resources.openRawResourceFd(R.raw.raw_resource)) { assertThat(afd).isNotNull(); } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java index 323511fc3..878777c10 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java @@ -3,8 +3,8 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.common.truth.TruthJUnit.assume; import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; import static org.robolectric.annotation.SQLiteMode.Mode.LEGACY; import static org.robolectric.shadows.ShadowLegacySQLiteConnection.convertSQLWithLocalizedUnicodeCollator; @@ -64,7 +64,7 @@ public class ShadowSQLiteConnectionTest { @Test public void testSqlConversion() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); assertThat(convertSQLWithLocalizedUnicodeCollator("select * from `routine`")) .isEqualTo("select * from `routine`"); @@ -88,7 +88,7 @@ public class ShadowSQLiteConnectionTest { @Test public void testSQLWithLocalizedOrUnicodeCollatorShouldBeSortedAsNoCase() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); database.execSQL("insert into routine(name) values ('الصحافة اليدوية')"); database.execSQL("insert into routine(name) values ('Hand press 1')"); database.execSQL("insert into routine(name) values ('hand press 2')"); @@ -116,28 +116,28 @@ public class ShadowSQLiteConnectionTest { @Test public void nativeOpen_addsConnectionToPool() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); assertThat(conn).isNotNull(); assertWithMessage("open").that(conn.isOpen()).isTrue(); } @Test public void nativeClose_closesConnection() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); ShadowLegacySQLiteConnection.nativeClose(ptr); assertWithMessage("open").that(conn.isOpen()).isFalse(); } @Test public void reset_closesConnection() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); ShadowLegacySQLiteConnection.reset(); assertWithMessage("open").that(conn.isOpen()).isFalse(); } @Test public void reset_clearsConnectionCache() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); final Map<Long, SQLiteConnection> connectionsMap = ReflectionHelpers.getField(connections, "connectionsMap"); @@ -149,7 +149,7 @@ public class ShadowSQLiteConnectionTest { @Test public void reset_clearsStatementCache() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); final Map<Long, SQLiteStatement> statementsMap = ReflectionHelpers.getField(connections, "statementsMap"); @@ -161,7 +161,7 @@ public class ShadowSQLiteConnectionTest { @Test public void error_resultsInSpecificExceptionWithCause() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); try { database.execSQL("insert into routine(name) values ('Hand press 1')"); ContentValues values = new ContentValues(1); @@ -178,7 +178,7 @@ public class ShadowSQLiteConnectionTest { @Test public void interruption_doesNotConcurrentlyModifyDatabase() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); Thread.currentThread().interrupt(); try { database.execSQL("insert into routine(name) values ('الصحافة اليدوية')"); @@ -190,7 +190,7 @@ public class ShadowSQLiteConnectionTest { @Test public void test_setUseInMemoryDatabase() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); assertThat(conn.isMemoryDatabase()).isFalse(); ShadowSQLiteConnection.setUseInMemoryDatabase(true); SQLiteDatabase inMemoryDb = createDatabase("in_memory.db"); @@ -201,7 +201,7 @@ public class ShadowSQLiteConnectionTest { @Test public void cancel_shouldCancelAllStatements() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); SQLiteStatement statement1 = database.compileStatement("insert into routine(name) values ('Hand press 1')"); SQLiteStatement statement2 = diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java index 4e7fb16ce..a527ec98f 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java @@ -10,6 +10,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; +import android.os.Handler; +import android.os.Looper; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -90,6 +92,29 @@ public class ShadowSubscriptionManagerTest { } @Test + public void + addOnSubscriptionsChangedListener_whenHasExecutorParameter_shouldCallbackImmediately() { + DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener(); + shadowOf(subscriptionManager) + .addOnSubscriptionsChangedListener(new Handler(Looper.getMainLooper())::post, listener); + + assertThat(listener.subscriptionChangedCount).isEqualTo(1); + } + + @Test + public void addOnSubscriptionsChangedListener_whenHasExecutorParameter_shouldAddListener() { + DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener(); + shadowOf(subscriptionManager) + .addOnSubscriptionsChangedListener(new Handler(Looper.getMainLooper())::post, listener); + + shadowOf(subscriptionManager) + .setActiveSubscriptionInfos( + SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo()); + + assertThat(listener.subscriptionChangedCount).isEqualTo(2); + } + + @Test public void removeOnSubscriptionsChangedListener_shouldRemoveListener() { DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener(); DummySubscriptionsChangedListener listener2 = new DummySubscriptionsChangedListener(); @@ -106,6 +131,21 @@ public class ShadowSubscriptionManagerTest { } @Test + public void hasOnSubscriptionsChangedListener_whenListenerNotExist_shouldReturnFalse() { + DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener(); + + assertThat(shadowOf(subscriptionManager).hasOnSubscriptionsChangedListener(listener)).isFalse(); + } + + @Test + public void hasOnSubscriptionsChangedListener_whenListenerExist_shouldReturnTrue() { + DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener(); + shadowOf(subscriptionManager).addOnSubscriptionsChangedListener(listener); + + assertThat(shadowOf(subscriptionManager).hasOnSubscriptionsChangedListener(listener)).isTrue(); + } + + @Test public void getActiveSubscriptionInfo_shouldReturnInfoWithSubId() { SubscriptionInfo expectedSubscriptionInfo = SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo(); @@ -331,6 +371,48 @@ public class ShadowSubscriptionManagerTest { .isEqualTo("123"); } + @Test + @Config(minSdk = TIRAMISU) + public void getPhoneNumberWithSource_phoneNumberNotSet_returnsEmptyString() { + assertThat( + subscriptionManager.getPhoneNumber( + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, + SubscriptionManager.PHONE_NUMBER_SOURCE_UICC)) + .isEqualTo(""); + assertThat( + subscriptionManager.getPhoneNumber( + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, + SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER)) + .isEqualTo(""); + assertThat( + subscriptionManager.getPhoneNumber( + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, + SubscriptionManager.PHONE_NUMBER_SOURCE_IMS)) + .isEqualTo(""); + } + + @Test + @Config(minSdk = TIRAMISU) + public void getPhoneNumberWithSource_setPhoneNumber_returnsPhoneNumber() { + shadowOf(subscriptionManager) + .setPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "123"); + assertThat( + subscriptionManager.getPhoneNumber( + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, + SubscriptionManager.PHONE_NUMBER_SOURCE_UICC)) + .isEqualTo("123"); + assertThat( + subscriptionManager.getPhoneNumber( + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, + SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER)) + .isEqualTo("123"); + assertThat( + subscriptionManager.getPhoneNumber( + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, + SubscriptionManager.PHONE_NUMBER_SOURCE_IMS)) + .isEqualTo("123"); + } + private static class DummySubscriptionsChangedListener extends SubscriptionManager.OnSubscriptionsChangedListener { private int subscriptionChangedCount; diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java index 8571627a4..ad3adebcf 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java @@ -27,6 +27,8 @@ import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK; import static android.telephony.TelephonyManager.CALL_STATE_RINGING; import static android.telephony.TelephonyManager.NETWORK_TYPE_EVDO_0; import static android.telephony.TelephonyManager.NETWORK_TYPE_LTE; +import static android.telephony.emergency.EmergencyNumber.EMERGENCY_NUMBER_SOURCE_DATABASE; +import static android.telephony.emergency.EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.junit.Assert.assertEquals; @@ -74,10 +76,12 @@ import android.telephony.TelephonyManager.BootstrapAuthenticationCallback; import android.telephony.TelephonyManager.CellInfoCallback; import android.telephony.UiccSlotInfo; import android.telephony.VisualVoicemailSmsFilterSettings; +import android.telephony.emergency.EmergencyNumber; import android.telephony.gba.UaSecurityProtocolIdentifier; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -374,6 +378,21 @@ public class ShadowTelephonyManagerTest { } @Test + @Config(minSdk = S) + public void shouldGiveCallStateForSubscription() { + PhoneStateListener listener = mock(PhoneStateListener.class); + telephonyManager.listen(listener, LISTEN_CALL_STATE); + + shadowOf(telephonyManager).setCallState(CALL_STATE_RINGING, "911"); + assertEquals(CALL_STATE_RINGING, telephonyManager.getCallStateForSubscription()); + verify(listener).onCallStateChanged(CALL_STATE_RINGING, "911"); + + shadowOf(telephonyManager).setCallState(CALL_STATE_OFFHOOK, "911"); + assertEquals(CALL_STATE_OFFHOOK, telephonyManager.getCallStateForSubscription()); + verify(listener).onCallStateChanged(CALL_STATE_OFFHOOK, null); + } + + @Test public void shouldGiveCallState() { PhoneStateListener listener = mock(PhoneStateListener.class); telephonyManager.listen(listener, LISTEN_CALL_STATE); @@ -803,6 +822,24 @@ public class ShadowTelephonyManagerTest { } @Test + @Config(minSdk = S) + public void setDataEnabledForReasonChangesIsDataEnabledForReason() { + int correctReason = TelephonyManager.DATA_ENABLED_REASON_POLICY; + int incorrectReason = TelephonyManager.DATA_ENABLED_REASON_USER; + + assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue(); + assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue(); + + telephonyManager.setDataEnabledForReason(correctReason, false); + assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isFalse(); + assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue(); + + telephonyManager.setDataEnabledForReason(correctReason, true); + assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue(); + assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue(); + } + + @Test public void setDataStateChangesDataState() { assertThat(telephonyManager.getDataState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED); shadowOf(telephonyManager).setDataState(TelephonyManager.DATA_CONNECTING); @@ -1068,4 +1105,36 @@ public class ShadowTelephonyManagerTest { public void getEmergencyCallback_notSet_returnsFalse() { assertThat(telephonyManager.getEmergencyCallbackMode()).isFalse(); } + + @Test + @Config(minSdk = R) + public void getEmergencyNumbersList_notSet_returnsEmptyList() { + assertThat(telephonyManager.getEmergencyNumberList()).isEmpty(); + } + + @Test + @Config(minSdk = R) + public void getEmergencyNumbersList_wasSet_returnsCorrectList() throws Exception { + EmergencyNumber emergencyNumber = + EmergencyNumber.class + .getConstructor( + String.class, + String.class, + String.class, + int.class, + List.class, + int.class, + int.class) + .newInstance( + "911", + "us", + "30", + EMERGENCY_NUMBER_SOURCE_DATABASE, + ImmutableList.of(), + EMERGENCY_SERVICE_CATEGORY_POLICE, + EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL); + ShadowTelephonyManager.setEmergencyNumberList( + ImmutableMap.of(0, ImmutableList.of(emergencyNumber))); + assertThat(telephonyManager.getEmergencyNumberList().get(0)).containsExactly(emergencyNumber); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java index 75df1101a..7c9bfafca 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java @@ -9,6 +9,7 @@ import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.N_MR1; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.robolectric.Shadows.shadowOf; @@ -68,7 +69,8 @@ public class ShadowUserManagerTest { UserHandle anotherProfile = newUserHandle(2); shadowOf(userManager).addUserProfile(anotherProfile); - assertThat(userManager.getUserProfiles()).containsExactly(Process.myUserHandle(), anotherProfile); + assertThat(userManager.getUserProfiles()) + .containsExactly(Process.myUserHandle(), anotherProfile); } @Test @@ -243,7 +245,8 @@ public class ShadowUserManagerTest { try { userManager.isManagedProfile(); fail("Expected exception"); - } catch (SecurityException expected) {} + } catch (SecurityException expected) { + } setPermissions(permission.MANAGE_USERS); @@ -317,6 +320,19 @@ public class ShadowUserManagerTest { } @Test + @Config(minSdk = R) + public void getUserHandles() { + assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(1); + shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(0); + assertThat(UserHandle.myUserId()).isEqualTo(UserHandle.USER_SYSTEM); + + UserHandle expectedUserHandle = shadowOf(userManager).addUser(10, "secondary_user", 0); + assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(2); + assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(1)) + .isEqualTo(expectedUserHandle); + } + + @Test @Config(minSdk = N_MR1, maxSdk = Q) public void isDemoUser() { // All methods are based on the current user, so no need to pass a UserHandle. @@ -565,6 +581,34 @@ public class ShadowUserManagerTest { } @Test + @Config(minSdk = Q) + public void removeSecondaryUser_noExistingUser_doesNotRemove() { + assertThat(shadowOf(userManager).removeUser(UserHandle.of(10))).isFalse(); + assertThat(userManager.getUserCount()).isEqualTo(1); + } + + @Test + @Config(minSdk = TIRAMISU) + public void removeUserWhenPossible_twoUsersRemoveOne_hasOneUserLeft() { + shadowOf(userManager).addUser(10, "secondary_user", 0); + assertThat( + userManager.removeUserWhenPossible( + UserHandle.of(10), /* overrideDevicePolicy= */ false)) + .isEqualTo(UserManager.REMOVE_RESULT_REMOVED); + assertThat(userManager.getUserCount()).isEqualTo(1); + } + + @Test + @Config(minSdk = TIRAMISU) + public void removeUserWhenPossible_nonExistingUser_fails() { + assertThat( + userManager.removeUserWhenPossible( + UserHandle.of(10), /* overrideDevicePolicy= */ false)) + .isEqualTo(UserManager.REMOVE_RESULT_ERROR_UNKNOWN); + assertThat(userManager.getUserCount()).isEqualTo(1); + } + + @Test @Config(minSdk = JELLY_BEAN_MR1) public void switchToSecondaryUser() { shadowOf(userManager).addUser(10, "secondary_user", 0); @@ -653,8 +697,8 @@ public class ShadowUserManagerTest { @Config(minSdk = LOLLIPOP) public void getProfiles_addedProfile_containsProfile() { shadowOf(userManager).addUser(TEST_USER_HANDLE, "", 0); - shadowOf(userManager).addProfile( - TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS); + shadowOf(userManager) + .addProfile(TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS); // getProfiles(userId) include user itself and asssociated profiles. assertThat(userManager.getProfiles(TEST_USER_HANDLE).get(0).id).isEqualTo(TEST_USER_HANDLE); @@ -850,7 +894,6 @@ public class ShadowUserManagerTest { assertThat(UserManager.supportsMultipleUsers()).isTrue(); } - @Test @Config(minSdk = Q) public void getUserSwitchability_shouldReturnLastSetSwitchability() { @@ -859,8 +902,7 @@ public class ShadowUserManagerTest { .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED); assertThat(userManager.getUserSwitchability()) .isEqualTo(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED); - shadowOf(userManager) - .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK); + shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK); assertThat(userManager.getUserSwitchability()).isEqualTo(UserManager.SWITCHABILITY_STATUS_OK); } @@ -880,8 +922,7 @@ public class ShadowUserManagerTest { shadowOf(userManager) .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED); assertThat(userManager.canSwitchUsers()).isFalse(); - shadowOf(userManager) - .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK); + shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK); assertThat(userManager.canSwitchUsers()).isTrue(); } @@ -889,7 +930,7 @@ public class ShadowUserManagerTest { @Config(minSdk = Q) public void getUserName_shouldReturnSetUserName() { shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK); - shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0); + shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0); shadowOf(userManager).switchUser(10); assertThat(userManager.getUserName()).isEqualTo(PROFILE_USER_NAME); } @@ -900,7 +941,7 @@ public class ShadowUserManagerTest { userManager.setUserIcon(TEST_USER_ICON); assertThat(userManager.getUserIcon()).isEqualTo(TEST_USER_ICON); - shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0); + shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0); shadowOf(userManager).switchUser(10); assertThat(userManager.getUserIcon()).isNull(); } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java index b8b57851c..ef5527abf 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java @@ -16,7 +16,6 @@ import android.content.Context; import android.media.AudioAttributes; import android.os.VibrationEffect; import android.os.Vibrator; -import android.os.vibrator.PrimitiveSegment; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; @@ -132,7 +131,7 @@ public class ShadowVibratorTest { @Config(minSdk = S) @Test - public void getVibrationEffectSegments_composeOnce_shouldReturnSameFragment() { + public void getPrimitiveSegmentsInPrimitiveEffects_composeOnce_shouldReturnSameFragment() { vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20) @@ -140,17 +139,17 @@ public class ShadowVibratorTest { .addPrimitive(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150) .compose()); - assertThat(shadowOf(vibrator).getVibrationEffectSegments()) + assertThat(shadowOf(vibrator).getPrimitiveSegmentsInPrimitiveEffects()) .isEqualTo( ImmutableList.of( - new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20), - new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.7f, /* delay= */ 50), - new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150))); + new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20), + new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.7f, /* delay= */ 50), + new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150))); } @Config(minSdk = S) @Test - public void getVibrationEffectSegments_composeTwice_shouldReturnTheLastComposition() { + public void getPrimitiveSegmentsInPrimitiveEffects_composeTwice_shouldReturnTheLastComposition() { vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20) @@ -164,12 +163,12 @@ public class ShadowVibratorTest { .addPrimitive(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150) .compose()); - assertThat(shadowOf(vibrator).getVibrationEffectSegments()) + assertThat(shadowOf(vibrator).getPrimitiveSegmentsInPrimitiveEffects()) .isEqualTo( ImmutableList.of( - new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.4f, /* delay= */ 120), - new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150), - new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150))); + new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.4f, /* delay= */ 120), + new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150), + new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150))); } @Config(minSdk = R) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java new file mode 100644 index 000000000..dbf7250ef --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java @@ -0,0 +1,95 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.Shadows.shadowOf; + +import android.content.Intent; +import android.net.Ikev2VpnProfile; +import android.net.VpnManager; +import android.net.VpnProfileState; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(AndroidJUnit4.class) +@Config(minSdk = VERSION_CODES.R) +public class ShadowVpnManagerTest { + private VpnManager vpnManager; + private ShadowVpnManager shadowVpnManager; + + @Before + public void setUp() throws Exception { + vpnManager = ApplicationProvider.getApplicationContext().getSystemService(VpnManager.class); + shadowVpnManager = shadowOf(vpnManager); + } + + @Test + public void provisionVpnProfile() { + Intent intent = new Intent("foo"); + shadowVpnManager.setProvisionVpnProfileResult(intent); + + assertThat( + vpnManager.provisionVpnProfile( + new Ikev2VpnProfile.Builder("server", "local.identity") + .setAuthPsk(new byte[0]) + .build())) + .isSameInstanceAs(intent); + + if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) { + VpnProfileState state = vpnManager.getProvisionedVpnProfileState(); + assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED); + assertThat(state.getSessionId()).isNull(); + } + } + + @Test + public void deleteVpnProfile() { + vpnManager.provisionVpnProfile( + new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build()); + vpnManager.deleteProvisionedVpnProfile(); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void deleteVpnProfile_tiramisu() { + vpnManager.provisionVpnProfile( + new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build()); + assertThat(vpnManager.getProvisionedVpnProfileState()).isNotNull(); + + vpnManager.deleteProvisionedVpnProfile(); + assertThat(vpnManager.getProvisionedVpnProfileState()).isNull(); + } + + @Test + public void startAndStopVpnProfile() { + vpnManager.provisionVpnProfile( + new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build()); + vpnManager.startProvisionedVpnProfile(); + vpnManager.stopProvisionedVpnProfile(); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void startAndStopVpnProfile_tiramisu() { + vpnManager.provisionVpnProfile( + new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build()); + String sessionKey = vpnManager.startProvisionedVpnProfileSession(); + VpnProfileState state = vpnManager.getProvisionedVpnProfileState(); + assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_CONNECTED); + assertThat(state.getSessionId()).isEqualTo(sessionKey); + assertThat(state.isAlwaysOn()).isFalse(); + assertThat(state.isLockdownEnabled()).isFalse(); + + vpnManager.stopProvisionedVpnProfile(); + state = vpnManager.getProvisionedVpnProfileState(); + assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED); + assertThat(state.getSessionId()).isNull(); + assertThat(state.isAlwaysOn()).isFalse(); + assertThat(state.isLockdownEnabled()).isFalse(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java index 7a1bee691..15dd9ec96 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java @@ -1,12 +1,17 @@ package org.robolectric.shadows; +import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -14,10 +19,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.robolectric.Shadows.shadowOf; +import android.app.Application; import android.app.admin.DeviceAdminService; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.net.ConnectivityManager; import android.net.DhcpInfo; import android.net.NetworkInfo; @@ -27,13 +34,17 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.MulticastLock; +import android.net.wifi.WifiManager.PnoScanResultsCallback; +import android.net.wifi.WifiSsid; import android.net.wifi.WifiUsabilityStatsEntry; import android.os.Build; import android.util.Pair; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,9 +58,7 @@ public class ShadowWifiManagerTest { @Before public void setUp() throws Exception { - wifiManager = - (WifiManager) - ApplicationProvider.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); } @Test @@ -494,8 +503,7 @@ public class ShadowWifiManagerTest { // THEN NetworkInfo networkInfo = ((ConnectivityManager) - ApplicationProvider.getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE)) + getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE)) .getActiveNetworkInfo(); assertThat(networkInfo.getType()).isEqualTo(ConnectivityManager.TYPE_WIFI); assertThat(networkInfo.isConnected()).isTrue(); @@ -784,13 +792,305 @@ public class ShadowWifiManagerTest { assertThat(shadowOf(wifiManager).getSoftApConfiguration().getSsid()).isEqualTo("foo"); } + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_nullCallback_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + /* callback= */ null)); + } + + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_nullExecutor_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})), + /* frequencies= */ null, + /* executor= */ null, + new TestPnoScanResultsCallback())); + } + + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_nullSsidList_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> + wifiManager.setExternalPnoScanRequest( + /* ssids= */ null, + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + new TestPnoScanResultsCallback())); + } + + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_emptySsidList_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> + wifiManager.setExternalPnoScanRequest( + /* ssids= */ List.of(), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + new TestPnoScanResultsCallback())); + } + + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_moreThan2Ssids_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> + wifiManager.setExternalPnoScanRequest( + List.of( + WifiSsid.fromBytes(new byte[] {1, 2, 3}), + WifiSsid.fromBytes(new byte[] {9, 8, 7, 6}), + WifiSsid.fromBytes(new byte[] {90, 81, 72, 63, 54})), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + new TestPnoScanResultsCallback())); + } + + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_moreThan10Frequencies_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> + wifiManager.setExternalPnoScanRequest( + List.of( + WifiSsid.fromBytes(new byte[] {1, 2, 3}), + WifiSsid.fromBytes(new byte[] {9, 8, 7, 6})), + new int[] {5160, 5180, 5200, 5220, 5240, 5260, 5280, 5300, 5320, 5340, 5360}, + Executors.newSingleThreadExecutor(), + new TestPnoScanResultsCallback())); + } + + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_validRequest_successCallbackInvoked() throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + callback); + + assertThat(callback.successfulRegistrations.take()).isNotNull(); + } + + @Test + @Config(minSdk = TIRAMISU) + public void + setExternalPnoScanRequest_outstandingRequest_failureCallbackInvokedWithAlreadyRegisteredStatus() + throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + callback); + + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {9, 2, 5})), + new int[] {5280}, + Executors.newSingleThreadExecutor(), + callback); + + assertThat(callback.failedRegistrations.take()) + .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED); + } + + @Test + @Config(minSdk = TIRAMISU) + public void setExternalPnoScanRequest_differentUid_failureCallbackInvokedWithBusyStatus() + throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + callback); + + int firstAppUid = ShadowProcess.myUid(); + int secondAppUid; + do { + secondAppUid = ShadowProcess.getRandomApplicationUid(); + } while (firstAppUid == secondAppUid); + ShadowProcess.setUid(secondAppUid); + + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + callback); + + assertThat(callback.failedRegistrations.take()) + .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY); + } + + @Test + @Config(minSdk = TIRAMISU) + public void clearExternalPnoScanRequest_outstandingRequest_callbackInvokedWithUnregisteredStatus() + throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})), + /* frequencies= */ null, + Executors.newSingleThreadExecutor(), + callback); + wifiManager.clearExternalPnoScanRequest(); + + assertThat(callback.removedRegistrations.take()) + .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED); + } + + @Test + @Config(minSdk = TIRAMISU) + public void clearExternalPnoScanRequest_wrongUid_callbackNotInvoked() throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + + wifiManager.setExternalPnoScanRequest( + List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})), + /* frequencies= */ null, + executor, + callback); + + int firstAppUid = ShadowProcess.myUid(); + int secondAppUid; + do { + secondAppUid = ShadowProcess.getRandomApplicationUid(); + } while (firstAppUid == secondAppUid); + ShadowProcess.setUid(secondAppUid); + + wifiManager.clearExternalPnoScanRequest(); + + executor.shutdown(); + + assertThat(executor.awaitTermination(5, MINUTES)).isTrue(); + assertThat(callback.removedRegistrations).isEmpty(); + } + + @Test + @Config(minSdk = TIRAMISU) + public void networksFoundFromPnoScan_matchingSsid_availableCallbackInvoked() throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3}); + ScanResult scanResult = new ScanResult(); + scanResult.setWifiSsid(wifiSsid); + + wifiManager.setExternalPnoScanRequest( + List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback); + shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult)); + + assertThat(callback.incomingScanResults.take()).containsExactly(scanResult); + } + + @Test + @Config(minSdk = TIRAMISU) + public void networksFoundFromPnoScan_matchingSsid_removedCallbackInvokedWithDeliveredStatus() + throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3}); + ScanResult scanResult = new ScanResult(); + scanResult.setWifiSsid(wifiSsid); + + wifiManager.setExternalPnoScanRequest( + List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback); + shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult)); + + assertThat(callback.removedRegistrations.take()) + .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED); + } + + @Test + @Config(minSdk = TIRAMISU) + public void networksFoundFromPnoScan_matchingSsid_scanResultsAvailableBroadcastSent() { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3}); + ScanResult scanResult = new ScanResult(); + scanResult.setWifiSsid(wifiSsid); + + wifiManager.setExternalPnoScanRequest( + List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback); + shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult)); + + Intent expectedIntent = new Intent(SCAN_RESULTS_AVAILABLE_ACTION); + expectedIntent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true); + expectedIntent.setPackage(getApplicationContext().getPackageName()); + + assertThat( + shadowOf((Application) getApplicationContext()).getBroadcastIntents().stream() + .anyMatch(expectedIntent::filterEquals)) + .isTrue(); + } + + @Test + @Config(minSdk = TIRAMISU) + public void networksFoundFromPnoScan_noMatchingSsid_availableCallbackNotInvoked() + throws Exception { + TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3}); + WifiSsid otherWifiSsid = WifiSsid.fromBytes(new byte[] {9, 8, 7, 6}); + ScanResult scanResult = new ScanResult(); + scanResult.setWifiSsid(otherWifiSsid); + + wifiManager.setExternalPnoScanRequest( + List.of(wifiSsid), /* frequencies= */ null, executor, callback); + shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult)); + + executor.shutdown(); + + assertThat(executor.awaitTermination(5, MINUTES)).isTrue(); + assertThat(callback.incomingScanResults).isEmpty(); + } + + private class TestPnoScanResultsCallback implements PnoScanResultsCallback { + LinkedBlockingQueue<List<ScanResult>> incomingScanResults = new LinkedBlockingQueue<>(); + LinkedBlockingQueue<Object> successfulRegistrations = new LinkedBlockingQueue<>(); + LinkedBlockingQueue<Integer> failedRegistrations = new LinkedBlockingQueue<>(); + LinkedBlockingQueue<Integer> removedRegistrations = new LinkedBlockingQueue<>(); + + @Override + public void onScanResultsAvailable(List<ScanResult> scanResults) { + incomingScanResults.add(scanResults); + } + + @Override + public void onRegisterSuccess() { + successfulRegistrations.add(new Object()); + } + + @Override + public void onRegisterFailed(int reason) { + failedRegistrations.add(reason); + } + + @Override + public void onRemoved(int reason) { + removedRegistrations.add(reason); + } + } + private void setDeviceOwner() { shadowOf( (DevicePolicyManager) - ApplicationProvider.getApplicationContext() - .getSystemService(Context.DEVICE_POLICY_SERVICE)) - .setDeviceOwner( - new ComponentName( - ApplicationProvider.getApplicationContext(), DeviceAdminService.class)); + getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE)) + .setDeviceOwner(new ComponentName(getApplicationContext(), DeviceAdminService.class)); } } diff --git a/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java b/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java index 3b06a4e32..612fd3bfa 100644 --- a/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java +++ b/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java @@ -1,8 +1,8 @@ package org.robolectric.util; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static org.junit.Assert.assertThrows; -import static org.junit.Assume.assumeTrue; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.After; @@ -56,7 +56,7 @@ public class SQLiteLibraryLoaderTest { @Test public void shouldExtractNativeLibrary() { - assumeTrue(SQLiteLibraryLoader.isOsSupported()); + assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue(); assertThat(loader.isLoaded()).isFalse(); loader.doLoad(); assertThat(loader.isLoaded()).isTrue(); diff --git a/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml new file mode 100644 index 000000000..cbda17e65 --- /dev/null +++ b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.robolectric"> + <uses-sdk android:targetSdkVersion="18"/> + + <application + android:appComponentFactory="org.robolectric.CustomAppComponentFactory"> + <receiver + android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithOneActionReceiver"> + <intent-filter> + <action android:name="org.robolectric.ACTION_CUSTOM_CONSTRUCTOR"/> + </intent-filter> + </receiver> + <receiver + android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithEmptyActionReceiver" /> + </application> +</manifest> diff --git a/sandbox/build.gradle b/sandbox/build.gradle index 64accd737..358b027c2 100644 --- a/sandbox/build.gradle +++ b/sandbox/build.gradle @@ -5,24 +5,24 @@ apply plugin: RoboJavaModulePlugin apply plugin: DeployedRoboJavaModulePlugin dependencies { - annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion" - annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion" + annotationProcessor libs.auto.service + annotationProcessor libs.error.prone.core api project(":annotations") api project(":utils") api project(":shadowapi") api project(":utils:reflector") - compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion" - api "javax.annotation:javax.annotation-api:1.3.2" - api "javax.inject:javax.inject:1" + compileOnly libs.auto.service.annotations + api libs.javax.annotation.api + api libs.javax.inject - api "org.ow2.asm:asm:${asmVersion}" - api "org.ow2.asm:asm-commons:${asmVersion}" - api "com.google.guava:guava:$guavaJREVersion" - compileOnly "com.google.code.findbugs:jsr305:3.0.2" + api libs.asm + api libs.asm.commons + api libs.guava + compileOnly libs.findbugs.jsr305 - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockito testImplementation project(":junit") } diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java index fac00226d..e1463a17b 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java @@ -10,6 +10,7 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Modifier; import java.util.List; import java.util.ListIterator; +import java.util.Objects; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ConstantDynamic; @@ -212,23 +213,25 @@ public class ClassInstrumentor { } /** - * Checks if the first instruction is a Jacoco load instructions. Robolectric is not capable at - * the moment of re-instrumenting Jacoco-instrumented constructors. + * Checks if the first or second instruction is a Jacoco load instruction. Robolectric is not + * capable at the moment of re-instrumenting Jacoco-instrumented constructors, so these are + * currently skipped. * * @param ctor constructor method node * @return whether or not the constructor can be instrumented */ private boolean isJacocoInstrumented(MethodNode ctor) { AbstractInsnNode[] insns = ctor.instructions.toArray(); - if (insns.length > 0) { - if (insns[0] instanceof LdcInsnNode - && ((LdcInsnNode) insns[0]).cst instanceof ConstantDynamic) { - ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) insns[0]).cst; + if (insns.length > 1) { + AbstractInsnNode node = insns[0]; + if (node instanceof LabelNode) { + node = insns[1]; + } + if ((node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof ConstantDynamic)) { + ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) node).cst; return cst.getName().equals("$jacocoData"); - } else if (insns.length > 1 - && insns[0] instanceof LabelNode - && insns[1] instanceof MethodInsnNode) { - return "$jacocoInit".equals(((MethodInsnNode) insns[1]).name); + } else if (node instanceof MethodInsnNode) { + return Objects.equals(((MethodInsnNode) node).name, "$jacocoInit"); } } return false; diff --git a/settings.gradle b/settings.gradle index 20f8fae14..1894b8c98 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include ":shadows:framework" include ":shadows:httpclient" include ":shadows:multidex" include ":shadows:playservices" +include ":shadows:versioning" include ":shadowapi" include ":errorprone" include ":nativeruntime" diff --git a/shadowapi/build.gradle b/shadowapi/build.gradle index f63d048e1..3f0064fb7 100644 --- a/shadowapi/build.gradle +++ b/shadowapi/build.gradle @@ -5,11 +5,11 @@ apply plugin: RoboJavaModulePlugin apply plugin: DeployedRoboJavaModulePlugin dependencies { - compileOnly "com.google.code.findbugs:jsr305:3.0.2" + compileOnly libs.findbugs.jsr305 api project(":annotations") api project(":utils") - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" -}
\ No newline at end of file + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockito +} diff --git a/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java b/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java index eaaee1a3d..8ae639971 100644 --- a/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java +++ b/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java @@ -10,7 +10,6 @@ import java.lang.reflect.Proxy; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import javax.annotation.Nullable; /** Collection of helper methods for calling methods and accessing fields reflectively. */ @SuppressWarnings(value = {"unchecked", "TypeParameterUnusedInFormals", "NewApi"}) @@ -45,9 +44,10 @@ public class ReflectionHelpers { * <p>The returned object will be an instance of the given class, but all methods will return * either the "default" value for primitives, or another deep proxy for non-primitive types. * - * <p>This should be used rarely, for cases where we need to create deep proxies in order not - * to crash. The inner proxies are impossible to configure, so there is no way to create - * meaningful behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions. + * <p>This should be used rarely, for cases where we need to create deep proxies in order not to + * crash. The inner proxies are impossible to configure, so there is no way to create meaningful + * behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions. + * * @param clazz the class to provide a proxy instance of. * @return a new "Deep Proxy" instance of the given class. */ @@ -127,7 +127,8 @@ public class ReflectionHelpers { * @param fieldName The field name. * @param fieldNewValue New value. */ - public static void setField(final Object object, final String fieldName, final Object fieldNewValue) { + public static void setField( + final Object object, final String fieldName, final Object fieldNewValue) { try { traverseClassHierarchy( object.getClass(), @@ -152,7 +153,8 @@ public class ReflectionHelpers { * @param fieldName The field name. * @param fieldNewValue New value. */ - public static void setField(Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) { + public static void setField( + Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) { try { Field field = type.getDeclaredField(fieldName); field.setAccessible(true); @@ -163,6 +165,22 @@ public class ReflectionHelpers { } /** + * Reflectively check if a class has a given field (static or non static). + * + * @param clazz Target class. + * @param fieldName The field name. + * @return boolean to indicate whether the field exists or not in clazz. + */ + public static boolean hasField(Class<?> clazz, String fieldName) { + try { + Field field = clazz.getDeclaredField(fieldName); + return (field != null); + } catch (NoSuchFieldException e) { + return false; + } + } + + /** * Reflectively get the value of a static field. * * @param field Field object. @@ -392,7 +410,9 @@ public class ReflectionHelpers { public static <T> T newInstance(Class<T> cl) { try { return cl.getDeclaredConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException + } catch (InstantiationException + | IllegalAccessException + | NoSuchMethodException | InvocationTargetException e) { throw new RuntimeException(e); } @@ -465,15 +485,15 @@ public class ReflectionHelpers { */ public static class ClassParameter<V> { public final Class<? extends V> clazz; - public final V val; + public final V value; - public ClassParameter(Class<? extends V> clazz, V val) { + public ClassParameter(Class<? extends V> clazz, V value) { this.clazz = clazz; - this.val = val; + this.value = value; } - public static <V> ClassParameter<V> from(Class<? extends V> clazz, V val) { - return new ClassParameter<>(clazz, val); + public static <V> ClassParameter<V> from(Class<? extends V> clazz, V value) { + return new ClassParameter<>(clazz, value); } public static ClassParameter<?>[] fromComponentLists(Class<?>[] classes, Object[] values) { @@ -496,7 +516,7 @@ public class ReflectionHelpers { public static Object[] getValues(ClassParameter<?>... classParameters) { Object[] values = new Object[classParameters.length]; for (int i = 0; i < classParameters.length; i++) { - Object paramValue = classParameters[i].val; + Object paramValue = classParameters[i].value; values[i] = paramValue; } return values; @@ -510,15 +530,15 @@ public class ReflectionHelpers { */ public static class StringParameter<V> { public final String className; - public final V val; + public final V value; - public StringParameter(String className, V val) { + public StringParameter(String className, V value) { this.className = className; - this.val = val; + this.value = value; } - public static <V> StringParameter<V> from(String className, V val) { - return new StringParameter<>(className, val); + public static <V> StringParameter<V> from(String className, V value) { + return new StringParameter<>(className, value); } } } diff --git a/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java b/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java index 56c489df1..e5f281ba6 100644 --- a/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java +++ b/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java @@ -141,7 +141,8 @@ public class ReflectionHelpersTest { } @Test - public void callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() { + public void + callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() { ExampleDescendant example = new ExampleDescendant(); int returnNumber = ReflectionHelpers.callInstanceMethod( @@ -282,23 +283,35 @@ public class ReflectionHelpersTest { } @Test - public void callConstructorReflectively_whenMultipleSignaturesExistForTheConstructor_callsConstructorWithCorrectSignature() { - ExampleClass ec = ReflectionHelpers.callConstructor(ExampleClass.class, ClassParameter.from(int.class, 16)); + public void + callConstructorReflectively_whenMultipleSignaturesExistForTheConstructor_callsConstructorWithCorrectSignature() { + ExampleClass ec = + ReflectionHelpers.callConstructor(ExampleClass.class, ClassParameter.from(int.class, 16)); assertWithMessage("index").that(ec.index).isEqualTo(16); assertWithMessage("name").that(ec.name).isNull(); } - @SuppressWarnings("serial") - private static class TestError extends Error { + @Test + public void callHasField_withstaticandregularmember() { + assertWithMessage("has field failed for member: unusedName") + .that(ReflectionHelpers.hasField(FieldTestClass.class, "unusedName")) + .isTrue(); + assertWithMessage("has field failed for member: unusedStaticName") + .that(ReflectionHelpers.hasField(FieldTestClass.class, "unusedStaticName")) + .isTrue(); + assertWithMessage("has field failed for non existant member: noname") + .that(ReflectionHelpers.hasField(FieldTestClass.class, "noname")) + .isFalse(); } @SuppressWarnings("serial") - private static class TestException extends Exception { - } + private static class TestError extends Error {} @SuppressWarnings("serial") - private static class TestRuntimeException extends RuntimeException { - } + private static class TestException extends Exception {} + + @SuppressWarnings("serial") + private static class TestRuntimeException extends RuntimeException {} @SuppressWarnings("unused") private static class ExampleBase { @@ -406,4 +419,11 @@ public class ReflectionHelpersTest { this.index = index; } } + + private static class FieldTestClass { + public String unusedName; + public static String unusedStaticName = "unusedStaticNameValue"; + + private FieldTestClass() {} + } } diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle index a273d5a6f..a2230b0fe 100644 --- a/shadows/framework/build.gradle +++ b/shadows/framework/build.gradle @@ -15,6 +15,8 @@ configurations { sqlite4java } +def sqlite4javaVersion = libs.versions.sqlite4java.get() + task copySqliteNatives(type: Copy) { from project.configurations.sqlite4java { include '**/*.dll' @@ -45,22 +47,20 @@ dependencies { api project(":pluginapi") api project(":sandbox") api project(":shadowapi") + api project(":shadows:versioning") api project(":utils") api project(":utils:reflector") + api "androidx.test:monitor:$axtMonitorVersion@aar" - implementation "com.google.errorprone:error_prone_annotations:$errorproneVersion" - compileOnly "com.google.code.findbugs:jsr305:3.0.2" - api "com.almworks.sqlite4java:sqlite4java:$sqlite4javaVersion" - compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true } - api "com.ibm.icu:icu4j:72.1" - api "androidx.annotation:annotation:1.1.0" - api "com.google.auto.value:auto-value-annotations:1.10.1" - annotationProcessor "com.google.auto.value:auto-value:1.10.1" + implementation libs.error.prone.annotations + compileOnly libs.findbugs.jsr305 + api libs.sqlite4java + compileOnly(AndroidSdk.MAX_SDK.coordinates) + api libs.icu4j + api libs.androidx.annotation + api libs.auto.value.annotations + annotationProcessor libs.auto.value - sqlite4java "com.almworks.sqlite4java:libsqlite4java-osx:$sqlite4javaVersion" - sqlite4java "com.almworks.sqlite4java:libsqlite4java-linux-amd64:$sqlite4javaVersion" - sqlite4java "com.almworks.sqlite4java:sqlite4java-win32-x64:$sqlite4javaVersion" - sqlite4java "com.almworks.sqlite4java:libsqlite4java-linux-i386:$sqlite4javaVersion" - sqlite4java "com.almworks.sqlite4java:sqlite4java-win32-x86:$sqlite4javaVersion" + sqlite4java libs.bundles.sqlite4java.native } diff --git a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java index 33276d916..33276d916 100755..100644 --- a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java +++ b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java new file mode 100644 index 000000000..e2b8f0df3 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java @@ -0,0 +1,138 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.TIRAMISU; + +import android.companion.AssociationInfo; +import android.net.MacAddress; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; + +/** Builder for {@link AssociationInfo}. */ +public class AssociationInfoBuilder { + private int id; + private int userId; + private String packageName; + private String deviceMacAddress; + private CharSequence displayName; + private String deviceProfile; + private boolean selfManaged; + private boolean notifyOnDeviceNearby; + private long approvedMs; + private long lastTimeConnectedMs; + + private AssociationInfoBuilder() {} + + public static AssociationInfoBuilder newBuilder() { + return new AssociationInfoBuilder(); + } + + public AssociationInfoBuilder setId(int id) { + this.id = id; + return this; + } + + public AssociationInfoBuilder setUserId(int userId) { + this.userId = userId; + return this; + } + + public AssociationInfoBuilder setPackageName(String packageName) { + this.packageName = packageName; + return this; + } + + public AssociationInfoBuilder setDeviceMacAddress(String deviceMacAddress) { + this.deviceMacAddress = deviceMacAddress; + return this; + } + + public AssociationInfoBuilder setDisplayName(CharSequence displayName) { + this.displayName = displayName; + return this; + } + + public AssociationInfoBuilder setDeviceProfile(String deviceProfile) { + this.deviceProfile = deviceProfile; + return this; + } + + public AssociationInfoBuilder setSelfManaged(boolean selfManaged) { + this.selfManaged = selfManaged; + return this; + } + + public AssociationInfoBuilder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby) { + this.notifyOnDeviceNearby = notifyOnDeviceNearby; + return this; + } + + public AssociationInfoBuilder setApprovedMs(long approvedMs) { + this.approvedMs = approvedMs; + return this; + } + + public AssociationInfoBuilder setLastTimeConnectedMs(long lastTimeConnectedMs) { + this.lastTimeConnectedMs = lastTimeConnectedMs; + return this; + } + + public AssociationInfo build() { + try { + if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) { + // We have two different constructors for AssociationInfo across + // T branches. aosp has the constructor that takes a new "revoked" parameter. + // Since there is not deterministic way to know which branch we are running in, + // we will reflect on the class to see if it has the mRevoked member. + // Based on the result we will either invoke the constructor with "revoked" or the + // one without this parameter. + if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) { + return ReflectionHelpers.callConstructor( + AssociationInfo.class, + ClassParameter.from(int.class, id), + ClassParameter.from(int.class, userId), + ClassParameter.from(String.class, packageName), + ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)), + ClassParameter.from(CharSequence.class, displayName), + ClassParameter.from(String.class, deviceProfile), + ClassParameter.from(boolean.class, selfManaged), + ClassParameter.from(boolean.class, notifyOnDeviceNearby), + ClassParameter.from(boolean.class, false /*revoked only supported in aosp*/), + ClassParameter.from(long.class, approvedMs), + ClassParameter.from(long.class, lastTimeConnectedMs)); + } else { + return ReflectionHelpers.callConstructor( + AssociationInfo.class, + ClassParameter.from(int.class, id), + ClassParameter.from(int.class, userId), + ClassParameter.from(String.class, packageName), + ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)), + ClassParameter.from(CharSequence.class, displayName), + ClassParameter.from(String.class, deviceProfile), + ClassParameter.from(boolean.class, selfManaged), + ClassParameter.from(boolean.class, notifyOnDeviceNearby), + ClassParameter.from(long.class, approvedMs), + ClassParameter.from(long.class, lastTimeConnectedMs)); + } + } else { + return ReflectionHelpers.callConstructor( + AssociationInfo.class, + ClassParameter.from(int.class, id), + ClassParameter.from(int.class, userId), + ClassParameter.from(String.class, packageName), + ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)), + ClassParameter.from(CharSequence.class, displayName), + ClassParameter.from(String.class, deviceProfile), + ClassParameter.from(Class.forName("android.companion.AssociatedDevice"), null), + ClassParameter.from(boolean.class, selfManaged), + ClassParameter.from(boolean.class, notifyOnDeviceNearby), + ClassParameter.from(boolean.class, false /*revoked*/), + ClassParameter.from(long.class, approvedMs), + ClassParameter.from(long.class, lastTimeConnectedMs), + ClassParameter.from(int.class, 0 /*systemDataSyncFlags*/)); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java new file mode 100644 index 000000000..70e54b190 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java @@ -0,0 +1,139 @@ +package org.robolectric.shadows; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manages remote address connections for {@link ShadowBluetoothGatt} and {@link + * ShadowBluetoothGattServer}. + */ +final class BluetoothConnectionManager { + + private static volatile BluetoothConnectionManager instance; + + /** Connection metadata for Gatt Server and Client connections. */ + private static class BluetoothConnectionMetadata { + boolean hasGattClientConnection = false; + boolean hasGattServerConnection = false; + + void setHasGattClientConnection(boolean hasGattClientConnection) { + this.hasGattClientConnection = hasGattClientConnection; + } + + void setHasGattServerConnection(boolean hasGattServerConnection) { + this.hasGattServerConnection = hasGattServerConnection; + } + + boolean hasGattClientConnection() { + return hasGattClientConnection; + } + + boolean hasGattServerConnection() { + return hasGattServerConnection; + } + + boolean isConnected() { + return hasGattClientConnection || hasGattServerConnection; + } + } + + private BluetoothConnectionManager() {} + + static BluetoothConnectionManager getInstance() { + if (instance == null) { + synchronized (BluetoothConnectionManager.class) { + if (instance == null) { + instance = new BluetoothConnectionManager(); + } + } + } + return instance; + } + + /** + * Map representing remote address connections, mapping a remote address to a {@link + * BluetoothConnectionMetadata}. + */ + private final Map<String, BluetoothConnectionMetadata> remoteAddressConnectionMap = + new HashMap<>(); + + /** + * Register a Gatt Client Connection. Intended for use by {@link + * ShadowBluetoothGatt#notifyConnection} when simulating a successful Gatt Client Connection. + */ + void registerGattClientConnection(String remoteAddress) { + if (!remoteAddressConnectionMap.containsKey(remoteAddress)) { + remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata()); + } + remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(true); + } + + /** + * Unregister a Gatt Client Connection. Intended for use by {@link + * ShadowBluetoothGatt#notifyDisconnection} when simulating a successful Gatt client + * disconnection. + */ + void unregisterGattClientConnection(String remoteAddress) { + if (remoteAddressConnectionMap.containsKey(remoteAddress)) { + remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(false); + } + } + + /** + * Register a Gatt Server Connection. Intended for use by {@link + * ShadowBluetoothGattServer#notifyConnection} when simulating a successful Gatt server + * connection. + */ + void registerGattServerConnection(String remoteAddress) { + if (!remoteAddressConnectionMap.containsKey(remoteAddress)) { + remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata()); + } + remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(true); + } + + /** + * Unregister a Gatt Server Connection. Intended for use by {@link + * ShadowBluetoothGattServer#notifyDisconnection} when simulating a successful Gatt server + * disconnection. + */ + void unregisterGattServerConnection(String remoteAddress) { + if (remoteAddressConnectionMap.containsKey(remoteAddress)) { + remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(false); + } + } + + /** + * Returns true if remote address has an active gatt client connection. + * + * @param remoteAddress remote address + */ + boolean hasGattClientConnection(String remoteAddress) { + return remoteAddressConnectionMap.containsKey(remoteAddress) + && remoteAddressConnectionMap.get(remoteAddress).hasGattClientConnection(); + } + + /** + * Returns true if remote address has an active gatt server connection. + * + * @param remoteAddress remote address + */ + boolean hasGattServerConnection(String remoteAddress) { + return remoteAddressConnectionMap.containsKey(remoteAddress) + && remoteAddressConnectionMap.get(remoteAddress).hasGattServerConnection(); + } + + /** + * Returns true if remote address has an active connection. + * + * @param remoteAddress remote address + */ + boolean isConnected(String remoteAddress) { + return remoteAddressConnectionMap.containsKey(remoteAddress) + && remoteAddressConnectionMap.get(remoteAddress).isConnected(); + } + + /** Clears all connection information */ + void resetConnections() { + this.remoteAddressConnectionMap.clear(); + } +}
\ No newline at end of file diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java new file mode 100644 index 000000000..597aef246 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java @@ -0,0 +1,170 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build; +import android.telephony.CellIdentityLte; +import android.telephony.CellInfo; +import android.telephony.ClosedSubscriberGroupInfo; +import androidx.annotation.RequiresApi; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Builder for {@link android.telephony.CellIdentityLte}. */ +@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) +public class CellIdentityLteBuilder { + + @Nullable private String mcc = null; + @Nullable private String mnc = null; + private int ci = CellInfo.UNAVAILABLE; + private int pci = CellInfo.UNAVAILABLE; + private int tac = CellInfo.UNAVAILABLE; + private int earfcn = CellInfo.UNAVAILABLE; + private int[] bands = new int[0]; + private int bandwidth = CellInfo.UNAVAILABLE; + @Nullable private String alphal = null; + @Nullable private String alphas = null; + private List<String> additionalPlmns = new ArrayList<>(); + + private CellIdentityLteBuilder() {} + + public static CellIdentityLteBuilder newBuilder() { + return new CellIdentityLteBuilder(); + } + + protected static CellIdentityLte getDefaultInstance() { + return reflector(CellIdentityLteReflector.class).newCellIdentityLte(); + } + + public CellIdentityLteBuilder setMcc(String mcc) { + this.mcc = mcc; + return this; + } + + public CellIdentityLteBuilder setMnc(String mnc) { + this.mnc = mnc; + return this; + } + + public CellIdentityLteBuilder setCi(int ci) { + this.ci = ci; + return this; + } + + public CellIdentityLteBuilder setPci(int pci) { + this.pci = pci; + return this; + } + + public CellIdentityLteBuilder setTac(int tac) { + this.tac = tac; + return this; + } + + public CellIdentityLteBuilder setEarfcn(int earfcn) { + this.earfcn = earfcn; + return this; + } + + public CellIdentityLteBuilder setBands(int[] bands) { + this.bands = bands; + return this; + } + + public CellIdentityLteBuilder setBandwidth(int bandwidth) { + this.bandwidth = bandwidth; + return this; + } + + public CellIdentityLteBuilder setLongOperatorName(String longOperatorName) { + this.alphal = longOperatorName; + return this; + } + + public CellIdentityLteBuilder setShortOperatorName(String shortOperatorName) { + this.alphas = shortOperatorName; + return this; + } + + public CellIdentityLteBuilder setAdditionalPlmns(List<String> additionalPlmns) { + this.additionalPlmns = additionalPlmns; + return this; + } + + public CellIdentityLte build() { + CellIdentityLteReflector cellIdentityLteReflector = reflector(CellIdentityLteReflector.class); + int apiLevel = RuntimeEnvironment.getApiLevel(); + if (apiLevel < Build.VERSION_CODES.N) { + return cellIdentityLteReflector.newCellIdentityLte( + mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac); + } else if (apiLevel < Build.VERSION_CODES.P) { + return cellIdentityLteReflector.newCellIdentityLte( + mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac, earfcn); + } else if (apiLevel < Build.VERSION_CODES.R) { + return cellIdentityLteReflector.newCellIdentityLte( + ci, pci, tac, earfcn, bandwidth, mcc, mnc, alphal, alphas); + } else { + return cellIdentityLteReflector.newCellIdentityLte( + ci, + pci, + tac, + earfcn, + bands, + bandwidth, + mcc, + mnc, + alphal, + alphas, + additionalPlmns, + /* csgInfo= */ null); + } + } + + private static int mccOrMncToInt(@Nullable String mccOrMnc) { + return mccOrMnc == null ? CellInfo.UNAVAILABLE : Integer.parseInt(mccOrMnc); + } + + @ForType(CellIdentityLte.class) + private interface CellIdentityLteReflector { + @Constructor + CellIdentityLte newCellIdentityLte(); + + @Constructor + CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac); + + @Constructor + CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac, int earfcn); + + @Constructor + CellIdentityLte newCellIdentityLte( + int ci, + int pci, + int tac, + int earfcn, + int bandwidth, + String mcc, + String mnc, + String alphal, + String alphas); + + @Constructor + CellIdentityLte newCellIdentityLte( + int ci, + int pci, + int tac, + int earfcn, + int[] bands, + int bandwidth, + String mcc, + String mnc, + String alphal, + String alphas, + Collection<String> additionalPlmns, + ClosedSubscriberGroupInfo csgInfo); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java new file mode 100644 index 000000000..22a0e75c0 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java @@ -0,0 +1,135 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build; +import android.telephony.CellIdentityNr; +import android.telephony.CellInfo; +import androidx.annotation.RequiresApi; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Builder for {@link android.telephony.CellIdentityNr}. */ +@RequiresApi(Build.VERSION_CODES.Q) +public class CellIdentityNrBuilder { + + private int pci = CellInfo.UNAVAILABLE; + private int tac = CellInfo.UNAVAILABLE; + private int nrarfcn = CellInfo.UNAVAILABLE; + private int[] bands = new int[0]; + @Nullable private String mcc = null; + @Nullable private String mnc = null; + private long nci = CellInfo.UNAVAILABLE; + @Nullable private String alphal = null; + @Nullable private String alphas = null; + private List<String> additionalPlmns = new ArrayList<>(); + + private CellIdentityNrBuilder() {} + + public static CellIdentityNrBuilder newBuilder() { + return new CellIdentityNrBuilder(); + } + + // An empty constructor is not available on Q. + @RequiresApi(Build.VERSION_CODES.R) + protected static CellIdentityNr getDefaultInstance() { + return reflector(CellIdentityNrReflector.class).newCellIdentityNr(); + } + + public CellIdentityNrBuilder setNci(long nci) { + this.nci = nci; + return this; + } + + public CellIdentityNrBuilder setPci(int pci) { + this.pci = pci; + return this; + } + + public CellIdentityNrBuilder setTac(int tac) { + this.tac = tac; + return this; + } + + public CellIdentityNrBuilder setNrarfcn(int nrarfcn) { + this.nrarfcn = nrarfcn; + return this; + } + + public CellIdentityNrBuilder setMcc(String mcc) { + this.mcc = mcc; + return this; + } + + public CellIdentityNrBuilder setMnc(String mnc) { + this.mnc = mnc; + return this; + } + + public CellIdentityNrBuilder setBands(int[] bands) { + this.bands = bands; + return this; + } + + public CellIdentityNrBuilder setLongOperatorName(String longOperatorName) { + this.alphal = longOperatorName; + return this; + } + + public CellIdentityNrBuilder setShortOperatorName(String shortOperatorName) { + this.alphas = shortOperatorName; + return this; + } + + public CellIdentityNrBuilder setAdditionalPlmns(List<String> additionalPlmns) { + this.additionalPlmns = additionalPlmns; + return this; + } + + public CellIdentityNr build() { + CellIdentityNrReflector cellIdentityReflector = reflector(CellIdentityNrReflector.class); + if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.R) { + return cellIdentityReflector.newCellIdentityNr( + pci, tac, nrarfcn, mcc, mnc, nci, alphal, alphas); + } else { + return cellIdentityReflector.newCellIdentityNr( + pci, tac, nrarfcn, bands, mcc, mnc, nci, alphal, alphas, additionalPlmns); + } + } + + @ForType(CellIdentityNr.class) + private interface CellIdentityNrReflector { + + @Constructor + CellIdentityNr newCellIdentityNr(); + + @Constructor + CellIdentityNr newCellIdentityNr( + int pci, + int tac, + int nrarfcn, + String mcc, + String mnc, + long nci, + String alphal, + String alphas); + + @Constructor + CellIdentityNr newCellIdentityNr( + int pci, + int tac, + int nrarfcn, + int[] bands, + String mcc, + String mnc, + long nci, + String alphal, + String alphas, + Collection<String> additionalPlmns); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java new file mode 100644 index 000000000..6f3f93420 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java @@ -0,0 +1,143 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build; +import android.telephony.CellIdentityLte; +import android.telephony.CellInfo; +import android.telephony.CellInfoLte; +import android.telephony.CellSignalStrengthLte; +import androidx.annotation.RequiresApi; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; +import org.robolectric.util.reflector.WithType; + +/** Builder for {@link android.telephony.CellInfoLte}. */ +@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) +public class CellInfoLteBuilder { + + private boolean isRegistered = false; + private long timeStamp = 0L; + private int cellConnectionStatus = 0; + private CellIdentityLte cellIdentity; + private CellSignalStrengthLte cellSignalStrength; + + private CellInfoLteBuilder() {} + + public static CellInfoLteBuilder newBuilder() { + return new CellInfoLteBuilder(); + } + + public CellInfoLteBuilder setRegistered(boolean isRegistered) { + this.isRegistered = isRegistered; + return this; + } + + public CellInfoLteBuilder setTimeStampNanos(long timeStamp) { + this.timeStamp = timeStamp; + return this; + } + + public CellInfoLteBuilder setCellConnectionStatus(int cellConnectionStatus) { + this.cellConnectionStatus = cellConnectionStatus; + return this; + } + + public CellInfoLteBuilder setCellIdentity(CellIdentityLte cellIdentity) { + this.cellIdentity = cellIdentity; + return this; + } + + public CellInfoLteBuilder setCellSignalStrength(CellSignalStrengthLte cellSignalStrength) { + this.cellSignalStrength = cellSignalStrength; + return this; + } + + public CellInfoLte build() { + int apiLevel = RuntimeEnvironment.getApiLevel(); + if (cellIdentity == null) { + if (apiLevel > Build.VERSION_CODES.Q) { + cellIdentity = CellIdentityLteBuilder.getDefaultInstance(); + } else { + cellIdentity = CellIdentityLteBuilder.newBuilder().build(); + } + } + if (cellSignalStrength == null) { + cellSignalStrength = CellSignalStrengthLteBuilder.getDefaultInstance(); + } + CellInfoLteReflector cellInfoLteReflector = reflector(CellInfoLteReflector.class); + if (apiLevel < Build.VERSION_CODES.TIRAMISU) { + CellInfoLte cellInfo = cellInfoLteReflector.newCellInfoLte(); + cellInfoLteReflector = reflector(CellInfoLteReflector.class, cellInfo); + cellInfoLteReflector.setCellIdentity(cellIdentity); + cellInfoLteReflector.setCellSignalStrength(cellSignalStrength); + CellInfoReflector cellInfoReflector = reflector(CellInfoReflector.class, cellInfo); + cellInfoReflector.setTimeStamp(timeStamp); + if (apiLevel <= Build.VERSION_CODES.KITKAT) { + cellInfoReflector.setRegisterd(isRegistered); + } else { + cellInfoReflector.setRegistered(isRegistered); + } + if (apiLevel > Build.VERSION_CODES.O_MR1) { + cellInfoReflector.setCellConnectionStatus(cellConnectionStatus); + } + return cellInfo; + } else { + try { + // This reflection is highly brittle but there is currently no choice as CellConfigLte is + // entirely @hide. + Class cellConfigLteClass = Class.forName("android.telephony.CellConfigLte"); + return cellInfoLteReflector.newCellInfoLte( + cellConnectionStatus, + isRegistered, + timeStamp, + cellIdentity, + cellSignalStrength, + ReflectionHelpers.callConstructor(cellConfigLteClass)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } + + @ForType(CellInfoLte.class) + private interface CellInfoLteReflector { + @Constructor + CellInfoLte newCellInfoLte(); + + @Constructor + CellInfoLte newCellInfoLte( + int cellConnectionStatus, + boolean isRegistered, + long timeStamp, + CellIdentityLte cellIdentity, + CellSignalStrengthLte cellSignalStrength, + @WithType("android.telephony.CellConfigLte") Object cellConfigLte); + + @Accessor("mCellIdentityLte") + void setCellIdentity(CellIdentityLte cellIdentity); + + @Accessor("mCellSignalStrengthLte") + void setCellSignalStrength(CellSignalStrengthLte cellSignalStrength); + } + + @ForType(CellInfo.class) + private interface CellInfoReflector { + + // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/kitkat-release/telephony/java/android/telephony/CellInfo.java#79 + @Accessor("mRegistered") + void setRegisterd(boolean registered); // NOTYPO + + @Accessor("mRegistered") + void setRegistered(boolean registered); + + @Accessor("mTimeStamp") + void setTimeStamp(long registered); + + @Accessor("mCellConnectionStatus") + void setCellConnectionStatus(int cellConnectionStatus); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java new file mode 100644 index 000000000..78195246e --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java @@ -0,0 +1,93 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build; +import android.os.Parcel; +import android.telephony.CellIdentityNr; +import android.telephony.CellInfoNr; +import android.telephony.CellSignalStrengthNr; +import androidx.annotation.RequiresApi; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Builder for {@link android.telephony.CellInfoNr}. */ +@RequiresApi(Build.VERSION_CODES.Q) +public class CellInfoNrBuilder { + + private boolean isRegistered = false; + private long timeStamp = 0L; + private int cellConnectionStatus = 0; + private CellIdentityNr cellIdentity; + private CellSignalStrengthNr cellSignalStrength; + + private CellInfoNrBuilder() {} + + public static CellInfoNrBuilder newBuilder() { + return new CellInfoNrBuilder(); + } + + public CellInfoNrBuilder setRegistered(boolean isRegistered) { + this.isRegistered = isRegistered; + return this; + } + + public CellInfoNrBuilder setTimeStampNanos(long timeStamp) { + this.timeStamp = timeStamp; + return this; + } + + public CellInfoNrBuilder setCellConnectionStatus(int cellConnectionStatus) { + this.cellConnectionStatus = cellConnectionStatus; + return this; + } + + public CellInfoNrBuilder setCellIdentity(CellIdentityNr cellIdentity) { + this.cellIdentity = cellIdentity; + return this; + } + + public CellInfoNrBuilder setCellSignalStrength(CellSignalStrengthNr cellSignalStrength) { + this.cellSignalStrength = cellSignalStrength; + return this; + } + + public CellInfoNr build() { + if (cellIdentity == null) { + cellIdentity = CellIdentityNrBuilder.getDefaultInstance(); + } + if (cellSignalStrength == null) { + cellSignalStrength = CellSignalStrengthNrBuilder.getDefaultInstance(); + } + // CellInfoNr has no default constructor below T so we write it to a Parcel. + if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.TIRAMISU) { + Parcel p = Parcel.obtain(); + p.writeInt(/* CellInfo#TYPE_NR */ 6); + p.writeInt(isRegistered ? 1 : 0); + p.writeLong(timeStamp); + p.writeInt(cellConnectionStatus); + cellIdentity.writeToParcel(p, 0); + cellSignalStrength.writeToParcel(p, 0); + p.setDataPosition(0); + CellInfoNr cellInfoNr = CellInfoNr.CREATOR.createFromParcel(p); + p.recycle(); + return cellInfoNr; + } else { + return reflector(CellInfoNrReflector.class) + .newCellInfoNr( + cellConnectionStatus, isRegistered, timeStamp, cellIdentity, cellSignalStrength); + } + } + + @ForType(CellInfoNr.class) + private interface CellInfoNrReflector { + @Constructor + CellInfoNr newCellInfoNr( + int cellConnectionStatus, + boolean isRegistered, + long timeStamp, + CellIdentityNr cellIdentity, + CellSignalStrengthNr cellSignalStrength); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java new file mode 100644 index 000000000..9b5d1a1ac --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java @@ -0,0 +1,96 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build; +import android.telephony.CellInfo; +import android.telephony.CellSignalStrengthLte; +import androidx.annotation.RequiresApi; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Builder for {@link android.telephony.CellSignalStrengthLte} */ +@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) +public class CellSignalStrengthLteBuilder { + + private int rssi = CellInfo.UNAVAILABLE; + private int rsrp = CellInfo.UNAVAILABLE; + private int rsrq = CellInfo.UNAVAILABLE; + private int rssnr = CellInfo.UNAVAILABLE; + private int cqiTableIndex = CellInfo.UNAVAILABLE; + private int cqi = CellInfo.UNAVAILABLE; + private int timingAdvance = CellInfo.UNAVAILABLE; + + private CellSignalStrengthLteBuilder() {} + + public static CellSignalStrengthLteBuilder newBuilder() { + return new CellSignalStrengthLteBuilder(); + } + + protected static CellSignalStrengthLte getDefaultInstance() { + return reflector(CellSignalStrengthLteReflector.class).newCellSignalStrength(); + } + + /** This is equivalent to {@code signalStrength} pre SDK Q. */ + public CellSignalStrengthLteBuilder setRssi(int rssi) { + this.rssi = rssi; + return this; + } + + public CellSignalStrengthLteBuilder setRsrp(int rsrp) { + this.rsrp = rsrp; + return this; + } + + public CellSignalStrengthLteBuilder setRsrq(int rsrq) { + this.rsrq = rsrq; + return this; + } + + public CellSignalStrengthLteBuilder setRssnr(int rssnr) { + this.rssnr = rssnr; + return this; + } + + public CellSignalStrengthLteBuilder setCqiTableIndex(int cqiTableIndex) { + this.cqiTableIndex = cqiTableIndex; + return this; + } + + public CellSignalStrengthLteBuilder setCqi(int cqi) { + this.cqi = cqi; + return this; + } + + public CellSignalStrengthLteBuilder setTimingAdvance(int timingAdvance) { + this.timingAdvance = timingAdvance; + return this; + } + + public CellSignalStrengthLte build() { + CellSignalStrengthLteReflector cellSignalStrengthReflector = + reflector(CellSignalStrengthLteReflector.class); + if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.S) { + return cellSignalStrengthReflector.newCellSignalStrength( + rssi, rsrp, rsrq, rssnr, cqi, timingAdvance); + } else { + return cellSignalStrengthReflector.newCellSignalStrength( + rssi, rsrp, rsrq, rssnr, cqiTableIndex, cqi, timingAdvance); + } + } + + @ForType(CellSignalStrengthLte.class) + private interface CellSignalStrengthLteReflector { + @Constructor + CellSignalStrengthLte newCellSignalStrength(); + + @Constructor + CellSignalStrengthLte newCellSignalStrength( + int rssi, int rsrp, int rsrq, int rssnr, int cqi, int timingAdvance); + + @Constructor + CellSignalStrengthLte newCellSignalStrength( + int rssi, int rsrp, int rsrq, int rssnr, int cqiTableIndex, int cqi, int timingAdvance); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java new file mode 100644 index 000000000..4f3f859bc --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java @@ -0,0 +1,140 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build; +import android.telephony.CellInfo; +import android.telephony.CellSignalStrengthNr; +import androidx.annotation.RequiresApi; +import java.util.ArrayList; +import java.util.List; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Builder for {@link android.telephony.CellSignalStrengthNr} */ +@RequiresApi(Build.VERSION_CODES.Q) +public class CellSignalStrengthNrBuilder { + + private int csiRrsp = CellInfo.UNAVAILABLE; + private int csiRsrq = CellInfo.UNAVAILABLE; + private int csiSinr = CellInfo.UNAVAILABLE; + private int csiCqiTableIndex = CellInfo.UNAVAILABLE; + private List<Byte> csiCqiReport = new ArrayList<>(); + private int ssRsrp = CellInfo.UNAVAILABLE; + private int ssRsrq = CellInfo.UNAVAILABLE; + private int ssSinr = CellInfo.UNAVAILABLE; + private int timingAdvance = CellInfo.UNAVAILABLE; + + private CellSignalStrengthNrBuilder() {} + + public static CellSignalStrengthNrBuilder newBuilder() { + return new CellSignalStrengthNrBuilder(); + } + + protected static CellSignalStrengthNr getDefaultInstance() { + return reflector(CellSignalStrengthNrReflector.class).newCellSignalStrengthNr(); + } + + public CellSignalStrengthNrBuilder setCsiRsrp(int csiRrsp) { + this.csiRrsp = csiRrsp; + return this; + } + + public CellSignalStrengthNrBuilder setCsiRsrq(int csiRsrq) { + this.csiRsrq = csiRsrq; + return this; + } + + public CellSignalStrengthNrBuilder setCsiSinr(int csiSinr) { + this.csiSinr = csiSinr; + return this; + } + + public CellSignalStrengthNrBuilder setCsiCqiTableIndex(int csiCqiTableIndex) { + this.csiCqiTableIndex = csiCqiTableIndex; + return this; + } + + public CellSignalStrengthNrBuilder setCsiCqiReport(List<Byte> csiCqiReport) { + this.csiCqiReport = csiCqiReport; + return this; + } + + public CellSignalStrengthNrBuilder setSsRsrp(int ssRsrp) { + this.ssRsrp = ssRsrp; + return this; + } + + public CellSignalStrengthNrBuilder setSsRsrq(int ssRsrq) { + this.ssRsrq = ssRsrq; + return this; + } + + public CellSignalStrengthNrBuilder setSsSinr(int ssSinr) { + this.ssSinr = ssSinr; + return this; + } + + public CellSignalStrengthNrBuilder setTimingAdvance(int timingAdvance) { + this.timingAdvance = timingAdvance; + return this; + } + + public CellSignalStrengthNr build() { + CellSignalStrengthNrReflector cellSignalStrengthReflector = + reflector(CellSignalStrengthNrReflector.class); + if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.TIRAMISU) { + return cellSignalStrengthReflector.newCellSignalStrengthNr( + csiRrsp, csiRsrq, csiSinr, ssRsrp, ssRsrq, ssSinr); + } else if (RuntimeEnvironment.getApiLevel() == Build.VERSION_CODES.TIRAMISU) { + return cellSignalStrengthReflector.newCellSignalStrengthNr( + csiRrsp, csiRsrq, csiSinr, csiCqiTableIndex, csiCqiReport, ssRsrp, ssRsrq, ssSinr); + } else { + return cellSignalStrengthReflector.newCellSignalStrengthNr( + csiRrsp, + csiRsrq, + csiSinr, + csiCqiTableIndex, + csiCqiReport, + ssRsrp, + ssRsrq, + ssSinr, + timingAdvance); + } + } + + @ForType(CellSignalStrengthNr.class) + private interface CellSignalStrengthNrReflector { + + @Constructor + CellSignalStrengthNr newCellSignalStrengthNr(); + + @Constructor + CellSignalStrengthNr newCellSignalStrengthNr( + int csRsrp, int csiRsrq, int csiSinr, int ssRsrp, int ssRsrq, int ssSinr); + + @Constructor + CellSignalStrengthNr newCellSignalStrengthNr( + int csRsrp, + int csiRsrq, + int csiSinr, + int csiCqiTableIndex, + List<Byte> csiCqiReport, + int ssRsrp, + int ssRsrq, + int ssSinr); + + @Constructor + CellSignalStrengthNr newCellSignalStrengthNr( + int csRsrp, + int csiRsrq, + int csiSinr, + int csiCqiTableIndex, + List<Byte> csiCqiReport, + int ssRsrp, + int ssRsrq, + int ssSinr, + int timingAdvance); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java index 841f7d5f8..459340273 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java @@ -10,6 +10,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.EncoderCapabilities; import android.media.MediaCodecInfo.VideoCapabilities; import android.media.MediaFormat; +import android.util.Range; import com.google.common.base.Preconditions; import org.robolectric.RuntimeEnvironment; import org.robolectric.util.ReflectionHelpers; @@ -266,6 +267,17 @@ public class MediaCodecInfoBuilder { void setFlagsSupported(int flagsSupported); } + /** Accessor interface for {@link VideoCapabilities}'s internals. */ + @ForType(VideoCapabilities.class) + interface VideoCapabilitiesReflector { + + @Accessor("mWidthRange") + void setWidthRange(Range<Integer> range); + + @Accessor("mHeightRange") + void setHeightRange(Range<Integer> range); + } + public CodecCapabilities build() { Preconditions.checkNotNull(mediaFormat, "mediaFormat is not set."); Preconditions.checkNotNull(profileLevels, "profileLevels is not set."); @@ -298,6 +310,16 @@ public class MediaCodecInfoBuilder { if (isVideoCodec) { VideoCapabilities videoCaps = createDefaultVideoCapabilities(caps, mediaFormat); + VideoCapabilitiesReflector videoCapsReflector = + Reflector.reflector(VideoCapabilitiesReflector.class, videoCaps); + if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) { + videoCapsReflector.setWidthRange( + new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_WIDTH))); + } + if (mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) { + videoCapsReflector.setHeightRange( + new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_HEIGHT))); + } capsReflector.setVideoCaps(videoCaps); } else { AudioCapabilities audioCaps = createDefaultAudioCapabilities(caps, mediaFormat); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java index 5da1409f3..d23045b24 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java @@ -1,6 +1,7 @@ package org.robolectric.shadows; import android.os.Build; +import android.os.Build.VERSION_CODES; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadow.api.ShadowPicker; @@ -10,6 +11,7 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> { private Class<? extends T> binaryShadowClass; private Class<? extends T> binary9ShadowClass; private Class<? extends T> binary10ShadowClass; + private Class<? extends T> binary14ShadowClass; public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass, Class<? extends T> binaryShadowClass, @@ -18,16 +20,19 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> { this.binaryShadowClass = binaryShadowClass; this.binary9ShadowClass = binary9ShadowClass; this.binary10ShadowClass = binary9ShadowClass; + this.binary14ShadowClass = binary9ShadowClass; } public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass, Class<? extends T> binaryShadowClass, Class<? extends T> binary9ShadowClass, - Class<? extends T> binary10ShadowClass) { + Class<? extends T> binary10ShadowClass, + Class<? extends T> binary14ShadowClass) { this.legacyShadowClass = legacyShadowClass; this.binaryShadowClass = binaryShadowClass; this.binary9ShadowClass = binary9ShadowClass; this.binary10ShadowClass = binary10ShadowClass; + this.binary14ShadowClass = binary14ShadowClass; } @Override @@ -35,10 +40,11 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> { if (ShadowAssetManager.useLegacy()) { return legacyShadowClass; } else { - if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { + if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) { + return binary14ShadowClass; + } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { return binary10ShadowClass; - } else - if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) { + } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) { return binary9ShadowClass; } else { return binaryShadowClass; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java index 47f7306a4..1d575a4b9 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java @@ -426,10 +426,10 @@ public class ShadowActivity extends ShadowContextThemeWrapper { @Implementation protected void runOnUiThread(Runnable action) { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { ShadowApplication.getInstance().getForegroundThreadScheduler().post(action); + } else { + reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java index 883dd2cad..70464bf9c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java @@ -26,7 +26,6 @@ import java.lang.reflect.Proxy; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import javax.annotation.Nonnull; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; @@ -34,6 +33,7 @@ import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.ReflectorObject; import org.robolectric.annotation.Resetter; +import org.robolectric.util.Logger; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; @@ -275,7 +275,12 @@ public class ShadowActivityThread { @Resetter public static void reset() { Object activityThread = RuntimeEnvironment.getActivityThread(); - Objects.requireNonNull(activityThread, "ShadowActivityThread.reset: ActivityThread not set"); - reflector(_ActivityThread_.class, activityThread).getActivities().clear(); + if (activityThread == null) { + Logger.warn( + "RuntimeEnvironment.getActivityThread() is null, an error likely occurred during test" + + " initialization."); + } else { + reflector(_ActivityThread_.class, activityThread).getActivities().clear(); + } } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java index eb2276c9e..eb2276c9e 100755..100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java index c0c9c7a8e..c0c9c7a8e 100755..100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java new file mode 100644 index 000000000..8771d6adb --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java @@ -0,0 +1,72 @@ +package org.robolectric.shadows; + + +import android.annotation.Nullable; +import android.content.res.AssetManager; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +// TODO: update path to released version. +// transliterated from +// https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_rXX/core/jni/android_util_AssetManager.cpp + +@Implements( + value = AssetManager.class, + minSdk = ShadowBuild.UPSIDE_DOWN_CAKE, + shadowPicker = ShadowAssetManager.Picker.class) +@SuppressWarnings("NewApi") +public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 { + + // static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint + // mnc, + // jstring locale, jint orientation, jint touchscreen, jint + // density, + // jint keyboard, jint keyboard_hidden, jint navigation, + // jint screen_width, jint screen_height, + // jint smallest_screen_width_dp, jint screen_width_dp, + // jint screen_height_dp, jint screen_layout, jint ui_mode, + // jint color_mode, jint major_version) { + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + protected static void nativeSetConfiguration( + long ptr, + int mcc, + int mnc, + @Nullable String locale, + int orientation, + int touchscreen, + int density, + int keyboard, + int keyboard_hidden, + int navigation, + int screen_width, + int screen_height, + int smallest_screen_width_dp, + int screen_width_dp, + int screen_height_dp, + int screen_layout, + int ui_mode, + int color_mode, + int grammaticalGender, // ignore for now? + int major_version) { + ShadowArscAssetManager10.nativeSetConfiguration( + ptr, + mcc, + mnc, + locale, + orientation, + touchscreen, + density, + keyboard, + keyboard_hidden, + navigation, + screen_width, + screen_height, + smallest_screen_width_dp, + screen_width_dp, + screen_height_dp, + screen_layout, + ui_mode, + color_mode, + major_version); + } +} +// namespace android diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java index 1f6e40ddf..19c5196f0 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java @@ -25,7 +25,8 @@ abstract public class ShadowAssetManager { ShadowLegacyAssetManager.class, ShadowArscAssetManager.class, ShadowArscAssetManager9.class, - ShadowArscAssetManager10.class); + ShadowArscAssetManager10.class, + ShadowArscAssetManager14.class); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java index 223435110..5f2d4fd3c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java @@ -93,6 +93,7 @@ public class ShadowAudioManager { private ImmutableList<Object> defaultDevicesForAttributes = ImmutableList.of(); private List<AudioDeviceInfo> inputDevices = new ArrayList<>(); private List<AudioDeviceInfo> outputDevices = new ArrayList<>(); + private List<AudioDeviceInfo> availableCommunicationDevices = new ArrayList<>(); private AudioDeviceInfo communicationDevice = null; public ShadowAudioManager() { @@ -451,6 +452,23 @@ public class ShadowAudioManager { } /** + * Sets the list of available communication devices represented by {@link AudioDeviceInfo}. + * + * <p>The previous list of communication devices is replaced and no notifications of the list of + * {@link AudioDeviceCallback} is done. + * + * <p>To add/remove devices one by one and trigger notifications for the list of {@link + * AudioDeviceCallback} please use one of the following methods {@link + * #addOutputDevice(AudioDeviceInfo, boolean)}, {@link #removeOutputDevice(AudioDeviceInfo, + * boolean)}. + */ + @TargetApi(VERSION_CODES.S) + public void setAvailableCommunicationDevices( + List<AudioDeviceInfo> availableCommunicationDevices) { + this.availableCommunicationDevices = new ArrayList<>(availableCommunicationDevices); + } + + /** * Adds an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}. */ @@ -497,6 +515,36 @@ public class ShadowAudioManager { } /** + * Adds an available communication {@link AudioDeviceInfo} and notifies the list of {@link + * AudioDeviceCallback} if the device was not present before and indicated by {@code + * notifyAudioDeviceCallbacks}. + */ + @TargetApi(VERSION_CODES.S) + public void addAvailableCommunicationDevice( + AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) { + boolean changed = + !this.availableCommunicationDevices.contains(communicationDevice) + && this.availableCommunicationDevices.add(communicationDevice); + if (changed && notifyAudioDeviceCallbacks) { + notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ true); + } + } + + /** + * Removes an available communication {@link AudioDeviceInfo} and notifies the list of {@link + * AudioDeviceCallback} if the device was present before and indicated by {@code + * notifyAudioDeviceCallbacks}. + */ + @TargetApi(VERSION_CODES.S) + public void removeAvailableCommunicationDevice( + AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) { + boolean changed = this.availableCommunicationDevices.remove(communicationDevice); + if (changed && notifyAudioDeviceCallbacks) { + notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ false); + } + } + + /** * Registers an {@link AudioDeviceCallback} object to receive notifications of changes to the set * of connected audio devices. * @@ -504,8 +552,10 @@ public class ShadowAudioManager { * * @see #addInputDevice(AudioDeviceInfo, boolean) * @see #addOutputDevice(AudioDeviceInfo, boolean) + * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean) * @see #removeInputDevice(AudioDeviceInfo, boolean) * @see #removeOutputDevice(AudioDeviceInfo, boolean) + * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean) */ @Implementation(minSdk = M) protected void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) { @@ -520,8 +570,10 @@ public class ShadowAudioManager { * * @see #addInputDevice(AudioDeviceInfo, boolean) * @see #addOutputDevice(AudioDeviceInfo, boolean) + * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean) * @see #removeInputDevice(AudioDeviceInfo, boolean) * @see #removeOutputDevice(AudioDeviceInfo, boolean) + * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean) */ @Implementation(minSdk = M) protected void unregisterAudioDeviceCallback(AudioDeviceCallback callback) { @@ -563,6 +615,11 @@ public class ShadowAudioManager { this.communicationDevice = null; } + @Implementation(minSdk = S) + protected List<AudioDeviceInfo> getAvailableCommunicationDevices() { + return availableCommunicationDevices; + } + @Implementation(minSdk = M) public AudioDeviceInfo[] getDevices(int flags) { List<AudioDeviceInfo> result = new ArrayList<>(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java index 5b051405f..c1e78be00 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java @@ -3,10 +3,23 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static com.google.common.base.Preconditions.checkNotNull; +import android.annotation.NonNull; +import android.media.AudioAttributes; +import android.media.AudioFormat; import android.media.AudioSystem; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Table; +import com.google.common.collect.Tables; +import java.util.Optional; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; /** Shadow for {@link AudioSystem}. */ @Implements(value = AudioSystem.class, isInAndroidSdk = false) @@ -17,6 +30,33 @@ public class ShadowAudioSystem { private static final int MAX_SAMPLE_RATE = 192000; private static final int MIN_SAMPLE_RATE = 4000; + /** + * Table to store key-pair of {@link AudioFormat} and {@link AudioAttributes#getUsage()} with + * value of support for Direct Playback. Used with {@link #setDirectPlaybackSupport(AudioFormat, + * AudioAttributes, int)}, and {@link #getDirectPlaybackSupport(AudioFormat, AudioAttributes)}. + */ + private static final Table<AudioFormat, Integer, Integer> directPlaybackSupportTable = + Tables.synchronizedTable(HashBasedTable.create()); + /** + * Table to store pair of {@link OffloadSupportFormat} and {@link + * AudioAttributes#getVolumeControlStream()} with a value of Offload Playback support. Used with + * {@link #native_get_offload_support}. The table uses {@link OffloadSupportFormat} rather than + * {@link AudioFormat} because {@link #native_get_offload_support} does not pass all the fields + * needed to reliably reconstruct {@link AudioFormat} instances. + */ + private static final Table<OffloadSupportFormat, Integer, Integer> offloadPlaybackSupportTable = + Tables.synchronizedTable(HashBasedTable.create()); + + /** + * Multimap to store whether a pair of {@link OffloadSupportFormat} and {@link + * AudioAttributes#getVolumeControlStream()} ()} support offloaded playback. Used with {@link + * #native_is_offload_supported}. The map uses {@link OffloadSupportFormat} keys rather than + * {@link AudioFormat} because {@link #native_is_offload_supported} does not pass all the fields + * needed to reliably reconstruct {@link AudioFormat} instances. + */ + private static final Multimap<OffloadSupportFormat, Integer> offloadSupportedMap = + Multimaps.synchronizedMultimap(HashMultimap.create()); + @Implementation(minSdk = S) protected static int native_getMaxChannelCount() { return MAX_CHANNEL_COUNT; @@ -38,4 +78,156 @@ public class ShadowAudioSystem { // https://cs.android.com/android/platform/superproject/+/master:system/media/audio/include/system/audio-base.h;l=197;drc=c84ca89fa5d660046364897482b202c797c8595e return 8; } + + /** + * Sets direct playback support for a key-pair of {@link AudioFormat} and {@link AudioAttributes}. + * As a result, calling {@link #getDirectPlaybackSupport} with the same pair of {@link + * AudioFormat} and {@link AudioAttributes} values will return the cached support value. + * + * @param format the audio format (codec, sample rate, channels) + * @param attr the {@link AudioAttributes} to be used for playback + * @param directPlaybackSupport the level of direct playback support to save for the format and + * attribute pair. Must be one of {@link AudioSystem#DIRECT_NOT_SUPPORTED}, {@link + * AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link AudioSystem#OFFLOAD_SUPPORTED}, {@link + * AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}, or a combination of {@link + * AudioSystem#DIRECT_OFFLOAD_SUPPORTED}, {@link AudioSystem#DIRECT_OFFLOAD_GAPLESS_SUPPORTED} + * and {@link AudioSystem#DIRECT_BITSTREAM_SUPPORTED} + */ + public static void setDirectPlaybackSupport( + @NonNull AudioFormat format, @NonNull AudioAttributes attr, int directPlaybackSupport) { + checkNotNull(format, "Illegal null AudioFormat"); + checkNotNull(attr, "Illegal null AudioAttributes"); + directPlaybackSupportTable.put(format, attr.getUsage(), directPlaybackSupport); + } + + /** + * Retrieves the stored direct playback support for the {@link AudioFormat} and {@link + * AudioAttributes}. If no value was stored for the key-pair then {@link + * AudioSystem#DIRECT_NOT_SUPPORTED} is returned. + * + * @param format the audio format (codec, sample rate, channels) to be used for playback + * @param attr the {@link AudioAttributes} to be used for playback + * @return the level of direct playback playback support for the format and attributes. + */ + @Implementation(minSdk = TIRAMISU) + protected static int getDirectPlaybackSupport( + @NonNull AudioFormat format, @NonNull AudioAttributes attr) { + return Optional.ofNullable(directPlaybackSupportTable.get(format, attr.getUsage())) + .orElse(AudioSystem.DIRECT_NOT_SUPPORTED); + } + + /** + * Sets offload playback support for a key-pair of {@link AudioFormat} and {@link + * AudioAttributes}. As a result, calling {@link AudioSystem#getOffloadSupport} with the same pair + * of {@link AudioFormat} and {@link AudioAttributes} values will return the cached support value. + * + * @param format the audio format (codec, sample rate, channels) + * @param attr the {@link AudioAttributes} to be used for playback + * @param offloadSupport the level of offload playback support to save for the format and + * attribute pair. Must be one of {@link AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link + * AudioSystem#OFFLOAD_SUPPORTED} or {@link AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}. + */ + public static void setOffloadPlaybackSupport( + @NonNull AudioFormat format, @NonNull AudioAttributes attr, int offloadSupport) { + checkNotNull(format, "Illegal null AudioFormat"); + checkNotNull(attr, "Illegal null AudioAttributes"); + offloadPlaybackSupportTable.put( + new OffloadSupportFormat( + format.getEncoding(), + format.getSampleRate(), + format.getChannelMask(), + format.getChannelIndexMask()), + attr.getVolumeControlStream(), + offloadSupport); + } + + /** + * Sets whether offload playback is supported for a key-pair of {@link AudioFormat} and {@link + * AudioAttributes}. As a result, calling {@link AudioSystem#isOffloadSupported} with the same + * pair of {@link AudioFormat} and {@link AudioAttributes} values will return {@code supported}. + * + * @param format the audio format (codec, sample rate, channels) + * @param attr the {@link AudioAttributes} to be used for playback + */ + public static void setOffloadSupported( + @NonNull AudioFormat format, @NonNull AudioAttributes attr, boolean supported) { + OffloadSupportFormat offloadSupportFormat = + new OffloadSupportFormat( + format.getEncoding(), + format.getSampleRate(), + format.getChannelMask(), + format.getChannelIndexMask()); + if (supported) { + offloadSupportedMap.put(offloadSupportFormat, attr.getVolumeControlStream()); + } else { + offloadSupportedMap.remove(offloadSupportFormat, attr.getVolumeControlStream()); + } + } + + @Implementation(minSdk = Q, maxSdk = R) + protected static boolean native_is_offload_supported( + int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) { + return offloadSupportedMap.containsEntry( + new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask), streamType); + } + + @Implementation(minSdk = S) + protected static int native_get_offload_support( + int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) { + return Optional.ofNullable( + offloadPlaybackSupportTable.get( + new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask), + streamType)) + .orElse(AudioSystem.OFFLOAD_NOT_SUPPORTED); + } + + @Resetter + public static void reset() { + directPlaybackSupportTable.clear(); + offloadPlaybackSupportTable.clear(); + offloadSupportedMap.clear(); + } + + /** + * Struct to hold specific values from {@link AudioFormat} which are used in {@link + * #native_get_offload_support} and {@link #native_is_offload_supported}. + */ + private static class OffloadSupportFormat { + public final int encoding; + public final int sampleRate; + public final int channelMask; + public final int channelIndexMask; + + public OffloadSupportFormat( + int encoding, int sampleRate, int channelMask, int channelIndexMask) { + this.encoding = encoding; + this.sampleRate = sampleRate; + this.channelMask = channelMask; + this.channelIndexMask = channelIndexMask; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OffloadSupportFormat)) { + return false; + } + OffloadSupportFormat that = (OffloadSupportFormat) o; + return encoding == that.encoding + && sampleRate == that.sampleRate + && channelMask == that.channelMask + && channelIndexMask == that.channelIndexMask; + } + + @Override + public int hashCode() { + int result = encoding; + result = 31 * result + sampleRate; + result = 31 * result + channelMask; + result = 31 * result + channelIndexMask; + return result; + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java index 45a5557a6..6408ce87c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java @@ -1,20 +1,37 @@ package org.robolectric.shadows; import static android.media.AudioTrack.ERROR_BAD_VALUE; +import static android.media.AudioTrack.ERROR_DEAD_OBJECT; import static android.media.AudioTrack.WRITE_BLOCKING; import static android.media.AudioTrack.WRITE_NON_BLOCKING; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import android.annotation.NonNull; +import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioTrack; import android.media.AudioTrack.WriteMode; +import android.media.PlaybackParams; +import android.os.Build.VERSION; +import android.os.Parcel; import android.util.Log; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -50,11 +67,24 @@ public class ShadowAudioTrack { protected static final int DEFAULT_MIN_BUFFER_SIZE = 1024; + // Copied from native code + // https://cs.android.com/android/platform/superproject/+/android13-release:frameworks/base/core/jni/android_media_AudioTrack.cpp?q=AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED + private static final int AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED = -20; + private static final String TAG = "ShadowAudioTrack"; - private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE; + /** Direct playback support checked from {@link #native_is_direct_output_supported}. */ + private static final Multimap<AudioFormatInfo, AudioAttributesInfo> directSupportedFormats = + Multimaps.synchronizedMultimap(HashMultimap.create()); + /** Non-PCM encodings allowed for creating an AudioTrack instance. */ + private static final Set<Integer> allowedNonPcmEncodings = + Collections.synchronizedSet(new HashSet<>()); + private static final List<OnAudioDataWrittenListener> audioDataWrittenListeners = new CopyOnWriteArrayList<>(); + private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE; + private int numBytesReceived; + private PlaybackParams playbackParams; @RealObject AudioTrack audioTrack; /** @@ -67,6 +97,61 @@ public class ShadowAudioTrack { minBufferSize = bufferSize; } + /** + * Adds support for direct playback for the pair of {@link AudioFormat} and {@link + * AudioAttributes} where the format encoding must be non-PCM. Calling {@link + * AudioTrack#isDirectPlaybackSupported(AudioFormat, AudioAttributes)} will return {@code true} + * for matching {@link AudioFormat} and {@link AudioAttributes}. The matching is performed against + * the format's {@linkplain AudioFormat#getEncoding() encoding}, {@linkplain + * AudioFormat#getSampleRate() sample rate}, {@linkplain AudioFormat#getChannelMask() channel + * mask} and {@linkplain AudioFormat#getChannelIndexMask() channel index mask}, and the + * attribute's {@linkplain AudioAttributes#getContentType() content type}, {@linkplain + * AudioAttributes#getUsage() usage} and {@linkplain AudioAttributes#getFlags() flags}. + * + * @param format The {@link AudioFormat}, which must be of a non-PCM encoding. If the encoding is + * PCM, the method will throw an {@link IllegalArgumentException}. + * @param attr The {@link AudioAttributes}. + */ + public static void addDirectPlaybackSupport( + @NonNull AudioFormat format, @NonNull AudioAttributes attr) { + checkNotNull(format); + checkNotNull(attr); + checkArgument(!isPcm(format.getEncoding())); + + directSupportedFormats.put( + new AudioFormatInfo( + format.getEncoding(), + format.getSampleRate(), + format.getChannelMask(), + format.getChannelIndexMask()), + new AudioAttributesInfo(attr.getContentType(), attr.getUsage(), attr.getFlags())); + } + + /** + * Clears all encodings that have been added for direct playback support with {@link + * #addDirectPlaybackSupport}. + */ + public static void clearDirectPlaybackSupportedFormats() { + directSupportedFormats.clear(); + } + + /** + * Add a non-PCM encoding for which {@link AudioTrack} instances are allowed to be created. + * + * @param encoding One of {@link AudioFormat} {@code ENCODING_} constants that represents a + * non-PCM encoding. If {@code encoding} is PCM, this method throws an {@link + * IllegalArgumentException}. + */ + public static void addAllowedNonPcmEncoding(int encoding) { + checkArgument(!isPcm(encoding)); + allowedNonPcmEncodings.add(encoding); + } + + /** Clears all encodings that have been added with {@link #addAllowedNonPcmEncoding(int)}. */ + public static void clearAllowedNonPcmEncodings() { + allowedNonPcmEncodings.clear(); + } + @Implementation(minSdk = N, maxSdk = P) protected static int native_get_FCC_8() { // Return the value hard-coded in native code: @@ -74,6 +159,20 @@ public class ShadowAudioTrack { return 8; } + @Implementation(minSdk = Q) + protected static boolean native_is_direct_output_supported( + int encoding, + int sampleRate, + int channelMask, + int channelIndexMask, + int contentType, + int usage, + int flags) { + return directSupportedFormats.containsEntry( + new AudioFormatInfo(encoding, sampleRate, channelMask, channelIndexMask), + new AudioAttributesInfo(contentType, usage, flags)); + } + /** Returns a predefined or default minimum buffer size. Audio format and config are neglected. */ @Implementation protected static int native_get_min_buff_size( @@ -81,24 +180,141 @@ public class ShadowAudioTrack { return minBufferSize; } + @Implementation(minSdk = P, maxSdk = Q) + protected int native_setup( + Object /*WeakReference<AudioTrack>*/ audioTrack, + Object /*AudioAttributes*/ attributes, + int[] sampleRate, + int channelMask, + int channelIndexMask, + int audioFormat, + int buffSizeInBytes, + int mode, + int[] sessionId, + long nativeAudioTrack, + boolean offload) { + // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem. + if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) { + return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED; + } + return AudioTrack.SUCCESS; + } + + @Implementation(minSdk = R, maxSdk = R) + protected int native_setup( + Object /*WeakReference<AudioTrack>*/ audioTrack, + Object /*AudioAttributes*/ attributes, + int[] sampleRate, + int channelMask, + int channelIndexMask, + int audioFormat, + int buffSizeInBytes, + int mode, + int[] sessionId, + long nativeAudioTrack, + boolean offload, + int encapsulationMode, + Object tunerConfiguration) { + // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem. + if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) { + return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED; + } + return AudioTrack.SUCCESS; + } + + @Implementation(minSdk = S, maxSdk = TIRAMISU) + protected int native_setup( + Object /*WeakReference<AudioTrack>*/ audioTrack, + Object /*AudioAttributes*/ attributes, + int[] sampleRate, + int channelMask, + int channelIndexMask, + int audioFormat, + int buffSizeInBytes, + int mode, + int[] sessionId, + long nativeAudioTrack, + boolean offload, + int encapsulationMode, + Object tunerConfiguration, + String opPackageName) { + // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem. + if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) { + return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED; + } + return AudioTrack.SUCCESS; + } + + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + protected int native_setup( + Object /*WeakReference<AudioTrack>*/ audioTrack, + Object /*AudioAttributes*/ attributes, + int[] sampleRate, + int channelMask, + int channelIndexMask, + int audioFormat, + int buffSizeInBytes, + int mode, + int[] sessionId, + @NonNull Parcel attributionSource, + long nativeAudioTrack, + boolean offload, + int encapsulationMode, + Object tunerConfiguration, + @NonNull String opPackageName) { + // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem. + if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) { + return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED; + } + return AudioTrack.SUCCESS; + } + /** - * Always return the number of bytes to write. This method returns immedidately even with {@link - * AudioTrack#WRITE_BLOCKING} + * Returns the number of bytes to write. This method returns immediately even with {@link + * AudioTrack#WRITE_BLOCKING}. If the {@link AudioTrack} instance was created with a non-PCM + * encoding and the encoding can no longer be played directly, the method will return {@link + * AudioTrack#ERROR_DEAD_OBJECT}; */ @Implementation(minSdk = M) protected final int native_write_byte( byte[] audioData, int offsetInBytes, int sizeInBytes, int format, boolean isBlocking) { + int encoding = audioTrack.getAudioFormat(); + // Assume that offload support does not change during the lifetime of the instance. + if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback()) + && !isPcm(encoding) + && !allowedNonPcmEncodings.contains(encoding)) { + return ERROR_DEAD_OBJECT; + } return sizeInBytes; } + @Implementation(minSdk = M) + public void setPlaybackParams(@NonNull PlaybackParams params) { + playbackParams = checkNotNull(params, "Illegal null params"); + } + + @Implementation(minSdk = M) + @NonNull + protected final PlaybackParams getPlaybackParams() { + return playbackParams; + } + /** - * Always return the number of bytes to write except with invalid parameters. Assumes AudioTrack - * is already initialized (object properly created). Do not block even if AudioTrack in offload - * mode is in STOPPING play state. This method returns immediately even with {@link - * AudioTrack#WRITE_BLOCKING} + * Returns the number of bytes to write, except with invalid parameters. If the {@link AudioTrack} + * was created for a non-PCM encoding that can no longer be played directly, it returns {@link + * AudioTrack#ERROR_DEAD_OBJECT}. Assumes {@link AudioTrack} is already initialized (object + * properly created). Do not block even if {@link AudioTrack} in offload mode is in STOPPING play + * state. This method returns immediately even with {@link AudioTrack#WRITE_BLOCKING} */ @Implementation(minSdk = LOLLIPOP) protected int write(@NonNull ByteBuffer audioData, int sizeInBytes, @WriteMode int writeMode) { + int encoding = audioTrack.getAudioFormat(); + // Assume that offload support does not change during the lifetime of the instance. + if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback()) + && !isPcm(encoding) + && !allowedNonPcmEncodings.contains(encoding)) { + return ERROR_DEAD_OBJECT; + } if (writeMode != WRITE_BLOCKING && writeMode != WRITE_NON_BLOCKING) { Log.e(TAG, "ShadowAudioTrack.write() called with invalid blocking mode"); return ERROR_BAD_VALUE; @@ -150,5 +366,103 @@ public class ShadowAudioTrack { @Resetter public static void resetTest() { audioDataWrittenListeners.clear(); + clearDirectPlaybackSupportedFormats(); + clearAllowedNonPcmEncodings(); + } + + private static boolean isPcm(int encoding) { + switch (encoding) { + case AudioFormat.ENCODING_PCM_8BIT: + case AudioFormat.ENCODING_PCM_16BIT: + case AudioFormat.ENCODING_PCM_24BIT_PACKED: + case AudioFormat.ENCODING_PCM_32BIT: + case AudioFormat.ENCODING_PCM_FLOAT: + return true; + default: + return false; + } + } + + /** + * Specific fields from {@link AudioFormat} that are used for detection of direct playback + * support. + * + * @see #native_is_direct_output_supported + */ + private static class AudioFormatInfo { + private final int encoding; + private final int sampleRate; + private final int channelMask; + private final int channelIndexMask; + + public AudioFormatInfo(int encoding, int sampleRate, int channelMask, int channelIndexMask) { + this.encoding = encoding; + this.sampleRate = sampleRate; + this.channelMask = channelMask; + this.channelIndexMask = channelIndexMask; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AudioFormatInfo)) { + return false; + } + + AudioFormatInfo other = (AudioFormatInfo) o; + return encoding == other.encoding + && sampleRate == other.sampleRate + && channelMask == other.channelMask + && channelIndexMask == other.channelIndexMask; + } + + @Override + public int hashCode() { + int result = encoding; + result = 31 * result + sampleRate; + result = 31 * result + channelMask; + result = 31 * result + channelIndexMask; + return result; + } + } + + /** + * Specific fields from {@link AudioAttributes} used for detection of direct playback support. + * + * @see #native_is_direct_output_supported + */ + private static class AudioAttributesInfo { + private final int contentType; + private final int usage; + private final int flags; + + public AudioAttributesInfo(int contentType, int usage, int flags) { + this.contentType = contentType; + this.usage = usage; + this.flags = flags; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AudioAttributesInfo)) { + return false; + } + + AudioAttributesInfo other = (AudioAttributesInfo) o; + return contentType == other.contentType && usage == other.usage && flags == other.flags; + } + + @Override + public int hashCode() { + int result = contentType; + result = 31 * result + usage; + result = 31 * result + flags; + return result; + } } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java index 737df1fb8..7b6ff7e37 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java @@ -18,6 +18,8 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; +import javax.annotation.Nullable; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -40,8 +42,12 @@ public class ShadowBluetoothGatt { private boolean isClosed = false; private byte[] writtenBytes; private byte[] readBytes; + // TODO: ShadowBluetoothGatt.services should be removed in favor of just using the real + // BluetoothGatt.mServices. private final Set<BluetoothGattService> discoverableServices = new HashSet<>(); private final ArrayList<BluetoothGattService> services = new ArrayList<>(); + private final Set<BluetoothGattCharacteristic> characteristicNotificationEnableSet = + new HashSet<>(); @RealObject private BluetoothGatt realBluetoothGatt; @ReflectorObject protected BluetoothGattReflector bluetoothGattReflector; @@ -185,6 +191,7 @@ public class ShadowBluetoothGatt { protected boolean discoverServices() { this.services.clear(); if (!this.discoverableServices.isEmpty()) { + // TODO: Don't store the services in the shadow. this.services.addAll(this.discoverableServices); if (this.getGattCallback() != null) { @@ -204,10 +211,39 @@ public class ShadowBluetoothGatt { */ @Implementation(minSdk = O) protected List<BluetoothGattService> getServices() { + // TODO: Remove this method when real BluetoothGatt#getServices() works. return new ArrayList<>(this.services); } /** + * Overrides {@link BluetoothGatt#getService} to return a service with given UUID. + * + * @return a service with given UUID that have been discovered through {@link + * ShadowBluetoothGatt#discoverServices}. + */ + @Implementation(minSdk = O) + @Nullable + protected BluetoothGattService getService(UUID uuid) { + // TODO: Remove this method when real BluetoothGatt#getService() works. + for (BluetoothGattService service : this.services) { + if (service.getUuid().equals(uuid)) { + return service; + } + } + return null; + } + + /** + * Overrides {@link BluetoothGatt#setCharacteristicNotification} so it returns true (false) if + * allowCharacteristicNotification (disallowCharacteristicNotification) is called. + */ + @Implementation(minSdk = O) + protected boolean setCharacteristicNotification( + BluetoothGattCharacteristic characteristic, boolean enable) { + return characteristicNotificationEnableSet.contains(characteristic) == enable; + } + + /** * Reads bytes from incoming characteristic if properties are valid and callback is set. Callback * responds with {@link BluetoothGattCallback#onCharacteristicWrite} and returns true when * successful. @@ -258,6 +294,16 @@ public class ShadowBluetoothGatt { return true; } + /** Allows the incoming characteristic to be set to enable notification. */ + public void allowCharacteristicNotification(BluetoothGattCharacteristic characteristic) { + characteristicNotificationEnableSet.add(characteristic); + } + + /** Disallows the incoming characteristic to be set to enable notification. */ + public void disallowCharacteristicNotification(BluetoothGattCharacteristic characteristic) { + characteristicNotificationEnableSet.remove(characteristic); + } + public void addDiscoverableService(BluetoothGattService service) { this.discoverableServices.add(service); } @@ -294,6 +340,49 @@ public class ShadowBluetoothGatt { return this.readBytes; } + public BluetoothConnectionManager getBluetoothConnectionManager() { + return BluetoothConnectionManager.getInstance(); + } + + /** + * Simulate a successful Gatt Client Conection with {@link BluetoothConnectionManager}. Performs a + * {@link BluetoothGattCallback#onConnectionStateChange} if available. + * + * @param remoteAddress address of Gatt client + */ + public void notifyConnection(String remoteAddress) { + BluetoothConnectionManager.getInstance().registerGattClientConnection(remoteAddress); + this.isConnected = true; + if (this.isCallbackAppropriate()) { + this.getGattCallback() + .onConnectionStateChange( + this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED); + } + } + + /** + * Simulate a successful Gatt Client Disconnection with {@link BluetoothConnectionManager}. + * Performs a {@link BluetoothGattCallback#onConnectionStateChange} if available. + * + * @param remoteAddress address of Gatt client + */ + public void notifyDisconnection(String remoteAddress) { + BluetoothConnectionManager.getInstance().unregisterGattClientConnection(remoteAddress); + if (this.isCallbackAppropriate()) { + this.getGattCallback() + .onConnectionStateChange( + this.realBluetoothGatt, + BluetoothGatt.GATT_SUCCESS, + BluetoothProfile.STATE_DISCONNECTED); + } + this.isConnected = false; + } + + private boolean isCallbackAppropriate() { + return this.getGattCallback() != null && this.isConnected; + } + + @ForType(BluetoothGatt.class) private interface BluetoothGattReflector { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java index 54b96265b..f13928859 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java @@ -3,13 +3,17 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.S; +import static java.util.stream.Collectors.toCollection; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; import android.content.Intent; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @@ -21,7 +25,8 @@ import org.robolectric.annotation.Implements; @NotThreadSafe @Implements(value = BluetoothHeadset.class) public class ShadowBluetoothHeadset { - private final List<BluetoothDevice> connectedDevices = new ArrayList<>(); + + private final Map<BluetoothDevice, Integer> bluetoothDevices = new HashMap<>(); private boolean allowsSendVendorSpecificResultCode = true; private BluetoothDevice activeBluetoothDevice; private boolean isVoiceRecognitionSupported = true; @@ -32,12 +37,29 @@ public class ShadowBluetoothHeadset { */ @Implementation protected List<BluetoothDevice> getConnectedDevices() { - return connectedDevices; + return bluetoothDevices.entrySet().stream() + .filter(entry -> entry.getValue() == BluetoothProfile.STATE_CONNECTED) + .map(Entry::getKey) + .collect(toCollection(ArrayList::new)); } /** Adds the given BluetoothDevice to the shadow's list of "connected devices" */ public void addConnectedDevice(BluetoothDevice device) { - connectedDevices.add(device); + addDevice(device, BluetoothProfile.STATE_CONNECTED); + } + + /** + * Adds the provided BluetoothDevice to the shadow profile's device list with an associated + * connectionState. The provided connection state will be returned by {@link + * ShadowBluetoothHeadset#getConnectionState}. + */ + public void addDevice(BluetoothDevice bluetoothDevice, int connectionState) { + bluetoothDevices.put(bluetoothDevice, connectionState); + } + + /** Remove the given BluetoothDevice from the shadow profile's device list */ + public void removeDevice(BluetoothDevice bluetoothDevice) { + bluetoothDevices.remove(bluetoothDevice); } /** @@ -49,9 +71,7 @@ public class ShadowBluetoothHeadset { */ @Implementation protected int getConnectionState(BluetoothDevice device) { - return connectedDevices.contains(device) - ? BluetoothProfile.STATE_CONNECTED - : BluetoothProfile.STATE_DISCONNECTED; + return bluetoothDevices.getOrDefault(device, BluetoothProfile.STATE_DISCONNECTED); } /** @@ -63,7 +83,7 @@ public class ShadowBluetoothHeadset { */ @Implementation protected boolean startVoiceRecognition(BluetoothDevice bluetoothDevice) { - if (bluetoothDevice == null || !connectedDevices.contains(bluetoothDevice)) { + if (bluetoothDevice == null || !getConnectedDevices().contains(bluetoothDevice)) { return false; } if (activeBluetoothDevice != null) { @@ -113,7 +133,7 @@ public class ShadowBluetoothHeadset { if (command == null) { throw new IllegalArgumentException("Command cannot be null"); } - return allowsSendVendorSpecificResultCode && connectedDevices.contains(device); + return allowsSendVendorSpecificResultCode && getConnectedDevices().contains(device); } @Nullable diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java index b0cc137fe..c1c887acc 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java @@ -23,6 +23,12 @@ public class ShadowBuild { private static String serialOverride = Build.UNKNOWN; /** + * Temporary constant for VERSION_CODES.UPSIDE_DOWN_CAKE. Will be removed and replaced once the + * constant is available upstream. + */ + public static final int UPSIDE_DOWN_CAKE = 34; + + /** * Sets the value of the {@link Build#DEVICE} field. * * <p>It will be reset for the next test. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java index 55b9b68c6..19be93acc 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java @@ -77,7 +77,18 @@ public class ShadowCameraManager { cameraTorches.put(cameraId, enabled); } - @Implementation(minSdk = Build.VERSION_CODES.S) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + protected CameraDevice openCameraDeviceUserAsync( + String cameraId, + CameraDevice.StateCallback callback, + Executor executor, + final int uid, + final int oomScoreOffset, + boolean overrideToPortrait) { + return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset); + } + + @Implementation(minSdk = Build.VERSION_CODES.S, maxSdk = Build.VERSION_CODES.TIRAMISU) protected CameraDevice openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java index cf1aac20e..a4b1fb79f 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java @@ -54,13 +54,13 @@ public abstract class ShadowChoreographer { * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode. */ public static void setFrameDelay(Duration delay) { - checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED); + checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY); frameDelay = delay; } /** See {@link #setFrameDelay(Duration)}. */ public static Duration getFrameDelay() { - checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED); + checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY); return frameDelay; } @@ -72,13 +72,13 @@ public abstract class ShadowChoreographer { * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode. */ public static void setPaused(boolean paused) { - checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED); + checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY); isPaused = paused; } /** See {@link #setPaused(boolean)}. */ public static boolean isPaused() { - checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED); + checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY); return isPaused; } @@ -109,11 +109,11 @@ public abstract class ShadowChoreographer { */ @Deprecated public static void setPostFrameCallbackDelay(int delayMillis) { - if (looperMode() == Mode.PAUSED) { + if (looperMode() == Mode.LEGACY) { + ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis); + } else { setPaused(delayMillis != 0); setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis)); - } else { - ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java index 933024646..75566ce68 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java @@ -12,6 +12,7 @@ import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S; import static android.os.Build.VERSION_CODES.TIRAMISU; +import static org.robolectric.util.reflector.Reflector.reflector; import android.os.MessageQueue; import android.os.SystemClock; @@ -29,9 +30,8 @@ import org.robolectric.annotation.RealObject; import org.robolectric.annotation.ReflectorObject; import org.robolectric.res.android.NativeObjRegistry; import org.robolectric.shadow.api.Shadow; -import org.robolectric.util.ReflectionHelpers; -import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Constructor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.WithType; @@ -86,7 +86,7 @@ public class ShadowDisplayEventReceiver { new NativeDisplayEventReceiver(new WeakReference<>((DisplayEventReceiver) receiver))); } - @Implementation(minSdk = R) + @Implementation(minSdk = R, maxSdk = TIRAMISU) protected static long nativeInit( WeakReference<DisplayEventReceiver> receiver, MessageQueue msgQueue, @@ -95,11 +95,21 @@ public class ShadowDisplayEventReceiver { return nativeInit(receiver, msgQueue); } - @Implementation(minSdk = KITKAT_WATCH) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + protected static long nativeInit( + WeakReference<DisplayEventReceiver> receiver, + WeakReference<Object> vsyncEventData, + MessageQueue msgQueue, + int vsyncSource, + int eventRegistration, + long layerHandle) { + return nativeInit(receiver, msgQueue); + } + + @Implementation(minSdk = KITKAT_WATCH, maxSdk = TIRAMISU) protected static void nativeDispose(long receiverPtr) { NativeDisplayEventReceiver receiver = nativeObjRegistry.unregister(receiverPtr); if (receiver != null) { - receiver.dispose(); } } @@ -141,24 +151,11 @@ public class ShadowDisplayEventReceiver { displayEventReceiverReflector.onVsync( ShadowSystem.nanoTime(), 0L, /* SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN */ 1); } else if (RuntimeEnvironment.getApiLevel() < TIRAMISU) { - try { - // onVsync takes a package-private VSyncData class as a parameter, thus reflection - // needs to be used - Object vsyncData = - ReflectionHelpers.callConstructor( - Class.forName("android.view.DisplayEventReceiver$VsyncEventData"), - ClassParameter.from(long.class, 1), /* id */ - ClassParameter.from(long.class, 10), /* frameDeadline */ - ClassParameter.from(long.class, 1)); /* frameInterval */ - - displayEventReceiverReflector.onVsync( - ShadowSystem.nanoTime(), - 0L, /* physicalDisplayId currently ignored */ - /* frame= */ 1, - vsyncData /* VsyncEventData */); - } catch (ClassNotFoundException e) { - throw new LinkageError("Unable to construct VsyncEventData", e); - } + displayEventReceiverReflector.onVsync( + ShadowSystem.nanoTime(), + 0L, /* physicalDisplayId currently ignored */ + /* frame= */ 1, + newVsyncEventData() /* VsyncEventData */); } else { displayEventReceiverReflector.onVsync( ShadowSystem.nanoTime(), @@ -240,6 +237,11 @@ public class ShadowDisplayEventReceiver { } private static Object /* VsyncEventData */ newVsyncEventData() { + VsyncEventDataReflector vsyncEventDataReflector = reflector(VsyncEventDataReflector.class); + if (RuntimeEnvironment.getApiLevel() < TIRAMISU) { + return vsyncEventDataReflector.newVsyncEventData( + /* id= */ 1, /* frameDeadline= */ 10, /* frameInterval= */ 1); + } try { // onVsync on T takes a package-private VsyncEventData class, which is itself composed of a // package private VsyncEventData.FrameTimeline class. So use reflection to build these up @@ -247,33 +249,26 @@ public class ShadowDisplayEventReceiver { Class.forName("android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline"); int timelineArrayLength = RuntimeEnvironment.getApiLevel() == TIRAMISU ? 1 : 7; - + FrameTimelineReflector frameTimelineReflector = reflector(FrameTimelineReflector.class); Object timelineArray = Array.newInstance(frameTimelineClass, timelineArrayLength); for (int i = 0; i < timelineArrayLength; i++) { - Array.set(timelineArray, i, newFrameTimeline(frameTimelineClass)); + Array.set(timelineArray, i, frameTimelineReflector.newFrameTimeline(1, 1, 10)); + } + if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) { + return vsyncEventDataReflector.newVsyncEventData( + timelineArray, /* preferredFrameTimelineIndex= */ 0, /* frameInterval= */ 1); + } else { + return vsyncEventDataReflector.newVsyncEventData( + timelineArray, + /* preferredFrameTimelineIndex= */ 0, + timelineArrayLength, + /* frameInterval= */ 1); } - - // get FrameTimeline[].class - Class<?> frameTimeLineArrayClass = - Class.forName("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;"); - return ReflectionHelpers.callConstructor( - Class.forName("android.view.DisplayEventReceiver$VsyncEventData"), - ClassParameter.from(frameTimeLineArrayClass, timelineArray), - ClassParameter.from(int.class, 0), /* frameDeadline */ - ClassParameter.from(long.class, 1)); /* frameInterval */ } catch (ClassNotFoundException e) { throw new LinkageError("Unable to construct VsyncEventData", e); } } - private static Object newFrameTimeline(Class<?> frameTimelineClass) { - return ReflectionHelpers.callConstructor( - frameTimelineClass, - ClassParameter.from(long.class, 1) /* vsync id */, - ClassParameter.from(long.class, 1) /* expectedPresentTime */, - ClassParameter.from(long.class, 10) /* deadline */); - } - /** Reflector interface for {@link DisplayEventReceiver}'s internals. */ @ForType(DisplayEventReceiver.class) protected interface DisplayEventReceiverReflector { @@ -295,5 +290,35 @@ public class ShadowDisplayEventReceiver { @Accessor("mCloseGuard") CloseGuard getCloseGuard(); + + @Accessor("mReceiverPtr") + long getReceiverPtr(); + } + + @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData") + interface VsyncEventDataReflector { + @Constructor + Object newVsyncEventData(long id, long frameDeadline, long frameInterval); + + @Constructor + Object newVsyncEventData( + @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;") + Object frameTimelineArray, + int preferredFrameTimelineIndex, + long frameInterval); + + @Constructor + Object newVsyncEventData( + @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;") + Object frameTimelineArray, + int preferredFrameTimelineIndex, + int timelineArrayLength, + long frameInterval); + } + + @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline") + interface FrameTimelineReflector { + @Constructor + Object newFrameTimeline(long id, long expectedPresentTime, long deadline); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java index 9f7957303..1aa08b09a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java @@ -1,6 +1,5 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.O_MR1; import static android.os.Build.VERSION_CODES.P; @@ -28,7 +27,6 @@ import java.util.List; import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.Nullable; -import org.robolectric.RuntimeEnvironment; import org.robolectric.android.Bootstrap; import org.robolectric.annotation.HiddenApi; import org.robolectric.annotation.Implementation; @@ -88,20 +86,26 @@ public class ShadowDisplayManagerGlobal { reflector(DisplayManagerGlobalReflector.class, instance); displayManagerGlobal.setDm(displayManager); displayManagerGlobal.setLock(new Object()); + List<Handler> displayListeners = createDisplayListeners(); + displayManagerGlobal.setDisplayListeners(displayListeners); + displayManagerGlobal.setDisplayInfoCache(new SparseArray<>()); + return instance; + } - List displayListeners = new CopyOnWriteArrayList(); + private static List<Handler> createDisplayListeners() { try { - // TODO: rexhoffman when we have sufficient detection in android dev replace - // this with a version check. + // The type for mDisplayListeners was changed from ArrayList to CopyOnWriteArrayList + // in some branches of T and U, so we need to reflect on DisplayManagerGlobal class + // to check the type of mDisplayListeners member before initializing appropriately. Field f = DisplayManagerGlobal.class.getDeclaredField("mDisplayListeners"); if (f.getType().isAssignableFrom(ArrayList.class)) { - displayListeners = new ArrayList(); + return new ArrayList<>(); + } else { + return new CopyOnWriteArrayList<>(); } } catch (NoSuchFieldException e) { + throw new RuntimeException(e); } - displayManagerGlobal.setDisplayListeners(displayListeners); - displayManagerGlobal.setDisplayInfoCache(new SparseArray<>()); - return instance; } @VisibleForTesting diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java index 791ece2ec..58cd55818 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java @@ -10,6 +10,7 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ColorSpace; +import android.graphics.ColorSpace.Named; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.DecodeException; import android.graphics.ImageDecoder.Source; @@ -247,14 +248,16 @@ public class ShadowImageDecoder { static String ImageDecoder_nGetMimeType(long nativePtr) { CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr); // return encodedFormatToString(decoder.mCodec.getEncodedFormat()); - throw new UnsupportedOperationException(); + // TODO: fix this properly. Just hardcode to png for now or just remove GraphicsMode.LEGACY + return "image/png"; } static ColorSpace ImageDecoder_nGetColorSpace(long nativePtr) { // auto colorType = codec.computeOutputColorType(codec.getInfo().colorType()); // sk_sp<SkColorSpace> colorSpace = codec.computeOutputColorSpace(colorType); // return GraphicsJNI.getColorSpace(colorSpace, colorType); - throw new UnsupportedOperationException(); + // TODO: fix this properly. Just hardcode to SRGB for now or just remove GraphicsMode.LEGACY + return ColorSpace.get(Named.SRGB); } // native method implementations... diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java index 0654fbc4f..b8564ca40 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java @@ -1,6 +1,5 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.S_V2; import static android.os.Build.VERSION_CODES.TIRAMISU; @@ -70,7 +69,7 @@ public class ShadowImageReader { return nativeImageSetup(image); } - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected int nativeImageSetup(Object /* Image */ image) { return nativeImageSetup((Image) image); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java index 50635c8b2..298fabec6 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java @@ -1,10 +1,14 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.robolectric.util.reflector.Reflector.reflector; import android.hardware.input.InputManager; +import android.util.SparseArray; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyEvent; @@ -13,13 +17,18 @@ import android.view.VerifiedKeyEvent; import android.view.VerifiedMotionEvent; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; /** Shadow for {@link InputManager} */ @Implements(value = InputManager.class, looseSignatures = true) public class ShadowInputManager { + @RealObject InputManager realInputManager; + @Implementation protected boolean injectInputEvent(InputEvent event, int mode) { // ignore @@ -37,6 +46,35 @@ public class ShadowInputManager { return new int[0]; } + @Implementation(maxSdk = TIRAMISU) + protected void populateInputDevicesLocked() throws ClassNotFoundException { + if (ReflectionHelpers.getField(realInputManager, "mInputDevicesChangedListener") == null) { + ReflectionHelpers.setField( + realInputManager, + "mInputDevicesChangedListener", + ReflectionHelpers.callConstructor( + Class.forName("android.hardware.input.InputManager$InputDevicesChangedListener"))); + } + + if (getInputDevices() == null) { + final int[] ids = realInputManager.getInputDeviceIds(); + + SparseArray<InputDevice> inputDevices = new SparseArray<>(); + for (int i = 0; i < ids.length; i++) { + inputDevices.put(ids[i], null); + } + setInputDevices(inputDevices); + } + } + + private SparseArray<InputDevice> getInputDevices() { + return reflector(InputManagerReflector.class, realInputManager).getInputDevices(); + } + + private void setInputDevices(SparseArray<InputDevice> devices) { + reflector(InputManagerReflector.class, realInputManager).setInputDevices(devices); + } + /** * Provides a local java implementation, since the real implementation is in system server + * native code. @@ -78,6 +116,17 @@ public class ShadowInputManager { @Resetter public static void reset() { - ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null); + if (SDK_INT < ShadowBuild.UPSIDE_DOWN_CAKE) { + ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null); + } + } + + @ForType(InputManager.class) + interface InputManagerReflector { + @Accessor("mInputDevices") + SparseArray<InputDevice> getInputDevices(); + + @Accessor("mInputDevices") + void setInputDevices(SparseArray<InputDevice> devices); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java index 42baa4c1f..bf87b3a5b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java @@ -2,6 +2,7 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.LOLLIPOP; +import android.app.Notification; import android.app.job.JobParameters; import android.app.job.JobService; import org.robolectric.annotation.Implementation; @@ -19,6 +20,14 @@ public class ShadowJobService extends ShadowService { this.isRescheduleNeeded = needsReschedule; } + /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */ + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + protected void setNotification( + JobParameters params, + int notificationId, + Notification notification, + int jobEndNotificationPolicy) {} + /** * Returns whether the job has finished running. When using this shadow this returns true after * {@link #jobFinished(JobParameters, boolean)} is called. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java index 4b28bafa2..e06f348bc 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java @@ -49,6 +49,7 @@ import org.robolectric.util.reflector.ForType; public class ShadowLauncherApps { private List<ShortcutInfo> shortcuts = new ArrayList<>(); private final Multimap<UserHandle, String> enabledPackages = HashMultimap.create(); + private final Multimap<UserHandle, ComponentName> enabledActivities = HashMultimap.create(); private final Multimap<UserHandle, LauncherActivityInfo> shortcutActivityList = HashMultimap.create(); private final Multimap<UserHandle, LauncherActivityInfo> activityList = HashMultimap.create(); @@ -100,6 +101,17 @@ public class ShadowLauncherApps { } /** + * Sets an activity referenced by ComponentName as enabled, to be checked by {@link + * #isActivityEnabled(ComponentName, UserHandle)}. + * + * @param userHandle the user handle to be set. + * @param componentName the component name of the activity to be enabled. + */ + public void setActivityEnabled(UserHandle userHandle, ComponentName componentName) { + enabledActivities.put(userHandle, componentName); + } + + /** * Adds a {@link LauncherActivityInfo} to be retrieved by {@link * #getShortcutConfigActivityList(String, UserHandle)}. * @@ -219,10 +231,9 @@ public class ShadowLauncherApps { "This method is not currently supported in Robolectric."); } - @Implementation + @Implementation(minSdk = L) protected boolean isActivityEnabled(ComponentName component, UserHandle user) { - throw new UnsupportedOperationException( - "This method is not currently supported in Robolectric."); + return enabledActivities.containsEntry(user, component); } /** diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java index f624b60d5..2fb348ebd 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java @@ -58,7 +58,7 @@ public class ShadowLegacyLooper extends ShadowLooper { @Resetter public static synchronized void resetThreadLoopers() { // do not use looperMode() here, because its cached value might already have been reset - if (ConfigurationRegistry.get(LooperMode.Mode.class) == LooperMode.Mode.PAUSED) { + if (ConfigurationRegistry.get(LooperMode.Mode.class) != LooperMode.Mode.LEGACY) { // ignore if realistic looper return; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java index 2a3a61b10..f954ac234 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java @@ -1,20 +1,38 @@ package org.robolectric.shadows; +import static org.robolectric.shadow.api.Shadow.newInstanceOf; +import static org.robolectric.util.reflector.Reflector.reflector; + import android.app.Application; import android.app.LoadedApk; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.os.Build.VERSION_CODES; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; @Implements(value = LoadedApk.class, isInAndroidSdk = false) public class ShadowLoadedApk { + @RealObject private LoadedApk realLoadedApk; + private boolean isClassLoaderInitialized = false; + private final Object classLoaderLock = new Object(); @Implementation public ClassLoader getClassLoader() { + // The AppComponentFactory was introduced from SDK 28. + if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) { + synchronized (classLoaderLock) { + if (!isClassLoaderInitialized) { + isClassLoaderInitialized = true; + tryInitAppComponentFactory(realLoadedApk); + } + } + } return this.getClass().getClassLoader(); } @@ -23,6 +41,35 @@ public class ShadowLoadedApk { return this.getClass().getClassLoader(); } + private void tryInitAppComponentFactory(LoadedApk realLoadedApk) { + if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) { + ApplicationInfo applicationInfo = realLoadedApk.getApplicationInfo(); + if (applicationInfo == null || applicationInfo.appComponentFactory == null) { + return; + } + _LoadedApk_ loadedApkReflector = reflector(_LoadedApk_.class, realLoadedApk); + if (!loadedApkReflector.getIncludeCode()) { + return; + } + String fullQualifiedClassName = + calculateFullQualifiedClassName( + applicationInfo.appComponentFactory, applicationInfo.packageName); + android.app.AppComponentFactory factory = + (android.app.AppComponentFactory) newInstanceOf(fullQualifiedClassName); + if (factory == null) { + factory = new android.app.AppComponentFactory(); + } + loadedApkReflector.setAppFactory(factory); + } + } + + private String calculateFullQualifiedClassName(String className, String packageName) { + if (packageName == null) { + return className; + } + return className.startsWith(".") ? packageName + className : className; + } + /** Accessor interface for {@link LoadedApk}'s internals. */ @ForType(LoadedApk.class) public interface _LoadedApk_ { @@ -32,5 +79,11 @@ public class ShadowLoadedApk { @Accessor("mResources") void setResources(Resources resources); + + @Accessor("mIncludeCode") + boolean getIncludeCode(); + + @Accessor("mAppComponentFactory") + void setAppFactory(Object appFactory); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java index 103907b93..9bef2d193 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java @@ -1,6 +1,5 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.N_MR1; @@ -409,7 +408,7 @@ public class ShadowMediaCodec { @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {} - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected void invalidateByteBufferLocked( @Nullable ByteBuffer[] buffers, int index, boolean input) {} @@ -417,14 +416,14 @@ public class ShadowMediaCodec { @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {} - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected void validateInputByteBufferLocked(@Nullable ByteBuffer[] buffers, int index) {} /** Prevents calling Android-only methods on basic ByteBuffer objects. */ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {} - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {} /** @@ -442,7 +441,7 @@ public class ShadowMediaCodec { } } - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected void validateOutputByteBufferLocked( @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) { validateOutputByteBuffer(buffers, index, info); @@ -452,14 +451,14 @@ public class ShadowMediaCodec { @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {} - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected void invalidateByteBuffersLocked(@Nullable ByteBuffer[] buffers) {} /** Prevents attempting to free non-direct ByteBuffer objects. */ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void freeByteBuffer(@Nullable ByteBuffer buffer) {} - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected void freeByteBufferLocked(@Nullable ByteBuffer buffer) {} /** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */ diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java index 587356009..f08a53c3f 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java @@ -37,6 +37,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Random; import java.util.TreeMap; import org.robolectric.annotation.Implementation; @@ -112,8 +113,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase { private static final Map<DataSource, Exception> exceptions = new HashMap<>(); private static final Map<DataSource, MediaInfo> mediaInfoMap = new HashMap<>(); - private static final MediaInfoProvider DEFAULT_MEDIA_INFO_PROVIDER = mediaInfoMap::get; - private static MediaInfoProvider mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER; + private static Optional<MediaInfoProvider> mediaInfoProvider = Optional.empty(); @RealObject private MediaPlayer player; @@ -650,7 +650,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase { * @see #setDataSource(DataSource) */ public void doSetDataSource(DataSource dataSource) { - MediaInfo mediaInfo = mediaInfoProvider.get(dataSource); + MediaInfo mediaInfo = getMediaInfo(dataSource); if (mediaInfo == null) { throw new IllegalArgumentException( "Don't know what to do with dataSource " @@ -663,17 +663,16 @@ public class ShadowMediaPlayer extends ShadowPlayerBase { } public static MediaInfo getMediaInfo(DataSource dataSource) { - return mediaInfoProvider.get(dataSource); + if (mediaInfoMap.containsKey(dataSource)) { + return mediaInfoMap.get(dataSource); + } + return mediaInfoProvider.map(provider -> provider.get(dataSource)).orElse(null); } /** * Adds a {@link MediaInfo} for a {@link DataSource}. - * - * <p>This overrides any {@link MediaInfoProvider} previously set by calling {@link - * #setMediaInfoProvider}, i.e., the provider will not be used for any {@link DataSource}. */ public static void addMediaInfo(DataSource dataSource, MediaInfo info) { - ShadowMediaPlayer.mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER; mediaInfoMap.put(dataSource, info); } @@ -685,7 +684,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase { * {@link MediaInfo} provided by this {@link MediaInfoProvider} will be used instead. */ public static void setMediaInfoProvider(MediaInfoProvider mediaInfoProvider) { - ShadowMediaPlayer.mediaInfoProvider = mediaInfoProvider; + ShadowMediaPlayer.mediaInfoProvider = Optional.of(mediaInfoProvider); } public static void addException(DataSource dataSource, RuntimeException e) { @@ -1536,7 +1535,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase { @Resetter public static void resetStaticState() { createListener = null; - mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER; + mediaInfoProvider = Optional.empty(); exceptions.clear(); mediaInfoMap.clear(); DataSource.reset(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java index 8b7b8fa86..7b045e836 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java @@ -1,6 +1,5 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.S; import static android.os.Build.VERSION_CODES.TIRAMISU; @@ -64,6 +63,12 @@ public class ShadowNativeFontsFontFamily { return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback); } + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + protected static long nBuild( + long builderPtr, String langTags, int variant, boolean isCustomFallback, boolean isDefaultFallback) { + return nBuild(builderPtr, langTags, variant, isCustomFallback); + } + @Implementation protected static long nGetReleaseNativeFamily() { return FontFamilyBuilderNatives.nGetReleaseNativeFamily(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java index 94fadb5ab..32d428088 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java @@ -813,7 +813,7 @@ public class ShadowNativePaint { paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics); } - @Implementation(minSdk = 10000) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected static float nGetRunCharacterAdvance( long paintPtr, char[] text, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java index 0f9e44d17..4cdfb4532 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java @@ -221,7 +221,7 @@ public class ShadowNfcAdapter { } if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { nfcAdapterReflector.setHasNfcFeature(false); - if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.CUR_DEVELOPMENT) { + if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.TIRAMISU) { nfcAdapterReflector.setHasBeamFeature(false); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java index 8fec6464a..16a7398f7 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java @@ -112,6 +112,15 @@ public class ShadowPaint { } @Implementation + protected void setStrikeThruText(boolean strikeThruText) { + if (strikeThruText) { + setFlags(flags | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + setFlags(flags & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } + + @Implementation protected Shader setShader(Shader shader) { this.shader = shader; return shader; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java index 50f8adf62..ee3bef016 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java @@ -12,6 +12,7 @@ import android.os.Message; import android.os.MessageQueue.IdleHandler; import android.os.SystemClock; import android.util.Log; +import com.google.common.base.Preconditions; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -58,6 +59,8 @@ public final class ShadowPausedLooper extends ShadowLooper { private static Set<Looper> loopingLoopers = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<Looper, Boolean>())); + private static boolean ignoreUncaughtExceptions = false; + @RealObject private Looper realLooper; private boolean isPaused = false; // the Executor that executes looper messages. Must be written to on looper thread @@ -317,6 +320,51 @@ public final class ShadowPausedLooper extends ShadowLooper { } /** + * By default Robolectric will put Loopers that throw uncaught exceptions in their loop method + * into an error state, where any future posting to the looper's queue will throw an error. + * + * <p>This API allows you to disable this behavior. Note this is a permanent setting - it is not + * reset between tests. + * + * @deprecated this method only exists to accommodate legacy tests with preexisting issues. + * Silently discarding exceptions is not recommended, and can lead to deadlocks. + */ + @Deprecated + public static void setIgnoreUncaughtExceptions(boolean shouldIgnore) { + ignoreUncaughtExceptions = shouldIgnore; + } + + /** + * Shadow loop to handle uncaught exceptions. Without this logic an uncaught exception on a looper + * thread will cause idle() to deadlock. + */ + @Implementation + protected static void loop() { + try { + reflector(LooperReflector.class).loop(); + } catch (Exception e) { + Looper realLooper = Preconditions.checkNotNull(Looper.myLooper()); + ShadowPausedMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue()); + + if (ignoreUncaughtExceptions) { + // ignore + } else { + shadowQueue.setUncaughtException(e); + // release any ControlRunnables currently in queue to prevent deadlocks + shadowQueue.drainQueue( + input -> { + if (input instanceof ControlRunnable) { + ((ControlRunnable) input).runLatch.countDown(); + return true; + } + return false; + }); + } + throw e; + } + } + + /** * If the given {@code lastMessageRead} is not null and the queue is now idle, get the idle * handlers and run them. This synchronization mirrors what happens in the real message queue * next() method, but does not block after running the idle handlers. @@ -345,21 +393,40 @@ public final class ShadowPausedLooper extends ShadowLooper { private abstract static class ControlRunnable implements Runnable { protected final CountDownLatch runLatch = new CountDownLatch(1); + private volatile RuntimeException exception; - public void waitTillComplete() { + @Override + public void run() { + try { + doRun(); + } catch (RuntimeException e) { + if (!ignoreUncaughtExceptions) { + exception = e; + } + throw e; + } finally { + runLatch.countDown(); + } + } + + protected abstract void doRun() throws RuntimeException; + + public void waitTillComplete() throws RuntimeException { try { runLatch.await(); } catch (InterruptedException e) { Log.w("ShadowPausedLooper", "wait till idle interrupted"); } + if (exception != null) { + throw exception; + } } } private class IdlingRunnable extends ControlRunnable { @Override - public void run() { - try { + public void doRun() { while (true) { Message msg = getNextExecutableMessage(); if (msg == null) { @@ -369,26 +436,20 @@ public final class ShadowPausedLooper extends ShadowLooper { shadowMsg(msg).recycleUnchecked(); triggerIdleHandlersIfNeeded(msg); } - } finally { - runLatch.countDown(); - } } } private class RunOneRunnable extends ControlRunnable { @Override - public void run() { - try { + public void doRun() { + Message msg = shadowQueue().getNextIgnoringWhen(); if (msg != null) { SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen()); msg.getTarget().dispatchMessage(msg); triggerIdleHandlersIfNeeded(msg); } - } finally { - runLatch.countDown(); - } } } @@ -408,6 +469,8 @@ public final class ShadowPausedLooper extends ShadowLooper { } looperExecutor.execute(runnable); runnable.waitTillComplete(); + // throw immediately if looper died while executing tasks + shadowQueue().checkQueueState(); } } @@ -422,6 +485,7 @@ public final class ShadowPausedLooper extends ShadowLooper { @Override public void execute(Runnable runnable) { + shadowQueue().checkQueueState(); executionQueue.add(runnable); } @@ -435,18 +499,22 @@ public final class ShadowPausedLooper extends ShadowLooper { Runnable runnable = executionQueue.take(); runnable.run(); } catch (InterruptedException e) { - // ignore + // ignored } } } + + @Override + protected void doRun() throws RuntimeException { + throw new UnsupportedOperationException(); + } } private class UnPauseRunnable extends ControlRunnable { @Override - public void run() { + public void doRun() { setLooperExecutor(new HandlerExecutor(new Handler(realLooper))); isPaused = false; - runLatch.countDown(); } } @@ -478,5 +546,8 @@ public final class ShadowPausedLooper extends ShadowLooper { @Direct void quitSafely(); + + @Direct + void loop(); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java index 162330aad..5caf01642 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java @@ -16,6 +16,9 @@ import android.os.Message; import android.os.MessageQueue; import android.os.MessageQueue.IdleHandler; import android.os.SystemClock; +import android.util.Log; +import androidx.annotation.VisibleForTesting; +import com.google.common.base.Predicate; import java.time.Duration; import java.util.ArrayList; import org.robolectric.RuntimeEnvironment; @@ -47,6 +50,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { new NativeObjRegistry<ShadowPausedMessageQueue>(ShadowPausedMessageQueue.class); private boolean isPolling = false; private ShadowPausedSystemClock.Listener clockListener; + private Exception uncaughtException = null; // shadow constructor instead of nativeInit because nativeInit signature has changed across SDK // versions @@ -55,7 +59,16 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { invokeConstructor(MessageQueue.class, realQueue, from(boolean.class, quitAllowed)); int ptr = (int) nativeQueueRegistry.register(this); reflector(MessageQueueReflector.class, realQueue).setPtr(ptr); - clockListener = () -> nativeWake(ptr); + clockListener = + () -> { + synchronized (realQueue) { + // only wake up the Looper thread if queue is non empty to reduce contention if many + // Looper threads are active + if (getMessages() != null) { + nativeWake(ptr); + } + } + }; ShadowPausedSystemClock.addStaticListener(clockListener); } @@ -210,8 +223,28 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { return reflector(MessageQueueReflector.class, realQueue).getQuitAllowed(); } + @VisibleForTesting void doEnqueueMessage(Message msg, long when) { - reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when); + enqueueMessage(msg, when); + } + + @Implementation + protected boolean enqueueMessage(Message msg, long when) { + synchronized (realQueue) { + if (uncaughtException != null) { + // looper thread has died + IllegalStateException e = + new IllegalStateException( + msg.getTarget() + + " sending message to a Looper thread that has died due to an uncaught" + + " exception", + uncaughtException); + Log.w("ShadowPausedMessageQueue", e); + msg.recycle(); + throw e; + } + return reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when); + } } Message getMessages() { @@ -283,7 +316,9 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { return Duration.ZERO; } while (next != null) { - when = shadowOfMsg(next).getWhen(); + if (next.getTarget() != null) { + when = shadowOfMsg(next).getWhen(); + } next = shadowOfMsg(next).internalGetNext(); } } @@ -309,7 +344,9 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { synchronized (realQueue) { Message next = getMessages(); while (next != null) { - count++; + if (next.getTarget() != null) { + count++; + } next = shadowOfMsg(next).internalGetNext(); } } @@ -323,12 +360,24 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { */ Message getNextIgnoringWhen() { synchronized (realQueue) { - Message head = getMessages(); - if (head != null) { - Message next = shadowOfMsg(head).internalGetNext(); - reflector(MessageQueueReflector.class, realQueue).setMessages(next); + Message prev = null; + Message msg = getMessages(); + // Head is blocked on synchronization barrier, find next asynchronous message. + if (msg != null && msg.getTarget() == null) { + do { + prev = msg; + msg = shadowOfMsg(msg).internalGetNext(); + } while (msg != null && !msg.isAsynchronous()); } - return head; + if (msg != null) { + Message next = shadowOfMsg(msg).internalGetNext(); + if (prev == null) { + reflector(MessageQueueReflector.class, realQueue).setMessages(next); + } else { + ReflectionHelpers.setField(prev, "next", next); + } + } + return msg; } } @@ -340,6 +389,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { msgQueue.setMessages(null); msgQueue.setIdleHandlers(new ArrayList<>()); msgQueue.setNextBarrierToken(0); + setUncaughtException(null); } private static ShadowPausedMessage shadowOfMsg(Message head) { @@ -378,10 +428,50 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { } } + /** + * Called when an uncaught exception occurred in this message queue's Looper thread. + * + * <p>In real android, by default an exception handler is installed which kills the entire process + * when an uncaught exception occurs. We don't want to do this in robolectric to isolate tests, so + * instead an uncaught exception puts the message queue into an error state, where any future + * interaction will rethrow the exception. + */ + void setUncaughtException(Exception e) { + synchronized (realQueue) { + this.uncaughtException = e; + } + } + + void checkQueueState() { + synchronized (realQueue) { + if (uncaughtException != null) { + throw new IllegalStateException( + "Looper thread that has died due to an uncaught exception", uncaughtException); + } + } + } + + /** + * Remove all messages from queue + * + * @param msgProcessor a callback to apply to each mesg + */ + void drainQueue(Predicate<Runnable> msgProcessor) { + synchronized (realQueue) { + Message msg = getMessages(); + while (msg != null) { + boolean unused = msgProcessor.apply(msg.getCallback()); + ShadowMessage shadowMsg = Shadow.extract(msg); + msg.recycle(); + msg = shadowMsg.getNext(); + } + } + } + /** Accessor interface for {@link MessageQueue}'s internals. */ @ForType(MessageQueue.class) private interface MessageQueueReflector { - + @Direct boolean enqueueMessage(Message msg, long when); Message next(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java index 839e28595..cbf52ae39 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java @@ -41,6 +41,7 @@ import android.content.integrity.IAppIntegrityManager; import android.content.pm.ICrossProfileApps; import android.content.pm.IShortcutService; import android.content.rollback.IRollbackManager; +import android.hardware.ISensorPrivacyManager; import android.hardware.biometrics.IAuthService; import android.hardware.biometrics.IBiometricService; import android.hardware.fingerprint.IFingerprintService; @@ -57,6 +58,7 @@ import android.net.IIpSecService; import android.net.INetworkPolicyManager; import android.net.INetworkScoreService; import android.net.ITetheringConnector; +import android.net.IVpnManager; import android.net.nsd.INsdManager; import android.net.vcn.IVcnManagementService; import android.net.wifi.IWifiManager; @@ -205,6 +207,8 @@ public class ShadowServiceManager { addBinderService(Context.UWB_SERVICE, IUwbAdapter.class); addBinderService(Context.VCN_MANAGEMENT_SERVICE, IVcnManagementService.class); addBinderService(Context.TRANSLATION_MANAGER_SERVICE, ITranslationManager.class); + addBinderService(Context.SENSOR_PRIVACY_SERVICE, ISensorPrivacyManager.class); + addBinderService(Context.VPN_MANAGEMENT_SERVICE, IVpnManager.class); } if (RuntimeEnvironment.getApiLevel() >= TIRAMISU) { addBinderService(Context.AMBIENT_CONTEXT_SERVICE, IAmbientContextManager.class); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java index a1895ff6b..c40d24e96 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java @@ -1,6 +1,5 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; @@ -63,7 +62,7 @@ public class ShadowSoundPool { return 1; } - @Implementation(minSdk = CUR_DEVELOPMENT) + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) protected int _play( int soundID, float leftVolume, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java index 529aaa405..1b239d188 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java @@ -21,6 +21,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executor; import org.robolectric.annotation.HiddenApi; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -282,6 +283,17 @@ public class ShadowSubscriptionManager { } /** + * Adds a listener to a local list of listeners. Will be triggered by {@link + * #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated. + */ + @Implementation(minSdk = R) + protected void addOnSubscriptionsChangedListener( + Executor executor, OnSubscriptionsChangedListener listener) { + listeners.add(listener); + listener.onSubscriptionsChanged(); + } + + /** * Removes a listener from a local list of listeners. Will be triggered by {@link * #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated. */ @@ -290,6 +302,16 @@ public class ShadowSubscriptionManager { listeners.remove(listener); } + /** + * Check if a listener exists in the {@link ShadowSubscriptionManager.listeners}. + * + * @param listener The listener to check. + * @return boolean True if the listener already added, otherwise false. + */ + public boolean hasOnSubscriptionsChangedListener(OnSubscriptionsChangedListener listener) { + return listeners.contains(listener); + } + /** Returns subscription Ids that were set via {@link #setActiveSubscriptionInfoList}. */ @Implementation(minSdk = LOLLIPOP_MR1) @HiddenApi @@ -405,6 +427,17 @@ public class ShadowSubscriptionManager { return phoneNumberMap.getOrDefault(subscriptionId, ""); } + /** + * Returns the phone number for the given {@code subscriptionId}, or an empty string if not + * available. {@code source} is ignored and will return the same as {@link #getPhoneNumber(int)}. + * + * <p>The phone number can be set by {@link #setPhoneNumber(int, String)} + */ + @Implementation(minSdk = TIRAMISU) + protected String getPhoneNumber(int subscriptionId, int source) { + return getPhoneNumber(subscriptionId); + } + /** Sets the phone number returned by {@link #getPhoneNumber(int)}. */ public void setPhoneNumber(int subscriptionId, String phoneNumber) { phoneNumberMap.put(subscriptionId, phoneNumber); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java index bc8528781..63d12e6a1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java @@ -11,6 +11,7 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import dalvik.system.CloseGuard; import java.util.concurrent.atomic.AtomicInteger; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.ReflectorObject; @@ -82,6 +83,13 @@ public class ShadowSurfaceControl { void initializeNativeObject() { surfaceControlReflector.setNativeObject(nativeObject.incrementAndGet()); + if (RuntimeEnvironment.getApiLevel() >= ShadowBuild.UPSIDE_DOWN_CAKE) { + try { + surfaceControlReflector.setFreeNativeResources(() -> {}); + } catch(Exception e) { + // tm branches not yet have mFreeNativeResources added while in partial U state + } + } } @ForType(SurfaceControl.class) @@ -94,5 +102,8 @@ public class ShadowSurfaceControl { @Direct void finalize(); + + @Accessor("mFreeNativeResources") + void setFreeNativeResources(Runnable runnable); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java index a2bb38aba..b9013704a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java @@ -13,10 +13,10 @@ public class ShadowSystem { */ @SuppressWarnings("unused") public static long nanoTime() { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis()); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { return ShadowLegacySystemClock.nanoTime(); + } else { + return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis()); } } @@ -27,10 +27,10 @@ public class ShadowSystem { */ @SuppressWarnings("unused") public static long currentTimeMillis() { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - return SystemClock.uptimeMillis(); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { return ShadowLegacySystemClock.currentTimeMillis(); + } else { + return SystemClock.uptimeMillis(); } } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java index cce1990a0..41fbf4e7d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java @@ -15,9 +15,9 @@ import android.media.AudioAttributes; import android.os.Handler; import android.os.Looper; import android.os.SystemVibrator; -import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.vibrator.VibrationEffectSegment; +import com.google.common.base.Preconditions; import java.util.List; import java.util.Optional; import org.robolectric.RuntimeEnvironment; @@ -25,7 +25,8 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.util.ReflectionHelpers; -@Implements(value = SystemVibrator.class, isInAndroidSdk = false) +/** Shadow for {@link SystemVibrator}. */ +@Implements(value = SystemVibrator.class, isInAndroidSdk = false, looseSignatures = true) public class ShadowSystemVibrator extends ShadowVibrator { private final Handler handler = new Handler(Looper.getMainLooper()); @@ -133,11 +134,14 @@ public class ShadowSystemVibrator extends ShadowVibrator { @Implementation(minSdk = S) protected void vibrate( - int uid, - String opPkg, - VibrationEffect effect, - String reason, - VibrationAttributes attributes) { + Object uid, Object opPkg, Object effect, Object reason, Object attributes) { + Preconditions.checkArgument(uid instanceof Integer); + Preconditions.checkArgument(opPkg == null || opPkg instanceof String); + // The SystemVibrator#vibrate needs effect NonNull. + Preconditions.checkArgument(effect instanceof VibrationEffect); + Preconditions.checkArgument(reason == null || reason instanceof String); + // The SystemVibrator#vibrate needs attributes NonNull. + Preconditions.checkArgument(attributes instanceof android.os.VibrationAttributes); if (effect instanceof VibrationEffect.Composed) { VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect; vibrationAttributesFromLastVibration = attributes; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java index 0864354d9..048abf03b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java @@ -52,6 +52,7 @@ import android.telephony.TelephonyDisplayInfo; import android.telephony.TelephonyManager; import android.telephony.TelephonyManager.CellInfoCallback; import android.telephony.VisualVoicemailSmsFilterSettings; +import android.telephony.emergency.EmergencyNumber; import android.text.TextUtils; import android.util.SparseArray; import android.util.SparseIntArray; @@ -59,13 +60,16 @@ import com.google.common.base.Ascii; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executor; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.HiddenApi; @@ -141,6 +145,7 @@ public class ShadowTelephonyManager { private String visualVoicemailPackageName = null; private SignalStrength signalStrength; private boolean dataEnabled = false; + private final Set<Integer> dataDisabledReasons = new HashSet<>(); private boolean isRttSupported; private final List<String> sentDialerSpecialCodes = new ArrayList<>(); private boolean hearingAidCompatibilitySupported = false; @@ -152,6 +157,7 @@ public class ShadowTelephonyManager { private VisualVoicemailSmsParams lastVisualVoicemailSmsParams; private VisualVoicemailSmsFilterSettings visualVoicemailSmsFilterSettings; private boolean emergencyCallbackMode; + private static Map<Integer, List<EmergencyNumber>> emergencyNumbersList; /** * Should be {@link TelephonyManager.BootstrapAuthenticationCallback} but this object was @@ -169,6 +175,7 @@ public class ShadowTelephonyManager { @Resetter public static void reset() { callComposerStatus = 0; + emergencyNumbersList = null; } @Implementation(minSdk = S) @@ -263,6 +270,13 @@ public class ShadowTelephonyManager { } /** Call state may be specified via {@link #setCallState(int)}. */ + @Implementation(minSdk = S) + protected int getCallStateForSubscription() { + checkReadPhoneStatePermission(); + return callState; + } + + /** Call state may be specified via {@link #setCallState(int)}. */ @Implementation protected int getCallState() { checkReadPhoneStatePermission(); @@ -1215,12 +1229,39 @@ public class ShadowTelephonyManager { } /** + * Implementation for {@link TelephonyManager#isDataEnabledForReason}. + * + * @return True by default, unless reason is set to false with {@link + * TelephonyManager#setDataEnabledForReason}. + */ + @Implementation(minSdk = Build.VERSION_CODES.S) + protected boolean isDataEnabledForReason(@TelephonyManager.DataEnabledReason int reason) { + checkReadPhoneStatePermission(); + return !dataDisabledReasons.contains(reason); + } + + /** * Implementation for {@link TelephonyManager#setDataEnabled}. Marked as public in order to allow * it to be used as a test API. */ @Implementation(minSdk = Build.VERSION_CODES.O) public void setDataEnabled(boolean enabled) { - dataEnabled = enabled; + setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER, enabled); + } + + /** + * Implementation for {@link TelephonyManager#setDataEnabledForReason}. Marked as public in order + * to allow it to be used as a test API. + */ + @Implementation(minSdk = Build.VERSION_CODES.S) + public void setDataEnabledForReason( + @TelephonyManager.DataEnabledReason int reason, boolean enabled) { + if (enabled) { + dataDisabledReasons.remove(reason); + } else { + dataDisabledReasons.add(reason); + } + dataEnabled = dataDisabledReasons.isEmpty(); } /** @@ -1374,4 +1415,25 @@ public class ShadowTelephonyManager { return sentIntent; } } + + /** + * Sets the emergency numbers list returned by {@link TelephonyManager#getEmergencyNumberList}. + */ + public static void setEmergencyNumberList( + Map<Integer, List<EmergencyNumber>> emergencyNumbersList) { + ShadowTelephonyManager.emergencyNumbersList = emergencyNumbersList; + } + + /** + * Implementation for {@link TelephonyManager#getEmergencyNumberList}. + * + * @return an immutable map by default, unless set with {@link #setEmergencyNumberList}. + */ + @Implementation(minSdk = R) + protected Map<Integer, List<EmergencyNumber>> getEmergencyNumberList() { + if (ShadowTelephonyManager.emergencyNumbersList != null) { + return ShadowTelephonyManager.emergencyNumbersList; + } + return ImmutableMap.of(); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java index 5c8de7314..00ad65840 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java @@ -600,6 +600,16 @@ public class ShadowUserManager { } @HiddenApi + @Implementation(minSdk = R) + protected List<UserHandle> getUserHandles(boolean excludeDying) { + ArrayList<UserHandle> userHandles = new ArrayList<>(); + for (int id : userManagerState.userSerialNumbers.keySet()) { + userHandles.addAll(userManagerState.userProfilesListMap.get(id)); + } + return userHandles; + } + + @HiddenApi @Implementation(minSdk = JELLY_BEAN_MR1) protected static int getMaxSupportedUsers() { return maxSupportedUsers; @@ -998,6 +1008,9 @@ public class ShadowUserManager { @Implementation(minSdk = JELLY_BEAN_MR1) protected boolean removeUser(int userHandle) { + if (!userManagerState.userInfoMap.containsKey(userHandle)) { + return false; + } userManagerState.userInfoMap.remove(userHandle); userManagerState.userPidMap.remove(userHandle); userManagerState.userSerialNumbers.remove(userHandle); @@ -1021,6 +1034,13 @@ public class ShadowUserManager { return removeUser(user.getIdentifier()); } + @Implementation(minSdk = TIRAMISU) + protected int removeUserWhenPossible(UserHandle user, boolean overrideDevicePolicy) { + return removeUser(user.getIdentifier()) + ? UserManager.REMOVE_RESULT_REMOVED + : UserManager.REMOVE_RESULT_ERROR_UNKNOWN; + } + @Implementation(minSdk = N) protected static boolean supportsMultipleUsers() { return isMultiUserSupported; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java index b66a0a414..276a31db3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java @@ -3,18 +3,19 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.R; import android.media.AudioAttributes; -import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; -import android.os.vibrator.VibrationEffectSegment; +import android.os.vibrator.PrimitiveSegment; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; +import org.robolectric.util.ReflectionHelpers; @Implements(Vibrator.class) public class ShadowVibrator { @@ -22,10 +23,10 @@ public class ShadowVibrator { static boolean cancelled; static long milliseconds; protected static long[] pattern; - protected static final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>(); + protected static final List<Object> vibrationEffectSegments = new ArrayList<>(); protected static final List<PrimitiveEffect> primitiveEffects = new ArrayList<>(); protected static final List<Integer> supportedPrimitives = new ArrayList<>(); - @Nullable protected static VibrationAttributes vibrationAttributesFromLastVibration; + @Nullable protected static Object vibrationAttributesFromLastVibration; @Nullable protected static AudioAttributes audioAttributesFromLastVibration; static int repeat; static boolean hasVibrator = true; @@ -81,9 +82,18 @@ public class ShadowVibrator { return repeat; } - /** Returns the last list of {@link VibrationEffectSegment}. */ - public List<VibrationEffectSegment> getVibrationEffectSegments() { - return vibrationEffectSegments; + /** Returns the last list of {@link PrimitiveSegment} vibrations in {@link PrimitiveEffect}. */ + @SuppressWarnings("JdkCollectors") // toImmutableList is only supported in Java 8+. + public List<PrimitiveEffect> getPrimitiveSegmentsInPrimitiveEffects() { + return vibrationEffectSegments.stream() + .filter(segment -> segment instanceof PrimitiveSegment) + .map( + segment -> + new PrimitiveEffect( + ReflectionHelpers.getField(segment, "mPrimitiveId"), + ReflectionHelpers.getField(segment, "mScale"), + ReflectionHelpers.getField(segment, "mDelay"))) + .collect(Collectors.toList()); } /** Returns the last list of {@link PrimitiveEffect}. */ @@ -108,9 +118,9 @@ public class ShadowVibrator { supportedPrimitives.addAll(primitives); } - /** Returns the {@link VibrationAttributes} from the last vibration. */ + /** Returns the {@link android.os.VibrationAttributes} from the last vibration. */ @Nullable - public VibrationAttributes getVibrationAttributesFromLastVibration() { + public Object getVibrationAttributesFromLastVibration() { return vibrationAttributesFromLastVibration; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java index 70cb36999..5d06f587e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java @@ -524,31 +524,29 @@ public class ShadowView { @Implementation protected boolean post(Runnable action) { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - return reflector(_View_.class, realView).post(action); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { ShadowApplication.getInstance().getForegroundThreadScheduler().post(action); return true; + } else { + return reflector(_View_.class, realView).post(action); } } @Implementation protected boolean postDelayed(Runnable action, long delayMills) { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - return reflector(_View_.class, realView).postDelayed(action, delayMills); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { ShadowApplication.getInstance() .getForegroundThreadScheduler() .postDelayed(action, delayMills); return true; + } else { + return reflector(_View_.class, realView).postDelayed(action, delayMills); } } @Implementation protected void postInvalidateDelayed(long delayMilliseconds) { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { ShadowApplication.getInstance() .getForegroundThreadScheduler() .postDelayed( @@ -559,17 +557,19 @@ public class ShadowView { } }, delayMilliseconds); + } else { + reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds); } } @Implementation protected boolean removeCallbacks(Runnable callback) { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - return reflector(_View_.class, realView).removeCallbacks(callback); - } else { + if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { ShadowLegacyLooper shadowLooper = Shadow.extract(Looper.getMainLooper()); shadowLooper.getScheduler().remove(callback); return true; + } else { + return reflector(_View_.class, realView).removeCallbacks(callback); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java index 28e668067..2f13fcf7d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java @@ -9,7 +9,7 @@ import android.view.ViewGroup; import java.io.PrintStream; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; import org.robolectric.annotation.RealObject; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.reflector.Direct; @@ -29,10 +29,10 @@ public class ShadowViewGroup extends ShadowView { () -> { reflector(ViewGroupReflector.class, realViewGroup).addView(child, index, params); }; - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - addViewRunnable.run(); - } else { + if (ShadowLooper.looperMode() == Mode.LEGACY) { shadowMainLooper().runPaused(addViewRunnable); + } else { + addViewRunnable.run(); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java new file mode 100644 index 000000000..99f807b07 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java @@ -0,0 +1,67 @@ +package org.robolectric.shadows; + +import android.content.Intent; +import android.net.PlatformVpnProfile; +import android.net.VpnManager; +import android.net.VpnProfileState; +import android.os.Build.VERSION_CODES; +import java.util.UUID; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +/** Shadow for {@link VpnManager}. */ +@Implements(value = VpnManager.class, minSdk = VERSION_CODES.R) +public class ShadowVpnManager { + + private VpnProfileState vpnProfileState; + private Intent provisionVpnProfileIntent; + + @Implementation + protected void deleteProvisionedVpnProfile() { + vpnProfileState = null; + } + + @Implementation(minSdk = VERSION_CODES.TIRAMISU) + protected VpnProfileState getProvisionedVpnProfileState() { + return vpnProfileState; + } + + /** + * @see #setProvisionVpnProfileResult(Intent). + */ + @Implementation + protected Intent provisionVpnProfile(PlatformVpnProfile profile) { + if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) { + vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false); + } + return provisionVpnProfileIntent; + } + + /** Sets the return value of #provisionVpnProfile(PlatformVpnProfile). */ + public void setProvisionVpnProfileResult(Intent intent) { + provisionVpnProfileIntent = intent; + } + + @Implementation + protected void startProvisionedVpnProfile() { + startProvisionedVpnProfileSession(); + } + + @Implementation(minSdk = VERSION_CODES.TIRAMISU) + protected String startProvisionedVpnProfileSession() { + String sessionKey = UUID.randomUUID().toString(); + if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) { + vpnProfileState = + new VpnProfileState(VpnProfileState.STATE_CONNECTED, sessionKey, false, false); + } + return sessionKey; + } + + @Implementation + protected void stopProvisionedVpnProfile() { + if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) { + vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false); + } + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java index 706d86e10..1a8131f01 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java @@ -569,7 +569,7 @@ public class ShadowWebView extends ShadowViewGroup { * * @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()} * @deprecated Do not depend on this method as it will be removed in a future update. The - * preferered method is to populate a fake web history to use for going back. + * preferred method is to populate a fake web history to use for going back. */ @Deprecated public void setCanGoBack(boolean canGoBack) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java index 8e933d93e..7221e69e7 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java @@ -5,6 +5,9 @@ import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static java.util.stream.Collectors.toList; import android.content.Context; import android.content.Intent; @@ -17,14 +20,19 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.MulticastLock; +import android.net.wifi.WifiSsid; import android.net.wifi.WifiUsabilityStatsEntry; +import android.os.Binder; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.ArraySet; import android.util.Pair; import com.google.common.collect.ImmutableList; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.BitSet; import java.util.HashSet; import java.util.LinkedHashMap; @@ -71,6 +79,8 @@ public class ShadowWifiManager { @RealObject WifiManager wifiManager; private WifiConfiguration apConfig; private SoftApConfiguration softApConfig; + private final Object pnoRequestLock = new Object(); + private PnoScanRequest outstandingPnoScanRequest = null; @Implementation protected boolean setWifiEnabled(boolean wifiEnabled) { @@ -657,4 +667,176 @@ public class ShadowWifiManager { this.predictionHorizonSec = predictionHorizonSec; } } + + /** Informs the {@link WifiManager} of a list of PNO {@link ScanResult}. */ + public void networksFoundFromPnoScan(List<ScanResult> scanResults) { + synchronized (pnoRequestLock) { + List<ScanResult> scanResultsCopy = List.copyOf(scanResults); + if (outstandingPnoScanRequest == null + || outstandingPnoScanRequest.ssids.stream() + .noneMatch( + ssid -> + scanResultsCopy.stream() + .anyMatch(scanResult -> scanResult.getWifiSsid().equals(ssid)))) { + return; + } + Executor executor = outstandingPnoScanRequest.executor; + InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback; + executor.execute(() -> callback.onScanResultsAvailable(scanResultsCopy)); + Intent intent = createPnoScanResultsBroadcastIntent(); + getContext().sendBroadcast(intent); + executor.execute( + () -> + callback.onRemoved( + InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED)); + outstandingPnoScanRequest = null; + } + } + + // Object needs to be used here since PnoScanResultsCallback is hidden. The looseSignatures spec + // requires that all args are of type Object. + @Implementation(minSdk = TIRAMISU) + @HiddenApi + protected void setExternalPnoScanRequest( + Object ssids, Object frequencies, Object executor, Object callback) { + synchronized (pnoRequestLock) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + + List<WifiSsid> pnoSsids = (List<WifiSsid>) ssids; + int[] pnoFrequencies = (int[]) frequencies; + Executor pnoExecutor = (Executor) executor; + InternalPnoScanResultsCallback pnoCallback = new InternalPnoScanResultsCallback(callback); + + if (pnoExecutor == null) { + throw new IllegalArgumentException("executor cannot be null"); + } + if (pnoSsids == null || pnoSsids.isEmpty()) { + // The real WifiServiceImpl throws an IllegalStateException in this case, so keeping it the + // same for consistency. + throw new IllegalStateException("Ssids can't be null or empty"); + } + if (pnoSsids.size() > 2) { + throw new IllegalArgumentException("Ssid list can't be greater than 2"); + } + if (pnoFrequencies != null && pnoFrequencies.length > 10) { + throw new IllegalArgumentException("Length of frequencies must be smaller than 10"); + } + int uid = Binder.getCallingUid(); + String packageName = getContext().getPackageName(); + + if (outstandingPnoScanRequest != null) { + pnoExecutor.execute( + () -> + pnoCallback.onRegisterFailed( + uid == outstandingPnoScanRequest.uid + ? InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED + : InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY)); + return; + } + + outstandingPnoScanRequest = + new PnoScanRequest(pnoSsids, pnoFrequencies, pnoExecutor, pnoCallback, packageName, uid); + pnoExecutor.execute(pnoCallback::onRegisterSuccess); + } + } + + @Implementation(minSdk = TIRAMISU) + @HiddenApi + protected void clearExternalPnoScanRequest() { + synchronized (pnoRequestLock) { + if (outstandingPnoScanRequest != null + && outstandingPnoScanRequest.uid == Binder.getCallingUid()) { + InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback; + outstandingPnoScanRequest.executor.execute( + () -> + callback.onRemoved( + InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED)); + outstandingPnoScanRequest = null; + } + } + } + + private static class PnoScanRequest { + private final List<WifiSsid> ssids; + private final List<Integer> frequencies; + private final Executor executor; + private final InternalPnoScanResultsCallback callback; + private final String packageName; + private final int uid; + + private PnoScanRequest( + List<WifiSsid> ssids, + int[] frequencies, + Executor executor, + InternalPnoScanResultsCallback callback, + String packageName, + int uid) { + this.ssids = List.copyOf(ssids); + this.frequencies = + frequencies == null ? List.of() : Arrays.stream(frequencies).boxed().collect(toList()); + this.executor = executor; + this.callback = callback; + this.packageName = packageName; + this.uid = uid; + } + } + + private Intent createPnoScanResultsBroadcastIntent() { + Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true); + intent.setPackage(outstandingPnoScanRequest.packageName); + return intent; + } + + private static class InternalPnoScanResultsCallback { + static final int REGISTER_PNO_CALLBACK_ALREADY_REGISTERED = 1; + static final int REGISTER_PNO_CALLBACK_RESOURCE_BUSY = 2; + static final int REMOVE_PNO_CALLBACK_RESULTS_DELIVERED = 1; + static final int REMOVE_PNO_CALLBACK_UNREGISTERED = 2; + + final Object callback; + final Method availableCallback; + final Method successCallback; + final Method failedCallback; + final Method removedCallback; + + InternalPnoScanResultsCallback(Object callback) { + this.callback = callback; + try { + Class<?> pnoCallbackClass = callback.getClass(); + availableCallback = pnoCallbackClass.getMethod("onScanResultsAvailable", List.class); + successCallback = pnoCallbackClass.getMethod("onRegisterSuccess"); + failedCallback = pnoCallbackClass.getMethod("onRegisterFailed", int.class); + removedCallback = pnoCallbackClass.getMethod("onRemoved", int.class); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("callback is not of type PnoScanResultsCallback", e); + } + } + + void onScanResultsAvailable(List<ScanResult> scanResults) { + invokeCallback(availableCallback, scanResults); + } + + void onRegisterSuccess() { + invokeCallback(successCallback); + } + + void onRegisterFailed(int reason) { + invokeCallback(failedCallback, reason); + } + + void onRemoved(int reason) { + invokeCallback(removedCallback, reason); + } + + void invokeCallback(Method method, Object... args) { + try { + method.invoke(callback, args); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Failed to invoke " + method.getName(), e); + } + } + } } diff --git a/shadows/httpclient/build.gradle b/shadows/httpclient/build.gradle index 332c83e08..4e77a09c3 100644 --- a/shadows/httpclient/build.gradle +++ b/shadows/httpclient/build.gradle @@ -21,17 +21,17 @@ dependencies { api project(":utils") // We should keep httpclient version for low level API compatibility. - earlyRuntime "org.apache.httpcomponents:httpcore:4.0.1" - api "org.apache.httpcomponents:httpclient:4.0.3" - compileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) { force = true } + earlyRuntime libs.apache.http.core + api libs.apache.http.client + compileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) testImplementation project(":robolectric") - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockito testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar" - testCompileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) { force = true } + testCompileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) testRuntimeOnly AndroidSdk.S.coordinates } diff --git a/shadows/playservices/build.gradle b/shadows/playservices/build.gradle index c3abbba05..b00983893 100644 --- a/shadows/playservices/build.gradle +++ b/shadows/playservices/build.gradle @@ -14,25 +14,20 @@ shadows { dependencies { compileOnly project(":shadows:framework") api project(":annotations") - api "com.google.guava:guava:$guavaJREVersion" + api libs.guava - compileOnly "androidx.fragment:fragment:1.2.0" - compileOnly "com.google.android.gms:play-services-base:8.4.0" - compileOnly "com.google.android.gms:play-services-basement:8.4.0" + compileOnly libs.bundles.play.services.base.for.shadows compileOnly AndroidSdk.MAX_SDK.coordinates testCompileOnly AndroidSdk.MAX_SDK.coordinates - testCompileOnly "com.google.android.gms:play-services-base:8.4.0" - testCompileOnly "com.google.android.gms:play-services-basement:8.4.0" + testCompileOnly libs.bundles.play.services.base.for.shadows testImplementation project(":robolectric") - testImplementation "junit:junit:$junitVersion" - testImplementation "com.google.truth:truth:$truthVersion" - testImplementation "org.mockito:mockito-core:$mockitoVersion" - testRuntimeOnly "androidx.fragment:fragment:1.2.0" - testRuntimeOnly "com.google.android.gms:play-services-base:8.4.0" - testRuntimeOnly "com.google.android.gms:play-services-basement:8.4.0" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.mockito + testRuntimeOnly libs.bundles.play.services.base.for.shadows testRuntimeOnly AndroidSdk.MAX_SDK.coordinates } diff --git a/shadows/versioning/Android.bp b/shadows/versioning/Android.bp new file mode 100644 index 000000000..b630e226e --- /dev/null +++ b/shadows/versioning/Android.bp @@ -0,0 +1,67 @@ +//############################################# +// Compile Robolectric utils +//############################################# + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "external_robolectric_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["external_robolectric_license"], +} + +java_library_host { + name: "Robolectric_shadows_versioning_upstream", + srcs: ["src/main/java/**/*.java"], + static_libs: [ + "robolectric-javax.annotation-api-1.2", + "Robolectric_shadowapi_upstream", + "Robolectric_utils_upstream", + "jsr305", + ], + libs: ["robolectric-host-android_all_upstream"], +} + +//############################################# +// Compile Robolectric utils tests +//############################################# + +java_test_host { + name: "Robolectric_shadows_versioning_tests_upstream", + srcs: ["src/test/java/**/AndroidVersionsEdgeCaseTest.java"], + static_libs: [ + "Robolectric_shadows_versioning_upstream", + "hamcrest", + "guava", + "junit", + "truth-prebuilt", + ], + test_suites: ["general-tests"], + test_options: { + unit_test: false, + }, +} + +//android_robolectric_test { +// enabled: true, +// +// name: "Robolectric_shadows_versioning_tests_e2e_upstream", +// +// srcs: [ +// "src/**/*.AndroidVersionsTest.java", +// ], +// +// java_resource_dirs: ["config"], +// +// libs: [ +// "androidx.test.core", +// "androidx.test.runner", +// ], +// +// instrumentation_for: "MyRoboApplication", +// +// upstream: true, +//} + + diff --git a/shadows/versioning/build.gradle b/shadows/versioning/build.gradle new file mode 100644 index 000000000..68a8fb769 --- /dev/null +++ b/shadows/versioning/build.gradle @@ -0,0 +1,21 @@ +import org.robolectric.gradle.DeployedRoboJavaModulePlugin +import org.robolectric.gradle.RoboJavaModulePlugin + +apply plugin: RoboJavaModulePlugin +apply plugin: DeployedRoboJavaModulePlugin + +configurations { + earlyRuntime +} + +dependencies { + api project(":shadowapi") + compileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK (AndroidSdk.s.coordinates) { force = true } + testImplementation project(":robolectric") + testImplementation libs.truth + testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar" + testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK + testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports +} + + diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java new file mode 100644 index 000000000..316365b39 --- /dev/null +++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java @@ -0,0 +1,23 @@ +package org.robolectric.versioning; + +import java.io.IOException; +import java.util.Properties; +import java.util.jar.JarFile; +import org.robolectric.versioning.AndroidVersions.AndroidRelease; + +/** + * Utility access method to allow robolectric to instantiate AndroidVersions without cluttering code + * completion for users of AndroidVersions's embedded Types of one per Android Releases. + */ +public final class AndroidVersionInitTools { + + private AndroidVersionInitTools() {} + + public static AndroidRelease computeReleaseVersion(JarFile jarFile) throws IOException { + return AndroidVersions.computeReleaseVersion(jarFile); + } + + public static AndroidRelease computeCurrentSdkFromBuildProps(Properties buildProps) { + return AndroidVersions.computeCurrentSdkFromBuildProps(buildProps); + } +} diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java new file mode 100644 index 000000000..631451883 --- /dev/null +++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java @@ -0,0 +1,779 @@ +package org.robolectric.versioning; + +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import static java.util.Arrays.asList; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import javax.annotation.Nullable; +import org.robolectric.util.Logger; +import org.robolectric.util.ReflectionHelpers; + +/** + * Android versioning is complicated.<br> + * 1) There is a yearly letter release with an increasing of one alpha step each year A-> B, B-> C, + * and so on. While commonly referenced these are not the release numbers. This class calls these + * shortcodes. Also minor version number releases (usually within the same year) will start with the + * same letter.<br> + * 2) There is an SDK_INT field in android.os.Build.VERSION that tracks a version of the internal + * SDK. While useful to track the actual released versions of Android, these are not the release + * number. More importantly, android.os.Build.VERSION uses code names to describe future versions. + * Multiple code names may be in development at once on different branches of Android.<br> + * 3) There is a yearly release major number followed by a minor number, which may or may not be + * used.<br> + * 4) Relevant logic and reasoning should match androidx.core.os.BuildCompat.java with the caveat + * that this class guess at the future release version number and short of the current dev branch. + * <br> + */ +public final class AndroidVersions { + + private AndroidVersions() {} + + /** Representation of an android release, one that has occurred, or is expected. */ + public abstract static class AndroidRelease implements Comparable<AndroidRelease> { + + /** + * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt may + * still be that of the prior release. + */ + public int getSdkInt() { + return ReflectionHelpers.getStaticField(this.getClass(), "SDK_INT"); + } + + /** + * single character short code for the release, multiple characters for minor releases (only + * minor version numbers increment - usually within the same year). + */ + public String getShortCode() { + return ReflectionHelpers.getStaticField(this.getClass(), "SHORT_CODE"); + } + + /** + * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt will + * guess at the likely sdk number. Your code will need to recompile if this value changes - + * including most modern build tools; bazle, soong all are full build systems - and as such + * organizations using them have no concerns. + */ + public boolean isReleased() { + return ReflectionHelpers.getStaticField(this.getClass(), "RELEASED"); + } + + /** major.minor version number as String. */ + public String getVersion() { + return ReflectionHelpers.getStaticField(this.getClass(), "VERSION"); + } + + /** + * Implements comparable. + * + * @param other the object to be compared. + * @return 1 if this is greater than other, 0 if equal, -1 if less + * @throws RuntimeException if other is not an instance of AndroidRelease. + */ + @Override + public int compareTo(AndroidRelease other) { + if (other == null) { + throw new RuntimeException( + "Only " + + AndroidVersions.class.getName() + + " should define Releases, illegal class " + + other.getClass()); + } + return Integer.compare(this.getSdkInt(), other.getSdkInt()); + } + + @Override + public String toString() { + return "Android " + + (this.isReleased() ? "" : "Future ") + + "Release: " + + this.getVersion() + + " ( sdk: " + + this.getSdkInt() + + " code: " + + this.getShortCode() + + " )"; + } + } + + /** + * Version: 4.1 <br> + * ShortCode: J <br> + * SDK API Level: 16 <br> + * release: true <br> + */ + public static final class J extends AndroidRelease { + + public static final int SDK_INT = 16; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "J"; + + public static final String VERSION = "4.1"; + } + + /** + * Version: 4.2 <br> + * ShortCode: JMR1 <br> + * SDK API Level: 17 <br> + * release: true <br> + */ + public static final class JMR1 extends AndroidRelease { + + public static final int SDK_INT = 17; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "JMR1"; + + public static final String VERSION = "4.2"; + } + + /** + * Version: 4.3 <br> + * ShortCode: JMR2 <br> + * SDK API Level: 18 <br> + * release: true <br> + */ + public static final class JMR2 extends AndroidRelease { + + public static final int SDK_INT = 18; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "JMR2"; + + public static final String VERSION = "4.3"; + } + + /** + * Version: 4.4 <br> + * ShortCode: K <br> + * SDK API Level: 19 <br> + * release: true <br> + */ + public static final class K extends AndroidRelease { + + public static final int SDK_INT = 19; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "K"; + + public static final String VERSION = "4.4"; + } + + // Skipping K Watch release, which was 20. + + /** + * Version: 5.0 <br> + * ShortCode: L <br> + * SDK API Level: 21 <br> + * release: true <br> + */ + public static final class L extends AndroidRelease { + + public static final int SDK_INT = 21; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "L"; + + public static final String VERSION = "5.0"; + } + + /** + * Version: 5.1 <br> + * ShortCode: LMR1 <br> + * SDK API Level: 22 <br> + * release: true <br> + */ + public static final class LMR1 extends AndroidRelease { + + public static final int SDK_INT = 22; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "LMR1"; + + public static final String VERSION = "5.1"; + } + + /** + * Version: 6.0 <br> + * ShortCode: M <br> + * SDK API Level: 23 <br> + * release: true <br> + */ + public static final class M extends AndroidRelease { + + public static final int SDK_INT = 23; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "M"; + + public static final String VERSION = "6.0"; + } + + /** + * Version: 7.0 <br> + * ShortCode: N <br> + * SDK API Level: 24 <br> + * release: true <br> + */ + public static final class N extends AndroidRelease { + + public static final int SDK_INT = 24; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "N"; + + public static final String VERSION = "7.0"; + } + + /** + * Release: 7.1 <br> + * ShortCode: NMR1 <br> + * SDK Framework: 25 <br> + * release: true <br> + */ + public static final class NMR1 extends AndroidRelease { + + public static final int SDK_INT = 25; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "NMR1"; + + private static final String VERSION = "7.1"; + } + + /** + * Release: 8.0 <br> + * ShortCode: O <br> + * SDK API Level: 26 <br> + * release: true <br> + */ + public static final class O extends AndroidRelease { + + public static final int SDK_INT = 26; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "O"; + + public static final String VERSION = "8.0"; + } + + /** + * Release: 8.1 <br> + * ShortCode: OMR1 <br> + * SDK API Level: 27 <br> + * release: true <br> + */ + public static final class OMR1 extends AndroidRelease { + + public static final int SDK_INT = 27; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "OMR1"; + + public static final String VERSION = "8.1"; + } + + /** + * Release: 9.0 <br> + * ShortCode: P <br> + * SDK API Level: 28 <br> + * release: true <br> + */ + public static final class P extends AndroidRelease { + + public static final int SDK_INT = 28; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "P"; + + public static final String VERSION = "9.0"; + } + + /** + * Release: 10.0 <br> + * ShortCode: Q <br> + * SDK API Level: 29 <br> + * release: true <br> + */ + public static final class Q extends AndroidRelease { + + public static final int SDK_INT = 29; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "Q"; + + public static final String VERSION = "10.0"; + } + + /** + * Release: 11.0 <br> + * ShortCode: R <br> + * SDK API Level: 30 <br> + * release: true <br> + */ + public static final class R extends AndroidRelease { + + public static final int SDK_INT = 30; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "R"; + + public static final String VERSION = "11.0"; + } + + /** + * Release: 12.0 <br> + * ShortCode: S <br> + * SDK API Level: 31 <br> + * release: true <br> + */ + public static final class S extends AndroidRelease { + + public static final int SDK_INT = 31; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "S"; + + public static final String VERSION = "12.0"; + } + + /** + * Release: 12.1 <br> + * ShortCode: Sv2 <br> + * SDK API Level: 32 <br> + * release: true <br> + */ + @SuppressWarnings("UPPER_SNAKE_CASE") + public static final class Sv2 extends AndroidRelease { + + public static final int SDK_INT = 32; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "Sv2"; + + public static final String VERSION = "12.1"; + } + + /** + * Release: 13.0 <br> + * ShortCode: T <br> + * SDK API Level: 33 <br> + * release: true <br> + */ + public static final class T extends AndroidRelease { + + public static final int SDK_INT = 33; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "T"; + + public static final String VERSION = "13.0"; + } + + /** + * Potential Release: 14.0 <br> + * ShortCode: U <br> + * SDK API Level: 34 <br> + * release: false <br> + */ + public static final class U extends AndroidRelease { + + public static final int SDK_INT = 34; + + public static final boolean RELEASED = true; + + public static final String SHORT_CODE = "U"; + + public static final String VERSION = "14.0"; + } + + /** + * Potential Release: 15.0 <br> + * ShortCode: V <br> + * SDK API Level: 34+ <br> + * release: false <br> + */ + public static final class V extends AndroidRelease { + + public static final int SDK_INT = 35; + + public static final boolean RELEASED = false; + + public static final String SHORT_CODE = "V"; + + public static final String VERSION = "15.0"; + } + + /** The current release this process is running on. */ + public static final AndroidRelease CURRENT; + + @Nullable + public static AndroidRelease getReleaseForSdkInt(@Nullable Integer sdkInt) { + if (sdkInt == null) { + return null; + } else { + return information.sdkIntToAllReleases.get(sdkInt); + } + } + + public static List<AndroidRelease> getReleases() { + List<AndroidRelease> output = new ArrayList<>(); + for (AndroidRelease release : information.allReleases) { + if (release.isReleased()) { + output.add(release); + } + } + return output; + } + + public static List<AndroidRelease> getUnreleased() { + List<AndroidRelease> output = new ArrayList<>(); + for (AndroidRelease release : information.allReleases) { + if (!release.isReleased()) { + output.add(release); + } + } + return output; + } + + /** + * Responsible for aggregating and interpreting the static state representing the current + * AndroidReleases known to AndroidVersions class. + */ + static class SdkInformation { + final List<AndroidRelease> allReleases; + final List<Class<? extends AndroidRelease>> classesWithIllegalNames; + final AndroidRelease latestRelease; + final AndroidRelease earliestUnreleased; + + // In the future we may need a multimap for sdkInts should they stay static across releases. + final Map<Integer, AndroidRelease> sdkIntToAllReleases = new HashMap<>(); + final Map<String, AndroidRelease> shortCodeToAllReleases = new HashMap<>(); + + // detected errors + final List<Map.Entry<AndroidRelease, AndroidRelease>> sdkIntCollisions = new ArrayList<>(); + Map.Entry<AndroidRelease, AndroidRelease> sdkApiMisordered = null; + + public SdkInformation( + List<AndroidRelease> releases, + List<Class<? extends AndroidRelease>> classesWithIllegalNames) { + this.allReleases = releases; + this.classesWithIllegalNames = classesWithIllegalNames; + AndroidRelease latestRelease = null; + AndroidRelease earliestUnreleased = null; + for (AndroidRelease release : allReleases) { + if (release.isReleased()) { + if (latestRelease == null || latestRelease.compareTo(release) > 0) { + latestRelease = release; + } + } else { + if (earliestUnreleased == null || earliestUnreleased.compareTo(release) < 0) { + earliestUnreleased = release; + } + } + } + this.latestRelease = latestRelease; + this.earliestUnreleased = earliestUnreleased; + verifyStaticInformation(); + } + + private void verifyStaticInformation() { + for (AndroidRelease release : this.allReleases) { + // Construct a map of all sdkInts to releases and note duplicates + AndroidRelease sdkCollision = this.sdkIntToAllReleases.put(release.getSdkInt(), release); + if (sdkCollision != null) { + this.sdkIntCollisions.add(new AbstractMap.SimpleEntry<>(release, sdkCollision)); + } + // Construct a map of all short codes to releases, and note duplicates + this.shortCodeToAllReleases.put(release.getShortCode(), release); + // There is no need to check for shortCode duplicates as the Field name must match the + // short code. + } + if (earliestUnreleased != null + && latestRelease != null + && latestRelease.getSdkInt() >= earliestUnreleased.getSdkInt()) { + sdkApiMisordered = new AbstractMap.SimpleEntry<>(latestRelease, earliestUnreleased); + } + } + + private void throwStaticErrors() { + StringBuilder errors = new StringBuilder(); + if (!this.classesWithIllegalNames.isEmpty()) { + errors + .append("The following classes do not follow the naming criteria for ") + .append("releases or do not have the short codes in ") + .append("their internal fields. Please correct them: ") + .append(this.classesWithIllegalNames) + .append("\n"); + } + if (sdkApiMisordered != null) { + errors + .append("The latest released sdk ") + .append(sdkApiMisordered.getKey().getShortCode()) + .append(" has a sdkInt greater than the earliest unreleased sdk ") + .append(sdkApiMisordered.getValue().getShortCode()) + .append("this implies sdks were released out of order which is highly unlikely.\n"); + } + if (!sdkIntCollisions.isEmpty()) { + errors.append( + "The following sdks have different shortCodes, but identical sdkInt " + "versions:\n"); + for (Map.Entry<AndroidRelease, AndroidRelease> entry : sdkIntCollisions) { + errors + .append("Both ") + .append(entry.getKey().getShortCode()) + .append(" and ") + .append(entry.getValue().getShortCode()) + .append("have the same sdkInt value of ") + .append(entry.getKey().getSdkInt()) + .append("\n"); + } + } + if (errors.length() > 0) { + throw new RuntimeException( + errors + .append("Please check the AndroidReleases defined ") + .append("in ") + .append(AndroidVersions.class.getName()) + .append("and ensure they are aligned with the versions of") + .append(" Android.") + .toString()); + } + } + + public AndroidRelease computeCurrentSdk( + int reportedVersion, String releaseName, String codename, List<String> activeCodeNames) { + Logger.info("Reported Version: " + reportedVersion); + Logger.info("Release Name: " + releaseName); + Logger.info("Code Name: " + codename); + Logger.info("Active Code Names: " + String.join(",", activeCodeNames)); + + AndroidRelease current = null; + // Special case "REL", which means the build is not a pre-release build. + if ("REL".equals(codename)) { + // the first letter of the code name equal to the release number. + current = sdkIntToAllReleases.get(reportedVersion); + if (current != null && !current.isReleased()) { + throw new RuntimeException( + "The current sdk " + + current.getShortCode() + + " has been released. Please update the contents of " + + AndroidVersions.class.getName() + + " to mark sdk " + + current.getShortCode() + + " as released."); + } + } else { + // Get known active code name letters + + List<String> activeCodenameLetter = new ArrayList<>(); + for (String name : activeCodeNames) { + activeCodenameLetter.add(name.toUpperCase(Locale.getDefault()).substring(0, 1)); + } + + // If the process is operating with a code name. + if (codename != null) { + StringBuilder detectedProblems = new StringBuilder(); + // This is safe for minor releases ( X.1 ) as long as they have added an entry + // corresponding to the sdk of that release and the prior major release is marked as + // "released" on its entry in this file. If not this class will fail to initialize. + // The assumption is that only one of the major or minor version of a code name + // is under development and unreleased at any give time (S or Sv2). + String foundCode = codename.toUpperCase(Locale.getDefault()).substring(0, 1); + int loc = activeCodenameLetter.indexOf(foundCode); + if (loc == -1) { + detectedProblems + .append("The current codename's (") + .append(codename) + .append(") first letter (") + .append(foundCode) + .append(") is not in the list of active code's first letters: ") + .append(activeCodenameLetter) + .append("\n"); + } else { + // attempt to find assume the fullname is the "shortCode", aka "Sv2", "OMR1". + current = shortCodeToAllReleases.get(codename); + // else, assume the fullname is the first letter is correct. + if (current == null) { + current = shortCodeToAllReleases.get(String.valueOf(foundCode)); + } + } + if (current == null) { + detectedProblems + .append("No known release is associated with the shortCode of \"") + .append(foundCode) + .append("\" or \"") + .append(codename) + .append("\"\n"); + } else if (current.isReleased()) { + detectedProblems + .append("The current sdk ") + .append(current.getShortCode()) + .append(" has been been marked as released. Please update the ") + .append("contents of current sdk jar to the released version.\n"); + } + if (detectedProblems.length() > 0) { + throw new RuntimeException(detectedProblems.toString()); + } + } + } + return current; + } + } + + /** + * Reads all AndroidReleases in this class and populates SdkInformation, checking for sanity in + * the shortCode, sdkInt, and release information. + * + * <p>All errors are stored and can be reported at once by asking the SdkInformation to throw a + * runtime exception after it has been populated. + */ + static SdkInformation gatherStaticSdkInformationFromThisClass() { + List<AndroidRelease> allReleases = new ArrayList<>(); + List<Class<? extends AndroidRelease>> classesWithIllegalNames = new ArrayList<>(); + for (Class<?> clazz : AndroidVersions.class.getClasses()) { + if (AndroidRelease.class.isAssignableFrom(clazz) + && !clazz.isInterface() + && !Modifier.isAbstract(clazz.getModifiers())) { + try { + AndroidRelease rel = (AndroidRelease) clazz.getDeclaredConstructor().newInstance(); + allReleases.add(rel); + // inspect field name - as this is our only chance to inspect it. + if (!rel.getClass().getSimpleName().equals(rel.getShortCode())) { + classesWithIllegalNames.add(rel.getClass()); + } + } catch (NoSuchMethodException + | InstantiationException + | IllegalArgumentException + | IllegalAccessException + | InvocationTargetException ex) { + throw new RuntimeException( + "Classes " + + clazz.getName() + + "should be accessible via " + + AndroidVersions.class.getCanonicalName() + + " and have a default public no-op constructor ", + ex); + } + } + } + Collections.sort(allReleases, AndroidRelease::compareTo); + + SdkInformation sdkInformation = new SdkInformation(allReleases, classesWithIllegalNames); + sdkInformation.throwStaticErrors(); + return sdkInformation; + } + + static AndroidRelease computeReleaseVersion(JarFile jarFile) throws IOException { + ZipEntry buildProp = jarFile.getEntry("build.prop"); + Properties buildProps = new Properties(); + buildProps.load(jarFile.getInputStream(buildProp)); + return computeCurrentSdkFromBuildProps(buildProps); + } + + static AndroidRelease computeCurrentSdkFromBuildProps(Properties buildProps) { + // 33, 34, 35 .... + String sdkVersionString = buildProps.getProperty("ro.build.version.sdk"); + int sdk = sdkVersionString == null ? 0 : Integer.parseInt(sdkVersionString); + // "REL" + String release = buildProps.getProperty("ro.build.version.release"); + // "Tiramasu", "UpsideDownCake" + String codename = buildProps.getProperty("ro.build.version.codename"); + // "Tiramasu,UpsideDownCake", "UpsideDownCake", "REL" + String codenames = buildProps.getProperty("ro.build.version.all_codenames"); + String[] allCodeNames = codenames == null ? new String[0] : codenames.split(","); + String[] activeCodeNames = + allCodeNames.length > 0 && allCodeNames[0].equals("REL") ? new String[0] : allCodeNames; + return information.computeCurrentSdk(sdk, release, codename, asList(activeCodeNames)); + } + + /** + * If we are working in android source, this code detects the list of active code names if any. + */ + private static List<String> getActiveCodeNamesIfAny(Class<?> targetClass) { + try { + Field activeCodeFields = targetClass.getDeclaredField("ACTIVE_CODENAMES"); + String[] activeCodeNames = (String[]) activeCodeFields.get(null); + return asList(activeCodeNames); + } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException ex) { + return new ArrayList<>(); + } + } + + private static final SdkInformation information; + + static { + AndroidRelease currentRelease = null; + information = gatherStaticSdkInformationFromThisClass(); + try { + Class<?> buildClass = + Class.forName("android.os.Build", false, Thread.currentThread().getContextClassLoader()); + System.out.println("build class " + buildClass); + Class<?> versionClass = null; + for (Class<?> c : buildClass.getClasses()) { + if (c.getSimpleName().equals("VERSION")) { + versionClass = c; + System.out.println("Version class " + versionClass); + break; + } + } + if (versionClass != null) { + // 33, 34, etc.... + int sdkInt = (int) ReflectionHelpers.getStaticField(versionClass, "SDK_INT"); + // Either unset, or 13, 14, etc.... + String release = ReflectionHelpers.getStaticField(versionClass, "RELEASE"); + // Either REL if release is set, or Tiramasu, UpsideDownCake, etc + String codename = ReflectionHelpers.getStaticField(versionClass, "CODENAME"); + List<String> activeCodeNames = getActiveCodeNamesIfAny(versionClass); + currentRelease = information.computeCurrentSdk(sdkInt, release, codename, activeCodeNames); + } + } catch (ClassNotFoundException | IllegalArgumentException | UnsatisfiedLinkError e) { + // No op, this class should be usable outside of a Robolectric sandbox. + } + CURRENT = currentRelease; + } +} diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java new file mode 100644 index 000000000..95b2c4266 --- /dev/null +++ b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java @@ -0,0 +1,71 @@ +package org.robolectric.versioning; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.robolectric.versioning.AndroidVersions.AndroidRelease; +import org.robolectric.versioning.AndroidVersions.SdkInformation; + +/** Test more esoteric versions mismatches in sdkInt numbers, and codenames. */ +@RunWith(JUnit4.class) +public final class AndroidVersionsEdgeCaseTest { + + /** + * sdkInt higher than any known release, claims it's released. Expects an error message to update + * to update the AndroidVersions.class + */ + @Test + public void sdkIntHigherThanKnownReleasesClaimsIsReleased_throwsException() { + AndroidRelease earliestUnrelease = null; + try { + SdkInformation information = AndroidVersions.gatherStaticSdkInformationFromThisClass(); + earliestUnrelease = information.earliestUnreleased; + information.computeCurrentSdk( + earliestUnrelease.getSdkInt(), earliestUnrelease.getVersion(), "REL", Arrays.asList()); + assertThat(this).isNull(); + } catch (RuntimeException e) { + assertThat(e) + .hasMessageThat() + .contains( + "The current sdk " + + earliestUnrelease.getShortCode() + + " has been released. Please update the contents of " + + AndroidVersions.class.getName() + + " to mark sdk " + + earliestUnrelease.getShortCode() + + " as released."); + assertThat(e).isInstanceOf(RuntimeException.class); + } + } + + /** + * sdkInt lower than known release, claims it's released. Expects an error message to update the + * jar. + */ + @Test + public void sdkIntReleasedButStillReportsCodeName_throwsException() { + AndroidRelease latestRelease = null; + try { + SdkInformation information = AndroidVersions.gatherStaticSdkInformationFromThisClass(); + latestRelease = information.latestRelease; + information.computeCurrentSdk( + latestRelease.getSdkInt(), + null, + information.latestRelease.getShortCode(), + Arrays.asList(latestRelease.getShortCode())); + assertThat(this).isNull(); + } catch (RuntimeException e) { + assertThat(e) + .hasMessageThat() + .contains( + "The current sdk " + + latestRelease.getShortCode() + + " has been been marked as released. Please update the contents of current sdk" + + " jar to the released version."); + assertThat(e).isInstanceOf(RuntimeException.class); + } + } +} diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java new file mode 100644 index 000000000..dba93cab2 --- /dev/null +++ b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java @@ -0,0 +1,205 @@ +package org.robolectric.versioning; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build.VERSION; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.versioning.AndroidVersions.T; + +/** + * Check versions information aligns with runtime information. Primarily, selected SDK with + * internally detected version number. + */ +@RunWith(RobolectricTestRunner.class) +public final class AndroidVersionsTest { + + @Test + @Config(sdk = T.SDK_INT) + public void testStandardInitializationT() { + assertThat(VERSION.SDK_INT).isEqualTo(33); + assertThat(VERSION.RELEASE).isEqualTo("13"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.T.SHORT_CODE).isEqualTo("T"); + assertThat(new AndroidVersions.T().getVersion()).isEqualTo("13.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("T"); + } + + @Test + @Config(sdk = 32) + public void testStandardInitializationSv2() { + assertThat(VERSION.SDK_INT).isEqualTo(32); + assertThat(VERSION.RELEASE).isEqualTo("12"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.Sv2.SHORT_CODE).isEqualTo("Sv2"); + assertThat(new AndroidVersions.Sv2().getVersion()).isEqualTo("12.1"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Sv2"); + } + + @Test + @Config(sdk = 31) + public void testStandardInitializationS() { + assertThat(VERSION.SDK_INT).isEqualTo(31); + assertThat(VERSION.RELEASE).isEqualTo("12"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.S.SHORT_CODE).isEqualTo("S"); + assertThat(new AndroidVersions.S().getVersion()).isEqualTo("12.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("S"); + } + + @Test + @Config(sdk = 30) + public void testStandardInitializationR() { + assertThat(VERSION.SDK_INT).isEqualTo(30); + assertThat(VERSION.RELEASE).isEqualTo("11"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.R.SHORT_CODE).isEqualTo("R"); + assertThat(new AndroidVersions.R().getVersion()).isEqualTo("11.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("R"); + } + + @Test + @Config(sdk = 29) + public void testStandardInitializationQ() { + assertThat(VERSION.SDK_INT).isEqualTo(29); + assertThat(VERSION.RELEASE).isEqualTo("10"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.Q.SHORT_CODE).isEqualTo("Q"); + assertThat(new AndroidVersions.Q().getVersion()).isEqualTo("10.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Q"); + } + + @Test + @Config(sdk = 28) + public void testStandardInitializationP() { + assertThat(VERSION.SDK_INT).isEqualTo(28); + assertThat(VERSION.RELEASE).isEqualTo("9"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.P.SHORT_CODE).isEqualTo("P"); + assertThat(new AndroidVersions.P().getVersion()).isEqualTo("9.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("P"); + } + + @Test + @Config(sdk = 27) + public void testStandardInitializationOMR1() { + assertThat(VERSION.SDK_INT).isEqualTo(27); + assertThat(VERSION.RELEASE).isEqualTo("8.1.0"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.OMR1.SHORT_CODE).isEqualTo("OMR1"); + assertThat(new AndroidVersions.OMR1().getVersion()).isEqualTo("8.1"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("OMR1"); + } + + @Test + @Config(sdk = 26) + public void testStandardInitializationO() { + assertThat(VERSION.SDK_INT).isEqualTo(26); + assertThat(VERSION.RELEASE).isEqualTo("8.0.0"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.O.SHORT_CODE).isEqualTo("O"); + assertThat(new AndroidVersions.O().getVersion()).isEqualTo("8.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("O"); + } + + @Test + @Config(sdk = 25) + public void testStandardInitializationNMR1() { + assertThat(VERSION.SDK_INT).isEqualTo(25); + assertThat(VERSION.RELEASE).isEqualTo("7.1"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.NMR1.SHORT_CODE).isEqualTo("NMR1"); + assertThat(new AndroidVersions.NMR1().getVersion()).isEqualTo("7.1"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("NMR1"); + } + + @Test + @Config(sdk = 24) + public void testStandardInitializationN() { + assertThat(VERSION.SDK_INT).isEqualTo(24); + assertThat(VERSION.RELEASE).isEqualTo("7.0"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.N.SHORT_CODE).isEqualTo("N"); + assertThat(new AndroidVersions.N().getVersion()).isEqualTo("7.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("N"); + } + + @Test + @Config(sdk = 23) + public void testStandardInitializationM() { + assertThat(VERSION.SDK_INT).isEqualTo(23); + assertThat(VERSION.RELEASE).isEqualTo("6.0.1"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.M.SHORT_CODE).isEqualTo("M"); + assertThat(new AndroidVersions.M().getVersion()).isEqualTo("6.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("M"); + } + + @Test + @Config(sdk = 22) + public void testStandardInitializationLMR1() { + assertThat(VERSION.SDK_INT).isEqualTo(22); + assertThat(VERSION.RELEASE).isEqualTo("5.1.1"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.LMR1.SHORT_CODE).isEqualTo("LMR1"); + assertThat(new AndroidVersions.LMR1().getVersion()).isEqualTo("5.1"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("LMR1"); + } + + @Test + @Config(sdk = 21) + public void testStandardInitializationL() { + assertThat(VERSION.SDK_INT).isEqualTo(21); + assertThat(VERSION.RELEASE).isEqualTo("5.0.2"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.L.SHORT_CODE).isEqualTo("L"); + assertThat(new AndroidVersions.L().getVersion()).isEqualTo("5.0"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("L"); + } + + @Test + @Config(sdk = 19) + public void testStandardInitializationK() { + assertThat(VERSION.SDK_INT).isEqualTo(19); + assertThat(VERSION.RELEASE).isEqualTo("4.4"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.K.SHORT_CODE).isEqualTo("K"); + assertThat(new AndroidVersions.K().getVersion()).isEqualTo("4.4"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("K"); + } + + @Test + @Config(sdk = 18) + public void testStandardInitializationJMR2() { + assertThat(VERSION.SDK_INT).isEqualTo(18); + assertThat(VERSION.RELEASE).isEqualTo("4.3"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.JMR2.SHORT_CODE).isEqualTo("JMR2"); + assertThat(new AndroidVersions.JMR2().getVersion()).isEqualTo("4.3"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("JMR2"); + } + + @Test + @Config(sdk = 17) + public void testStandardInitializationJMR1() { + assertThat(VERSION.SDK_INT).isEqualTo(17); + assertThat(VERSION.RELEASE).isEqualTo("4.2.2"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.JMR1.SHORT_CODE).isEqualTo("JMR1"); + assertThat(new AndroidVersions.JMR1().getVersion()).isEqualTo("4.2"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("JMR1"); + } + + @Test + @Config(sdk = 16) + public void testStandardInitializationJ() { + assertThat(VERSION.SDK_INT).isEqualTo(16); + assertThat(VERSION.RELEASE).isEqualTo("4.1.2"); + assertThat(VERSION.CODENAME).isEqualTo("REL"); + assertThat(AndroidVersions.J.SHORT_CODE).isEqualTo("J"); + assertThat(new AndroidVersions.J().getVersion()).isEqualTo("4.1"); + assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("J"); + } +} diff --git a/shadows/versioning/src/test/resources/AndroidManifest.xml b/shadows/versioning/src/test/resources/AndroidManifest.xml new file mode 100644 index 000000000..65383ac0b --- /dev/null +++ b/shadows/versioning/src/test/resources/AndroidManifest.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.robolectric"> + <uses-sdk android:targetSdkVersion="33" android:minSdkVersion="33"/> + <application android:name="android.app.Application"> + </application> +</manifest> diff --git a/testapp/build.gradle b/testapp/build.gradle index 651ced0ea..0abf895ae 100644 --- a/testapp/build.gradle +++ b/testapp/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'com.android.library' android { compileSdk 33 + namespace 'org.robolectric.testapp' defaultConfig { minSdk 16 diff --git a/utils/build.gradle b/utils/build.gradle index c10cca279..c31c9a0e0 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.robolectric.gradle.DeployedRoboJavaModulePlugin import org.robolectric.gradle.RoboJavaModulePlugin @@ -13,7 +14,7 @@ spotless { } } -tasks.withType(GenerateModuleMetadata) { +tasks.withType(GenerateModuleMetadata).configureEach { // We don't want to release gradle module metadata now to avoid // potential compatibility problems. enabled = false @@ -26,7 +27,7 @@ compileKotlin { // in production. If utils module starts to add Kotlin code in main source // set, we can remove this destinationDirectory modification. destinationDirectory = file("${projectDir}/build/classes/java/main") - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } afterEvaluate { @@ -48,20 +49,19 @@ afterEvaluate { dependencies { api project(":annotations") api project(":pluginapi") - api "javax.inject:javax.inject:1" - api "javax.annotation:javax.annotation-api:1.3.2" + api libs.javax.inject + api libs.javax.annotation.api // For @VisibleForTesting and ByteStreams - implementation "com.google.guava:guava:$guavaJREVersion" - compileOnly "com.google.code.findbugs:jsr305:3.0.2" + implementation libs.guava + compileOnly libs.findbugs.jsr305 - testCompileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion" - testAnnotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion" - testAnnotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion" - implementation "com.google.errorprone:error_prone_annotations:$errorproneVersion" + testCompileOnly libs.auto.service.annotations + testAnnotationProcessor libs.auto.service + testAnnotationProcessor libs.error.prone.core + implementation libs.error.prone.annotations - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation libs.junit4 + testImplementation libs.truth + testImplementation libs.kotlin.stdlib } diff --git a/utils/reflector/build.gradle b/utils/reflector/build.gradle index 302734505..140e98700 100644 --- a/utils/reflector/build.gradle +++ b/utils/reflector/build.gradle @@ -5,12 +5,12 @@ apply plugin: RoboJavaModulePlugin apply plugin: DeployedRoboJavaModulePlugin dependencies { - api "org.ow2.asm:asm:${asmVersion}" - api "org.ow2.asm:asm-commons:${asmVersion}" - api "org.ow2.asm:asm-util:${asmVersion}" + api libs.asm + api libs.asm.commons + api libs.asm.util api project(":utils") testImplementation project(":shadowapi") - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" + testImplementation libs.junit4 + testImplementation libs.truth } diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java new file mode 100644 index 000000000..d69c39145 --- /dev/null +++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java @@ -0,0 +1,11 @@ +package org.robolectric.util.reflector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Indicates that the annotated method is a constructor. */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Constructor {} diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java index 2873e2864..12f855f2b 100644 --- a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java +++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java @@ -38,6 +38,8 @@ public class Reflector { private static final boolean DEBUG = false; private static final AtomicInteger COUNTER = new AtomicInteger(); private static final Map<Class<?>, Constructor<?>> cache = new ConcurrentHashMap<>(); + private static final Map<Class<?>, Object> staticReflectorCache = new ConcurrentHashMap<>(); + /** * Returns an object which provides accessors for invoking otherwise inaccessible static methods * and fields. @@ -56,6 +58,10 @@ public class Reflector { * @param target the target object */ public static <T> T reflector(Class<T> iClass, Object target) { + if (target == null && staticReflectorCache.containsKey(iClass)) { + return (T) staticReflectorCache.get(iClass); + } + Class<?> targetClass = determineTargetClass(iClass); Constructor<? extends T> ctor = (Constructor<? extends T>) cache.get(iClass); @@ -68,11 +74,15 @@ public class Reflector { () -> Reflector.<T>createReflectorClass(iClass, targetClass)); ctor = reflectorClass.getConstructor(targetClass); ctor.setAccessible(true); + cache.put(iClass, ctor); } - cache.put(iClass, ctor); + T instance = ctor.newInstance(target); + if (target == null) { + staticReflectorCache.put(iClass, instance); + } + return instance; - return ctor.newInstance(target); } catch (NoSuchMethodException | InstantiationException | IllegalAccessException diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java index d3e366855..ea9b45c6d 100644 --- a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java +++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java @@ -15,6 +15,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.HashSet; import java.util.Set; +import javax.annotation.Nullable; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; @@ -29,6 +30,8 @@ class ReflectorClassWriter extends ClassWriter { private static final Type CLASS_TYPE = Type.getType(Class.class); private static final Type FIELD_TYPE = Type.getType(Field.class); private static final Type METHOD_TYPE = Type.getType(Method.class); + private static final Type CONSTRUCTOR_TYPE = Type.getType(java.lang.reflect.Constructor.class); + private static final Type STRING_TYPE = Type.getType(String.class); private static final Type STRINGBUILDER_TYPE = Type.getType(StringBuilder.class); @@ -45,6 +48,8 @@ class ReflectorClassWriter extends ClassWriter { findMethod(Class.class, "getDeclaredField", new Class<?>[] {String.class}); private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_METHOD = findMethod(Class.class, "getDeclaredMethod", new Class<?>[] {String.class, Class[].class}); + private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_CONSTRUCTOR = + findMethod(Class.class, "getDeclaredConstructor", new Class<?>[] {Class[].class}); private static final org.objectweb.asm.commons.Method ACCESSIBLE_OBJECT$SET_ACCESSIBLE = findMethod(AccessibleObject.class, "setAccessible", new Class<?>[] {boolean.class}); private static final org.objectweb.asm.commons.Method FIELD$GET = @@ -53,6 +58,9 @@ class ReflectorClassWriter extends ClassWriter { findMethod(Field.class, "set", new Class<?>[] {Object.class, Object.class}); private static final org.objectweb.asm.commons.Method METHOD$INVOKE = findMethod(Method.class, "invoke", new Class<?>[] {Object.class, Object[].class}); + private static final org.objectweb.asm.commons.Method CONSTRUCTOR$NEWINSTANCE = + findMethod( + java.lang.reflect.Constructor.class, "newInstance", new Class<?>[] {Object[].class}); private static final org.objectweb.asm.commons.Method THROWABLE$GET_CAUSE = findMethod(Throwable.class, "getCause", new Class<?>[] {}); private static final org.objectweb.asm.commons.Method OBJECT_INIT = @@ -118,8 +126,11 @@ class ReflectorClassWriter extends ClassWriter { if (method.isDefault()) continue; Accessor accessor = method.getAnnotation(Accessor.class); + Constructor constructor = method.getAnnotation(Constructor.class); if (accessor != null) { new AccessorMethodWriter(method, accessor).write(); + } else if (constructor != null) { + new ConstructorMethodWriter(method).write(); } else { new ReflectorMethodWriter(method).write(); } @@ -251,6 +262,135 @@ class ReflectorClassWriter extends ClassWriter { } } + private class ConstructorMethodWriter extends BaseAdapter { + + private final String constructorRefName; + private final Type[] targetParamTypes; + + private ConstructorMethodWriter(Method method) { + super(method); + int myMethodNumber = nextMethodNumber++; + this.constructorRefName = "constructor" + myMethodNumber; + this.targetParamTypes = resolveParamTypes(iMethod); + } + + void write() { + // write field to hold method reference... + visitField( + ACC_PRIVATE | ACC_STATIC, + constructorRefName, + CONSTRUCTOR_TYPE.getDescriptor(), + null, + null); + + visitCode(); + + // pseudocode: + // try { + // return constructorN.newInstance(*args); + // } catch (InvocationTargetException e) { + // throw e.getCause(); + // } catch (ReflectiveOperationException e) { + // throw new AssertionError("Error invoking reflector method in ClassLoader " + + // Instrumentation.class.getClassLoader(), e); + // } + Label tryStart = new Label(); + Label tryEnd = new Label(); + Label handleInvocationTargetException = new Label(); + visitTryCatchBlock( + tryStart, + tryEnd, + handleInvocationTargetException, + INVOCATION_TARGET_EXCEPTION_TYPE.getInternalName()); + Label handleReflectiveOperationException = new Label(); + visitTryCatchBlock( + tryStart, + tryEnd, + handleReflectiveOperationException, + REFLECTIVE_OPERATION_EXCEPTION_TYPE.getInternalName()); + + mark(tryStart); + loadOriginalConstructorRef(); + loadArgArray(); + invokeVirtual(CONSTRUCTOR_TYPE, CONSTRUCTOR$NEWINSTANCE); + mark(tryEnd); + + castForReturn(iMethod.getReturnType()); + returnValue(); + + mark(handleInvocationTargetException); + + int exceptionLocalVar = newLocal(THROWABLE_TYPE); + storeLocal(exceptionLocalVar); + loadLocal(exceptionLocalVar); + invokeVirtual(THROWABLE_TYPE, THROWABLE$GET_CAUSE); + throwException(); + mark(handleReflectiveOperationException); + exceptionLocalVar = newLocal(REFLECTIVE_OPERATION_EXCEPTION_TYPE); + storeLocal(exceptionLocalVar); + newInstance(STRINGBUILDER_TYPE); + dup(); + invokeConstructor(STRINGBUILDER_TYPE, OBJECT_INIT); + push("Error invoking reflector method in ClassLoader "); + invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND); + push(targetType); + invokeVirtual(CLASS_TYPE, CLASS$GET_CLASS_LOADER); + invokeStatic(STRING_TYPE, STRING$VALUE_OF); + invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND); + invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$TO_STRING); + int messageLocalVar = newLocal(STRING_TYPE); + storeLocal(messageLocalVar); + newInstance(ASSERTION_ERROR_TYPE); + dup(); + loadLocal(messageLocalVar); + loadLocal(exceptionLocalVar); + invokeConstructor(ASSERTION_ERROR_TYPE, ASSERTION_ERROR_INIT); + throwException(); + + endMethod(); + } + + private void loadOriginalConstructorRef() { + // pseudocode: + // if (constructorN == null) { + // constructorN = targetClass.getDeclaredConstructor(paramTypes); + // constructorN.setAccessible(true); + // } + // -> constructor reference on stack + getStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE); + dup(); + Label haveConstructorRef = newLabel(); + ifNonNull(haveConstructorRef); + pop(); + + // pseudocode: + // targetClass.getDeclaredConstructor(paramTypes); + push(targetType); + Type[] paramTypes = targetParamTypes; + push(paramTypes.length); + newArray(CLASS_TYPE); + for (int i = 0; i < paramTypes.length; i++) { + dup(); + push(i); + push(paramTypes[i]); + arrayStore(CLASS_TYPE); + } + invokeVirtual(CLASS_TYPE, CLASS$GET_DECLARED_CONSTRUCTOR); + + // pseudocode: + // <constructor>.setAccessible(true); + dup(); + push(true); + invokeVirtual(CONSTRUCTOR_TYPE, ACCESSIBLE_OBJECT$SET_ACCESSIBLE); + + // pseudocode: + // constructorN = constructor; + dup(); + putStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE); + mark(haveConstructorRef); + } + } + private class ReflectorMethodWriter extends BaseAdapter { private final String methodRefName; @@ -375,35 +515,6 @@ class ReflectorClassWriter extends ClassWriter { putStatic(reflectorType, methodRefName, METHOD_TYPE); mark(haveMethodRef); } - - private Type[] resolveParamTypes(Method iMethod) { - Class<?>[] iParamTypes = iMethod.getParameterTypes(); - Annotation[][] paramAnnotations = iMethod.getParameterAnnotations(); - - Type[] targetParamTypes = new Type[iParamTypes.length]; - for (int i = 0; i < iParamTypes.length; i++) { - Class<?> paramType = findWithType(paramAnnotations[i]); - if (paramType == null) { - paramType = iParamTypes[i]; - } - targetParamTypes[i] = Type.getType(paramType); - } - return targetParamTypes; - } - - private Class<?> findWithType(Annotation[] paramAnnotation) { - for (Annotation annotation : paramAnnotation) { - if (annotation instanceof WithType) { - String withTypeName = ((WithType) annotation).value(); - try { - return Class.forName(withTypeName, true, iClass.getClassLoader()); - } catch (ClassNotFoundException e1) { - // it's okay, ignore - } - } - } - return null; - } } private static String[] getInternalNames(final Class<?>[] types) { @@ -494,5 +605,35 @@ class ReflectorClassWriter extends ClassWriter { void loadNull() { visitInsn(Opcodes.ACONST_NULL); } + + protected Type[] resolveParamTypes(Method iMethod) { + Class<?>[] iParamTypes = iMethod.getParameterTypes(); + Annotation[][] paramAnnotations = iMethod.getParameterAnnotations(); + + Type[] targetParamTypes = new Type[iParamTypes.length]; + for (int i = 0; i < iParamTypes.length; i++) { + Class<?> paramType = findWithType(paramAnnotations[i]); + if (paramType == null) { + paramType = iParamTypes[i]; + } + targetParamTypes[i] = Type.getType(paramType); + } + return targetParamTypes; + } + + @Nullable + private Class<?> findWithType(Annotation[] paramAnnotation) { + for (Annotation annotation : paramAnnotation) { + if (annotation instanceof WithType) { + String withTypeName = ((WithType) annotation).value(); + try { + return Class.forName(withTypeName, true, iClass.getClassLoader()); + } catch (ClassNotFoundException e1) { + // it's okay, ignore + } + } + } + return null; + } } } diff --git a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java index 8baf3d63e..74dc88487 100644 --- a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java +++ b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java @@ -133,6 +133,25 @@ public class ReflectorTest { time("saved accessor", 10_000_000, () -> fieldBySavedReflector(accessor)); } + @Ignore + @Test + public void constructorPerf() { + SomeClass i = new SomeClass("c"); + + System.out.println("reflection = " + Collections.singletonList(methodByReflectionHelpers(i))); + System.out.println("accessor = " + Collections.singletonList(methodByReflector(i))); + + _SomeClass_ accessor = reflector(_SomeClass_.class, i); + + time("ReflectionHelpers", 10_000_000, this::constructorByReflectionHelpers); + time("accessor", 10_000_000, () -> constructorByReflector()); + time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor)); + + time("ReflectionHelpers", 10_000_000, () -> constructorByReflectionHelpers()); + time("accessor", 10_000_000, () -> constructorByReflector()); + time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor)); + } + @Test public void nonExistentMethod_throwsAssertionError() { SomeClass i = new SomeClass("c"); @@ -143,6 +162,11 @@ public class ReflectorTest { assertThat(ex).hasCauseThat().isInstanceOf(NoSuchMethodException.class); } + @Test + public void reflector_constructor() { + assertThat(staticReflector.newSomeClass("sdfsdf")).isNotNull(); + } + ////////////////////// /** Accessor interface for {@link SomeClass}'s internals. */ @@ -170,6 +194,9 @@ public class ReflectorTest { @Accessor("mD") int getD(); + @Constructor + SomeClass newSomeClass(String c); + String someMethod(String a, String b); String nonExistentMethod(String a, String b, String c); @@ -251,6 +278,20 @@ public class ReflectorTest { return reflector.someMethod("a", "b"); } + private SomeClass constructorByReflectionHelpers() { + return ReflectionHelpers.callConstructor( + SomeClass.class, ClassParameter.from(String.class, "a")); + } + + private SomeClass constructorByReflector() { + _SomeClass_ accessor = reflector(_SomeClass_.class); + return accessor.newSomeClass("a"); + } + + private SomeClass constructorBySavedReflector(_SomeClass_ reflector) { + return reflector.newSomeClass("a"); + } + private String fieldByReflectionHelpers(SomeClass o) { ReflectionHelpers.setField(o, "c", "abc"); return ReflectionHelpers.getField(o, "c"); diff --git a/utils/src/main/java/org/robolectric/util/Util.java b/utils/src/main/java/org/robolectric/util/Util.java index b7292ad93..b7292ad93 100755..100644 --- a/utils/src/main/java/org/robolectric/util/Util.java +++ b/utils/src/main/java/org/robolectric/util/Util.java |